From beb848373b28624bad8a2314b6b0496f5d007937 Mon Sep 17 00:00:00 2001 From: Zach Fox Date: Mon, 13 Mar 2017 10:15:06 -0700 Subject: [PATCH] Update from zfox23/PALv2 branch --- .../resources/icons/profilePicLoading.gif | Bin 0 -> 12294 bytes .../resources/qml/controls-uit/CheckBox.qml | 2 +- .../resources/qml/controls-uit/Table.qml | 7 +- .../qml/controls-uit/TabletComboBox.qml | 2 +- interface/resources/qml/hifi/NameCard.qml | 755 ++++++++-------- interface/resources/qml/hifi/Pal.qml | 832 ++++++++++++++---- .../qml/styles-uit/HifiConstants.qml | 2 +- .../src/scripting/HMDScriptingInterface.h | 3 +- scripts/defaultScripts.js | 2 +- scripts/system/makeUserConnection.js | 372 ++++++++ scripts/system/pal.js | 257 +++++- 11 files changed, 1681 insertions(+), 553 deletions(-) create mode 100644 interface/resources/icons/profilePicLoading.gif create mode 100644 scripts/system/makeUserConnection.js diff --git a/interface/resources/icons/profilePicLoading.gif b/interface/resources/icons/profilePicLoading.gif new file mode 100644 index 0000000000000000000000000000000000000000..7e85f7cb6aae0e060ec01b5d022603e5bf1e9864 GIT binary patch literal 12294 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 literal 0 HcmV?d00001 diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 09a0e04148..35b81b44b4 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -88,7 +88,7 @@ Original.CheckBox { label: Label { text: control.text colorScheme: checkBox.colorScheme - x: checkBox.boxSize / 2 + x: 2 wrapMode: Text.Wrap enabled: checkBox.enabled } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index c7e0809b29..21bd8b60dc 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -48,11 +48,12 @@ TableView { HiFiGlyphs { id: titleSort text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn - color: hifi.colors.baseGrayHighlight + color: hifi.colors.darkGray + opacity: 0.6; size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0) + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter @@ -89,7 +90,7 @@ TableView { Rectangle { color: "#00000000" anchors { fill: parent; margins: -2 } - radius: hifi.dimensions.borderRadius + //radius: hifi.dimensions.borderRadius border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight border.width: 2 } diff --git a/interface/resources/qml/controls-uit/TabletComboBox.qml b/interface/resources/qml/controls-uit/TabletComboBox.qml index e5dec315e5..e58a465298 100644 --- a/interface/resources/qml/controls-uit/TabletComboBox.qml +++ b/interface/resources/qml/controls-uit/TabletComboBox.qml @@ -183,7 +183,7 @@ FocusScope { anchors.leftMargin: hifi.dimensions.textPadding anchors.verticalCenter: parent.verticalCenter id: popupText - text: listView.model[index] ? listView.model[index] : "" + text: listView.model[index] ? listView.model[index] : (listView.model.get(index).text ? listView.model.get(index).text : "") size: hifi.fontSizes.textFieldInput color: hifi.colors.baseGray } diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 020a85b46d..fcde5817fc 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -14,388 +14,453 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtGraphicalEffects 1.0 import "../styles-uit" +import "toolbars" Item { id: thisNameCard - // Anchors - anchors { - verticalCenter: parent.verticalCenter - leftMargin: 10 - rightMargin: 10 - } + // Size + width: isMyCard ? pal.myCardWidth - anchors.leftMargin : pal.nearbyNameCardWidth; + height: isMyCard ? pal.myCardHeight : pal.rowHeight; + anchors.left: parent.left + anchors.leftMargin: 5 + anchors.top: parent.top; // Properties + property string profileUrl: ""; + property string defaultBaseUrl: "http://highfidelity.com"; + property string connectionStatus : "" property string uuid: "" property string displayName: "" property string userName: "" property real displayNameTextPixelSize: 18 - property int usernameTextHeight: 12 + property int usernameTextPixelSize: 14 property real audioLevel: 0.0 property real avgAudioLevel: 0.0 property bool isMyCard: false property bool selected: false property bool isAdmin: false - property bool currentlyEditingDisplayName: false + property string imageMaskColor: pal.color; + property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : imageMaskColor)) - /* User image commented out for now - will probably be re-introduced later. - Column { + Item { id: avatarImage + visible: profileUrl !== ""; // Size - height: parent.height - width: height + height: isMyCard ? 70 : 42; + width: visible ? height : 0; + anchors.top: parent.top; + anchors.topMargin: isMyCard ? 0 : 8; + anchors.left: parent.left + clip: true Image { id: userImage - source: "../../icons/defaultNameCardUser.png" + source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (defaultBaseUrl + profileUrl)) : ""; + mipmap: true; // Anchors + anchors.fill: parent + } + AnimatedImage { + source: "../../icons/profilePicLoading.gif" + anchors.fill: parent; + visible: userImage.status != Image.Ready; + } + // Circular mask + Rectangle { + id: avatarImageMask; + visible: avatarImage.visible; + anchors.verticalCenter: avatarImage.verticalCenter; + anchors.horizontalCenter: avatarImage.horizontalCenter; + width: avatarImage.width * 2; + height: avatarImage.height * 2; + color: "transparent" + radius: avatarImage.height; + border.color: imageMaskColor; + border.width: avatarImage.height/2; + } + StateImage { + id: infoHoverImage; + visible: avatarImageMouseArea.containsMouse ? true : false; + imageURL: "../../images/info-icon-2-state.svg"; + size: 32; + buttonState: 1; + anchors.centerIn: parent; + } + MouseArea { + id: avatarImageMouseArea; + anchors.fill: parent + enabled: selected || isMyCard; + hoverEnabled: enabled + onClicked: { + /* + THIS WILL OPEN THE BROWSER TO THE USER'S INFO PAGE! + I've no idea how to do this yet.. + */ + } + } + } + + // Colored border around avatarImage + Rectangle { + id: avatarImageBorder; + visible: avatarImage.visible; + anchors.verticalCenter: avatarImage.verticalCenter; + anchors.horizontalCenter: avatarImage.horizontalCenter; + width: avatarImage.width + border.width; + height: avatarImage.height + border.width; + color: "transparent" + radius: avatarImage.height; + border.color: profilePicBorderColor; + border.width: 4; + } + + // DisplayName field for my card + Rectangle { + id: myDisplayName + visible: isMyCard + // Size + width: parent.width - avatarImage.width - anchors.leftMargin*2 - anchors.rightMargin; + height: 40 + // Anchors + anchors.top: avatarImage.top + anchors.left: avatarImage.right + anchors.leftMargin: 5; + anchors.rightMargin: 5; + // Style + color: myDisplayNameMouseArea.containsMouse ? hifi.colors.lightGrayText : hifi.colors.textFieldLightBackground + border.color: hifi.colors.blueHighlight + border.width: 0 + TextInput { + id: myDisplayNameText + // Properties + text: thisNameCard.displayName + maximumLength: 256 + clip: true + // Size width: parent.width height: parent.height - } - } - */ - Item { - id: textContainer - // Size - width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin - height: selected || isMyCard ? childrenRect.height : childrenRect.height - 15 - anchors.verticalCenter: parent.verticalCenter - - // DisplayName field for my card - Rectangle { - id: myDisplayName - visible: isMyCard - // Size - width: parent.width + 70 - height: 35 // Anchors - anchors.top: parent.top + anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: -10 + anchors.leftMargin: 10 + anchors.right: parent.right + anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin // Style - color: hifi.colors.textFieldLightBackground - border.color: hifi.colors.blueHighlight - border.width: 0 - TextInput { - id: myDisplayNameText - // Properties - text: thisNameCard.displayName - maximumLength: 256 - clip: true - // Size - width: parent.width - height: parent.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: 10 - anchors.right: parent.right - anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin - // Style - color: hifi.colors.darkGray - FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } - font.family: firaSansSemiBold.name - font.pixelSize: displayNameTextPixelSize - selectionColor: hifi.colors.blueHighlight - selectedTextColor: "black" - // Text Positioning - verticalAlignment: TextInput.AlignVCenter - horizontalAlignment: TextInput.AlignLeft - // Signals - onEditingFinished: { - pal.sendToScript({method: 'displayNameUpdate', params: text}) - cursorPosition = 0 - focus = false - myDisplayName.border.width = 0 - color = hifi.colors.darkGray - currentlyEditingDisplayName = false - } - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: { - myDisplayName.border.width = 1 - myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); - myDisplayNameText.focus = true - myDisplayNameText.color = "black" - currentlyEditingDisplayName = true - } - onDoubleClicked: { - myDisplayNameText.selectAll(); - myDisplayNameText.focus = true; - currentlyEditingDisplayName = true - } - onEntered: myDisplayName.color = hifi.colors.lightGrayText - onExited: myDisplayName.color = hifi.colors.textFieldLightBackground - } - // Edit pencil glyph - HiFiGlyphs { - id: editGlyph - text: hifi.glyphs.editPencil - // Text Size - size: displayNameTextPixelSize*1.5 - // Anchors - anchors.right: parent.right - anchors.rightMargin: 5 - anchors.verticalCenter: parent.verticalCenter - // Style - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - color: hifi.colors.baseGray - } - } - // Spacer for DisplayName for my card - Item { - id: myDisplayNameSpacer - width: 1 - height: 4 - // Anchors - anchors.top: myDisplayName.bottom - } - // DisplayName container for others' cards - Item { - id: displayNameContainer - visible: !isMyCard - // Size - width: parent.width - height: displayNameTextPixelSize + 4 - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // DisplayName Text for others' cards - FiraSansSemiBold { - id: displayNameText - // Properties - text: thisNameCard.displayName - elide: Text.ElideRight - // Size - width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width - // Anchors - anchors.top: parent.top - anchors.left: parent.left - // Text Size - size: displayNameTextPixelSize - // Text Positioning - verticalAlignment: Text.AlignVCenter - // Style - color: hifi.colors.darkGray - } - TextMetrics { - id: displayNameTextMetrics - font: displayNameText.font - text: displayNameText.text - } - // "ADMIN" label for other users' cards - RalewaySemiBold { - id: adminLabelText - visible: isAdmin - text: "ADMIN" - // Text size - size: displayNameText.size - 4 - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: displayNameText.right - // Style - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - // Alignment - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignTop - } - // This Rectangle refers to the [?] popup button next to "ADMIN" - Item { - id: adminLabelQuestionMark - visible: isAdmin - // Size - width: 20 - height: displayNameText.height - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: adminLabelText.right - RalewayRegular { - id: adminLabelQuestionMarkText - text: "[?]" - size: adminLabelText.size - font.capitalization: Font.AllUppercase - color: hifi.colors.redHighlight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - anchors.fill: parent - } - MouseArea { - anchors.fill: parent - acceptedButtons: Qt.LeftButton - hoverEnabled: true - onClicked: letterbox(hifi.glyphs.question, - "Domain Admin", - "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") - onEntered: adminLabelQuestionMarkText.color = "#94132e" - onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight - } - } - } - - // UserName Text - FiraSansRegular { - id: userNameText - // Properties - text: thisNameCard.userName - elide: Text.ElideRight - visible: thisNameCard.displayName - // Size - width: parent.width - // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom - // Text Size - size: thisNameCard.usernameTextHeight + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.blueHighlight + selectedTextColor: "black" // Text Positioning - verticalAlignment: Text.AlignVCenter + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignLeft + autoScroll: false; + // Signals + onEditingFinished: { + pal.sendToScript({method: 'displayNameUpdate', params: text}) + cursorPosition = 0 + focus = false + myDisplayName.border.width = 0 + color = hifi.colors.darkGray + pal.currentlyEditingDisplayName = false + autoScroll = false; + } + } + MouseArea { + id: myDisplayNameMouseArea; + anchors.fill: parent + hoverEnabled: true + onClicked: { + myDisplayName.border.width = 1 + myDisplayNameText.focus ? myDisplayNameText.cursorPosition = myDisplayNameText.positionAt(mouseX, mouseY, TextInput.CursorOnCharacter) : myDisplayNameText.selectAll(); + myDisplayNameText.focus = true + myDisplayNameText.color = "black" + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + onDoubleClicked: { + myDisplayNameText.selectAll(); + myDisplayNameText.focus = true; + pal.currentlyEditingDisplayName = true + myDisplayNameText.autoScroll = true; + } + } + // Edit pencil glyph + HiFiGlyphs { + id: editGlyph + text: hifi.glyphs.editPencil + // Text Size + size: displayNameTextPixelSize*1.5 + // Anchors + anchors.right: parent.right + anchors.rightMargin: 5 + anchors.verticalCenter: parent.verticalCenter // Style + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter color: hifi.colors.baseGray } - - // Spacer - Item { - id: userNameSpacer - height: 4 - width: parent.width - // Anchors - anchors.top: userNameText.bottom - } - - // VU Meter - Rectangle { - id: nameCardVUMeter - // Size - width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width - height: 8 - // Anchors - anchors.top: userNameSpacer.bottom - // Style - radius: 4 - color: "#c5c5c5" - visible: isMyCard || selected - // Rectangle for the zero-gain point on the VU meter - Rectangle { - id: vuMeterZeroGain - visible: gainSlider.visible - // Size - width: 4 - height: 18 - // Style - color: hifi.colors.darkGray - // Anchors - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 - } - // Rectangle for the VU meter line - Rectangle { - id: vuMeterLine - width: gainSlider.width - visible: gainSlider.visible - // Style - color: vuMeterBase.color - radius: nameCardVUMeter.radius - height: nameCardVUMeter.height / 2 - anchors.verticalCenter: nameCardVUMeter.verticalCenter - } - // Rectangle for the VU meter base - Rectangle { - id: vuMeterBase - // Anchors - anchors.fill: parent - visible: isMyCard || selected - // Style - color: parent.color - radius: parent.radius - } - // Rectangle for the VU meter audio level - Rectangle { - id: vuMeterLevel - visible: isMyCard || selected - // Size - width: (thisNameCard.audioLevel) * parent.width - // Style - color: parent.color - radius: parent.radius - // Anchors - anchors.bottom: parent.bottom - anchors.top: parent.top - anchors.left: parent.left - } - // Gradient for the VU meter audio level - LinearGradient { - anchors.fill: vuMeterLevel - source: vuMeterLevel - start: Qt.point(0, 0) - end: Qt.point(parent.width, 0) - gradient: Gradient { - GradientStop { position: 0.0; color: "#2c8e72" } - GradientStop { position: 0.9; color: "#1fc6a6" } - GradientStop { position: 0.91; color: "#ea4c5f" } - GradientStop { position: 1.0; color: "#ea4c5f" } - } - } - } - - // Per-Avatar Gain Slider - Slider { - id: gainSlider - // Size - width: parent.width - height: 14 - // Anchors - anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // DisplayName container for others' cards + Item { + id: displayNameContainer + visible: !isMyCard + // Size + width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin; + height: displayNameTextPixelSize + 4 + // Anchors + anchors.top: pal.activeTab == "connectionsTab" ? undefined : avatarImage.top; + anchors.bottom: pal.activeTab == "connectionsTab" ? avatarImage.bottom : undefined; + anchors.left: avatarImage.right + anchors.leftMargin: avatarImage.visible ? 5 : 0; + // DisplayName Text for others' cards + FiraSansSemiBold { + id: displayNameText // Properties - visible: !isMyCard && selected - value: Users.getAvatarGain(uuid) - minimumValue: -60.0 - maximumValue: 20.0 - stepSize: 5 - updateValueWhileDragging: true - onValueChanged: updateGainFromQML(uuid, value, false) - onPressedChanged: { - if (!pressed) { - updateGainFromQML(uuid, value, true) - } + text: thisNameCard.displayName + elide: Text.ElideRight + // Size + width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width + // Anchors + anchors.top: parent.top + anchors.left: parent.left + // Text Size + size: displayNameTextPixelSize + // Text Positioning + verticalAlignment: Text.AlignTop + // Style + color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) + ? hifi.colors.blueHighlight : (pal.activeTab == "nearbyTab" ? hifi.colors.darkGray : hifi.colors.greenShadow); + MouseArea { + id: displayNameTextMouseArea; + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + hoverEnabled: enabled + onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + } + } + TextMetrics { + id: displayNameTextMetrics + font: displayNameText.font + text: displayNameText.text + } + // "ADMIN" label for other users' cards + RalewaySemiBold { + id: adminLabelText + visible: isAdmin + text: "ADMIN" + // Text size + size: displayNameText.size - 4 + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: displayNameText.right + // Style + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + // Alignment + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + } + // This Rectangle refers to the [?] popup button next to "ADMIN" + Item { + id: adminLabelQuestionMark + visible: isAdmin + // Size + width: 20 + height: displayNameText.height + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: adminLabelText.right + RalewayRegular { + id: adminLabelQuestionMarkText + text: "[?]" + size: adminLabelText.size + font.capitalization: Font.AllUppercase + color: hifi.colors.redHighlight + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + anchors.fill: parent } MouseArea { anchors.fill: parent - onWheel: { - // Do nothing. - } - onDoubleClicked: { - gainSlider.value = 0.0 - } - onPressed: { - // Pass through to Slider - mouse.accepted = false - } - onReleased: { - // the above mouse.accepted seems to make this - // never get called, nonetheless... - mouse.accepted = false - } - } - style: SliderStyle { - groove: Rectangle { - color: "#c5c5c5" - implicitWidth: gainSlider.width - implicitHeight: 4 - radius: 2 - opacity: 0 - } - handle: Rectangle { - anchors.centerIn: parent - color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" - implicitWidth: 10 - implicitHeight: 16 - } + hoverEnabled: true + onClicked: letterbox(hifi.glyphs.question, + "Domain Admin", + "This user is an admin on this domain. Admins can Silence and Ban other users at their discretion - so be extra nice!") + onEntered: adminLabelQuestionMarkText.color = "#94132e" + onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight } } } + // UserName Text + FiraSansRegular { + id: userNameText + // Properties + text: thisNameCard.userName + elide: Text.ElideRight + visible: thisNameCard.displayName + // Size + width: parent.width + height: usernameTextPixelSize + 4 + // Anchors + anchors.top: isMyCard ? myDisplayName.bottom : undefined; + anchors.bottom: isMyCard ? undefined : avatarImage.bottom + anchors.left: avatarImage.right; + anchors.leftMargin: avatarImage.visible ? 5 : 0; + // Text Size + size: usernameTextPixelSize; + // Text Positioning + verticalAlignment: Text.AlignBottom + // Style + color: (pal.activeTab == "nearbyTab" && (displayNameTextMouseArea.containsMouse || userNameTextMouseArea.containsMouse)) ? hifi.colors.blueHighlight : hifi.colors.greenShadow; + MouseArea { + id: userNameTextMouseArea; + anchors.fill: parent + enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== ""; + hoverEnabled: enabled + onClicked: pal.sendToScript({method: 'goToUser', params: thisNameCard.userName}); + } + } + // VU Meter + Rectangle { + id: nameCardVUMeter + // Size + width: isMyCard ? myDisplayName.width - 20 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * (gainSlider.width); + height: 8 + // Anchors + anchors.bottom: isMyCard ? avatarImage.bottom : parent.bottom; + anchors.bottomMargin: isMyCard ? 0 : height; + anchors.left: isMyCard ? userNameText.left : parent.left; + // Style + radius: 4 + color: "#c5c5c5" + visible: isMyCard || (selected && pal.activeTab == "nearbyTab") + // Rectangle for the zero-gain point on the VU meter + Rectangle { + id: vuMeterZeroGain + visible: gainSlider.visible + // Size + width: 4 + height: 18 + // Style + color: hifi.colors.darkGray + // Anchors + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4 + } + // Rectangle for the VU meter line + Rectangle { + id: vuMeterLine + width: gainSlider.width + visible: gainSlider.visible + // Style + color: vuMeterBase.color + radius: nameCardVUMeter.radius + height: nameCardVUMeter.height / 2 + anchors.verticalCenter: nameCardVUMeter.verticalCenter + } + // Rectangle for the VU meter base + Rectangle { + id: vuMeterBase + // Anchors + anchors.fill: parent + visible: isMyCard || selected + // Style + color: parent.color + radius: parent.radius + } + // Rectangle for the VU meter audio level + Rectangle { + id: vuMeterLevel + visible: isMyCard || selected + // Size + width: (thisNameCard.audioLevel) * parent.width + // Style + color: parent.color + radius: parent.radius + // Anchors + anchors.bottom: parent.bottom + anchors.top: parent.top + anchors.left: parent.left + } + // Gradient for the VU meter audio level + LinearGradient { + anchors.fill: vuMeterLevel + source: vuMeterLevel + start: Qt.point(0, 0) + end: Qt.point(parent.width, 0) + gradient: Gradient { + GradientStop { position: 0.0; color: "#2c8e72" } + GradientStop { position: 0.9; color: "#1fc6a6" } + GradientStop { position: 0.91; color: "#ea4c5f" } + GradientStop { position: 1.0; color: "#ea4c5f" } + } + } + } + + // Per-Avatar Gain Slider + Slider { + id: gainSlider + // Size + width: thisNameCard.width; + height: 14 + // Anchors + anchors.verticalCenter: nameCardVUMeter.verticalCenter; + anchors.left: nameCardVUMeter.left; + // Properties + visible: !isMyCard && selected && pal.activeTab == "nearbyTab"; + value: Users.getAvatarGain(uuid) + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + updateValueWhileDragging: true + onValueChanged: { + if (uuid !== "") { + updateGainFromQML(uuid, value, false); + } + } + onPressedChanged: { + if (!pressed) { + updateGainFromQML(uuid, value, true) + } + } + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + gainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + style: SliderStyle { + groove: Rectangle { + color: "#c5c5c5" + implicitWidth: gainSlider.width + implicitHeight: 4 + radius: 2 + opacity: 0 + } + handle: Rectangle { + anchors.centerIn: parent + color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F" + implicitWidth: 10 + implicitHeight: 16 + } + } + } + function updateGainFromQML(avatarUuid, sliderValue, isReleased) { Users.setAvatarGain(avatarUuid, sliderValue); if (isReleased) { diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 25362d98f1..5fb11b4e2f 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -28,16 +28,21 @@ Rectangle { // Style color: "#E3E3E3"; // Properties - property int myCardHeight: 90; - property int rowHeight: 70; + property int myCardWidth: palContainer.width - upperRightInfoContainer.width; + property int myCardHeight: 82; + property int rowHeight: 60; property int actionButtonWidth: 55; - property int actionButtonAllowance: actionButtonWidth * 2; - property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth; - property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance); - property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}); // valid dummy until set + property int locationColumnWidth: 170; + property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: ""}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. - property var userModelData: []; // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. + property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. + property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; + property var activeTab: "nearbyTab"; + property int usernameAvailability; + property bool currentlyEditingDisplayName: false HifiConstants { id: hifi; } @@ -59,24 +64,39 @@ Rectangle { category: "pal"; property bool filtered: false; property int nearDistance: 30; - property int sortIndicatorColumn: 1; - property int sortIndicatorOrder: Qt.AscendingOrder; + property int nearbySortIndicatorColumn: 1; + property int nearbySortIndicatorOrder: Qt.AscendingOrder; + property int connectionsSortIndicatorColumn: 0; + property int connectionsSortIndicatorOrder: Qt.AscendingOrder; } - function getSelectedSessionIDs() { + function getSelectedNearbySessionIDs() { var sessionIDs = []; - table.selection.forEach(function (userIndex) { - sessionIDs.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + var datum = nearbyUserModelData[userIndex]; + if (datum) { // Might have been filtered out + sessionIDs.push(datum.sessionId); + } }); return sessionIDs; } - function refreshWithFilter() { - // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. - var userIds = getSelectedSessionIDs(); - var params = {filter: filter.checked && {distance: settings.nearDistance}}; + function getSelectedConnectionsUserNames() { + var userNames = []; + connectionsTable.selection.forEach(function (userIndex) { + var datum = connectionsUserModelData[userIndex]; + if (datum) { + userNames.push(datum.userName); + } + }); + return userNames; + } + function refreshNearbyWithFilter() { + // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. + var userIds = getSelectedNearbySessionIDs(); + var params = {filter: inViewCheckbox.checked && {distance: settings.nearDistance}}; if (userIds.length > 0) { params.selected = [[userIds[0]], true, true]; } - pal.sendToScript({method: 'refresh', params: params}); + pal.sendToScript({method: 'refreshNearby', params: params}); } // This is the container for the PAL @@ -90,8 +110,6 @@ Rectangle { color: pal.color; // Anchors anchors.centerIn: pal; - // Properties - radius: hifi.dimensions.borderRadius; // This contains the current user's NameCard and will contain other information in the future Rectangle { @@ -103,75 +121,351 @@ Rectangle { color: pal.color; // Anchors anchors.top: palContainer.top; - // Properties - radius: hifi.dimensions.borderRadius; - // This NameCard refers to the current user's NameCard (the one above the table) + // This NameCard refers to the current user's NameCard (the one above the nearbyTable) NameCard { id: myCard; // Properties + profileUrl: myData.profileUrl; + imageMaskColor: pal.color; displayName: myData.displayName; userName: myData.userName; audioLevel: myData.audioLevel; avgAudioLevel: myData.avgAudioLevel; isMyCard: true; // Size - width: minNameCardWidth; + width: myCardWidth; height: parent.height; // Anchors + anchors.top: parent.top anchors.left: parent.left; } - Row { - HifiControls.CheckBox { - id: filter; - checked: settings.filtered; - text: "in view"; - boxSize: reload.height * 0.70; - onCheckedChanged: refreshWithFilter(); + Item { + id: upperRightInfoContainer; + width: 160; + height: parent.height; + anchors.top: parent.top; + anchors.right: parent.right; + + RalewayRegular { + id: availabilityText; + text: "set availability"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.top: availabilityComboBox.bottom; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.baseGrayHighlight; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; } - HifiControls.GlyphButton { - id: reload; - glyph: hifi.glyphs.reload; - width: reload.height; - onClicked: refreshWithFilter(); - } - spacing: 50; - anchors { - right: parent.right; - top: parent.top; - topMargin: 10; + HifiControls.TabletComboBox { + id: availabilityComboBox; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + // Size + width: parent.width; + height: 40; + currentIndex: usernameAvailability; + model: ListModel { + id: availabilityComboBoxListItems + ListElement { text: "Everyone"; value: "all"; } + ListElement { text: "Friends Only"; value: "friends"; } + ListElement { text: "Appear Offline"; value: "none" } + } + onCurrentIndexChanged: { pal.sendToScript({method: 'setAvailability', params: availabilityComboBoxListItems.get(currentIndex).value})} } } } - // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle - Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.top: myInfo.bottom; - anchors.left: parent.left; + Item { + id: palTabContainer; + // Anchors + anchors { + top: myInfo.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; } Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.bottom: table.top; - anchors.left: parent.left; + id: tabSelectorContainer; + // Anchors + anchors { + top: parent.top; + topMargin: 2; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width; + height: 35 - anchors.topMargin; + Rectangle { + id: nearbyTabSelector; + // Anchors + anchors { + top: parent.top; + left: parent.left; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "nearbyTab" ? pal.color : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "nearbyTab") { + refreshNearbyWithFilter(); + } + activeTab = "nearbyTab"; + } + } + + // "NEARBY" Text Container + Item { + id: nearbyTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // "NEARBY" text + RalewaySemiBold { + id: nearbyTabSelectorText; + text: "NEARBY"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + // "In View" Checkbox + HifiControls.CheckBox { + id: inViewCheckbox; + visible: activeTab == "nearbyTab"; + anchors.right: reloadNearbyContainer.left; + anchors.rightMargin: 25; + anchors.verticalCenter: parent.verticalCenter; + checked: settings.filtered; + text: "in view"; + boxSize: 24; + onCheckedChanged: refreshNearbyWithFilter(); + } + // Refresh button + Rectangle { + id: reloadNearbyContainer + visible: activeTab == "nearbyTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadNearby.height; + width: height; + HifiControls.GlyphButton { + id: reloadNearby; + width: reloadNearby.height; + glyph: hifi.glyphs.reload; + onClicked: refreshNearbyWithFilter(); + } + } + } + } + Rectangle { + id: connectionsTabSelector; + // Anchors + anchors { + top: parent.top; + left: nearbyTabSelector.right; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "connectionsTab" ? pal.color : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "connectionsTab") { + pal.sendToScript({method: 'refreshConnections'}); + } + activeTab = "connectionsTab"; + connectionsLoading.visible = true; + } + } + + // "CONNECTIONS" Text Container + Item { + id: connectionsTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // Refresh button + Rectangle { + visible: activeTab == "connectionsTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadConnections.height; + width: height; + HifiControls.GlyphButton { + id: reloadConnections; + width: reloadConnections.height; + glyph: hifi.glyphs.reload; + onClicked: { + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + } + } + // "CONNECTIONS" text + RalewaySemiBold { + id: connectionsTabSelectorText; + text: "CONNECTIONS"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: hifi.colors.redHighlight; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + TextMetrics { + id: connectionsTabSelectorTextMetrics; + text: connectionsTabSelectorText.text; + } + + // This Rectangle refers to the [?] popup button next to "CONNECTIONS" + Rectangle { + color: connectionsTabSelector.color; + width: 20; + height: connectionsTabSelectorText.height - 2; + anchors.left: connectionsTabSelectorTextContainer.left; + anchors.top: connectionsTabSelectorTextContainer.top; + anchors.topMargin: 1; + anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42; + RalewayRegular { + id: connectionsHelpText; + text: "[?]"; + size: connectionsTabSelectorText.size + 6; + font.capitalization: Font.AllUppercase; + color: connectionsTabSelectorMouseArea.containsMouse ? hifi.colors.redAccent : hifi.colors.redHighlight; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + id: connectionsTabSelectorMouseArea; + anchors.fill: parent; + hoverEnabled: true; + onClicked: letterbox(hifi.glyphs.question, + "Connections and Friends", + "Purple borders around profile pictures are Connections.
" + + "When your availability is set to Everyone, Connections can see your username and location.

" + + "Green borders around profile pictures are Friends.
" + + "When your availability is set to Friends, only Friends can see your username and location."); + } + } + } + } } + Item { + id: tabBorders; + anchors.fill: parent; + property var color: hifi.colors.lightGray; + property int borderWeight: 3; + // Left border + Rectangle { + color: parent.color; + anchors { + left: parent.left; + bottom: parent.bottom; + } + width: parent.borderWeight; + height: parent.height - (activeTab == "nearbyTab" ? 0 : tabSelectorContainer.height); + } + // Right border + Rectangle { + color: parent.color; + anchors { + right: parent.right; + bottom: parent.bottom; + } + width: parent.borderWeight; + height: parent.height - (activeTab == "nearbyTab" ? tabSelectorContainer.height : 0); + } + // Bottom border + Rectangle { + color: parent.color; + anchors { + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + height: parent.borderWeight; + } + // Border between buttons + Rectangle { + color: parent.color; + anchors { + horizontalCenter: parent.horizontalCenter; + top: parent.top; + } + width: parent.borderWeight; + height: tabSelectorContainer.height + width; + } + // Border above selected tab + Rectangle { + color: parent.color; + anchors { + top: parent.top; + left: parent.left; + leftMargin: activeTab == "nearbyTab" ? 0 : parent.width/2; + } + width: parent.width/2; + height: parent.borderWeight; + } + // Border below unselected tab + Rectangle { + color: parent.color; + anchors { + top: parent.top; + topMargin: tabSelectorContainer.height; + left: parent.left; + leftMargin: activeTab == "nearbyTab" ? parent.width/2 : 0; + } + width: parent.width/2; + height: parent.borderWeight; + } + } + + /***************************************** + NEARBY TAB + *****************************************/ + Rectangle { + id: nearbyTab; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12 + (iAmAdmin ? -adminTab.anchors.topMargin : 0); + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "nearbyTab"; + // Rectangle that houses "ADMIN" string Rectangle { id: adminTab; // Size - width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2; + width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 6; height: 40; // Anchors - anchors.bottom: myInfo.bottom; - anchors.bottomMargin: -10; - anchors.right: myInfo.right; + anchors.top: parent.top; + anchors.topMargin: -30; + anchors.right: parent.right; // Properties visible: iAmAdmin; // Style color: hifi.colors.tableRowLightEven; - radius: hifi.dimensions.borderRadius; border.color: hifi.colors.lightGrayText; border.width: 2; // "ADMIN" text @@ -194,27 +488,23 @@ Rectangle { verticalAlignment: Text.AlignTop; } } - // This TableView refers to the table (below the current user's NameCard) + // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) HifiControls.Table { - id: table; - // Size - height: palContainer.height - myInfo.height - 4; - width: palContainer.width - 4; + id: nearbyTable; // Anchors - anchors.left: parent.left; - anchors.top: myInfo.bottom; + anchors.fill: parent; // Properties centerHeaderText: true; sortIndicatorVisible: true; headerVisible: true; - sortIndicatorColumn: settings.sortIndicatorColumn; - sortIndicatorOrder: settings.sortIndicatorOrder; + sortIndicatorColumn: settings.nearbySortIndicatorColumn; + sortIndicatorOrder: settings.nearbySortIndicatorOrder; onSortIndicatorColumnChanged: { - settings.sortIndicatorColumn = sortIndicatorColumn; + settings.nearbySortIndicatorColumn = sortIndicatorColumn; sortModel(); } onSortIndicatorOrderChanged: { - settings.sortIndicatorOrder = sortIndicatorOrder; + settings.nearbySortIndicatorOrder = sortIndicatorOrder; sortModel(); } @@ -229,8 +519,8 @@ Rectangle { TableViewColumn { id: displayNameHeader; role: "displayName"; - title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES"); - width: nameCardWidth; + title: nearbyTable.rowCount + (nearbyTable.rowCount === 1 ? " NAME" : " NAMES"); + width: nearbyNameCardWidth; movable: false; resizable: false; } @@ -258,16 +548,14 @@ Rectangle { resizable: false; } model: ListModel { - id: userModel; + id: nearbyUserModel; } - // This Rectangle refers to each Row in the table. + // This Rectangle refers to each Row in the nearbyTable. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: styleData.selected ? rowHeight : rowHeight - 15; - color: styleData.selected - ? hifi.colors.orangeHighlight - : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + height: styleData.selected ? rowHeight + 15 : rowHeight; + color: rowColor(styleData.selected, styleData.alternate); } // This Item refers to the contents of each Cell @@ -282,8 +570,11 @@ Rectangle { NameCard { id: nameCard; // Properties + profileUrl: (model && model.profileUrl) || ""; + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); displayName: styleData.value; userName: model ? model.userName : ""; + connectionStatus: model ? model.connection : ""; audioLevel: model ? model.audioLevel : 0.0; avgAudioLevel: model ? model.avgAudioLevel : 0.0; visible: !isCheckBox && !isButton && !isAvgAudio; @@ -291,7 +582,7 @@ Rectangle { selected: styleData.selected; isAdmin: model && model.admin; // Size - width: nameCardWidth; + width: nearbyNameCardWidth; height: parent.height; // Anchors anchors.left: parent.left; @@ -316,8 +607,8 @@ Rectangle { // cannot change mute status when ignoring if (!model["ignore"]) { var newValue = !model["personalMute"]; - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming Users["personalMute"](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId); } @@ -340,15 +631,15 @@ Rectangle { boxSize: 24; onClicked: { var newValue = !model[styleData.role]; - userModel.setProperty(model.userIndex, styleData.role, newValue); - userModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, styleData.role, newValue); + nearbyUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming Users[styleData.role](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); if (styleData.role === "ignore") { - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming if (newValue) { - ignored[model.sessionId] = userModelData[model.userIndex]; + ignored[model.sessionId] = nearbyUserModelData[model.userIndex]; } else { delete ignored[model.sessionId]; } @@ -373,8 +664,8 @@ Rectangle { Users[styleData.role](model.sessionId); UserActivityLogger["palAction"](styleData.role, model.sessionId); if (styleData.role === "kick") { - userModelData.splice(model.userIndex, 1); - userModel.remove(model.userIndex); // after changing userModelData, b/c ListModel can frob the data + nearbyUserModelData.splice(model.userIndex, 1); + nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data } } // muted/error glyphs @@ -397,10 +688,10 @@ Rectangle { Rectangle { // Size width: 2; - height: table.height; + height: nearbyTable.height; // Anchors anchors.left: adminTab.left; - anchors.top: table.top; + anchors.top: nearbyTable.top; // Properties visible: iAmAdmin; color: hifi.colors.lightGrayText; @@ -412,34 +703,36 @@ Rectangle { } // This Rectangle refers to the [?] popup button next to "NAMES" Rectangle { + id: helpText; color: hifi.colors.tableBackgroundLight; width: 20; height: hifi.dimensions.tableHeaderHeight - 2; - anchors.left: table.left; - anchors.top: table.top; + anchors.left: nearbyTable.left; + anchors.top: nearbyTable.top; anchors.topMargin: 1; - anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; + anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; RalewayRegular { - id: helpText; text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: hifi.colors.darkGray; + color: helpTextMouseArea.containsMouse ? hifi.colors.baseGrayHighlight : hifi.colors.darkGray; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { + id: helpTextMouseArea; anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Display Names", "Bold names in the list are avatar display names.
" + - "If a display name isn't set, a unique session display name is assigned." + - "

Administrators of this domain can also see the username or machine ID associated with each avatar present."); - onEntered: helpText.color = hifi.colors.baseGrayHighlight; - onExited: helpText.color = hifi.colors.darkGray; + "Purple borders around profile pictures are connections.
" + + "Green borders around profile pictures are friends.
" + + "(TEMPORARY LANGUAGE) In some situations, you can also see others' usernames.
" + + "If you can see someone's username, you can GoTo them by selecting them in the PAL, then clicking their name.
" + + "
If someone's display name isn't set, a unique session display name is assigned to them.
" + + "
Administrators of this domain can also see the username or machine ID associated with each avatar present."); } } // This Rectangle refers to the [?] popup button next to "ADMIN" @@ -449,7 +742,7 @@ Rectangle { width: 20; height: 28; anchors.right: adminTab.right; - anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth; + anchors.rightMargin: 12 + hifi.dimensions.scrollbarBackgroundWidth; anchors.top: adminTab.top; anchors.topMargin: 2; RalewayRegular { @@ -457,37 +750,204 @@ Rectangle { text: "[?]"; size: hifi.fontSizes.tableHeading + 2; font.capitalization: Font.AllUppercase; - color: hifi.colors.redHighlight; + color: adminHelpTextMouseArea.containsMouse ? "#94132e" : hifi.colors.redHighlight; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; anchors.fill: parent; } MouseArea { + id: adminHelpTextMouseArea; anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Admin Actions", "Silence mutes a user's microphone. Silenced users can unmute themselves by clicking "UNMUTE" on their toolbar.

" + "Ban removes a user from this domain and prevents them from returning. Admins can un-ban users from the Sandbox Domain Settings page."); - onEntered: adminHelpText.color = "#94132e"; - onExited: adminHelpText.color = hifi.colors.redHighlight; } } + } // "Nearby" Tab + + + /***************************************** + CONNECTIONS TAB + *****************************************/ + Rectangle { + id: connectionsTab; + color: "#E3E3E3"; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12; + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "connectionsTab"; + + AnimatedImage { + id: connectionsLoading; + source: "../../icons/profilePicLoading.gif" + width: 120; + height: width; + anchors.centerIn: parent; + visible: true; + } + + // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) + HifiControls.Table { + id: connectionsTable; + visible: !connectionsLoading.visible; + // Anchors + anchors.fill: parent; + // Properties + centerHeaderText: true; + sortIndicatorVisible: true; + headerVisible: true; + sortIndicatorColumn: settings.connectionsSortIndicatorColumn; + sortIndicatorOrder: settings.connectionsSortIndicatorOrder; + onSortIndicatorColumnChanged: { + settings.connectionsSortIndicatorColumn = sortIndicatorColumn; + sortConnectionsModel(); + } + onSortIndicatorOrderChanged: { + settings.connectionsSortIndicatorOrder = sortIndicatorOrder; + sortConnectionsModel(); + } + + TableViewColumn { + id: connectionsUserNameHeader; + role: "userName"; + title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + width: connectionsNameCardWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "placeName"; + title: "LOCATION"; + width: locationColumnWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "friends"; + title: "FRIEND"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + + model: ListModel { + id: connectionsUserModel; + } + + // This Rectangle refers to each Row in the connectionsTable. + rowDelegate: Rectangle { + // Size + height: rowHeight; + color: rowColor(styleData.selected, styleData.alternate); + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: connectionsItemCell; + + // This NameCard refers to the cell that contains a connection's UserName + NameCard { + id: connectionsNameCard; + // Properties + visible: styleData.role === "userName"; + profileUrl: (model && model.profileUrl) || ""; + imageMaskColor: rowColor(styleData.selected, styleData.row % 2); + displayName: model ? model.userName : ""; + userName: ""; + connectionStatus : model ? model.connection : ""; + selected: styleData.selected; + // Size + width: connectionsNameCardWidth; + height: parent.height; + // Anchors + anchors.left: parent.left; + } + + // LOCATION data + FiraSansSemiBold { + id: connectionsLocationData + // Properties + visible: styleData.role === "placeName"; + text: (model && model.placeName) || ""; + elide: Text.ElideRight; + // Size + width: parent.width; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + // Style + color: connectionsLocationDataMouseArea.containsMouse ? hifi.colors.blueHighlight : hifi.colors.darkGray; + MouseArea { + id: connectionsLocationDataMouseArea; + anchors.fill: parent + hoverEnabled: enabled + enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" + onClicked: pal.sendToScript({method: 'goToUser', params: model.userName}); + } + } + + // "Friends" checkbox + HifiControls.CheckBox { + id: friendsCheckBox; + visible: styleData.role === "friends"; + anchors.centerIn: parent; + checked: model ? (model["connection"] === "friend" ? true : false) : false; + boxSize: 24; + onClicked: { + var newValue = !model[styleData.role]; + connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue); + connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + // Insert line here about actually taking the friend/unfriend action + // Also insert line here about logging the activity, similar to the commented line below + //UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + + // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript + // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by + // "checked:" statement above. + checked = Qt.binding(function() { return (model["connection"] === "friend" ? true : false)}); + } + } + } + } + } // "Connections" Tab + } // palTabContainer HifiControls.Keyboard { id: keyboard; - raised: myCard.currentlyEditingDisplayName && HMD.active; + raised: currentlyEditingDisplayName && HMD.mounted; numeric: parent.punctuationMode; anchors { bottom: parent.bottom; left: parent.left; right: parent.right; } - } - } + } // Keyboard - // Timer used when selecting table rows that aren't yet present in the model + /* + THIS WILL BE THE BROWSER THAT OPENS THE USER'S INFO PAGE! + I've no idea how to do this yet.. + + HifiTablet.TabletAddressDialog { + id: userInfoViewer; + visible: false; + } + */ + + + } // PAL container + + // Timer used when selecting nearbyTable rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) Timer { property bool selected; // Selected or deselected? @@ -495,17 +955,20 @@ Rectangle { id: selectionTimer; onTriggered: { if (selected) { - table.selection.clear(); // for now, no multi-select - table.selection.select(userIndex); - table.positionViewAtRow(userIndex, ListView.Beginning); + nearbyTable.selection.clear(); // for now, no multi-select + nearbyTable.selection.select(userIndex); + nearbyTable.positionViewAtRow(userIndex, ListView.Beginning); } else { - table.selection.deselect(userIndex); + nearbyTable.selection.deselect(userIndex); } } } - function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml - var data = optionalData || userModelData, length = data.length; + function rowColor(selected, alternate) { + return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + } + function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml + var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { if (data[i].sessionId === sessionId) { return i; @@ -515,10 +978,10 @@ Rectangle { } function fromScript(message) { switch (message.method) { - case 'users': + case 'nearbyUsers': var data = message.params; var index = -1; - index = findSessionIndex('', data); + index = findNearbySessionIndex('', data); if (index !== -1) { iAmAdmin = Users.canKick; myData = data[index]; @@ -526,22 +989,28 @@ Rectangle { } else { console.log("This user's data was not found in the user list. PAL will not function properly."); } - userModelData = data; + nearbyUserModelData = data; for (var ignoredID in ignored) { - index = findSessionIndex(ignoredID); + index = findNearbySessionIndex(ignoredID); if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up. - userModelData.push(ignored[ignoredID]); + nearbyUserModelData.push(ignored[ignoredID]); } else { // Already appears in PAL; update properties of existing element in model data - userModelData[index] = ignored[ignoredID]; + nearbyUserModelData[index] = ignored[ignoredID]; } } sortModel(); break; + case 'connections': + var data = message.params; + connectionsUserModelData = data; + sortConnectionsModel(); + connectionsLoading.visible = false; + break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; var alreadyRefreshed = message.params[2]; - var userIndex = findSessionIndex(sessionIds[0]); + var userIndex = findNearbySessionIndex(sessionIds[0]); if (sessionIds.length > 1) { letterbox("", "", 'Only one user can be selected at a time.'); } else if (userIndex < 0) { @@ -554,11 +1023,11 @@ Rectangle { } else { // If we've already refreshed the PAL and found the avatar in the model if (alreadyRefreshed === true) { - // Wait a little bit before trying to actually select the avatar in the table + // Wait a little bit before trying to actually select the avatar in the nearbyTable selectionTimer.interval = 250; } else { // If we've found the avatar in the model and didn't need to refresh, - // select the avatar in the table immediately + // select the avatar in the nearbyTable immediately selectionTimer.interval = 0; } selectionTimer.selected = selected; @@ -569,25 +1038,25 @@ Rectangle { // Received an "updateUsername()" request from the JS case 'updateUsername': // The User ID (UUID) is the first parameter in the message. - var userId = message.params[0]; - // The text that goes in the userName field is the second parameter in the message. - var userName = message.params[1]; - var admin = message.params[2]; - // If the userId is empty, we're updating "myData". - if (!userId) { - myData.userName = userName; - myCard.userName = userName; // Defensive programming - } else { - // Get the index in userModel and userModelData associated with the passed UUID - var userIndex = findSessionIndex(userId); - if (userIndex != -1) { - // Set the userName appropriately - userModel.setProperty(userIndex, "userName", userName); - userModelData[userIndex].userName = userName; // Defensive programming - // Set the admin status appropriately - userModel.setProperty(userIndex, "admin", admin); - userModelData[userIndex].admin = admin; // Defensive programming + var userId = message.params.sessionId; + // If the userId is empty, we're probably updating "myData". + if (userId) { + // Get the index in nearbyUserModel and nearbyUserModelData associated with the passed UUID + var userIndex = findNearbySessionIndex(userId); + if (userIndex !== -1) { + ['userName', 'admin', 'connection', 'profileUrl', 'placeName'].forEach(function (name) { + var value = message.params[name]; + if (value === undefined) { + return; + } + nearbyUserModel.setProperty(userIndex, name, value); + nearbyUserModelData[userIndex][name] = value; // for refill after sort + }); } + // In this "else if" case, the only param of the message is the profile pic URL. + } else if (message.params.profileUrl) { + myData.profileUrl = message.params.profileUrl; + myCard.profileUrl = message.params.profileUrl; } break; case 'updateAudioLevel': @@ -601,12 +1070,12 @@ Rectangle { myData.avgAudioLevel = avgAudioLevel; myCard.avgAudioLevel = avgAudioLevel; } else { - var userIndex = findSessionIndex(userId); + var userIndex = findNearbySessionIndex(userId); if (userIndex != -1) { - userModel.setProperty(userIndex, "audioLevel", audioLevel); - userModelData[userIndex].audioLevel = audioLevel; // Defensive programming - userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); - userModelData[userIndex].avgAudioLevel = avgAudioLevel; + nearbyUserModel.setProperty(userIndex, "audioLevel", audioLevel); + nearbyUserModelData[userIndex].audioLevel = audioLevel; // Defensive programming + nearbyUserModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + nearbyUserModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } @@ -618,18 +1087,21 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; + case 'updateAvailability': + usernameAvailability = message.params; + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } } function sortModel() { - var column = table.getColumn(table.sortIndicatorColumn); + var column = nearbyTable.getColumn(nearbyTable.sortIndicatorColumn); var sortProperty = column ? column.role : "displayName"; - var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var before = (nearbyTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var after = -1 * before; // get selection(s) before sorting - var selectedIDs = getSelectedSessionIDs(); - userModelData.sort(function (a, b) { + var selectedIDs = getSelectedNearbySessionIDs(); + nearbyUserModelData.sort(function (a, b) { var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); switch (true) { case (aValue < bValue): return before; @@ -637,39 +1109,77 @@ Rectangle { default: return 0; } }); - table.selection.clear(); + nearbyTable.selection.clear(); - userModel.clear(); + nearbyUserModel.clear(); var userIndex = 0; var newSelectedIndexes = []; - userModelData.forEach(function (datum) { + nearbyUserModelData.forEach(function (datum) { function init(property) { if (datum[property] === undefined) { - datum[property] = false; + // These properties must have values of type 'string'. + if (property === 'userName' || property === 'profileUrl' || property === 'placeName' || property === 'connection') { + datum[property] = ""; + // All other properties must have values of type 'bool'. + } else { + datum[property] = false; + } } } - ['personalMute', 'ignore', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'mute', 'kick', 'admin', 'userName', 'profileUrl', 'placeName', 'connection'].forEach(init); datum.userIndex = userIndex++; - userModel.append(datum); + nearbyUserModel.append(datum); if (selectedIDs.indexOf(datum.sessionId) != -1) { newSelectedIndexes.push(datum.userIndex); } }); if (newSelectedIndexes.length > 0) { - table.selection.select(newSelectedIndexes); - table.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + nearbyTable.selection.select(newSelectedIndexes); + nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + } + } + function sortConnectionsModel() { + var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + var sortProperty = column ? column.role : "userName"; + var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var after = -1 * before; + // get selection(s) before sorting + var selectedIDs = getSelectedConnectionsUserNames(); + connectionsUserModelData.sort(function (a, b) { + var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); + switch (true) { + case (aValue < bValue): return before; + case (aValue > bValue): return after; + default: return 0; + } + }); + connectionsTable.selection.clear(); + + connectionsUserModel.clear(); + var userIndex = 0; + var newSelectedIndexes = []; + connectionsUserModelData.forEach(function (datum) { + datum.userIndex = userIndex++; + connectionsUserModel.append(datum); + if (selectedIDs.indexOf(datum.sessionId) != -1) { + newSelectedIndexes.push(datum.userIndex); + } + }); + if (newSelectedIndexes.length > 0) { + connectionsTable.selection.select(newSelectedIndexes); + connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); } } signal sendToScript(var message); function noticeSelection() { var userIds = []; - table.selection.forEach(function (userIndex) { - userIds.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + userIds.push(nearbyUserModelData[userIndex].sessionId); }); pal.sendToScript({method: 'selected', params: userIds}); } Connections { - target: table.selection; + target: nearbyTable.selection; onSelectionChanged: pal.noticeSelection(); } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 031e80283e..e00175d4e7 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -171,7 +171,7 @@ Item { readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9 readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24 readonly property real tableHeading: dimensions.largeScreen ? 12 : 10 - readonly property real tableHeadingIcon: dimensions.largeScreen ? 40 : 33 + readonly property real tableHeadingIcon: dimensions.largeScreen ? 60 : 33 readonly property real tableText: dimensions.largeScreen ? 15 : 12 readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9 readonly property real iconButton: dimensions.largeScreen ? 13 : 9 diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 276e23d2d5..7ecafdcbcb 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -27,7 +27,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition) Q_PROPERTY(glm::quat orientation READ getOrientation) - Q_PROPERTY(bool mounted READ isMounted) + Q_PROPERTY(bool mounted READ isMounted NOTIFY mountedChanged) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) @@ -80,6 +80,7 @@ public: signals: bool shouldShowHandControllersChanged(); + void mountedChanged(); public: HMDScriptingInterface(); diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 5d8813e988..240c1de4c3 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -21,10 +21,10 @@ var DEFAULT_SCRIPTS = [ "system/snapshot.js", "system/help.js", "system/pal.js", // "system/mod.js", // older UX, if you prefer + "system/makeUserConnection.js", "system/goto.js", "system/marketplaces/marketplaces.js", "system/edit.js", - "system/tablet-users.js", "system/selectAudioDevice.js", "system/notifications.js", "system/controllers/controllerDisplayManager.js", diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js new file mode 100644 index 0000000000..a93687eda7 --- /dev/null +++ b/scripts/system/makeUserConnection.js @@ -0,0 +1,372 @@ +"use strict"; +// +// friends.js +// scripts/developer/tests/performance/ +// +// Created by David Kelly on 3/7/2017. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +const version = 0.1; +const label = "Friends"; +const MAX_AVATAR_DISTANCE = 1.0; +const GRIP_MIN = 0.05; +const MESSAGE_CHANNEL = "io.highfidelity.friends"; +const STATES = { + inactive : 0, + waiting: 1, + friending: 2, +}; +const STATE_STRINGS = ["inactive", "waiting", "friending"]; +const WAITING_INTERVAL = 100; // ms +const FRIENDING_INTERVAL = 100; // ms +const FRIENDING_TIME = 3000; // ms +const OVERLAY_COLORS = [{red: 0x00, green: 0xFF, blue: 0x00}, {red: 0x00, green: 0x00, blue: 0xFF}]; +const FRIENDING_HAPTIC_STRENGTH = 0.5; +const FRIENDING_SUCCESS_HAPTIC_STRENGTH = 1.0; +const HAPTIC_DURATION = 20; + +var currentHand; +var isWaiting = false; +var nearbyAvatars = []; +var state = STATES.inactive; +var waitingInterval; +var friendingInterval; +var entity; +var makingFriends = false; // really just for visualizations for now +var animHandlerId; +var entityDimensionMultiplier = 1.0; + +function debug() { + var stateString = "<" + STATE_STRINGS[state] + ">"; + var versionString = "v" + version; + print.apply(null, [].concat.apply([label, versionString, stateString], [].map.call(arguments, JSON.stringify))); +} + +function handToString(hand) { + if (hand === Controller.Standard.RightHand) { + return "RightHand"; + } else if (hand === Controller.Standard.LeftHand) { + return "LeftHand"; + } + return ""; +} + +function handToHaptic(hand) { + if (hand === Controller.Standard.RightHand) { + return 1; + } else if (hand === Controller.Standard.LeftHand) { + return 0; + } + return -1; +} + + +function getHandPosition(avatar, hand) { + if (!hand) { + debug("calling getHandPosition with no hand!"); + return; + } + var jointName = handToString(hand) + "Middle1"; + return avatar.getJointPosition(avatar.getJointIndex(jointName)); +} + +function shakeHandsAnimation(animationProperties) { + // all we are doing here is moving the right hand to a spot + // that is in front of and a bit above the hips. Basing how + // far in front as scaling with the avatar's height (say hips + // to head distance) + var headIndex = MyAvatar.getJointIndex("Head"); + var offset = 0.5; // default distance of hand in front of you + var result = {}; + if (headIndex) { + offset = 0.8 * MyAvatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y; + } + var handPos = Vec3.multiply(offset, {x: -0.25, y: 0.8, z: 1.3}); + result.rightHandPosition = handPos; + result.rightHandRotation = Quat.fromPitchYawRollDegrees(90, 0, 90); + return result; +} + +// this is called frequently, but usually does nothing +function updateVisualization() { + if (state == STATES.inactive) { + if (entity) { + entity = Entities.deleteEntity(entity); + } + return; + } + + var color = state == STATES.waiting ? OVERLAY_COLORS[0] : OVERLAY_COLORS[1]; + var position = getHandPosition(MyAvatar, currentHand); + + // temp code, though all of this stuff really is temp... + if (makingFriends) { + color = { red: 0xFF, green: 0x00, blue: 0x00 }; + } + + // TODO: make the size scale with avatar, up to + // the actual size of MAX_AVATAR_DISTANCE + var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand))); + var d = entityDimensionMultiplier * Vec3.distance(wrist, position); + var dimension = {x: d, y: d, z: d}; + if (!entity) { + var props = { + type: "Sphere", + color: color, + position: position, + dimensions: dimension + } + entity = Entities.addEntity(props); + } else { + Entities.editEntity(entity, {dimensions: dimension, position: position, color: color}); + } + +} + +// this should find the nearest avatars, returning an array of avatar, hand pairs. Currently +// looking at distance between hands. +function findNearbyAvatars() { + var nearbyAvatars = []; + var handPos = getHandPosition(MyAvatar, currentHand); + AvatarList.getAvatarIdentifiers().forEach(function (identifier) { + if (!identifier) { return; } + var avatar = AvatarList.getAvatar(identifier); + var distanceR = Vec3.distance(getHandPosition(avatar, Controller.Standard.RightHand), handPos); + var distanceL = Vec3.distance(getHandPosition(avatar, Controller.Standard.LeftHand), handPos); + var distance = Math.min(distanceL, distanceR); + if (distance < MAX_AVATAR_DISTANCE) { + if (distance == distanceR) { + nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.RightHand}); + } else { + nearbyAvatars.push({avatar: identifier, hand: Controller.Standard.LeftHand}); + } + } + }); + return nearbyAvatars; +} + +function startHandshake(fromKeyboard) { + if (fromKeyboard) { + debug("adding animation"); + animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []); + } + debug("starting handshake for", currentHand); + state = STATES.waiting; + waitingInterval = Script.setInterval( + function () { + debug("currentHand", handToString(currentHand)); + messageSend({ + key: "waiting", + hand: handToString(currentHand) + }); + }, WAITING_INTERVAL); +} + +function endHandshake() { + debug("ending handshake for", currentHand); + currentHand = undefined; + state = STATES.inactive; + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } + if (friendingInterval) { + friendingInterval = Script.clearInterval(friendingInterval); + } + if (animHandlerId) { + debug("removing animation"); + MyAvatar.removeAnimationStateHandler(animHandlerId); + } +} + +function updateTriggers(value, fromKeyboard, hand) { + if (currentHand && hand !== currentHand) { + debug("currentHand", currentHand, "ignoring messages from", hand); + return; + } + if (!currentHand) { + currentHand = hand; + } + // ok now, we are either initiating or quitting... + var isGripping = value > GRIP_MIN; + if (isGripping) { + if (state != STATES.inactive) { + return; + } else { + startHandshake(fromKeyboard); + } + } else { + if (state != STATES.inactive) { + endHandshake(); + } else { + return; + } + } +} + +function messageSend(message) { + Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message)); +} + +function isNearby(id, hand) { + for(var i = 0; i < nearbyAvatars.length; i++) { + if (nearbyAvatars[i].avatar == id && handToString(nearbyAvatars[i].hand) == hand) { + return true; + } + } + return false; +} + +// this should be where we make the appropriate friend call. For now just make the +// visualization change. +function makeFriends(id) { + // temp code to just flash the visualization really (for now!) + makingFriends = true; + Controller.triggerHapticPulse(FRIENDING_SUCCESS_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + Script.setTimeout(function () { makingFriends = false; entityDimensionMultiplier = 1.0; }, 1000); +} +// we change states, start the friendingInterval where we check +// to be sure the hand is still close enough. If not, we terminate +// the interval, go back to the waiting state. If we make it +// the entire FRIENDING_TIME, we make friends. +function startFriending(id, hand) { + var count = 0; + debug("friending", id, "hand", hand); + state = STATES.friending; + Controller.triggerHapticPulse(FRIENDING_HAPTIC_STRENGTH, HAPTIC_DURATION, handToHaptic(currentHand)); + if (waitingInterval) { + waitingInterval = Script.clearInterval(waitingInterval); + } + friendingInterval = Script.setInterval(function () { + nearbyAvatars = findNearbyAvatars(); + entityDimensionMultiplier = 1.0 + 2.0 * ++count * FRIENDING_INTERVAL / FRIENDING_TIME; + // insure senderID is still nearby + if (state != STATES.friending) { + debug("stopping friending interval, state changed"); + friendingInterval = Script.clearInterval(friendingInterval); + } + if (!isNearby(id, hand)) { + // gotta go back to waiting + debug(id, "moved, back to waiting"); + friendingInterval = Script.clearInterval(friendingInterval); + startHandshake(); + } else if (count > FRIENDING_TIME/FRIENDING_INTERVAL) { + debug("made friends with " + id); + makeFriends(id); + friendingInterval = Script.clearInterval(friendingInterval); + } + }, FRIENDING_INTERVAL); +} +/* +A simple sequence diagram: + + Avatar A Avatar B + | | + | <---------(waiting) --- startHandshake + startHandshake -- (waiting) -----> | + | | + | <-------(friending) -- startFriending + startFriending -- (friending) ---> | + | | + | friends + friends | + | ` | +*/ +function messageHandler(channel, messageString, senderID) { + if (channel !== MESSAGE_CHANNEL) { + return; + } + if (state == STATES.inactive) { + return; + } + if (MyAvatar.sessionUUID === senderID) { // ignore my own + return; + } + var message = {}; + try { + message = JSON.parse(messageString); + } catch (e) { + debug(e); + } + switch (message.key) { + case "waiting": + case "friending": + if (state == STATES.waiting) { + if (message.key == "friending" && message.id != MyAvatar.sessionUUID) { + // for now, just ignore these. Hmm + debug("ignoring friending message", message, "from", senderID); + break; + } + nearbyAvatars = findNearbyAvatars(); + if (isNearby(senderID, message.hand)) { + // if we are responding to a friending message (they didn't send a + // waiting before noticing us and friending), don't bother with sending + // a friending message? + messageSend({ + key: "friending", + id: senderID, + hand: handToString(currentHand) + }); + startFriending(senderID, message.hand); + } else { + // for now, ignore this. Hmm. + if (message.key == "friending") { + debug(senderID, "is friending us, but not close enough??"); + } + } + } + break; + default: + debug("unknown message", message); + } +} + +Messages.subscribe(MESSAGE_CHANNEL); +Messages.messageReceived.connect(messageHandler); + + +function makeGripHandler(hand) { + // determine if we are gripping or un-gripping + return function (value) { + updateTriggers(value, false, hand); + }; +} + +function keyPressEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat) { + updateTriggers(1.0, true, Controller.Standard.RightHand); + } +} +function keyReleaseEvent(event) { + if ((event.text === "x") && !event.isAutoRepeat) { + updateTriggers(0.0, true, Controller.Standard.RightHand); + } +} +// map controller actions +var friendsMapping = Controller.newMapping(Script.resolvePath('') + '-grip'); +friendsMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand)); +friendsMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand)); + +// setup keyboard initiation + +Controller.keyPressEvent.connect(keyPressEvent); +Controller.keyReleaseEvent.connect(keyReleaseEvent); + +// it is easy to forget this and waste a lot of time for nothing +friendsMapping.enable(); + +// connect updateVisualization to update frequently +Script.update.connect(updateVisualization); + +Script.scriptEnding.connect(function () { + debug("removing controller mappings"); + friendsMapping.disable(); + debug("removing key mappings"); + Controller.keyPressEvent.disconnect(keyPressEvent); + Controller.keyReleaseEvent.disconnect(keyReleaseEvent); + debug("disconnecting updateVisualization"); + Script.update.disconnect(updateVisualization); +}); + diff --git a/scripts/system/pal.js b/scripts/system/pal.js index fdb6cbcaf5..ff0fb80c4d 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,6 @@ "use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation, GlobalServices*/ /* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // pal.js @@ -14,6 +14,8 @@ (function() { // BEGIN LOCAL_SCOPE +var populateNearbyUserList, color, textures, removeOverlays, controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged, receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL, createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged; // forward references; + // hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed // something, will revisit as this is sorta horrible. var UNSELECTED_TEXTURES = { @@ -97,9 +99,8 @@ ExtendedOverlay.prototype.hover = function (hovering) { if (this.key === lastHoveringId) { if (hovering) { return; - } else { - lastHoveringId = 0; } + lastHoveringId = 0; } this.editOverlay({color: color(this.selected, hovering, this.audioLevel)}); if (this.model) { @@ -214,9 +215,8 @@ function convertDbToLinear(decibels) { // but, your perception is that something 2x as loud is +10db // so we go from -60 to +20 or 1/64x to 4x. For now, we can // maybe scale the signal this way?? - return Math.pow(2, decibels/10.0); + return Math.pow(2, decibels / 10.0); } - function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var data; switch (message.method) { @@ -247,7 +247,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See }); } break; - case 'refresh': + case 'refreshNearby': data = {}; ExtendedOverlay.some(function (overlay) { // capture the audio data data[overlay.key] = overlay; @@ -257,8 +257,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See if (message.params.filter !== undefined) { Settings.setValue('pal/filtered', !!message.params.filter); } - populateUserList(message.params.selected, data); - UserActivityLogger.palAction("refresh", ""); + populateNearbyUserList(message.params.selected, data); + UserActivityLogger.palAction("refresh_nearby", ""); + break; + case 'refreshConnections': + getConnectionData(); + UserActivityLogger.palAction("refresh_connections", ""); break; case 'displayNameUpdate': if (MyAvatar.displayName !== message.params) { @@ -266,6 +270,18 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See UserActivityLogger.palAction("display_name_change", ""); } break; + case 'goToUser': + location.goToUser(message.params); + UserActivityLogger.palAction("go_to_user", ""); + break; + case 'setAvailability': + GlobalServices.findableBy = message.params; + UserActivityLogger.palAction("set_availability", ""); + print('Setting availability:', JSON.stringify(message)); + break; + case 'getAvailability': + findableByChanged(GlobalServices.findableBy); + break; default: print('Unrecognized message from Pal.qml:', JSON.stringify(message)); } @@ -274,6 +290,147 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See function sendToQml(message) { tablet.sendToQml(message); } +function updateUser(data) { + print('PAL update:', JSON.stringify(data)); + sendToQml({ method: 'updateUsername', params: data }); +} +// +// User management services +// +// These are prototype versions that will be changed when the back end changes. +var METAVERSE_BASE = 'https://metaverse.highfidelity.com'; + +function request(url, callback) { // cb(error, responseOfCorrectContentType) of url. General for 'get' text/html/json, but without redirects. + var httpRequest = new XMLHttpRequest(); + // QT bug: apparently doesn't handle onload. Workaround using readyState. + httpRequest.onreadystatechange = function () { + var READY_STATE_DONE = 4; + var HTTP_OK = 200; + if (httpRequest.readyState >= READY_STATE_DONE) { + var error = (httpRequest.status !== HTTP_OK) && httpRequest.status.toString() + ':' + httpRequest.statusText, + response = !error && httpRequest.responseText, + contentType = !error && httpRequest.getResponseHeader('content-type'); + if (!error && contentType.indexOf('application/json') === 0) { + try { + response = JSON.parse(response); + } catch (e) { + error = e; + } + } + callback(error, response); + } + }; + httpRequest.open("GET", url, true); + httpRequest.send(); +} +function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise. + request(url, function (error, response) { + if (error || (response.status !== 'success')) { + print("Error: unable to get", url, error || response.status); + return; + } + callback(response.data); + }); +} +function getProfilePicture(username, callback) { // callback(url) if successfull. (Logs otherwise) + // FIXME Prototype scrapes profile picture. We should include in user status, and also make available somewhere for myself + request(METAVERSE_BASE + '/users/' + username, function (error, html) { + var matched = !error && html.match(/img class="users-img" src="([^"]*)"/); + if (!matched) { + print('Error: Unable to get profile picture for', username, error); + return; + } + callback(matched[1]); + }); +} +function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise) + // The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us, + // and pretending that they are all connections. + function getData(cb) { + requestJSON(METAVERSE_BASE + '/api/v1/users?status=online', function (connectionsData) { + + // The above does not include friend status. Fetch that separately. + requestJSON(METAVERSE_BASE + '/api/v1/user/friends', function (friendsData) { + var users = connectionsData.users || [], friends = friendsData.friends || []; + users.forEach(function (user) { + user.connection = (friends.indexOf(user.username) < 0) ? 'connection' : 'friend'; + }); + + // The back end doesn't include the profile picture data, but we can add that here. + // For our current purposes, there's no need to be fancy and try to reduce latency by doing some number of requests in parallel, + // so these requests are all sequential. + function addPicture(index) { + if (index >= users.length) { + return cb(users); + } + var user = users[index]; + getProfilePicture(user.username, function (url) { + user.profileUrl = url; + addPicture(index + 1); + }); + } + addPicture(0); + }); + }); + } + + if (domain) { + // The back end doesn't keep sessionUUID in the location data yet. Fake it by finding the avatar closest to the path. + var positions = {}; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + positions[id || ''] = AvatarList.getAvatar(id).position; // Don't use null id as a key. Properties must be a string, and we don't want 'null'. + }); + getData(function (users) { + // The endpoint in getData doesn't take a domain filter. So filter out the unwanted stuff now. + domain = domain.slice(1, -1); // without curly braces + users = users.filter(function (user) { return (user.location.domain || (user.location.root && user.location.root.domain) || {}).id === domain; }); + + // Now fill in the sessionUUID as if it were in the data all along. + users.forEach(function (user) { + var coordinates = user.location.path.match(/\/([^,]+)\,([^,]+),([^\/]+)\//); + if (coordinates) { + var position = {x: Number(coordinates[1]), y: Number(coordinates[2]), z: Number(coordinates[3])}; + var none = 'not found', closestId = none, bestDistance = Infinity, distance, id; + for (id in positions) { + distance = Vec3.distance(position, positions[id]); + if (distance < bestDistance) { + closestId = id; + bestDistance = distance; + } + } + if (closestId !== none) { + user.location.sessionUUID = closestId; + } + } + }); + + callback(users); + }); + } else { // We don't need to filter, nor add any sessionUUID data + getData(callback); + } +} + +function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick. + function frob(user) { // get into the right format + return { + sessionId: user.location.sessionUUID || '', + userName: user.username, + connection: user.connection, + profileUrl: user.profileUrl, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + } + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'connections', params: users.map(frob) }); + } + }); +} // // Main operations. @@ -285,15 +442,16 @@ function addAvatarNode(id) { solid: true, alpha: 0.8, color: color(selected, false, 0.0), - ignoreRayIntersection: false}, selected, !conserveResources); + ignoreRayIntersection: false + }, selected, !conserveResources); } // Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter. var avatarsOfInterest = {}; -function populateUserList(selectData, oldAudioData) { - var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}; - var data = [], avatars = AvatarList.getAvatarIdentifiers(); - avatarsOfInterest = {}; - var myPosition = filter && Camera.position, +function populateNearbyUserList(selectData, oldAudioData) { + var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')}, + data = [], + avatars = AvatarList.getAvatarIdentifiers(), + myPosition = filter && Camera.position, frustum = filter && Camera.frustum, verticalHalfAngle = filter && (frustum.fieldOfView / 2), horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio), @@ -301,7 +459,8 @@ function populateUserList(selectData, oldAudioData) { front = filter && Quat.getFront(orientation), verticalAngleNormal = filter && Quat.getRight(orientation), horizontalAngleNormal = filter && Quat.getUp(orientation); - avatars.forEach(function (id) { // sorting the identifiers is just an aid for debugging + avatarsOfInterest = {}; + avatars.forEach(function (id) { var avatar = AvatarList.getAvatar(id); var name = avatar.sessionDisplayName; if (!name) { @@ -323,8 +482,10 @@ function populateUserList(selectData, oldAudioData) { } var oldAudio = oldAudioData && oldAudioData[id]; var avatarPalDatum = { + profileUrl: '', displayName: name, userName: '', + connection: '', sessionId: id || '', audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0, avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 0.0, @@ -337,12 +498,19 @@ function populateUserList(selectData, oldAudioData) { // Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin. Users.requestUsernameFromID(id); avatarsOfInterest[id] = true; + } else { + // Return our username from the Account API + avatarPalDatum.userName = Account.username; + getProfilePicture(avatarPalDatum.userName, function (url) { + sendToQml({ method: 'updateUsername', params: { profileUrl: url } }); + }); } data.push(avatarPalDatum); print('PAL data:', JSON.stringify(avatarPalDatum)); }); + getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain). conserveResources = Object.keys(avatarsOfInterest).length > 20; - sendToQml({ method: 'users', params: data }); + sendToQml({ method: 'nearbyUsers', params: data }); if (selectData) { selectData[2] = true; sendToQml({ method: 'select', params: selectData }); @@ -351,15 +519,15 @@ function populateUserList(selectData, oldAudioData) { // The function that handles the reply from the server function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { - var data = [ - (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. + var data = { + sessionId: (MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially. // If we get username (e.g., if in future we receive it when we're friends), use it. // Otherwise, use valid machineFingerprint (which is not valid when not an admin). - username || (Users.canKick && machineFingerprint) || '', - isAdmin - ]; + userName: username || (Users.canKick && machineFingerprint) || '', + admin: isAdmin + }; // Ship the data off to QML - sendToQml({ method: 'updateUsername', params: data }); + updateUser(data); } var pingPong = true; @@ -381,16 +549,12 @@ function updateOverlays() { var target = avatar.position; var distance = Vec3.distance(target, eye); var offset = 0.2; - - // base offset on 1/2 distance from hips to head if we can - var headIndex = avatar.getJointIndex("Head"); + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can if (headIndex > 0) { offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2; } - // get diff between target and eye (a vector pointing to the eye from avatar position) - var diff = Vec3.subtract(target, eye); - // move a bit in front, towards the camera target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); @@ -418,7 +582,7 @@ function updateOverlays() { overlay.deleteOverlay(); } }); - // We could re-populateUserList if anything added or removed, but not for now. + // We could re-populateNearbyUserList if anything added or removed, but not for now. HighlightedEntity.updateOverlays(); } function removeOverlays() { @@ -543,6 +707,7 @@ function startup() { Messages.subscribe(CHANNEL); Messages.messageReceived.connect(receiveMessage); Users.avatarDisconnected.connect(avatarDisconnected); + GlobalServices.findableByChanged.connect(findableByChanged); } startup(); @@ -579,7 +744,8 @@ function onTabletButtonClicked() { tablet.loadQMLSource("../Pal.qml"); onPalScreen = true; Users.requestsDomainListData = true; - populateUserList(); + populateNearbyUserList(); + findableByChanged(GlobalServices.findableBy); isWired = true; Script.update.connect(updateOverlays); Controller.mousePressEvent.connect(handleMouseEvent); @@ -607,8 +773,7 @@ function onTabletScreenChanged(type, url) { // var CHANNEL = 'com.highfidelity.pal'; function receiveMessage(channel, messageString, senderID) { - if ((channel !== CHANNEL) || - (senderID !== MyAvatar.sessionUUID)) { + if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) { return; } var message = JSON.parse(messageString); @@ -633,7 +798,7 @@ function scaleAudio(val) { if (val <= LOUDNESS_FLOOR) { audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; } else { - audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE; + audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE; } if (audioLevel > 1.0) { audioLevel = 1; @@ -659,14 +824,14 @@ function getAudioLevel(id) { audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); // decay avgAudioLevel - avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + avgAudioLevel = Math.max((1 - AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); data.avgAudioLevel = avgAudioLevel; data.audioLevel = audioLevel; // now scale for the gain. Also, asked to boost the low end, so one simple way is // to take sqrt of the value. Lets try that, see how it feels. - avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel *(sessionGains[id] || 0.75))); + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel * (sessionGains[id] || 0.75))); } return [audioLevel, avgAudioLevel]; } @@ -677,9 +842,8 @@ function createAudioInterval(interval) { return Script.setInterval(function () { var param = {}; AvatarList.getAvatarIdentifiers().forEach(function (id) { - var level = getAudioLevel(id); - // qml didn't like an object with null/empty string for a key, so... - var userId = id || 0; + var level = getAudioLevel(id), + userId = id || 0; // qml didn't like an object with null/empty string for a key, so... param[userId] = level; }); sendToQml({method: 'updateAudioLevel', params: param}); @@ -691,6 +855,20 @@ function avatarDisconnected(nodeID) { sendToQml({method: 'avatarDisconnected', params: [nodeID]}); } +function findableByChanged(usernameAvailability) { + // Update PAL availability dropdown + // Default to "friends" if undeterminable + var availability = 1; + if (usernameAvailability === "all") { + availability = 0; + } else if (usernameAvailability === "friends") { + availability = 1; + } else if (usernameAvailability === "none") { + availability = 2; + } + sendToQml({ method: 'updateAvailability', params: availability }); +} + function clearLocalQMLDataAndClosePAL() { sendToQml({ method: 'clearLocalQMLData' }); } @@ -708,6 +886,7 @@ function shutdown() { Messages.subscribe(CHANNEL); Messages.messageReceived.disconnect(receiveMessage); Users.avatarDisconnected.disconnect(avatarDisconnected); + GlobalServices.findableByChanged.disconnect(findableByChanged); off(); }