Merge pull request #9873 from highfidelity/PAL_v2

PAL v2.0
This commit is contained in:
Zach Fox 2017-03-31 17:00:41 -07:00 committed by GitHub
commit 8a96969471
30 changed files with 2840 additions and 674 deletions

View file

@ -437,17 +437,20 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage>
while (message->getBytesLeftToRead()) {
// parse out the UUID being ignored from the packet
QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
// Reset the lastBroadcastTime for the ignored avatar to 0
// so the AvatarMixer knows it'll have to send identity data about the ignored avatar
// to the ignorer if the ignorer unignores.
nodeData->setLastBroadcastTime(ignoredUUID, 0);
// Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0
// so the AvatarMixer knows it'll have to send identity data about the ignorer
// to the ignored if the ignorer unignores.
auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID);
AvatarMixerClientData* ignoredNodeData = reinterpret_cast<AvatarMixerClientData*>(ignoredNode->getLinkedData());
ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0);
if (nodeList->nodeWithUUID(ignoredUUID)) {
// Reset the lastBroadcastTime for the ignored avatar to 0
// so the AvatarMixer knows it'll have to send identity data about the ignored avatar
// to the ignorer if the ignorer unignores.
nodeData->setLastBroadcastTime(ignoredUUID, 0);
// Reset the lastBroadcastTime for the ignorer (FROM THE PERSPECTIVE OF THE IGNORED) to 0
// so the AvatarMixer knows it'll have to send identity data about the ignorer
// to the ignored if the ignorer unignores.
auto ignoredNode = nodeList->nodeWithUUID(ignoredUUID);
AvatarMixerClientData* ignoredNodeData = reinterpret_cast<AvatarMixerClientData*>(ignoredNode->getLinkedData());
ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0);
}
if (addToIgnore) {
senderNode->addIgnoredNode(ignoredUUID);

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 576 576" style="enable-background:new 0 0 576 576;" xml:space="preserve">
<style type="text/css">
.st0{fill:#6D6E71;}
.st1{fill:#1398BB;}
</style>
<path class="st0" d="M498.5,272.1c0-1.8,0.2-3.6,0.4-5.4c-15.5,3.9-31,8-46.4,12.5c-3.9,1.1-7.8,3.5-10.6,6.4
c-10.1,10.2-19.7,21-29.6,31.6c-2.5-1.6-4.8-3-7-4.5c-35.4-23-70.9-45.8-106.2-69c-8.9-5.8-17.2-6.3-26.5-1.3
c-15.8,8.5-31.7,16.8-47.8,24.9c-2.3,1.1-5.7,0.1-8.5,0.1c0.3-2.8-0.3-6.3,1.1-8.3c7.6-10.6,15.5-21,23.8-31c3-3.7,7.3-7.6,11.6-8.7
c20.4-5.2,41.1-10.8,62.4-8.9c16.2,1.4,32.3,5,48.4,8c8.1,1.5,15.1,0.7,22.1-4.6c17.5-13.3,35.6-25.9,53.4-38.8
c1.6-1.2,3.9-2.2,4.5-3.9c1-2.7,2.1-6.8,0.8-8.4c-1.6-2-5.6-2.7-8.6-2.7c-1.9,0-3.8,2.2-5.6,3.5c-16.6,12-33.3,23.7-49.6,36.1
c-5.7,4.4-10.9,5.4-17.8,3.7c-27.4-7-55.1-11.2-83.2-4.6c-12.4,2.9-25.4,4.7-36.8,9.8c-7.4,3.3-13.6,4.7-21.4,3.7
c-10.3-1.4-19-4.9-27-11.9c-14.5-12.7-29.9-24.5-44.9-36.7c-1.6-1.3-3.3-3.3-4.9-3.3c-3,0-7,0.4-8.7,2.2c-1.4,1.5-0.6,5.6,0.2,8.3
c0.5,1.7,2.8,2.8,4.3,4.1c14.6,11.9,29.6,23.4,43.9,35.8c9.8,8.5,20,15.1,33.4,15.3c1.6,0,3.2,0.8,5.5,1.4
c-6.1,7.9-11.7,15.1-17.2,22.5c-7.3,9.7-7.7,18.5-1.1,26.4c6.6,8.1,16.2,9.6,26.7,4.1c15.3-7.9,30.8-15.7,45.9-24.1
c5.7-3.1,10-3,15.4,0.6c35.1,23,70.4,45.8,105.7,68.6c1.9,1.2,3.8,2.4,5.5,3.9c3.9,3.6,4.9,7.9,2.6,12.8c-2.3,4.9-6.3,7.1-11.5,6.2
c-2.8-0.5-5.5-1.9-8-3.3c-18-10.6-35.9-21.4-53.8-32.1c-2.1-1.3-4.4-3.3-6.5-3.2c-2.7,0.1-6.3,1.3-7.8,3.3c-1.2,1.6-0.3,5.5,0.9,7.7
c1,1.9,3.7,3.1,5.8,4.4c16.3,9.9,32.8,19.5,49,29.6c2.6,1.6,5.6,5.9,5.1,8.2c-0.7,3.1-4.3,6.9-7.2,7.6c-3.9,0.9-9,0.1-12.7-1.8
c-14-7.1-27.7-14.8-41.5-22.3c-1.8-1-3.7-2.7-5.4-2.5c-3.2,0.4-7.3,1-9,3.1c-1.2,1.5,0.5,5.9,1.9,8.6c0.7,1.5,3.2,2.3,5,3.2
c12.3,6.9,24.8,13.5,37,20.8c2.4,1.4,5.4,5.6,4.9,7.6c-0.8,3.1-4,7-7,7.8c-3.6,1-8.5,0.2-12.1-1.5c-10.4-4.7-20.5-10.2-30.7-15.4
c-7.4-3.7-12-3.4-13.9,1.4c-2.9,6.9,2.4,9.1,7,11.5c7.4,3.8,14.8,7.5,22.2,11.3c1.4,0.7,2.6,1.8,4.5,3.1c-16.9,8.5-32.4,0.9-48.5,0
c8.7-19.4,6.1-27.4-9-35.7c-2.5-8.3-3-15.8-6.7-20.9c-3.9-5.3-11-8.1-17.7-12.8c-0.2-9.3-5.8-16.9-16.1-20.6
c-10.6-3.9-19.5,0.2-27,7.7c-12.5-11.7-19.9-12.4-36.3-3.8c-4.3-5.7-9.3-11-12.9-17.2c-8.6-14.6-20.8-23.1-37.5-26.5
c-10.7-2.2-21-6.5-31.5-9.7c-0.4-0.1-0.9-0.3-1.3-0.4c0.3,0.8,0.5,1.7,0.6,2.5c0.2,2.3-0.6,5.7-2.3,7.4c-1.2,1.2-2.3,2.5-3.8,3.3
c0,0,0,0,0,0c0.7,0.3,1.4,0.6,2.2,0.8c1.3,0.4,2.6,0.8,3.8,1.2c13.9,4.4,27.8,8.8,41.7,13.3c2.1,0.7,4.5,1.5,5.8,3
c9,11.5,17.7,23.1,26.6,34.9c-2.9,4.2-5.7,8-8.1,12c-6,9.9-5.3,20.9,2.9,28.3c4.3,3.8,11.5,4.4,15.4,8.5c4.1,4.2,4.8,11.5,8.6,16.1
c6.1,7.6,15.2,7.6,23.9,6.2c9.1,13.6,17,16.6,32.2,12.8c10.4,11.1,21.4,14.3,30.7,7.1c5.3-4.1,10.1-4.4,16-3.3
c7.1,1.3,14.2,2.3,21.3,3.5c15.1,2.4,29,0,41.2-10c1.3-1.1,3.1-2,4.7-2.1c15.2-1.3,24.3-9.3,26.9-24.6c14.2-1.6,24.1-8.3,26.9-22.8
c1-0.2,1.6-0.5,2.3-0.5c17.2-1.8,26.5-13.8,24.5-31.1c-0.2-2,1-4.7,2.5-6.3c8.1-9,16.2-18.1,24.9-26.6c3.5-3.4,8.1-6.2,12.8-7.6
c13.9-4.2,28.1-7.6,42.1-11.7C500.9,278.6,498.4,275.6,498.5,272.1z M170.8,362.9c-3,4-7.6,4-11.5,1.3c-3.9-2.7-5.8-6.9-3.1-11.2
c4.6-7.4,9.8-14.5,15.1-21.5c1.3-1.8,4.1-2.5,5.2-3.2c7.9,0,12.5,6.9,9,12.8C181.2,348.7,176.1,355.9,170.8,362.9z M195.9,384.7
c-3.5,5.1-9.1,6.4-13.4,3.5c-4.5-3.1-5.3-8.3-1.7-13.6c9.1-13.6,18.5-27.1,27.6-40.6c2.9-4.3,7-6.3,11.5-4.2c2.8,1.3,4.6,4.6,6.9,7
c-1.6,3.5-2.4,5.9-3.7,7.8C214.1,358,205,371.4,195.9,384.7z M224.8,399.7c-1.4,2-4.3,2.9-5.6,3.7c-8.2,0.3-13.1-7.1-9.3-13.1
c7.2-11.3,14.8-22.4,22.7-33.3c2.9-4,7.7-4.8,12-1.7c4.1,3,5.6,7.3,2.7,11.8C240,378.1,232.5,389,224.8,399.7z M263.2,394.1
c-3.5,5.2-6.6,10.8-10.9,15.2c-1.8,1.8-6.7,2.3-9.2,1.2c-1.6-0.7-2.6-6.2-1.6-8.4c2.9-5.7,6.8-11.1,11-16c1.4-1.7,5.3-2.3,7.6-1.7
c2,0.5,3.5,3.4,5,5.1C264.2,391.6,263.9,393,263.2,394.1z"/>
<path class="st1" d="M288.4,507.4c-58.8,0-114-22.9-155.6-64.4c-41.5-41.5-64.4-96.8-64.4-155.6s22.9-114,64.4-155.6
c41.5-41.5,96.8-64.4,155.6-64.4s114,22.9,155.6,64.4c41.5,41.5,64.4,96.8,64.4,155.6s-22.9,114-64.4,155.6S347.1,507.4,288.4,507.4
z M288.4,80.8c-55.2,0-107,21.5-146.1,60.5s-60.5,90.9-60.5,146.1c0,55.2,21.5,107,60.5,146.1c39,39,90.9,60.5,146.1,60.5
c55.2,0,107-21.5,146.1-60.5c39-39,60.5-90.9,60.5-146.1c0-55.2-21.5-107-60.5-146.1C395.4,102.3,343.5,80.8,288.4,80.8z"/>
<path class="st1" d="M288.4,541.5c-67.9,0-131.7-26.4-179.7-74.4c-48-48-74.4-111.8-74.4-179.7s26.4-131.7,74.4-179.7
c48-48,111.8-74.4,179.7-74.4s131.7,26.4,179.7,74.4s74.4,111.8,74.4,179.7s-26.4,131.7-74.4,179.7S356.3,541.5,288.4,541.5z
M288.4,41.1c-65.8,0-127.6,25.6-174.1,72.1c-46.5,46.5-72.1,108.3-72.1,174.1S67.7,415,114.3,461.5
c46.5,46.5,108.3,72.1,174.1,72.1S416,508,462.5,461.5c46.5-46.5,72.1-108.3,72.1-174.1S509,159.8,462.5,113.3
C416,66.8,354.1,41.1,288.4,41.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View file

@ -90,7 +90,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

@ -216,7 +216,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

@ -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,6 @@ TableView {
Rectangle {
color: "#00000000"
anchors { fill: parent; margins: -2 }
radius: hifi.dimensions.borderRadius
border.color: isLightColorScheme ? hifi.colors.lightGrayText : hifi.colors.baseGrayHighlight
border.width: 2
}

View file

@ -8,6 +8,8 @@ Item {
property alias url: root.url
property alias scriptURL: root.userScriptUrl
property alias eventBridge: eventBridgeWrapper.eventBridge
property alias canGoBack: root.canGoBack;
property var goBack: root.goBack;
property bool keyboardEnabled: true // FIXME - Keyboard HMD only: Default to false
property bool keyboardRaised: false
property bool punctuationMode: false
@ -101,11 +103,11 @@ Item {
}
onNewViewRequested:{
// desktop is not defined for web-entities
if (desktop) {
var component = Qt.createComponent("../Browser.qml");
var newWindow = component.createObject(desktop);
request.openIn(newWindow.webView);
// desktop is not defined for web-entities or tablet
if (typeof desktop !== "undefined") {
desktop.openBrowserWindow(request, profile);
} else {
console.log("onNewViewRequested: desktop not defined");
}
}
}

View file

@ -490,6 +490,13 @@ FocusScope {
desktop.forceActiveFocus();
}
function openBrowserWindow(request, profile) {
var component = Qt.createComponent("../Browser.qml");
var newWindow = component.createObject(desktop);
newWindow.webView.profile = profile;
request.openIn(newWindow.webView);
}
FocusHack { id: focusHack; }
Rectangle {

View file

@ -0,0 +1,154 @@
//
// ComboDialog.qml
// qml/hifi
//
// Created by Zach Fox on 3/31/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
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import "../styles-uit"
Item {
property var dialogTitleText;
property var optionTitleText;
property var optionBodyText;
property var optionValues;
property var selectedOptionIndex;
property int dialogWidth;
property int dialogHeight;
property int comboOptionTextSize: 18;
FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; }
FontLoader { id: ralewaySemiBold; source: "../../fonts/Raleway-SemiBold.ttf"; }
visible: false;
id: combo;
anchors.fill: parent;
onVisibleChanged: {
populateComboListViewModel();
}
Rectangle {
id: dialogBackground;
anchors.fill: parent;
color: "black";
opacity: 0.5;
}
Rectangle {
id: dialogContainer;
color: "white";
anchors.centerIn: dialogBackground;
width: dialogWidth;
height: dialogHeight;
RalewayRegular {
id: dialogTitle;
text: dialogTitleText;
anchors.top: parent.top;
anchors.topMargin: 20;
anchors.left: parent.left;
anchors.leftMargin: 20;
size: 24;
color: 'black';
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignTop;
}
ListModel {
id: comboListViewModel;
}
ListView {
id: comboListView;
anchors.top: dialogTitle.bottom;
anchors.topMargin: 20;
anchors.bottom: parent.bottom;
anchors.left: parent.left;
anchors.right: parent.right;
model: comboListViewModel;
delegate: comboListViewDelegate;
Component {
id: comboListViewDelegate;
Rectangle {
id: comboListViewItemContainer;
// Size
height: childrenRect.height + 10;
width: dialogContainer.width;
color: selectedOptionIndex === index ? '#cee6ff' : 'white';
Rectangle {
id: comboOptionSelected;
visible: selectedOptionIndex === index ? true : false;
color: hifi.colors.blueAccent;
anchors.left: parent.left;
anchors.leftMargin: 20;
anchors.top: parent.top;
anchors.topMargin: 20;
width: 25;
height: width;
radius: width;
border.width: 3;
border.color: hifi.colors.blueHighlight;
}
RalewaySemiBold {
id: optionTitle;
text: titleText;
anchors.top: parent.top;
anchors.left: comboOptionSelected.right;
anchors.leftMargin: 20;
anchors.right: parent.right;
height: 30;
size: comboOptionTextSize;
wrapMode: Text.WordWrap;
}
RalewayRegular {
id: optionBody;
text: bodyText;
anchors.top: optionTitle.bottom;
anchors.bottom: parent.bottom;
anchors.left: comboOptionSelected.right;
anchors.leftMargin: 25;
anchors.right: parent.right;
size: comboOptionTextSize;
wrapMode: Text.WordWrap;
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
hoverEnabled: true;
onEntered: comboListViewItemContainer.color = hifi.colors.blueHighlight
onExited: comboListViewItemContainer.color = selectedOptionIndex === index ? '#cee6ff' : 'white';
onClicked: {
GlobalServices.findableBy = optionValue;
UserActivityLogger.palAction("set_availability", optionValue);
print('Setting availability:', optionValue);
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
combo.visible = false;
}
}
function populateComboListViewModel() {
comboListViewModel.clear();
optionTitleText.forEach(function(titleText, index) {
comboListViewModel.insert(index, {"titleText": titleText, "bodyText": optionBodyText[index], "optionValue": optionValues[index]});
});
}
}

View file

@ -14,392 +14,499 @@ import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import "../styles-uit"
import "toolbars"
// references Users, UserActivityLogger, MyAvatar, Vec3, Quat, AddressManager from root context
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: AddressManager.metaverseServerUrl;
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 bool isPresent: true
property string profilePicBorderColor: (connectionStatus == "connection" ? hifi.colors.indigoAccent : (connectionStatus == "friend" ? hifi.colors.greenHighlight : "transparent"))
/* User image commented out for now - will probably be re-introduced later.
Column {
Item {
id: avatarImage
visible: profileUrl !== "" && userName !== "";
// 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
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Item {
width: userImage.width;
height: userImage.height;
Rectangle {
anchors.centerIn: parent;
width: userImage.width; // This works because userImage is square
height: width;
radius: width;
}
}
}
}
AnimatedImage {
source: "../../icons/profilePicLoading.gif"
anchors.fill: parent;
visible: userImage.status != Image.Ready;
}
StateImage {
id: infoHoverImage;
visible: false;
imageURL: "../../images/info-icon-2-state.svg";
size: 32;
buttonState: 1;
anchors.centerIn: parent;
}
MouseArea {
anchors.fill: parent
enabled: selected || isMyCard;
hoverEnabled: enabled
onClicked: {
userInfoViewer.url = defaultBaseUrl + "/users/" + userName;
userInfoViewer.visible = true;
}
onEntered: infoHoverImage.visible = true;
onExited: infoHoverImage.visible = false;
}
}
// 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 - anchors.rightMargin*2;
height: 40
// Anchors
anchors.top: avatarImage.top
anchors.left: avatarImage.right
anchors.leftMargin: avatarImage.visible ? 5 : 0;
anchors.rightMargin: 5;
// 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
}
}
*/
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.blueAccent
selectedTextColor: "black"
// Text Positioning
verticalAlignment: Text.AlignVCenter
verticalAlignment: TextInput.AlignVCenter
horizontalAlignment: TextInput.AlignLeft
autoScroll: false;
// Signals
onEditingFinished: {
if (MyAvatar.displayName !== text) {
MyAvatar.displayName = text;
UserActivityLogger.palAction("display_name_change", text);
}
cursorPosition = 0
focus = false
myDisplayName.border.width = 0
color = hifi.colors.darkGray
pal.currentlyEditingDisplayName = false
autoScroll = false;
}
}
MouseArea {
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;
}
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
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 && pal.activeTab !== "connectionsTab"
// Size
width: parent.width - anchors.leftMargin - avatarImage.width - anchors.leftMargin;
height: displayNameTextPixelSize + 4
// Anchors
anchors.top: avatarImage.top;
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: hifi.colors.darkGray;
MouseArea {
anchors.fill: parent
enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent;
hoverEnabled: enabled
onClicked: {
goToUserInDomain(thisNameCard.uuid);
UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid);
}
onEntered: {
displayNameText.color = hifi.colors.blueHighlight;
userNameText.color = hifi.colors.blueHighlight;
}
onExited: {
displayNameText.color = hifi.colors.darkGray
userNameText.color = hifi.colors.blueAccent;
}
}
}
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
}
enabled: isPresent
hoverEnabled: enabled
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 === "Unknown user" ? "not logged in" : thisNameCard.userName;
elide: Text.ElideRight
visible: thisNameCard.userName !== "";
// Size
width: parent.width
height: pal.activeTab == "nearbyTab" || isMyCard ? usernameTextPixelSize + 4 : parent.height;
// Anchors
anchors.top: isMyCard ? myDisplayName.bottom : (pal.activeTab == "nearbyTab" ? displayNameContainer.bottom : parent.top);
anchors.left: avatarImage.right;
anchors.leftMargin: avatarImage.visible ? 5 : 0;
anchors.rightMargin: 5;
// Text Size
size: pal.activeTab == "nearbyTab" || isMyCard ? usernameTextPixelSize : displayNameTextPixelSize;
// Text Positioning
verticalAlignment: Text.AlignVCenter;
// Style
color: hifi.colors.blueAccent;
MouseArea {
anchors.fill: parent
enabled: selected && pal.activeTab == "nearbyTab" && thisNameCard.userName !== "" && isPresent;
hoverEnabled: enabled
onClicked: {
goToUserInDomain(thisNameCard.uuid);
UserActivityLogger.palAction("go_to_user_in_domain", thisNameCard.uuid);
}
onEntered: {
displayNameText.color = hifi.colors.blueHighlight;
userNameText.color = hifi.colors.blueHighlight;
}
onExited: {
displayNameText.color = hifi.colors.darkGray;
userNameText.color = hifi.colors.blueAccent;
}
}
}
// 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")) && isPresent
// 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" && isPresent;
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) {
UserActivityLogger.palAction("avatar_gain_changed", avatarUuid);
}
}
// Function body by Howard Stearns 2017-01-08
function goToUserInDomain(avatarUuid) {
var avatar = AvatarList.getAvatar(avatarUuid);
if (!avatar) {
console.log("This avatar is no longer present. goToUserInDomain() failed.");
return;
}
var vector = Vec3.subtract(avatar.position, MyAvatar.position);
var distance = Vec3.length(vector);
var target = Vec3.multiply(Vec3.normalize(vector), distance - 2.0);
// FIXME: We would like the avatar to recompute the avatar's "maybe fly" test at the new position, so that if high enough up,
// the avatar goes into fly mode rather than falling. However, that is not exposed to Javascript right now.
// FIXME: it would be nice if this used the same teleport steps and smoothing as in the teleport.js script.
// Note, however, that this script allows teleporting to a person in the air, while teleport.js is going to a grounded target.
MyAvatar.orientation = Quat.lookAtSimple(MyAvatar.position, avatar.position);
MyAvatar.position = Vec3.sum(MyAvatar.position, target);
}
}

File diff suppressed because it is too large Load diff

View file

@ -172,7 +172,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

@ -40,9 +40,10 @@ void DiscoverabilityManager::updateLocation() {
auto accountManager = DependencyManager::get<AccountManager>();
auto addressManager = DependencyManager::get<AddressManager>();
auto& domainHandler = DependencyManager::get<NodeList>()->getDomainHandler();
bool discoverable = (_mode.get() != Discoverability::None);
if (_mode.get() != Discoverability::None && accountManager->isLoggedIn()) {
if (accountManager->isLoggedIn()) {
// construct a QJsonObject given the user's current address information
QJsonObject rootObject;
@ -50,34 +51,40 @@ void DiscoverabilityManager::updateLocation() {
QString pathString = addressManager->currentPath();
const QString PATH_KEY_IN_LOCATION = "path";
locationObject.insert(PATH_KEY_IN_LOCATION, pathString);
const QString CONNECTED_KEY_IN_LOCATION = "connected";
locationObject.insert(CONNECTED_KEY_IN_LOCATION, domainHandler.isConnected());
locationObject.insert(CONNECTED_KEY_IN_LOCATION, discoverable && domainHandler.isConnected());
if (!addressManager->getRootPlaceID().isNull()) {
const QString PLACE_ID_KEY_IN_LOCATION = "place_id";
locationObject.insert(PLACE_ID_KEY_IN_LOCATION,
uuidStringWithoutCurlyBraces(addressManager->getRootPlaceID()));
if (discoverable) { // Don't consider changes to these as update-worthy if we're not discoverable.
const QString PATH_KEY_IN_LOCATION = "path";
locationObject.insert(PATH_KEY_IN_LOCATION, pathString);
if (!addressManager->getRootPlaceID().isNull()) {
const QString PLACE_ID_KEY_IN_LOCATION = "place_id";
locationObject.insert(PLACE_ID_KEY_IN_LOCATION,
uuidStringWithoutCurlyBraces(addressManager->getRootPlaceID()));
}
if (!domainHandler.getUUID().isNull()) {
const QString DOMAIN_ID_KEY_IN_LOCATION = "domain_id";
locationObject.insert(DOMAIN_ID_KEY_IN_LOCATION,
uuidStringWithoutCurlyBraces(domainHandler.getUUID()));
}
// in case the place/domain isn't in the database, we send the network address and port
auto& domainSockAddr = domainHandler.getSockAddr();
const QString NETWORK_ADDRESS_KEY_IN_LOCATION = "network_address";
locationObject.insert(NETWORK_ADDRESS_KEY_IN_LOCATION, domainSockAddr.getAddress().toString());
const QString NETWORK_ADDRESS_PORT_IN_LOCATION = "network_port";
locationObject.insert(NETWORK_ADDRESS_PORT_IN_LOCATION, domainSockAddr.getPort());
const QString NODE_ID_IN_LOCATION = "node_id";
const int UUID_REAL_LENGTH = 36;
locationObject.insert(NODE_ID_IN_LOCATION, DependencyManager::get<NodeList>()->getSessionUUID().toString().mid(1, UUID_REAL_LENGTH));
}
if (!domainHandler.getUUID().isNull()) {
const QString DOMAIN_ID_KEY_IN_LOCATION = "domain_id";
locationObject.insert(DOMAIN_ID_KEY_IN_LOCATION,
uuidStringWithoutCurlyBraces(domainHandler.getUUID()));
}
// in case the place/domain isn't in the database, we send the network address and port
auto& domainSockAddr = domainHandler.getSockAddr();
const QString NETWORK_ADRESS_KEY_IN_LOCATION = "network_address";
locationObject.insert(NETWORK_ADRESS_KEY_IN_LOCATION, domainSockAddr.getAddress().toString());
const QString NETWORK_ADDRESS_PORT_IN_LOCATION = "network_port";
locationObject.insert(NETWORK_ADDRESS_PORT_IN_LOCATION, domainSockAddr.getPort());
const QString FRIENDS_ONLY_KEY_IN_LOCATION = "friends_only";
locationObject.insert(FRIENDS_ONLY_KEY_IN_LOCATION, (_mode.get() == Discoverability::Friends));
const QString AVAILABILITY_KEY_IN_LOCATION = "availability";
locationObject.insert(AVAILABILITY_KEY_IN_LOCATION, findableByString(static_cast<Discoverability::Mode>(_mode.get())));
JSONCallbackParameters callbackParameters;
callbackParameters.jsonCallbackReceiver = this;
@ -139,19 +146,29 @@ void DiscoverabilityManager::setDiscoverabilityMode(Discoverability::Mode discov
// update the setting to the new value
_mode.set(static_cast<int>(discoverabilityMode));
if (static_cast<int>(_mode.get()) == Discoverability::None) {
// if we just got set to no discoverability, make sure that we delete our location in DB
removeLocation();
} else {
// we have a discoverability mode that says we should send a location, do that right away
updateLocation();
}
updateLocation(); // update right away
emit discoverabilityModeChanged(discoverabilityMode);
}
}
QString DiscoverabilityManager::findableByString(Discoverability::Mode discoverabilityMode) {
if (discoverabilityMode == Discoverability::None) {
return "none";
} else if (discoverabilityMode == Discoverability::Friends) {
return "friends";
} else if (discoverabilityMode == Discoverability::Connections) {
return "connections";
} else if (discoverabilityMode == Discoverability::All) {
return "all";
} else {
qDebug() << "GlobalServices findableByString called with an unrecognized value.";
return "";
}
}
void DiscoverabilityManager::setVisibility() {
Menu* menu = Menu::getInstance();

View file

@ -19,6 +19,7 @@ namespace Discoverability {
enum Mode {
None,
Friends,
Connections,
All
};
}
@ -42,6 +43,9 @@ public slots:
signals:
void discoverabilityModeChanged(Discoverability::Mode discoverabilityMode);
public:
static QString findableByString(Discoverability::Mode discoverabilityMode);
private slots:
void handleHeartbeatResponse(QNetworkReply& requestReply);

View file

@ -10,6 +10,7 @@
//
#include "HFWebEngineRequestInterceptor.h"
#include "NetworkingConstants.h"
#include <QtCore/QDebug>
@ -20,8 +21,11 @@ bool isAuthableHighFidelityURL(const QUrl& url) {
"highfidelity.com", "highfidelity.io",
"metaverse.highfidelity.com", "metaverse.highfidelity.io"
};
const auto& scheme = url.scheme();
const auto& host = url.host();
return url.scheme() == "https" && HF_HOSTS.contains(url.host());
return (scheme == "https" && HF_HOSTS.contains(host)) ||
((scheme == NetworkingConstants::METAVERSE_SERVER_URL.scheme()) && (host == NetworkingConstants::METAVERSE_SERVER_URL.host()));
}
void HFWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) {

View file

@ -53,33 +53,19 @@ void GlobalServicesScriptingInterface::loggedOut() {
emit GlobalServicesScriptingInterface::disconnected(QString("logout"));
}
QString GlobalServicesScriptingInterface::findableByString(Discoverability::Mode discoverabilityMode) const {
if (discoverabilityMode == Discoverability::None) {
return "none";
} else if (discoverabilityMode == Discoverability::Friends) {
return "friends";
} else if (discoverabilityMode == Discoverability::All) {
return "all";
} else {
qDebug() << "GlobalServices findableByString called with an unrecognized value.";
return "";
}
}
QString GlobalServicesScriptingInterface::getFindableBy() const {
auto discoverabilityManager = DependencyManager::get<DiscoverabilityManager>();
return findableByString(discoverabilityManager->getDiscoverabilityMode());
return DiscoverabilityManager::findableByString(discoverabilityManager->getDiscoverabilityMode());
}
void GlobalServicesScriptingInterface::setFindableBy(const QString& discoverabilityMode) {
auto discoverabilityManager = DependencyManager::get<DiscoverabilityManager>();
if (discoverabilityMode.toLower() == "none") {
discoverabilityManager->setDiscoverabilityMode(Discoverability::None);
} else if (discoverabilityMode.toLower() == "friends") {
discoverabilityManager->setDiscoverabilityMode(Discoverability::Friends);
} else if (discoverabilityMode.toLower() == "connections") {
discoverabilityManager->setDiscoverabilityMode(Discoverability::Connections);
} else if (discoverabilityMode.toLower() == "all") {
discoverabilityManager->setDiscoverabilityMode(Discoverability::All);
} else {
@ -88,7 +74,7 @@ void GlobalServicesScriptingInterface::setFindableBy(const QString& discoverabil
}
void GlobalServicesScriptingInterface::discoverabilityModeChanged(Discoverability::Mode discoverabilityMode) {
emit findableByChanged(findableByString(discoverabilityMode));
emit findableByChanged(DiscoverabilityManager::findableByString(discoverabilityMode));
}
DownloadInfoResult::DownloadInfoResult() :

View file

@ -18,6 +18,7 @@
#include <QScriptValue>
#include <QString>
#include <QStringList>
#include <DiscoverabilityManager.h>
class DownloadInfoResult {
public:
@ -35,7 +36,7 @@ class GlobalServicesScriptingInterface : public QObject {
Q_OBJECT
Q_PROPERTY(QString username READ getUsername)
Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy)
Q_PROPERTY(QString findableBy READ getFindableBy WRITE setFindableBy NOTIFY findableByChanged)
public:
static GlobalServicesScriptingInterface* getInstance();
@ -65,8 +66,6 @@ private:
GlobalServicesScriptingInterface();
~GlobalServicesScriptingInterface();
QString findableByString(Discoverability::Mode discoverabilityMode) const;
bool _downloading;
};

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

@ -235,6 +235,14 @@ void WindowScriptingInterface::shareSnapshot(const QString& path, const QUrl& hr
qApp->shareSnapshot(path, href);
}
void WindowScriptingInterface::makeConnection(bool success, const QString& userNameOrError) {
if (success) {
emit connectionAdded(userNameOrError);
} else {
emit connectionError(userNameOrError);
}
}
bool WindowScriptingInterface::isPhysicsEnabled() {
return qApp->isPhysicsEnabled();
}
@ -255,7 +263,7 @@ int WindowScriptingInterface::openMessageBox(QString title, QString text, int bu
}
int WindowScriptingInterface::createMessageBox(QString title, QString text, int buttons, int defaultButton) {
auto messageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text,
auto messageBox = DependencyManager::get<OffscreenUi>()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text,
static_cast<QFlags<QMessageBox::StandardButton>>(buttons), static_cast<QMessageBox::StandardButton>(defaultButton));
connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int)));

View file

@ -56,6 +56,7 @@ public slots:
void showAssetServer(const QString& upload = "");
void copyToClipboard(const QString& text);
void takeSnapshot(bool notify = true, bool includeAnimated = false, float aspectRatio = 0.0f);
void makeConnection(bool success, const QString& userNameOrError);
void shareSnapshot(const QString& path, const QUrl& href = QUrl(""));
bool isPhysicsEnabled();
@ -74,6 +75,9 @@ signals:
void snapshotShared(const QString& error);
void processingGif();
void connectionAdded(const QString& connectionName);
void connectionError(const QString& errorString);
void messageBoxClosed(int id, int button);
// triggered when window size or position changes

View file

@ -49,6 +49,8 @@
#include "ui/DomainConnectionModel.h"
#include "scripting/AudioDeviceScriptingInterface.h"
#include "ui/AvatarInputs.h"
#include "avatar/AvatarManager.h"
#include "scripting/GlobalServicesScriptingInterface.h"
static const float DPI = 30.47f;
static const float INCHES_TO_METERS = 1.0f / 39.3701f;
@ -194,6 +196,8 @@ void Web3DOverlay::loadSourceURL() {
_webSurface->getRootContext()->setContextProperty("DCModel", DependencyManager::get<DomainConnectionModel>().data());
_webSurface->getRootContext()->setContextProperty("AudioDevice", AudioDeviceScriptingInterface::getInstance());
_webSurface->getRootContext()->setContextProperty("AvatarInputs", AvatarInputs::getInstance());
_webSurface->getRootContext()->setContextProperty("GlobalServices", GlobalServicesScriptingInterface::getInstance());
_webSurface->getRootContext()->setContextProperty("AvatarList", DependencyManager::get<AvatarManager>().data());
_webSurface->getRootContext()->setContextProperty("pathToFonts", "../../");
tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data());

View file

@ -340,7 +340,7 @@ class AvatarData : public QObject, public SpatiallyNestable {
Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness)
Q_PROPERTY(float audioAverageLoudness READ getAudioAverageLoudness WRITE setAudioAverageLoudness)
Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName)
Q_PROPERTY(QString displayName READ getDisplayName WRITE setDisplayName NOTIFY displayNameChanged)
// sessionDisplayName is sanitized, defaulted version displayName that is defined by the AvatarMixer rather than by Interface clients.
// The result is unique among all avatars present at the time.
Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName WRITE setSessionDisplayName)
@ -614,6 +614,9 @@ public:
signals:
void displayNameChanged();
public slots:
void sendAvatarDataPacket();
void sendIdentityPacket();

View file

@ -41,7 +41,7 @@ class AddressManager : public QObject, public Dependency {
Q_PROPERTY(QString pathname READ currentPath)
Q_PROPERTY(QString placename READ getPlaceName)
Q_PROPERTY(QString domainId READ getDomainId)
Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl)
Q_PROPERTY(QUrl metaverseServerUrl READ getMetaverseServerUrl NOTIFY metaverseServerUrlChanged)
public:
Q_INVOKABLE QString protocolVersion();
using PositionGetter = std::function<glm::vec3()>;
@ -123,6 +123,8 @@ signals:
void goBackPossible(bool isPossible);
void goForwardPossible(bool isPossible);
void metaverseServerUrlChanged();
protected:
AddressManager();
private slots:

View file

@ -85,6 +85,7 @@ class TabletProxy : public QObject {
Q_OBJECT
Q_PROPERTY(QString name READ getName)
Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode)
Q_PROPERTY(bool tabletShown MEMBER _tabletShown NOTIFY tabletShownChanged)
public:
TabletProxy(QString name);
@ -206,6 +207,13 @@ signals:
*/
void screenChanged(QVariant type, QVariant url);
/** jsdoc
* Signaled when the tablet becomes visible or becomes invisible
* @function TabletProxy#isTabletShownChanged
* @returns {Signal}
*/
void tabletShownChanged();
protected slots:
void addButtonsToHomeScreen();
void desktopWindowClosed();
@ -224,6 +232,7 @@ protected:
QObject* _qmlOffscreenSurface { nullptr };
QmlWindowClass* _desktopWindow { nullptr };
bool _toolbarMode { false };
bool _tabletShown { false };
enum class State { Uninitialized, Home, Web, Menu, QML };
State _state { State::Uninitialized };

View file

@ -143,7 +143,7 @@ void XMLHttpRequestClass::open(const QString& method, const QString& url, bool a
if (url.toLower().left(METAVERSE_API_URL.length()) == METAVERSE_API_URL) {
auto accountManager = DependencyManager::get<AccountManager>();
if (_url.scheme() == "https" && accountManager->hasValidAccessToken()) {
if (accountManager->hasValidAccessToken()) {
static const QString HTTP_AUTHORIZATION_HEADER = "Authorization";
QString bearerString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token;
_request.setRawHeader(HTTP_AUTHORIZATION_HEADER.toLocal8Bit(), bearerString.toLocal8Bit());

View file

@ -21,10 +21,10 @@ var DEFAULT_SCRIPTS_COMBINED = [
"system/snapshot.js",
"system/help.js",
"system/pal.js", // "system/mod.js", // older UX, if you prefer
"system/makeUserConnection.js",
"system/tablet-goto.js",
"system/marketplaces/marketplaces.js",
"system/edit.js",
"system/tablet-users.js",
"system/selectAudioDevice.js",
"system/notifications.js",
"system/dialTone.js",

View file

@ -0,0 +1,848 @@
"use strict";
//
// makeUserConnetion.js
// scripts/system
//
// 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
//
(function() { // BEGIN LOCAL_SCOPE
const label = "makeUserConnection";
const MAX_AVATAR_DISTANCE = 0.2; // m
const GRIP_MIN = 0.05; // goes from 0-1, so 5% pressed is pressed
const MESSAGE_CHANNEL = "io.highfidelity.makeUserConnection";
const STATES = {
inactive : 0,
waiting: 1,
connecting: 2,
makingConnection: 3
};
const STATE_STRINGS = ["inactive", "waiting", "connecting", "makingConnection"];
const WAITING_INTERVAL = 100; // ms
const CONNECTING_INTERVAL = 100; // ms
const MAKING_CONNECTION_TIMEOUT = 800; // ms
const CONNECTING_TIME = 1600; // ms
const PARTICLE_RADIUS = 0.15; // m
const PARTICLE_ANGLE_INCREMENT = 360/45; // 1hz
const HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/4beat_sweep.wav";
const SUCCESSFUL_HANDSHAKE_SOUND_URL = "https://s3-us-west-1.amazonaws.com/hifi-content/davidkelly/production/audio/3rdbeat_success_bell.wav";
const HAPTIC_DATA = {
initial: { duration: 20, strength: 0.6}, // duration is in ms
background: { duration: 100, strength: 0.3 }, // duration is in ms
success: { duration: 60, strength: 1.0} // duration is in ms
};
const PARTICLE_EFFECT_PROPS = {
"alpha": 0.8,
"azimuthFinish": Math.PI,
"azimuthStart": -1*Math.PI,
"emitRate": 500,
"emitSpeed": 0.0,
"emitterShouldTrail": 1,
"isEmitting": 1,
"lifespan": 3,
"maxParticles": 1000,
"particleRadius": 0.003,
"polarStart": 1,
"polarFinish": 1,
"radiusFinish": 0.008,
"radiusStart": 0.0025,
"speedSpread": 0.025,
"textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png",
"color": {"red": 255, "green": 255, "blue": 255},
"colorFinish": {"red": 0, "green": 164, "blue": 255},
"colorStart": {"red": 255, "green": 255, "blue": 255},
"emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71},
"emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0},
"accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0},
"dimensions": {"x":0.05, "y": 0.05, "z": 0.05},
"type": "ParticleEffect"
};
const MAKING_CONNECTION_PARTICLE_PROPS = {
"alpha": 0.07,
"alphaStart":0.011,
"alphaSpread": 0,
"alphaFinish": 0,
"azimuthFinish": Math.PI,
"azimuthStart": -1*Math.PI,
"emitRate": 2000,
"emitSpeed": 0.0,
"emitterShouldTrail": 1,
"isEmitting": 1,
"lifespan": 3.6,
"maxParticles": 4000,
"particleRadius": 0.048,
"polarStart": 0,
"polarFinish": 1,
"radiusFinish": 0.3,
"radiusStart": 0.04,
"speedSpread": 0.01,
"radiusSpread": 0.9,
"textures": "http://hifi-content.s3.amazonaws.com/alan/dev/Particles/Bokeh-Particle.png",
"color": {"red": 200, "green": 170, "blue": 255},
"colorFinish": {"red": 0, "green": 134, "blue": 255},
"colorStart": {"red": 185, "green": 222, "blue": 255},
"emitOrientation": {"w": -0.71, "x":0.0, "y":0.0, "z": 0.71},
"emitAcceleration": {"x": 0.0, "y": 0.0, "z": 0.0},
"accelerationSpread": {"x": 0.0, "y": 0.0, "z": 0.0},
"dimensions": {"x":0.05, "y": 0.05, "z": 0.05},
"type": "ParticleEffect"
};
var currentHand;
var state = STATES.inactive;
var connectingInterval;
var waitingInterval;
var makingConnectionTimeout;
var animHandlerId;
var connectingId;
var connectingHand;
var waitingList = {};
var particleEffect;
var waitingBallScale;
var particleRotationAngle = 0.0;
var makingConnectionParticleEffect;
var makingConnectionEmitRate = 2000;
var particleEmitRate = 500;
var handshakeInjector;
var successfulHandshakeInjector;
var handshakeSound;
var successfulHandshakeSound;
function debug() {
var stateString = "<" + STATE_STRINGS[state] + ">";
var connecting = "[" + connectingId + "/" + connectingHand + "]";
print.apply(null, [].concat.apply([label, stateString, JSON.stringify(waitingList), connecting], [].map.call(arguments, JSON.stringify)));
}
function cleanId(guidWithCurlyBraces) {
return guidWithCurlyBraces.slice(1, -1);
}
function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request.
var httpRequest = new XMLHttpRequest(), key;
// 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');
debug('FIXME REMOVE: server response', options, error, response, contentType);
if (!error && contentType.indexOf('application/json') === 0) { // ignoring charset, etc.
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
callback(error, response);
}
};
if (typeof options === 'string') {
options = {uri: options};
}
if (options.url) {
options.uri = options.url;
}
if (!options.method) {
options.method = 'GET';
}
if (options.body && (options.method === 'GET')) { // add query parameters
var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&';
for (key in options.body) {
params.push(key + '=' + options.body[key]);
}
options.uri += appender + params.join('&');
delete options.body;
}
if (options.json) {
options.headers = options.headers || {};
options.headers["Content-type"] = "application/json";
options.body = JSON.stringify(options.body);
}
debug("FIXME REMOVE: final options to send", options);
for (key in options.headers || {}) {
httpRequest.setRequestHeader(key, options.headers[key]);
}
httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body);
}
function handToString(hand) {
if (hand === Controller.Standard.RightHand) {
return "RightHand";
} else if (hand === Controller.Standard.LeftHand) {
return "LeftHand";
}
debug("handToString called without valid hand!");
return "";
}
function stringToHand(hand) {
if (hand == "RightHand") {
return Controller.Standard.RightHand;
} else if (hand == "LeftHand") {
return Controller.Standard.LeftHand;
}
debug("stringToHand called with bad hand string:", hand);
return 0;
}
function handToHaptic(hand) {
if (hand === Controller.Standard.RightHand) {
return 1;
} else if (hand === Controller.Standard.LeftHand) {
return 0;
}
debug("handToHaptic called without a valid hand!");
return -1;
}
function stopWaiting() {
if (waitingInterval) {
waitingInterval = Script.clearInterval(waitingInterval);
}
}
function stopConnecting() {
if (connectingInterval) {
connectingInterval = Script.clearInterval(connectingInterval);
}
}
function stopMakingConnection() {
if (makingConnectionTimeout) {
makingConnectionTimeout = Script.clearTimeout(makingConnectionTimeout);
}
}
// This returns the position of the palm, really. Which relies on the avatar
// having the expected middle1 joint. TODO: fallback for when this isn't part
// of the avatar?
function getHandPosition(avatar, hand) {
if (!hand) {
debug("calling getHandPosition with no hand! (returning avatar position but this is a BUG)");
debug(new Error().stack);
return avatar.position;
}
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;
}
function positionFractionallyTowards(posA, posB, frac) {
return Vec3.sum(posA, Vec3.multiply(frac, Vec3.subtract(posB, posA)));
}
function deleteParticleEffect() {
if (particleEffect) {
particleEffect = Entities.deleteEntity(particleEffect);
}
}
function deleteMakeConnectionParticleEffect() {
if (makingConnectionParticleEffect) {
makingConnectionParticleEffect = Entities.deleteEntity(makingConnectionParticleEffect);
}
}
function stopHandshakeSound() {
if (handshakeInjector) {
handshakeInjector.stop();
handshakeInjector = null;
}
}
function calcParticlePos(myHand, otherHand, otherOrientation, reset) {
if (reset) {
particleRotationAngle = 0.0;
}
var position = positionFractionallyTowards(myHand, otherHand, 0.5);
particleRotationAngle += PARTICLE_ANGLE_INCREMENT; // about 0.5 hz
var radius = Math.min(PARTICLE_RADIUS, PARTICLE_RADIUS * particleRotationAngle / 360);
var axis = Vec3.mix(Quat.getFront(MyAvatar.orientation), Quat.inverse(Quat.getFront(otherOrientation)), 0.5);
return Vec3.sum(position, Vec3.multiplyQbyV(Quat.angleAxis(particleRotationAngle, axis), {x: 0, y: radius, z: 0}));
}
// this is called frequently, but usually does nothing
function updateVisualization() {
if (state == STATES.inactive) {
deleteParticleEffect();
deleteMakeConnectionParticleEffect();
// this should always be true if inactive, but just in case:
currentHand = undefined;
return;
}
var myHandPosition = getHandPosition(MyAvatar, currentHand);
var otherHand;
var otherOrientation;
if (connectingId) {
var other = AvatarList.getAvatar(connectingId);
if (other) {
otherOrientation = other.orientation;
otherHand = getHandPosition(other, stringToHand(connectingHand));
}
}
var wrist = MyAvatar.getJointPosition(MyAvatar.getJointIndex(handToString(currentHand)));
var d = Math.min(MAX_AVATAR_DISTANCE, Vec3.distance(wrist, myHandPosition));
switch (state) {
case STATES.waiting:
// no visualization while waiting
deleteParticleEffect();
deleteMakeConnectionParticleEffect();
stopHandshakeSound();
break;
case STATES.connecting:
var particleProps = {};
// put the position between the 2 hands, if we have a connectingId. This
// helps define the plane in which the particles move.
positionFractionallyTowards(myHandPosition, otherHand, 0.5);
// now manage the rest of the entity
if (!particleEffect) {
particleRotationAngle = 0.0;
particleEmitRate = 500;
particleProps = PARTICLE_EFFECT_PROPS;
particleProps.isEmitting = 0;
particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation);
particleProps.parentID = MyAvatar.sessionUUID;
particleEffect = Entities.addEntity(particleProps, true);
} else {
particleProps.position = calcParticlePos(myHandPosition, otherHand, otherOrientation);
particleProps.isEmitting = 1;
Entities.editEntity(particleEffect, particleProps);
}
if (!makingConnectionParticleEffect) {
var props = MAKING_CONNECTION_PARTICLE_PROPS;
props.parentID = MyAvatar.sessionUUID;
makingConnectionEmitRate = 2000;
props.emitRate = makingConnectionEmitRate;
props.position = myHandPosition;
makingConnectionParticleEffect = Entities.addEntity(props, true);
} else {
makingConnectionEmitRate *= 0.5;
Entities.editEntity(makingConnectionParticleEffect, {emitRate: makingConnectionEmitRate, position: myHandPosition, isEmitting: 1});
}
break;
case STATES.makingConnection:
particleEmitRate = Math.max(50, particleEmitRate * 0.5);
Entities.editEntity(makingConnectionParticleEffect, {emitRate: 0, isEmitting: 0, position: myHandPosition});
Entities.editEntity(particleEffect, {position: calcParticlePos(myHandPosition, otherHand, otherOrientation), emitRate: particleEmitRate});
break;
default:
debug("unexpected state", state);
break;
}
}
function isNearby(id, hand) {
if (currentHand) {
var handPos = getHandPosition(MyAvatar, currentHand);
var avatar = AvatarList.getAvatar(id);
if (avatar) {
var otherHand = stringToHand(hand);
var distance = Vec3.distance(getHandPosition(avatar, otherHand), handPos);
return (distance < MAX_AVATAR_DISTANCE);
}
}
return false;
}
function findNearestWaitingAvatar() {
var handPos = getHandPosition(MyAvatar, currentHand);
var minDistance = MAX_AVATAR_DISTANCE;
var nearestAvatar = {};
Object.keys(waitingList).forEach(function (identifier) {
var avatar = AvatarList.getAvatar(identifier);
if (avatar) {
var hand = stringToHand(waitingList[identifier]);
var distance = Vec3.distance(getHandPosition(avatar, hand), handPos);
if (distance < minDistance) {
minDistance = distance;
nearestAvatar = {avatar: identifier, hand: hand};
}
}
});
return nearestAvatar;
}
// As currently implemented, we select the closest waiting avatar (if close enough) and send
// them a connectionRequest. If nobody is close enough we send a waiting message, and wait for a
// connectionRequest. If the 2 people who want to connect are both somewhat out of range when they
// initiate the shake, they will race to see who sends the connectionRequest after noticing the
// waiting message. Either way, they will start connecting eachother at that point.
function startHandshake(fromKeyboard) {
if (fromKeyboard) {
debug("adding animation");
// just in case order of press/unpress is broken
if (animHandlerId) {
animHandlerId = MyAvatar.removeAnimationStateHandler(animHandlerId);
}
animHandlerId = MyAvatar.addAnimationStateHandler(shakeHandsAnimation, []);
}
debug("starting handshake for", currentHand);
pollCount = 0;
state = STATES.waiting;
connectingId = undefined;
connectingHand = undefined;
// just in case
stopWaiting();
stopConnecting();
stopMakingConnection();
var nearestAvatar = findNearestWaitingAvatar();
if (nearestAvatar.avatar) {
connectingId = nearestAvatar.avatar;
connectingHand = handToString(nearestAvatar.hand);
debug("sending connectionRequest to", connectingId);
messageSend({
key: "connectionRequest",
id: connectingId,
hand: handToString(currentHand)
});
} else {
// send waiting message
debug("sending waiting message");
messageSend({
key: "waiting",
hand: handToString(currentHand)
});
lookForWaitingAvatar();
}
}
function endHandshake() {
debug("ending handshake for", currentHand);
deleteParticleEffect();
deleteMakeConnectionParticleEffect();
currentHand = undefined;
// note that setting the state to inactive should really
// only be done here, unless we change how the triggering works,
// as we ignore the key release event when inactive. See updateTriggers
// below.
state = STATES.inactive;
connectingId = undefined;
connectingHand = undefined;
stopWaiting();
stopConnecting();
stopMakingConnection();
stopHandshakeSound();
// send done to let connection know you are not making connections now
messageSend({
key: "done"
});
if (animHandlerId) {
debug("removing animation");
MyAvatar.removeAnimationStateHandler(animHandlerId);
}
// No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us in a weird state.
request({uri: requestUrl, method: 'DELETE'}, debug);
}
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) {
debug("updateTriggers called - gripping", handToString(hand));
if (state != STATES.inactive) {
return;
} else {
startHandshake(fromKeyboard);
}
} else {
// TODO: should we end handshake even when inactive? Ponder
debug("updateTriggers called -- no longer gripping", handToString(hand));
if (state != STATES.inactive) {
endHandshake();
} else {
return;
}
}
}
function messageSend(message) {
Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message));
}
function lookForWaitingAvatar() {
// we started with nobody close enough, but maybe I've moved
// or they did. Note that 2 people doing this race, so stop
// as soon as you have a connectingId (which means you got their
// message before noticing they were in range in this loop)
// just in case we reenter before stopping
stopWaiting();
debug("started looking for waiting avatars");
waitingInterval = Script.setInterval(function () {
if (state == STATES.waiting && !connectingId) {
// find the closest in-range avatar, and send connection request
var nearestAvatar = findNearestWaitingAvatar();
if (nearestAvatar.avatar) {
connectingId = nearestAvatar.avatar;
connectingHand = handToString(nearestAvatar.hand);
debug("sending connectionRequest to", connectingId);
messageSend({
key: "connectionRequest",
id: connectingId,
hand: handToString(currentHand)
});
}
} else {
// something happened, stop looking for avatars to connect
stopWaiting();
debug("stopped looking for waiting avatars");
}
}, WAITING_INTERVAL);
}
/* There is a mini-state machine after entering STATES.makingConnection.
We make a request (which might immediately succeed, fail, or neither.
If we immediately fail, we tell the user.
Otherwise, we wait MAKING_CONNECTION_TIMEOUT. At that time, we poll until success or fail.
*/
var result, requestBody, pollCount = 0, requestUrl = location.metaverseServerUrl + '/api/v1/user/connection_request';
function connectionRequestCompleted() { // Final result is in. Do effects.
if (result.status === 'success') { // set earlier
if (!successfulHandshakeInjector) {
successfulHandshakeInjector = Audio.playSound(successfulHandshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true});
} else {
successfulHandshakeInjector.restart();
}
Controller.triggerHapticPulse(HAPTIC_DATA.success.strength, HAPTIC_DATA.success.duration, handToHaptic(currentHand));
// don't change state (so animation continues while gripped)
// but do send a notification, by calling the slot that emits the signal for it
Window.makeConnection(true, result.connection.new_connection ? "You and " + result.connection.username + " are now connected!" : result.connection.username);
return;
} // failed
endHandshake();
debug("failing with result data", result);
// IWBNI we also did some fail sound/visual effect.
Window.makeConnection(false, result.connection);
}
var POLL_INTERVAL_MS = 200, POLL_LIMIT = 5;
function handleConnectionResponseAndMaybeRepeat(error, response) {
// If response is 'pending', set a short timeout to try again.
// If we fail other than pending, set result and immediately call connectionRequestCompleted.
// If we succceed, set result and call connectionRequestCompleted immediately (if we've been polling), and otherwise on a timeout.
if (response && (response.connection === 'pending')) {
debug(response, 'pollCount', pollCount);
if (pollCount++ >= POLL_LIMIT) { // server will expire, but let's not wait that long.
debug('POLL LIMIT REACHED; TIMEOUT: expired message generated by CLIENT');
result = {status: 'error', connection: 'expired'};
connectionRequestCompleted();
} else { // poll
Script.setTimeout(function () {
request({
uri: requestUrl,
// N.B.: server gives bad request if we specify json content type, so don't do that.
body: requestBody
}, handleConnectionResponseAndMaybeRepeat);
}, POLL_INTERVAL_MS);
}
} else if (error || (response.status !== 'success')) {
debug('server fail', error, response.status);
result = error ? {status: 'error', connection: error} : response;
connectionRequestCompleted();
} else {
debug('server success', result);
result = response;
if (pollCount++) {
connectionRequestCompleted();
} else { // Wait for other guy, so that final succcess is at roughly the same time.
Script.setTimeout(connectionRequestCompleted, MAKING_CONNECTION_TIMEOUT);
}
}
}
// this should be where we make the appropriate connection call. For now just make the
// visualization change.
function makeConnection(id) {
// send done to let the connection know you have made connection.
messageSend({
key: "done",
connectionId: id
});
state = STATES.makingConnection;
// continue the haptic background until the timeout fires. When we make calls, we will have an interval
// probably, in which we do this.
Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, MAKING_CONNECTION_TIMEOUT, handToHaptic(currentHand));
requestBody = {node_id: cleanId(MyAvatar.sessionUUID), proposed_node_id: cleanId(id)}; // for use when repeating
// This will immediately set response if successfull (e.g., the other guy got his request in first), or immediate failure,
// and will otherwise poll (using the requestBody we just set).
request({ //
uri: requestUrl,
method: 'POST',
json: true,
body: {user_connection_request: requestBody}
}, handleConnectionResponseAndMaybeRepeat);
}
// we change states, start the connectionInterval 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 CONNECTING_TIME, we make the connection.
function startConnecting(id, hand) {
var count = 0;
debug("connecting", id, "hand", hand);
// do we need to do this?
connectingId = id;
connectingHand = hand;
state = STATES.connecting;
// play sound
if (!handshakeInjector) {
handshakeInjector = Audio.playSound(handshakeSound, {position: getHandPosition(MyAvatar, currentHand), volume: 0.5, localOnly: true});
} else {
handshakeInjector.restart();
}
// send message that we are connecting with them
messageSend({
key: "connecting",
id: id,
hand: handToString(currentHand)
});
Controller.triggerHapticPulse(HAPTIC_DATA.initial.strength, HAPTIC_DATA.initial.duration, handToHaptic(currentHand));
connectingInterval = Script.setInterval(function () {
count += 1;
Controller.triggerHapticPulse(HAPTIC_DATA.background.strength, HAPTIC_DATA.background.duration, handToHaptic(currentHand));
if (state != STATES.connecting) {
debug("stopping connecting interval, state changed");
stopConnecting();
} else if (!isNearby(id, hand)) {
// gotta go back to waiting
debug(id, "moved, back to waiting");
stopConnecting();
messageSend({
key: "done"
});
startHandshake();
} else if (count > CONNECTING_TIME/CONNECTING_INTERVAL) {
debug("made connection with " + id);
makeConnection(id);
stopConnecting();
}
}, CONNECTING_INTERVAL);
}
/*
A simple sequence diagram: NOTE that the ConnectionAck is somewhat
vestigial, and probably should be removed shortly.
Avatar A Avatar B
| |
| <-----(waiting) ----- startHandshake
startHandshake - (connectionRequest) -> |
| |
| <----(connectionAck) -------- |
| <-----(connecting) -- startConnecting
startConnecting ---(connecting) ----> |
| |
| connected
connected |
| <--------- (done) ---------- |
| ---------- (done) ---------> |
*/
function messageHandler(channel, messageString, senderID) {
if (channel !== MESSAGE_CHANNEL) {
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":
// add this guy to waiting object. Any other message from this person will
// remove it from the list
waitingList[senderID] = message.hand;
break;
case "connectionRequest":
delete waitingList[senderID];
if (state == STATES.waiting && message.id == MyAvatar.sessionUUID && (!connectingId || connectingId == senderID)) {
// you were waiting for a connection request, so send the ack. Or, you and the other
// guy raced and both send connectionRequests. Handle that too
connectingId = senderID;
connectingHand = message.hand;
messageSend({
key: "connectionAck",
id: senderID,
hand: handToString(currentHand)
});
} else {
if (state == STATES.waiting && connectingId == senderID) {
// the person you are trying to connect sent a request to someone else. See the
// if statement above. So, don't cry, just start the handshake over again
startHandshake();
}
}
break;
case "connectionAck":
delete waitingList[senderID];
if (state == STATES.waiting && (!connectingId || connectingId == senderID)) {
if (message.id == MyAvatar.sessionUUID) {
// start connecting...
connectingId = senderID;
connectingHand = message.hand;
stopWaiting();
startConnecting(senderID, message.hand);
} else {
if (connectingId) {
// this is for someone else (we lost race in connectionRequest),
// so lets start over
startHandshake();
}
}
}
// TODO: check to see if we are waiting for this but the person we are connecting sent it to
// someone else, and try again
break;
case "connecting":
delete waitingList[senderID];
if (state == STATES.waiting && senderID == connectingId) {
// temporary logging
if (connectingHand != message.hand) {
debug("connecting hand", connectingHand, "not same as connecting hand in message", message.hand);
}
connectingHand = message.hand;
if (message.id != MyAvatar.sessionUUID) {
// the person we were trying to connect is connecting to someone else
// so try again
startHandshake();
break;
}
startConnecting(senderID, message.hand);
}
break;
case "done":
delete waitingList[senderID];
if (state == STATES.connecting && connectingId == senderID) {
// if they are done, and didn't connect us, terminate our
// connecting
if (message.connectionId !== MyAvatar.sessionUUID) {
stopConnecting();
// now just call startHandshake. Should be ok to do so without a
// value for isKeyboard, as we should not change the animation
// state anyways (if any)
startHandshake();
}
} else {
// if waiting or inactive, lets clear the connecting id. If in makingConnection,
// do nothing
if (state != STATES.makingConnection && connectingId == senderID) {
connectingId = undefined;
connectingHand = undefined;
if (state != STATES.inactive) {
startHandshake();
}
}
}
break;
default:
debug("unknown message", message);
break;
}
}
Messages.subscribe(MESSAGE_CHANNEL);
Messages.messageReceived.connect(messageHandler);
function makeGripHandler(hand, animate) {
// determine if we are gripping or un-gripping
if (animate) {
return function(value) {
updateTriggers(value, true, hand);
};
} else {
return function (value) {
updateTriggers(value, false, hand);
};
}
}
function keyPressEvent(event) {
if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) {
updateTriggers(1.0, true, Controller.Standard.RightHand);
}
}
function keyReleaseEvent(event) {
if ((event.text === "x") && !event.isAutoRepeat && !event.isShifted && !event.isMeta && !event.isControl && !event.isAlt) {
updateTriggers(0.0, true, Controller.Standard.RightHand);
}
}
// map controller actions
var connectionMapping = Controller.newMapping(Script.resolvePath('') + '-grip');
connectionMapping.from(Controller.Standard.LeftGrip).peek().to(makeGripHandler(Controller.Standard.LeftHand));
connectionMapping.from(Controller.Standard.RightGrip).peek().to(makeGripHandler(Controller.Standard.RightHand));
// setup keyboard initiation
Controller.keyPressEvent.connect(keyPressEvent);
Controller.keyReleaseEvent.connect(keyReleaseEvent);
// xbox controller cuz that's important
connectionMapping.from(Controller.Standard.RB).peek().to(makeGripHandler(Controller.Standard.RightHand, true));
// it is easy to forget this and waste a lot of time for nothing
connectionMapping.enable();
// connect updateVisualization to update frequently
Script.update.connect(updateVisualization);
// load the sounds when the script loads
handshakeSound = SoundCache.getSound(HANDSHAKE_SOUND_URL);
successfulHandshakeSound = SoundCache.getSound(SUCCESSFUL_HANDSHAKE_SOUND_URL);
Script.scriptEnding.connect(function () {
debug("removing controller mappings");
connectionMapping.disable();
debug("removing key mappings");
Controller.keyPressEvent.disconnect(keyPressEvent);
Controller.keyReleaseEvent.disconnect(keyReleaseEvent);
debug("disconnecting updateVisualization");
Script.update.disconnect(updateVisualization);
deleteParticleEffect();
deleteMakeConnectionParticleEffect();
});
}()); // END LOCAL_SCOPE

