mirror of
https://github.com/overte-org/overte.git
synced 2025-08-10 19:03:07 +02:00
Update from zfox23/PALv2 branch
This commit is contained in:
parent
49efd8ad42
commit
beb848373b
11 changed files with 1681 additions and 553 deletions
BIN
interface/resources/icons/profilePicLoading.gif
Normal file
BIN
interface/resources/icons/profilePicLoading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -88,7 +88,7 @@ Original.CheckBox {
|
||||||
label: Label {
|
label: Label {
|
||||||
text: control.text
|
text: control.text
|
||||||
colorScheme: checkBox.colorScheme
|
colorScheme: checkBox.colorScheme
|
||||||
x: checkBox.boxSize / 2
|
x: 2
|
||||||
wrapMode: Text.Wrap
|
wrapMode: Text.Wrap
|
||||||
enabled: checkBox.enabled
|
enabled: checkBox.enabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,11 +48,12 @@ TableView {
|
||||||
HiFiGlyphs {
|
HiFiGlyphs {
|
||||||
id: titleSort
|
id: titleSort
|
||||||
text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn
|
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
|
size: hifi.fontSizes.tableHeadingIcon
|
||||||
anchors {
|
anchors {
|
||||||
left: titleText.right
|
left: titleText.right
|
||||||
leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0)
|
leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0)
|
||||||
right: parent.right
|
right: parent.right
|
||||||
rightMargin: hifi.dimensions.tablePadding
|
rightMargin: hifi.dimensions.tablePadding
|
||||||
verticalCenter: titleText.verticalCenter
|
verticalCenter: titleText.verticalCenter
|
||||||
|
@ -89,7 +90,7 @@ TableView {
|
||||||
Rectangle {
|
Rectangle {
|
||||||
color: "#00000000"
|
color: "#00000000"
|
||||||
anchors { fill: parent; margins: -2 }
|
anchors { fill: parent; margins: -2 }
|
||||||
radius: hifi.dimensions.borderRadius
|
//radius: hifi.dimensions.borderRadius
|
||||||
border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
|
border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
|
||||||
border.width: 2
|
border.width: 2
|
||||||
}
|
}
|
||||||
|
|
|
@ -183,7 +183,7 @@ FocusScope {
|
||||||
anchors.leftMargin: hifi.dimensions.textPadding
|
anchors.leftMargin: hifi.dimensions.textPadding
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
id: popupText
|
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
|
size: hifi.fontSizes.textFieldInput
|
||||||
color: hifi.colors.baseGray
|
color: hifi.colors.baseGray
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,388 +14,453 @@ import QtQuick.Controls 1.4
|
||||||
import QtQuick.Controls.Styles 1.4
|
import QtQuick.Controls.Styles 1.4
|
||||||
import QtGraphicalEffects 1.0
|
import QtGraphicalEffects 1.0
|
||||||
import "../styles-uit"
|
import "../styles-uit"
|
||||||
|
import "toolbars"
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: thisNameCard
|
id: thisNameCard
|
||||||
// Anchors
|
// Size
|
||||||
anchors {
|
width: isMyCard ? pal.myCardWidth - anchors.leftMargin : pal.nearbyNameCardWidth;
|
||||||
verticalCenter: parent.verticalCenter
|
height: isMyCard ? pal.myCardHeight : pal.rowHeight;
|
||||||
leftMargin: 10
|
anchors.left: parent.left
|
||||||
rightMargin: 10
|
anchors.leftMargin: 5
|
||||||
}
|
anchors.top: parent.top;
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
|
property string profileUrl: "";
|
||||||
|
property string defaultBaseUrl: "http://highfidelity.com";
|
||||||
|
property string connectionStatus : ""
|
||||||
property string uuid: ""
|
property string uuid: ""
|
||||||
property string displayName: ""
|
property string displayName: ""
|
||||||
property string userName: ""
|
property string userName: ""
|
||||||
property real displayNameTextPixelSize: 18
|
property real displayNameTextPixelSize: 18
|
||||||
property int usernameTextHeight: 12
|
property int usernameTextPixelSize: 14
|
||||||
property real audioLevel: 0.0
|
property real audioLevel: 0.0
|
||||||
property real avgAudioLevel: 0.0
|
property real avgAudioLevel: 0.0
|
||||||
property bool isMyCard: false
|
property bool isMyCard: false
|
||||||
property bool selected: false
|
property bool selected: false
|
||||||
property bool isAdmin: 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.
|
Item {
|
||||||
Column {
|
|
||||||
id: avatarImage
|
id: avatarImage
|
||||||
|
visible: profileUrl !== "";
|
||||||
// Size
|
// Size
|
||||||
height: parent.height
|
height: isMyCard ? 70 : 42;
|
||||||
width: height
|
width: visible ? height : 0;
|
||||||
|
anchors.top: parent.top;
|
||||||
|
anchors.topMargin: isMyCard ? 0 : 8;
|
||||||
|
anchors.left: parent.left
|
||||||
|
clip: true
|
||||||
Image {
|
Image {
|
||||||
id: userImage
|
id: userImage
|
||||||
source: "../../icons/defaultNameCardUser.png"
|
source: profileUrl !== "" ? ((0 === profileUrl.indexOf("http")) ? profileUrl : (defaultBaseUrl + profileUrl)) : "";
|
||||||
|
mipmap: true;
|
||||||
// Anchors
|
// 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
|
width: parent.width
|
||||||
height: parent.height
|
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
|
||||||
anchors.top: parent.top
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
anchors.left: parent.left
|
anchors.left: parent.left
|
||||||
anchors.leftMargin: -10
|
anchors.leftMargin: 10
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: editGlyph.width + editGlyph.anchors.rightMargin
|
||||||
// Style
|
// Style
|
||||||
color: hifi.colors.textFieldLightBackground
|
color: hifi.colors.darkGray
|
||||||
border.color: hifi.colors.blueHighlight
|
FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; }
|
||||||
border.width: 0
|
font.family: firaSansSemiBold.name
|
||||||
TextInput {
|
font.pixelSize: displayNameTextPixelSize
|
||||||
id: myDisplayNameText
|
selectionColor: hifi.colors.blueHighlight
|
||||||
// Properties
|
selectedTextColor: "black"
|
||||||
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
|
|
||||||
// Text Positioning
|
// 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
|
// Style
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
color: hifi.colors.baseGray
|
color: hifi.colors.baseGray
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Spacer
|
// DisplayName container for others' cards
|
||||||
Item {
|
Item {
|
||||||
id: userNameSpacer
|
id: displayNameContainer
|
||||||
height: 4
|
visible: !isMyCard
|
||||||
width: parent.width
|
// Size
|
||||||
// Anchors
|
width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin;
|
||||||
anchors.top: userNameText.bottom
|
height: displayNameTextPixelSize + 4
|
||||||
}
|
// Anchors
|
||||||
|
anchors.top: pal.activeTab == "connectionsTab" ? undefined : avatarImage.top;
|
||||||
// VU Meter
|
anchors.bottom: pal.activeTab == "connectionsTab" ? avatarImage.bottom : undefined;
|
||||||
Rectangle {
|
anchors.left: avatarImage.right
|
||||||
id: nameCardVUMeter
|
anchors.leftMargin: avatarImage.visible ? 5 : 0;
|
||||||
// Size
|
// DisplayName Text for others' cards
|
||||||
width: isMyCard ? myDisplayName.width - 70 : ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width
|
FiraSansSemiBold {
|
||||||
height: 8
|
id: displayNameText
|
||||||
// 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
|
|
||||||
// Properties
|
// Properties
|
||||||
visible: !isMyCard && selected
|
text: thisNameCard.displayName
|
||||||
value: Users.getAvatarGain(uuid)
|
elide: Text.ElideRight
|
||||||
minimumValue: -60.0
|
// Size
|
||||||
maximumValue: 20.0
|
width: isAdmin ? Math.min(displayNameTextMetrics.tightBoundingRect.width + 8, parent.width - adminLabelText.width - adminLabelQuestionMark.width + 8) : parent.width
|
||||||
stepSize: 5
|
// Anchors
|
||||||
updateValueWhileDragging: true
|
anchors.top: parent.top
|
||||||
onValueChanged: updateGainFromQML(uuid, value, false)
|
anchors.left: parent.left
|
||||||
onPressedChanged: {
|
// Text Size
|
||||||
if (!pressed) {
|
size: displayNameTextPixelSize
|
||||||
updateGainFromQML(uuid, value, true)
|
// 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 {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
onWheel: {
|
hoverEnabled: true
|
||||||
// Do nothing.
|
onClicked: letterbox(hifi.glyphs.question,
|
||||||
}
|
"Domain Admin",
|
||||||
onDoubleClicked: {
|
"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!")
|
||||||
gainSlider.value = 0.0
|
onEntered: adminLabelQuestionMarkText.color = "#94132e"
|
||||||
}
|
onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
function updateGainFromQML(avatarUuid, sliderValue, isReleased) {
|
||||||
Users.setAvatarGain(avatarUuid, sliderValue);
|
Users.setAvatarGain(avatarUuid, sliderValue);
|
||||||
if (isReleased) {
|
if (isReleased) {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -171,7 +171,7 @@ Item {
|
||||||
readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9
|
readonly property real textFieldInputLabel: dimensions.largeScreen ? 13 : 9
|
||||||
readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24
|
readonly property real textFieldSearchIcon: dimensions.largeScreen ? 30 : 24
|
||||||
readonly property real tableHeading: dimensions.largeScreen ? 12 : 10
|
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 tableText: dimensions.largeScreen ? 15 : 12
|
||||||
readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9
|
readonly property real buttonLabel: dimensions.largeScreen ? 13 : 9
|
||||||
readonly property real iconButton: dimensions.largeScreen ? 13 : 9
|
readonly property real iconButton: dimensions.largeScreen ? 13 : 9
|
||||||
|
|
|
@ -27,7 +27,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(glm::vec3 position READ getPosition)
|
Q_PROPERTY(glm::vec3 position READ getPosition)
|
||||||
Q_PROPERTY(glm::quat orientation READ getOrientation)
|
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(bool showTablet READ getShouldShowTablet)
|
||||||
Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID)
|
Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID)
|
||||||
Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID)
|
Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID)
|
||||||
|
@ -80,6 +80,7 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
bool shouldShowHandControllersChanged();
|
bool shouldShowHandControllersChanged();
|
||||||
|
void mountedChanged();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
HMDScriptingInterface();
|
HMDScriptingInterface();
|
||||||
|
|
|
@ -21,10 +21,10 @@ var DEFAULT_SCRIPTS = [
|
||||||
"system/snapshot.js",
|
"system/snapshot.js",
|
||||||
"system/help.js",
|
"system/help.js",
|
||||||
"system/pal.js", // "system/mod.js", // older UX, if you prefer
|
"system/pal.js", // "system/mod.js", // older UX, if you prefer
|
||||||
|
"system/makeUserConnection.js",
|
||||||
"system/goto.js",
|
"system/goto.js",
|
||||||
"system/marketplaces/marketplaces.js",
|
"system/marketplaces/marketplaces.js",
|
||||||
"system/edit.js",
|
"system/edit.js",
|
||||||
"system/tablet-users.js",
|
|
||||||
"system/selectAudioDevice.js",
|
"system/selectAudioDevice.js",
|
||||||
"system/notifications.js",
|
"system/notifications.js",
|
||||||
"system/controllers/controllerDisplayManager.js",
|
"system/controllers/controllerDisplayManager.js",
|
||||||
|
|
372
scripts/system/makeUserConnection.js
Normal file
372
scripts/system/makeUserConnection.js
Normal 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);
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use strict";
|
"use strict";
|
||||||
/* jslint vars: true, plusplus: true, forin: true*/
|
/*jslint vars:true, plusplus:true, forin:true*/
|
||||||
/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */
|
/*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 }] */
|
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
|
||||||
//
|
//
|
||||||
// pal.js
|
// pal.js
|
||||||
|
@ -14,6 +14,8 @@
|
||||||
|
|
||||||
(function() { // BEGIN LOCAL_SCOPE
|
(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
|
// hardcoding these as it appears we cannot traverse the originalTextures in overlays??? Maybe I've missed
|
||||||
// something, will revisit as this is sorta horrible.
|
// something, will revisit as this is sorta horrible.
|
||||||
var UNSELECTED_TEXTURES = {
|
var UNSELECTED_TEXTURES = {
|
||||||
|
@ -97,9 +99,8 @@ ExtendedOverlay.prototype.hover = function (hovering) {
|
||||||
if (this.key === lastHoveringId) {
|
if (this.key === lastHoveringId) {
|
||||||
if (hovering) {
|
if (hovering) {
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
lastHoveringId = 0;
|
|
||||||
}
|
}
|
||||||
|
lastHoveringId = 0;
|
||||||
}
|
}
|
||||||
this.editOverlay({color: color(this.selected, hovering, this.audioLevel)});
|
this.editOverlay({color: color(this.selected, hovering, this.audioLevel)});
|
||||||
if (this.model) {
|
if (this.model) {
|
||||||
|
@ -214,9 +215,8 @@ function convertDbToLinear(decibels) {
|
||||||
// but, your perception is that something 2x as loud is +10db
|
// 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
|
// so we go from -60 to +20 or 1/64x to 4x. For now, we can
|
||||||
// maybe scale the signal this way??
|
// 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.
|
function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml.
|
||||||
var data;
|
var data;
|
||||||
switch (message.method) {
|
switch (message.method) {
|
||||||
|
@ -247,7 +247,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'refresh':
|
case 'refreshNearby':
|
||||||
data = {};
|
data = {};
|
||||||
ExtendedOverlay.some(function (overlay) { // capture the audio data
|
ExtendedOverlay.some(function (overlay) { // capture the audio data
|
||||||
data[overlay.key] = overlay;
|
data[overlay.key] = overlay;
|
||||||
|
@ -257,8 +257,12 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
|
||||||
if (message.params.filter !== undefined) {
|
if (message.params.filter !== undefined) {
|
||||||
Settings.setValue('pal/filtered', !!message.params.filter);
|
Settings.setValue('pal/filtered', !!message.params.filter);
|
||||||
}
|
}
|
||||||
populateUserList(message.params.selected, data);
|
populateNearbyUserList(message.params.selected, data);
|
||||||
UserActivityLogger.palAction("refresh", "");
|
UserActivityLogger.palAction("refresh_nearby", "");
|
||||||
|
break;
|
||||||
|
case 'refreshConnections':
|
||||||
|
getConnectionData();
|
||||||
|
UserActivityLogger.palAction("refresh_connections", "");
|
||||||
break;
|
break;
|
||||||
case 'displayNameUpdate':
|
case 'displayNameUpdate':
|
||||||
if (MyAvatar.displayName !== message.params) {
|
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", "");
|
UserActivityLogger.palAction("display_name_change", "");
|
||||||
}
|
}
|
||||||
break;
|
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:
|
default:
|
||||||
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
|
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) {
|
function sendToQml(message) {
|
||||||
tablet.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.
|
// Main operations.
|
||||||
|
@ -285,15 +442,16 @@ function addAvatarNode(id) {
|
||||||
solid: true,
|
solid: true,
|
||||||
alpha: 0.8,
|
alpha: 0.8,
|
||||||
color: color(selected, false, 0.0),
|
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.
|
// Each open/refresh will capture a stable set of avatarsOfInterest, within the specified filter.
|
||||||
var avatarsOfInterest = {};
|
var avatarsOfInterest = {};
|
||||||
function populateUserList(selectData, oldAudioData) {
|
function populateNearbyUserList(selectData, oldAudioData) {
|
||||||
var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')};
|
var filter = Settings.getValue('pal/filtered') && {distance: Settings.getValue('pal/nearDistance')},
|
||||||
var data = [], avatars = AvatarList.getAvatarIdentifiers();
|
data = [],
|
||||||
avatarsOfInterest = {};
|
avatars = AvatarList.getAvatarIdentifiers(),
|
||||||
var myPosition = filter && Camera.position,
|
myPosition = filter && Camera.position,
|
||||||
frustum = filter && Camera.frustum,
|
frustum = filter && Camera.frustum,
|
||||||
verticalHalfAngle = filter && (frustum.fieldOfView / 2),
|
verticalHalfAngle = filter && (frustum.fieldOfView / 2),
|
||||||
horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio),
|
horizontalHalfAngle = filter && (verticalHalfAngle * frustum.aspectRatio),
|
||||||
|
@ -301,7 +459,8 @@ function populateUserList(selectData, oldAudioData) {
|
||||||
front = filter && Quat.getFront(orientation),
|
front = filter && Quat.getFront(orientation),
|
||||||
verticalAngleNormal = filter && Quat.getRight(orientation),
|
verticalAngleNormal = filter && Quat.getRight(orientation),
|
||||||
horizontalAngleNormal = filter && Quat.getUp(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 avatar = AvatarList.getAvatar(id);
|
||||||
var name = avatar.sessionDisplayName;
|
var name = avatar.sessionDisplayName;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
|
@ -323,8 +482,10 @@ function populateUserList(selectData, oldAudioData) {
|
||||||
}
|
}
|
||||||
var oldAudio = oldAudioData && oldAudioData[id];
|
var oldAudio = oldAudioData && oldAudioData[id];
|
||||||
var avatarPalDatum = {
|
var avatarPalDatum = {
|
||||||
|
profileUrl: '',
|
||||||
displayName: name,
|
displayName: name,
|
||||||
userName: '',
|
userName: '',
|
||||||
|
connection: '',
|
||||||
sessionId: id || '',
|
sessionId: id || '',
|
||||||
audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0,
|
audioLevel: (oldAudio && oldAudio.audioLevel) || 0.0,
|
||||||
avgAudioLevel: (oldAudio && oldAudio.avgAudioLevel) || 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.
|
// Everyone needs to see admin status. Username and fingerprint returns default constructor output if the requesting user isn't an admin.
|
||||||
Users.requestUsernameFromID(id);
|
Users.requestUsernameFromID(id);
|
||||||
avatarsOfInterest[id] = true;
|
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);
|
data.push(avatarPalDatum);
|
||||||
print('PAL data:', JSON.stringify(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;
|
conserveResources = Object.keys(avatarsOfInterest).length > 20;
|
||||||
sendToQml({ method: 'users', params: data });
|
sendToQml({ method: 'nearbyUsers', params: data });
|
||||||
if (selectData) {
|
if (selectData) {
|
||||||
selectData[2] = true;
|
selectData[2] = true;
|
||||||
sendToQml({ method: 'select', params: selectData });
|
sendToQml({ method: 'select', params: selectData });
|
||||||
|
@ -351,15 +519,15 @@ function populateUserList(selectData, oldAudioData) {
|
||||||
|
|
||||||
// The function that handles the reply from the server
|
// The function that handles the reply from the server
|
||||||
function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
|
function usernameFromIDReply(id, username, machineFingerprint, isAdmin) {
|
||||||
var data = [
|
var data = {
|
||||||
(MyAvatar.sessionUUID === id) ? '' : id, // Pal.qml recognizes empty id specially.
|
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.
|
// 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).
|
// Otherwise, use valid machineFingerprint (which is not valid when not an admin).
|
||||||
username || (Users.canKick && machineFingerprint) || '',
|
userName: username || (Users.canKick && machineFingerprint) || '',
|
||||||
isAdmin
|
admin: isAdmin
|
||||||
];
|
};
|
||||||
// Ship the data off to QML
|
// Ship the data off to QML
|
||||||
sendToQml({ method: 'updateUsername', params: data });
|
updateUser(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
var pingPong = true;
|
var pingPong = true;
|
||||||
|
@ -381,16 +549,12 @@ function updateOverlays() {
|
||||||
var target = avatar.position;
|
var target = avatar.position;
|
||||||
var distance = Vec3.distance(target, eye);
|
var distance = Vec3.distance(target, eye);
|
||||||
var offset = 0.2;
|
var offset = 0.2;
|
||||||
|
var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position)
|
||||||
// base offset on 1/2 distance from hips to head if we can
|
var headIndex = avatar.getJointIndex("Head"); // base offset on 1/2 distance from hips to head if we can
|
||||||
var headIndex = avatar.getJointIndex("Head");
|
|
||||||
if (headIndex > 0) {
|
if (headIndex > 0) {
|
||||||
offset = avatar.getAbsoluteJointTranslationInObjectFrame(headIndex).y / 2;
|
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
|
// move a bit in front, towards the camera
|
||||||
target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset));
|
target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset));
|
||||||
|
|
||||||
|
@ -418,7 +582,7 @@ function updateOverlays() {
|
||||||
overlay.deleteOverlay();
|
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();
|
HighlightedEntity.updateOverlays();
|
||||||
}
|
}
|
||||||
function removeOverlays() {
|
function removeOverlays() {
|
||||||
|
@ -543,6 +707,7 @@ function startup() {
|
||||||
Messages.subscribe(CHANNEL);
|
Messages.subscribe(CHANNEL);
|
||||||
Messages.messageReceived.connect(receiveMessage);
|
Messages.messageReceived.connect(receiveMessage);
|
||||||
Users.avatarDisconnected.connect(avatarDisconnected);
|
Users.avatarDisconnected.connect(avatarDisconnected);
|
||||||
|
GlobalServices.findableByChanged.connect(findableByChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
startup();
|
startup();
|
||||||
|
@ -579,7 +744,8 @@ function onTabletButtonClicked() {
|
||||||
tablet.loadQMLSource("../Pal.qml");
|
tablet.loadQMLSource("../Pal.qml");
|
||||||
onPalScreen = true;
|
onPalScreen = true;
|
||||||
Users.requestsDomainListData = true;
|
Users.requestsDomainListData = true;
|
||||||
populateUserList();
|
populateNearbyUserList();
|
||||||
|
findableByChanged(GlobalServices.findableBy);
|
||||||
isWired = true;
|
isWired = true;
|
||||||
Script.update.connect(updateOverlays);
|
Script.update.connect(updateOverlays);
|
||||||
Controller.mousePressEvent.connect(handleMouseEvent);
|
Controller.mousePressEvent.connect(handleMouseEvent);
|
||||||
|
@ -607,8 +773,7 @@ function onTabletScreenChanged(type, url) {
|
||||||
//
|
//
|
||||||
var CHANNEL = 'com.highfidelity.pal';
|
var CHANNEL = 'com.highfidelity.pal';
|
||||||
function receiveMessage(channel, messageString, senderID) {
|
function receiveMessage(channel, messageString, senderID) {
|
||||||
if ((channel !== CHANNEL) ||
|
if ((channel !== CHANNEL) || (senderID !== MyAvatar.sessionUUID)) {
|
||||||
(senderID !== MyAvatar.sessionUUID)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var message = JSON.parse(messageString);
|
var message = JSON.parse(messageString);
|
||||||
|
@ -633,7 +798,7 @@ function scaleAudio(val) {
|
||||||
if (val <= LOUDNESS_FLOOR) {
|
if (val <= LOUDNESS_FLOOR) {
|
||||||
audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE;
|
audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE;
|
||||||
} else {
|
} else {
|
||||||
audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE;
|
audioLevel = (val - (LOUDNESS_FLOOR - 1)) * LOUDNESS_SCALE;
|
||||||
}
|
}
|
||||||
if (audioLevel > 1.0) {
|
if (audioLevel > 1.0) {
|
||||||
audioLevel = 1;
|
audioLevel = 1;
|
||||||
|
@ -659,14 +824,14 @@ function getAudioLevel(id) {
|
||||||
audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2);
|
audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2);
|
||||||
|
|
||||||
// decay avgAudioLevel
|
// 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.avgAudioLevel = avgAudioLevel;
|
||||||
data.audioLevel = audioLevel;
|
data.audioLevel = audioLevel;
|
||||||
|
|
||||||
// now scale for the gain. Also, asked to boost the low end, so one simple way is
|
// 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.
|
// 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];
|
return [audioLevel, avgAudioLevel];
|
||||||
}
|
}
|
||||||
|
@ -677,9 +842,8 @@ function createAudioInterval(interval) {
|
||||||
return Script.setInterval(function () {
|
return Script.setInterval(function () {
|
||||||
var param = {};
|
var param = {};
|
||||||
AvatarList.getAvatarIdentifiers().forEach(function (id) {
|
AvatarList.getAvatarIdentifiers().forEach(function (id) {
|
||||||
var level = getAudioLevel(id);
|
var level = getAudioLevel(id),
|
||||||
// qml didn't like an object with null/empty string for a key, so...
|
userId = id || 0; // qml didn't like an object with null/empty string for a key, so...
|
||||||
var userId = id || 0;
|
|
||||||
param[userId] = level;
|
param[userId] = level;
|
||||||
});
|
});
|
||||||
sendToQml({method: 'updateAudioLevel', params: param});
|
sendToQml({method: 'updateAudioLevel', params: param});
|
||||||
|
@ -691,6 +855,20 @@ function avatarDisconnected(nodeID) {
|
||||||
sendToQml({method: 'avatarDisconnected', params: [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() {
|
function clearLocalQMLDataAndClosePAL() {
|
||||||
sendToQml({ method: 'clearLocalQMLData' });
|
sendToQml({ method: 'clearLocalQMLData' });
|
||||||
}
|
}
|
||||||
|
@ -708,6 +886,7 @@ function shutdown() {
|
||||||
Messages.subscribe(CHANNEL);
|
Messages.subscribe(CHANNEL);
|
||||||
Messages.messageReceived.disconnect(receiveMessage);
|
Messages.messageReceived.disconnect(receiveMessage);
|
||||||
Users.avatarDisconnected.disconnect(avatarDisconnected);
|
Users.avatarDisconnected.disconnect(avatarDisconnected);
|
||||||
|
GlobalServices.findableByChanged.disconnect(findableByChanged);
|
||||||
off();
|
off();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue