Update from zfox23/PALv2 branch

This commit is contained in:
Zach Fox 2017-03-13 10:15:06 -07:00
parent 49efd8ad42
commit beb848373b
11 changed files with 1681 additions and 553 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 <b>Silence</b> and <b>Ban</b> 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 <b>Silence</b> and <b>Ban</b> 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) {

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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();

View file

@ -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",

View file

@ -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);
});

View file

@ -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();
}