View file

@ -95,12 +95,14 @@ var NotificationType = {
CONNECTION_REFUSED: 3,
EDIT_ERROR: 4,
TABLET: 5,
CONNECTION: 6,
properties: [
{ text: "Snapshot" },
{ text: "Level of Detail" },
{ text: "Connection Refused" },
{ text: "Edit error" },
{ text: "Tablet" }
{ text: "Tablet" },
{ text: "Connection" }
],
getTypeFromMenuItem: function(menuItemName) {
if (menuItemName.substr(menuItemName.length - NOTIFICATION_MENU_ITEM_POST.length) !== NOTIFICATION_MENU_ITEM_POST) {
@ -545,6 +547,14 @@ function processingGif() {
createNotification("Processing GIF snapshot...", NotificationType.SNAPSHOT);
}
function connectionAdded(connectionName) {
createNotification(connectionName, NotificationType.CONNECTION);
}
function connectionError(error) {
createNotification(wordWrap("Error trying to make connection: " + error), NotificationType.CONNECTION);
}
// handles mouse clicks on buttons
function mousePressEvent(event) {
var pickRay,
@ -645,6 +655,8 @@ Menu.menuItemEvent.connect(menuItemEvent);
Window.domainConnectionRefused.connect(onDomainConnectionRefused);
Window.snapshotTaken.connect(onSnapshotTaken);
Window.processingGif.connect(processingGif);
Window.connectionAdded.connect(connectionAdded);
Window.connectionError.connect(connectionError);
Window.notifyEditError = onEditError;
Window.notify = onNotify;
Tablet.tabletNotification.connect(tabletNotification);

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*/
/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */
//
// pal.js
@ -14,6 +14,12 @@
(function() { // BEGIN LOCAL_SCOPE
var populateNearbyUserList, color, textures, removeOverlays,
controllerComputePickRay, onTabletButtonClicked, onTabletScreenChanged,
receiveMessage, avatarDisconnected, clearLocalQMLDataAndClosePAL,
createAudioInterval, tablet, CHANNEL, getConnectionData, findableByChanged,
avatarAdded, avatarRemoved, avatarSessionChanged; // 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 +103,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 +219,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 +251,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,14 +261,45 @@ 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 'displayNameUpdate':
if (MyAvatar.displayName !== message.params) {
MyAvatar.displayName = message.params;
UserActivityLogger.palAction("display_name_change", "");
}
case 'refreshConnections':
print('Refreshing Connections...');
getConnectionData();
UserActivityLogger.palAction("refresh_connections", "");
break;
case 'removeFriend':
friendUserName = message.params;
request({
uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName,
method: 'DELETE'
}, function (error, response) {
print(JSON.stringify(response));
if (error || (response.status !== 'success')) {
print("Error: unable to unfriend", friendUserName, error || response.status);
return;
}
getConnectionData();
});
break
case 'addFriend':
friendUserName = message.params;
request({
uri: METAVERSE_BASE + '/api/v1/user/friends',
method: 'POST',
json: true,
body: {
username: friendUserName,
}
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to friend " + friendUserName, error || response.status);
return;
}
getConnectionData(); // For now, just refresh all connection data. Later, just refresh the one friended row.
}
);
break;
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
@ -274,6 +309,141 @@ 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 = location.metaverseServerUrl;
function request(options, callback) { // cb(error, responseOfCorrectContentType) of url. A subset of npm request.
var httpRequest = new XMLHttpRequest(), key;
// 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) { // ignoring charset, etc.
try {
response = JSON.parse(response);
} catch (e) {
error = e;
}
}
callback(error, response);
}
};
if (typeof options === 'string') {
options = {uri: options};
}
if (options.url) {
options.uri = options.url;
}
if (!options.method) {
options.method = 'GET';
}
if (options.body && (options.method === 'GET')) { // add query parameters
var params = [], appender = (-1 === options.uri.search('?')) ? '?' : '&';
for (key in options.body) {
params.push(key + '=' + options.body[key]);
}
options.uri += appender + params.join('&');
delete options.body;
}
if (options.json) {
options.headers = options.headers || {};
options.headers["Content-type"] = "application/json";
options.body = JSON.stringify(options.body);
}
for (key in options.headers || {}) {
httpRequest.setRequestHeader(key, options.headers[key]);
}
httpRequest.open(options.method, options.uri, true);
httpRequest.send(options.body);
}
function requestJSON(url, callback) { // callback(data) if successfull. Logs otherwise.
request({
uri: 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({
uri: 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.
url = METAVERSE_BASE + '/api/v1/users?'
if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces
} else {
url += 'filter=connections'; // regardless of whether online
}
requestJSON(url, function (connectionsData) {
// 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.
var users = connectionsData.users;
function addPicture(index) {
if (index >= users.length) {
return callback(users);
}
var user = users[index];
getProfilePicture(user.username, function (url) {
user.profileUrl = url;
addPicture(index + 1);
});
}
addPicture(0);
});
}
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
var formattedSessionId = user.location.node_id || '';
if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
formattedSessionId = "{" + formattedSessionId + "}";
}
return {
sessionId: formattedSessionId,
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 +455,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 +472,8 @@ function populateUserList(selectData, oldAudioData) {
forward = filter && Quat.getForward(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,26 +495,33 @@ 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,
admin: false,
personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null
ignore: !!id && Users.getIgnoreStatus(id) // ditto
ignore: !!id && Users.getIgnoreStatus(id), // ditto
isPresent: true
};
if (id) {
addAvatarNode(id); // No overlay for ourselves
// 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;
}
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 +530,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 +560,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 +593,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 +718,9 @@ function startup() {
Messages.subscribe(CHANNEL);
Messages.messageReceived.connect(receiveMessage);
Users.avatarDisconnected.connect(avatarDisconnected);
AvatarList.avatarAddedEvent.connect(avatarAdded);
AvatarList.avatarRemovedEvent.connect(avatarRemoved);
AvatarList.avatarSessionChangedEvent.connect(avatarSessionChanged);
}
startup();
@ -556,6 +734,7 @@ function off() {
Script.update.disconnect(updateOverlays);
Controller.mousePressEvent.disconnect(handleMouseEvent);
Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent);
tablet.tabletShownChanged.disconnect(tabletVisibilityChanged);
isWired = false;
}
if (audioTimer) {
@ -567,6 +746,12 @@ function off() {
Users.requestsDomainListData = false;
}
function tabletVisibilityChanged() {
if (!tablet.tabletShown) {
tablet.gotoHomeScreen();
}
}
var onPalScreen = false;
var shouldActivateButton = false;
@ -577,9 +762,10 @@ function onTabletButtonClicked() {
} else {
shouldActivateButton = true;
tablet.loadQMLSource("../Pal.qml");
tablet.tabletShownChanged.connect(tabletVisibilityChanged);
onPalScreen = true;
Users.requestsDomainListData = true;
populateUserList();
populateNearbyUserList();
isWired = true;
Script.update.connect(updateOverlays);
Controller.mousePressEvent.connect(handleMouseEvent);
@ -607,8 +793,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 +818,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 +844,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 +862,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});
@ -695,6 +879,18 @@ function clearLocalQMLDataAndClosePAL() {
sendToQml({ method: 'clearLocalQMLData' });
}
function avatarAdded(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarAdded'] });
}
function avatarRemoved(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarRemoved'] });
}
function avatarSessionChanged(avatarID) {
sendToQml({ method: 'palIsStale', params: [avatarID, 'avatarSessionChanged'] });
}
function shutdown() {
if (onPalScreen) {
tablet.gotoHomeScreen();
@ -708,6 +904,9 @@ function shutdown() {
Messages.subscribe(CHANNEL);
Messages.messageReceived.disconnect(receiveMessage);
Users.avatarDisconnected.disconnect(avatarDisconnected);
AvatarList.avatarAddedEvent.disconnect(avatarAdded);
AvatarList.avatarRemovedEvent.disconnect(avatarRemoved);
AvatarList.avatarSessionChangedEvent.disconnect(avatarSessionChanged);
off();
}

View file

@ -16,7 +16,6 @@
MyAvatar, Menu */
(function() { // BEGIN LOCAL_SCOPE
var tabletShown = false;
var tabletRezzed = false;
var activeHand = null;
var DEFAULT_WIDTH = 0.4375;
@ -93,7 +92,7 @@
}
function showTabletUI() {
tabletShown = true;
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = true;
if (!tabletRezzed || !tabletIsValid()) {
closeTabletUI()
@ -117,7 +116,7 @@
}
function hideTabletUI() {
tabletShown = false;
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false;
if (!UIWebTablet) {
return;
}
@ -141,7 +140,7 @@
}
function closeTabletUI() {
tabletShown = false;
Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false;
if (UIWebTablet) {
if (UIWebTablet.onClose) {
UIWebTablet.onClose();
@ -168,6 +167,7 @@
var now = Date.now();
// close the WebTablet if it we go into toolbar mode.
var tabletShown = Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown;
var toolbarMode = Tablet.getTablet("com.highfidelity.interface.tablet.system").toolbarMode;
var visibleToOthers = Settings.getValue("tabletVisibleToOthers");