mirror of
https://github.com/JulianGro/overte.git
synced 2025-04-08 18:02:13 +02:00
commit
8a96969471
30 changed files with 2840 additions and 674 deletions
|
@ -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);
|
||||
|
|
48
interface/resources/icons/connection.svg
Normal file
48
interface/resources/icons/connection.svg
Normal 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 |
BIN
interface/resources/icons/profilePicLoading.gif
Normal file
BIN
interface/resources/icons/profilePicLoading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
154
interface/resources/qml/hifi/ComboDialog.qml
Normal file
154
interface/resources/qml/hifi/ComboDialog.qml
Normal 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]});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() :
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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",
|
||||
|
|
848
scripts/system/makeUserConnection.js
Normal file
848
scripts/system/makeUserConnection.js
Normal 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
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
|
||||
|
|
Loading…
Reference in a new issue