diff --git a/interface/resources/icons/profilePicLoading.gif b/interface/resources/icons/profilePicLoading.gif
new file mode 100644
index 0000000000..7e85f7cb6a
Binary files /dev/null and b/interface/resources/icons/profilePicLoading.gif differ
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();
}