diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index d2bfdde7ea..f09caac4b6 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -187,7 +187,7 @@ void AvatarMixer::start() { // NOTE: nodeData->getAvatar() might be side effected, must be called when access to node/nodeData -// is guarenteed to not be accessed by other thread +// is guaranteed to not be accessed by other thread void AvatarMixer::manageDisplayName(const SharedNodePointer& node) { AvatarMixerClientData* nodeData = reinterpret_cast(node->getLinkedData()); if (nodeData && nodeData->getAvatarSessionDisplayNameMustChange()) { @@ -437,17 +437,20 @@ void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer 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(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(ignoredNode->getLinkedData()); + ignoredNodeData->setLastBroadcastTime(senderNode->getUUID(), 0); + } if (addToIgnore) { senderNode->addIgnoredNode(ignoredUUID); diff --git a/assignment-client/src/avatars/AvatarMixerSlavePool.h b/assignment-client/src/avatars/AvatarMixerSlavePool.h index 6bef0515bb..e6ac2a1f4e 100644 --- a/assignment-client/src/avatars/AvatarMixerSlavePool.h +++ b/assignment-client/src/avatars/AvatarMixerSlavePool.h @@ -49,7 +49,7 @@ private: bool _stop { false }; }; -// Slave pool for audio mixers +// Slave pool for avatar mixers // AvatarMixerSlavePool is not thread-safe! It should be instantiated and used from a single thread. class AvatarMixerSlavePool { using Queue = tbb::concurrent_queue; diff --git a/interface/resources/QtWebEngine/UIDelegates/Menu.qml b/interface/resources/QtWebEngine/UIDelegates/Menu.qml index 5176d9d11e..1bbbbd6cbe 100644 --- a/interface/resources/QtWebEngine/UIDelegates/Menu.qml +++ b/interface/resources/QtWebEngine/UIDelegates/Menu.qml @@ -1,7 +1,6 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 as Controls -import "../../qml/menus" import "../../qml/controls-uit" import "../../qml/styles-uit" diff --git a/interface/resources/icons/connection.svg b/interface/resources/icons/connection.svg new file mode 100644 index 0000000000..05b23abf9a --- /dev/null +++ b/interface/resources/icons/connection.svg @@ -0,0 +1,48 @@ + + + + + + + + diff --git a/interface/resources/icons/profilePicLoading.gif b/interface/resources/icons/profilePicLoading.gif new file mode 100644 index 0000000000..4500f4dda3 Binary files /dev/null and b/interface/resources/icons/profilePicLoading.gif differ diff --git a/interface/resources/qml/Browser.qml b/interface/resources/qml/Browser.qml index bd98e42709..c4e0c85642 100644 --- a/interface/resources/qml/Browser.qml +++ b/interface/resources/qml/Browser.qml @@ -33,6 +33,10 @@ ScrollingWindow { addressBar.text = webview.url } + function setProfile(profile) { + webview.profile = profile; + } + function showPermissionsBar(){ permissionsContainer.visible=true; } diff --git a/interface/resources/qml/TabletBrowser.qml b/interface/resources/qml/TabletBrowser.qml new file mode 100644 index 0000000000..312b811928 --- /dev/null +++ b/interface/resources/qml/TabletBrowser.qml @@ -0,0 +1,186 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.2 +import QtWebChannel 1.0 +import QtWebEngine 1.2 + +import "controls" +import "styles" as HifiStyles +import "styles-uit" +import "windows" +import HFWebEngineProfile 1.0 + +Item { + id: root + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } + //width: parent.width + height: 600 + property variant permissionsBar: {'securityOrigin':'none','feature':'none'} + property alias url: webview.url + property WebEngineView webView: webview + property alias eventBridge: eventBridgeWrapper.eventBridge + property bool canGoBack: webview.canGoBack + property bool canGoForward: webview.canGoForward + + + signal loadingChanged(int status) + + x: 0 + y: 0 + + function goBack() { + webview.goBack(); + } + + function goForward() { + webview.goForward(); + } + + function gotoPage(url) { + webview.url = url; + } + + function setProfile(profile) { + webview.profile = profile; + } + + function reloadPage() { + webview.reloadAndBypassCache(); + webview.setActiveFocusOnPress(true); + webview.setEnabled(true); + } + + Item { + id:item + width: parent.width + implicitHeight: parent.height + + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + WebEngineView { + id: webview + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? parent.height - keyboard.height : parent.height + + profile: HFWebEngineProfile { + id: webviewProfile + storageName: "qmlWebEngine" + } + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: webview.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + // Ensure the JS from the web-engine makes it to our logging + webview.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("TabletBrowser"); + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + webview.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface"; + web.address = url; + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onActiveFocusOnPressChanged: { + console.log("on active focus changed"); + setActiveFocusOnPress(true); + } + + onNewViewRequested:{ + // desktop is not defined for web-entities + if (stackRoot.isDesktop) { + var component = Qt.createComponent("./Browser.qml"); + var newWindow = component.createObject(desktop); + request.openIn(newWindow.webView); + } else { + var component = Qt.createComponent("./TabletBrowser.qml"); + + if (component.status != Component.Ready) { + if (component.status == Component.Error) { + console.log("Error: " + component.errorString()); + } + return; + } + var newWindow = component.createObject(); + newWindow.setProfile(webview.profile); + request.openIn(newWindow.webView); + newWindow.eventBridge = web.eventBridge; + stackRoot.push(newWindow); + newWindow.webView.forceActiveFocus(); + } + } + } + + } // item + + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } + + } // dialog diff --git a/interface/resources/qml/controls-uit/CheckBox.qml b/interface/resources/qml/controls-uit/CheckBox.qml index 9fc484586a..60e136d13a 100644 --- a/interface/resources/qml/controls-uit/CheckBox.qml +++ b/interface/resources/qml/controls-uit/CheckBox.qml @@ -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 } diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml index 2dea535c06..be6a439e57 100644 --- a/interface/resources/qml/controls-uit/ComboBox.qml +++ b/interface/resources/qml/controls-uit/ComboBox.qml @@ -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 } diff --git a/interface/resources/qml/controls-uit/Table.qml b/interface/resources/qml/controls-uit/Table.qml index c7e0809b29..11d1920f95 100644 --- a/interface/resources/qml/controls-uit/Table.qml +++ b/interface/resources/qml/controls-uit/Table.qml @@ -48,11 +48,12 @@ TableView { HiFiGlyphs { id: titleSort text: sortIndicatorOrder == Qt.AscendingOrder ? hifi.glyphs.caratUp : hifi.glyphs.caratDn - color: hifi.colors.baseGrayHighlight + color: hifi.colors.darkGray + opacity: 0.6; size: hifi.fontSizes.tableHeadingIcon anchors { left: titleText.right - leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 3 : 0) + leftMargin: -hifi.fontSizes.tableHeadingIcon / 3 - (centerHeaderText ? 5 : 0) right: parent.right rightMargin: hifi.dimensions.tablePadding verticalCenter: titleText.verticalCenter @@ -89,7 +90,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 } diff --git a/interface/resources/qml/controls/TabletWebView.qml b/interface/resources/qml/controls/TabletWebView.qml new file mode 100644 index 0000000000..890215a714 --- /dev/null +++ b/interface/resources/qml/controls/TabletWebView.qml @@ -0,0 +1,271 @@ +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtWebEngine 1.1 +import QtWebChannel 1.0 +import "../controls-uit" as HiFiControls +import "../styles" as HifiStyles +import "../styles-uit" +import HFWebEngineProfile 1.0 +import HFTabletWebEngineProfile 1.0 +import "../" +Item { + id: web + width: parent.width + height: parent.height + property var parentStackItem: null + property int headerHeight: 38 + property alias url: root.url + property string address: url + property alias scriptURL: root.userScriptUrl + property alias eventBridge: eventBridgeWrapper.eventBridge + property bool keyboardEnabled: HMD.active + property bool keyboardRaised: false + property bool punctuationMode: false + property bool isDesktop: false + property WebEngineView view: root + + + Row { + id: buttons + HifiConstants { id: hifi } + HifiStyles.HifiConstants { id: hifistyles } + height: headerHeight + spacing: 4 + anchors.top: parent.top + anchors.topMargin: 8 + anchors.left: parent.left + anchors.leftMargin: 8 + HiFiGlyphs { + id: back; + enabled: true; + text: hifi.glyphs.backward + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: stackRoot.goBack() } + } + + HiFiGlyphs { + id: forward; + enabled: stackRoot.currentItem.canGoForward; + text: hifi.glyphs.forward + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: stackRoot.currentItem.goForward() } + } + + HiFiGlyphs { + id: reload; + enabled: true; + text: webview.loading ? hifi.glyphs.close : hifi.glyphs.reload + color: enabled ? hifistyles.colors.text : hifistyles.colors.disabledText + size: 48 + MouseArea { anchors.fill: parent; onClicked: stackRoot.currentItem.reloadPage(); } + } + + } + + TextField { + id: addressBar + height: 30 + anchors.right: parent.right + anchors.rightMargin: 8 + anchors.left: buttons.right + anchors.leftMargin: 0 + anchors.verticalCenter: buttons.verticalCenter + focus: true + text: address + Component.onCompleted: ScriptDiscoveryService.scriptsModelFilter.filterRegExp = new RegExp("^.*$", "i") + + Keys.onPressed: { + switch (event.key) { + case Qt.Key_Enter: + case Qt.Key_Return: + event.accepted = true; + if (text.indexOf("http") != 0) { + text = "http://" + text; + } + //root.hidePermissionsBar(); + web.keyboardRaised = false; + stackRoot.currentItem.gotoPage(text); + break; + + + } + } + } + + + + StackView { + id: stackRoot + width: parent.width + height: parent.height - web.headerHeight + anchors.top: buttons.bottom + //property var goBack: currentItem.goBack(); + property WebEngineView view: root + + initialItem: root; + + function goBack() { + if (depth > 1 ) { + if (currentItem.canGoBack) { + currentItem.goBack(); + } else { + stackRoot.pop(); + currentItem.webView.forceActiveFocus(); + web.address = currentItem.webView.url; + } + } else { + if (currentItem.canGoBack) { + currentItem.goBack(); + } else if (parentStackItem) { + web.parentStackItem.pop(); + } + } + } + + QtObject { + id: eventBridgeWrapper + WebChannel.id: "eventBridgeWrapper" + property var eventBridge; + } + + WebEngineView { + id: root + objectName: "webEngineView" + x: 0 + y: 0 + width: parent.width + height: keyboardEnabled && keyboardRaised ? (parent.height - keyboard.height) : parent.height + profile: HFTabletWebEngineProfile { + id: webviewTabletProfile + storageName: "qmlTabletWebEngine" + } + + property WebEngineView webView: root + function reloadPage() { + root.reload(); + } + + function gotoPage(url) { + root.url = url; + } + + property string userScriptUrl: "" + + // creates a global EventBridge object. + WebEngineScript { + id: createGlobalEventBridge + sourceCode: eventBridgeJavaScriptToInject + injectionPoint: WebEngineScript.DocumentCreation + worldId: WebEngineScript.MainWorld + } + + // detects when to raise and lower virtual keyboard + WebEngineScript { + id: raiseAndLowerKeyboard + injectionPoint: WebEngineScript.Deferred + sourceUrl: resourceDirectoryUrl + "/html/raiseAndLowerKeyboard.js" + worldId: WebEngineScript.MainWorld + } + + // User script. + WebEngineScript { + id: userScript + sourceUrl: root.userScriptUrl + injectionPoint: WebEngineScript.DocumentReady // DOM ready but page load may not be finished. + worldId: WebEngineScript.MainWorld + } + + userScripts: [ createGlobalEventBridge, raiseAndLowerKeyboard, userScript ] + + property string newUrl: "" + + webChannel.registeredObjects: [eventBridgeWrapper] + + Component.onCompleted: { + // Ensure the JS from the web-engine makes it to our logging + root.javaScriptConsoleMessage.connect(function(level, message, lineNumber, sourceID) { + console.log("WebView.qml"); + console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); + }); + + root.profile.httpUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36" + + } + + onFeaturePermissionRequested: { + grantFeaturePermission(securityOrigin, feature, true); + } + + onLoadingChanged: { + keyboardRaised = false; + punctuationMode = false; + keyboard.resetShiftMode(false); + console.log("[DR] -> printing user string " + root.profile.httpUserAgent); + // Required to support clicking on "hifi://" links + if (WebEngineView.LoadStartedStatus == loadRequest.status) { + var url = loadRequest.url.toString(); + if (urlHandler.canHandleUrl(url)) { + if (urlHandler.handleUrl(url)) { + root.stop(); + } + } + } + } + + onNewViewRequested:{ + // desktop is not defined for web-entities + if (web.isDesktop) { + var component = Qt.createComponent("../Browser.qml"); + var newWindow = component.createObject(desktop); + newWindow.setProfile(root.profile); + request.openIn(newWindow.webView); + } else { + var component = Qt.createComponent("../TabletBrowser.qml"); + + if (component.status != Component.Ready) { + if (component.status == Component.Error) { + console.log("Error: " + component.errorString()); + } + return; + } + var newWindow = component.createObject(); + newWindow.setProfile(root.profile); + request.openIn(newWindow.webView); + newWindow.eventBridge = web.eventBridge; + stackRoot.push(newWindow); + } + } + } + + HiFiControls.Keyboard { + id: keyboard + raised: web.keyboardEnabled && web.keyboardRaised + numeric: web.punctuationMode + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + } + } + + } + + Component.onCompleted: { + web.isDesktop = (typeof desktop !== "undefined"); + address = url; + } + + Keys.onPressed: { + switch(event.key) { + case Qt.Key_L: + if (event.modifiers == Qt.ControlModifier) { + event.accepted = true + addressBar.selectAll() + addressBar.forceActiveFocus() + } + break; + } + } +} diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index ae96590e97..a3badd7e1f 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -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"); } } } diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index cc64d0f2b4..d8aedf6666 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -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 { diff --git a/interface/resources/qml/hifi/ComboDialog.qml b/interface/resources/qml/hifi/ComboDialog.qml new file mode 100644 index 0000000000..f5bb7dcfcc --- /dev/null +++ b/interface/resources/qml/hifi/ComboDialog.qml @@ -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]}); + }); + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 020a85b46d..86cc0218a4 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -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 Silence and Ban other users at their discretion - so be extra nice!") - onEntered: adminLabelQuestionMarkText.color = "#94132e" - onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight - } - } - } - - // UserName Text - FiraSansRegular { - id: userNameText - // Properties - text: thisNameCard.userName - elide: Text.ElideRight - visible: thisNameCard.displayName - // Size - width: parent.width - // Anchors - anchors.top: isMyCard ? myDisplayNameSpacer.bottom : displayNameContainer.bottom - // Text Size - size: thisNameCard.usernameTextHeight + color: hifi.colors.darkGray + FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + font.family: firaSansSemiBold.name + font.pixelSize: displayNameTextPixelSize + selectionColor: hifi.colors.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 Silence and Ban other users at their discretion - so be extra nice!") + onEntered: adminLabelQuestionMarkText.color = "#94132e" + onExited: adminLabelQuestionMarkText.color = hifi.colors.redHighlight } } } + // UserName Text + FiraSansRegular { + id: userNameText + // Properties + text: thisNameCard.userName === "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); + } } diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 25362d98f1..06360ce183 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -16,7 +16,8 @@ import QtQuick.Controls 1.4 import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" -import "../controls-uit" as HifiControls +import "../controls-uit" as HifiControlsUit +import "../controls" as HifiControls // references HMD, Users, UserActivityLogger from root context @@ -28,25 +29,36 @@ Rectangle { // Style color: "#E3E3E3"; // Properties - property int myCardHeight: 90; - property int rowHeight: 70; + property int myCardWidth: width - upperRightInfoContainer.width; + property int myCardHeight: 80; + property int rowHeight: 60; property int actionButtonWidth: 55; - property int actionButtonAllowance: actionButtonWidth * 2; - property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth; - property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance); - property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}); // valid dummy until set + property int locationColumnWidth: 170; + property int nearbyNameCardWidth: nearbyTable.width - (iAmAdmin ? (actionButtonWidth * 4) : (actionButtonWidth * 2)) - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property int connectionsNameCardWidth: connectionsTable.width - locationColumnWidth - actionButtonWidth - 4 - hifi.dimensions.scrollbarBackgroundWidth; + property var myData: ({profileUrl: "", displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true, placeName: "", connection: "", isPresent: true}); // valid dummy until set property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring. - property var userModelData: []; // This simple list is essentially a mirror of the userModel listModel without all the extra complexities. + property var nearbyUserModelData: []; // This simple list is essentially a mirror of the nearbyUserModel listModel without all the extra complexities. + property var connectionsUserModelData: []; // This simple list is essentially a mirror of the connectionsUserModel listModel without all the extra complexities. property bool iAmAdmin: false; + property var activeTab: "nearbyTab"; + property bool currentlyEditingDisplayName: false + property bool punctuationMode: false; HifiConstants { id: hifi; } // The letterbox used for popup messages LetterboxMessage { - id: letterboxMessage; z: 999; // Force the popup on top of everything else } + // The ComboDialog used for setting availability + ComboDialog { + id: comboDialog; + z: 999; // Force the ComboDialog on top of everything else + dialogWidth: parent.width - 100; + dialogHeight: parent.height - 100; + } function letterbox(headerGlyph, headerText, message) { letterboxMessage.headerGlyph = headerGlyph; letterboxMessage.headerText = headerText; @@ -54,124 +66,280 @@ Rectangle { letterboxMessage.visible = true; letterboxMessage.popupRadius = 0; } + function popupComboDialog(dialogTitleText, optionTitleText, optionBodyText, optionValues) { + comboDialog.dialogTitleText = dialogTitleText; + comboDialog.optionTitleText = optionTitleText; + comboDialog.optionBodyText = optionBodyText; + comboDialog.optionValues = optionValues; + comboDialog.selectedOptionIndex = ['all', 'connections', 'friends', 'none'].indexOf(GlobalServices.findableBy); + comboDialog.populateComboListViewModel(); + comboDialog.visible = true; + } Settings { id: settings; category: "pal"; property bool filtered: false; property int nearDistance: 30; - property int sortIndicatorColumn: 1; - property int sortIndicatorOrder: Qt.AscendingOrder; + property int nearbySortIndicatorColumn: 1; + property int nearbySortIndicatorOrder: Qt.AscendingOrder; + property int connectionsSortIndicatorColumn: 0; + property int connectionsSortIndicatorOrder: Qt.AscendingOrder; } - function getSelectedSessionIDs() { + function getSelectedNearbySessionIDs() { var sessionIDs = []; - table.selection.forEach(function (userIndex) { - sessionIDs.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + var datum = nearbyUserModelData[userIndex]; + if (datum) { // Might have been filtered out + sessionIDs.push(datum.sessionId); + } }); return sessionIDs; } - function refreshWithFilter() { - // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. - var userIds = getSelectedSessionIDs(); - var params = {filter: filter.checked && {distance: settings.nearDistance}}; + function getSelectedConnectionsUserNames() { + var userNames = []; + connectionsTable.selection.forEach(function (userIndex) { + var datum = connectionsUserModelData[userIndex]; + if (datum) { + userNames.push(datum.userName); + } + }); + return userNames; + } + function refreshNearbyWithFilter() { + // We should just be able to set settings.filtered to inViewCheckbox.checked, but see #3249, so send to .js for saving. + var userIds = getSelectedNearbySessionIDs(); + var params = {filter: inViewCheckbox.checked && {distance: settings.nearDistance}}; if (userIds.length > 0) { params.selected = [[userIds[0]], true, true]; } - pal.sendToScript({method: 'refresh', params: params}); + pal.sendToScript({method: 'refreshNearby', params: params}); } - // This is the container for the PAL - Rectangle { - property bool punctuationMode: false; - id: palContainer; - // Size - width: pal.width - 10; - height: pal.height - 10; - // Style - color: pal.color; + Item { + id: palTabContainer; // Anchors - anchors.centerIn: pal; - // Properties - radius: hifi.dimensions.borderRadius; - - // This contains the current user's NameCard and will contain other information in the future + anchors { + top: myInfo.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } Rectangle { - id: myInfo; - // Size - width: palContainer.width; - height: myCardHeight; - // Style - color: pal.color; + id: tabSelectorContainer; // Anchors - anchors.top: palContainer.top; - // Properties - radius: hifi.dimensions.borderRadius; - // This NameCard refers to the current user's NameCard (the one above the table) - NameCard { - id: myCard; - // Properties - displayName: myData.displayName; - userName: myData.userName; - audioLevel: myData.audioLevel; - avgAudioLevel: myData.avgAudioLevel; - isMyCard: true; - // Size - width: minNameCardWidth; - height: parent.height; + anchors { + top: parent.top; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width; + height: 50; + Rectangle { + id: nearbyTabSelector; // Anchors - anchors.left: parent.left; - } - Row { - HifiControls.CheckBox { - id: filter; - checked: settings.filtered; - text: "in view"; - boxSize: reload.height * 0.70; - onCheckedChanged: refreshWithFilter(); - } - HifiControls.GlyphButton { - id: reload; - glyph: hifi.glyphs.reload; - width: reload.height; - onClicked: refreshWithFilter(); - } - spacing: 50; anchors { - right: parent.right; top: parent.top; - topMargin: 10; + left: parent.left; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "nearbyTab" ? "white" : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "nearbyTab") { + refreshNearbyWithFilter(); + } + activeTab = "nearbyTab"; + connectionsHelpText.color = hifi.colors.baseGray; + } + } + + // "NEARBY" Text Container + Item { + id: nearbyTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // "NEARBY" text + RalewaySemiBold { + id: nearbyTabSelectorText; + text: "NEARBY"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: activeTab === "nearbyTab" ? hifi.colors.blueAccent : hifi.colors.baseGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + // "In View" Checkbox + HifiControlsUit.CheckBox { + id: inViewCheckbox; + visible: activeTab == "nearbyTab"; + anchors.right: reloadNearbyContainer.left; + anchors.rightMargin: 25; + anchors.verticalCenter: parent.verticalCenter; + checked: settings.filtered; + text: "in view"; + boxSize: 24; + onCheckedChanged: refreshNearbyWithFilter(); + } + // Refresh button + Rectangle { + id: reloadNearbyContainer + visible: activeTab == "nearbyTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadNearby.height; + width: height; + HifiControlsUit.GlyphButton { + id: reloadNearby; + width: reloadNearby.height; + glyph: hifi.glyphs.reload; + onClicked: { + refreshNearbyWithFilter(); + } + } + } + } + } + Rectangle { + id: connectionsTabSelector; + // Anchors + anchors { + top: parent.top; + left: nearbyTabSelector.right; + } + width: parent.width/2; + height: parent.height; + color: activeTab == "connectionsTab" ? "white" : "#CCCCCC"; + MouseArea { + anchors.fill: parent; + onClicked: { + if (activeTab != "connectionsTab") { + connectionsLoading.visible = false; + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + activeTab = "connectionsTab"; + connectionsHelpText.color = hifi.colors.blueAccent; + } + } + + // "CONNECTIONS" Text Container + Item { + id: connectionsTabSelectorTextContainer; + anchors.fill: parent; + anchors.leftMargin: 15; + // Refresh button + Rectangle { + visible: activeTab == "connectionsTab"; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 6; + height: reloadConnections.height; + width: height; + HifiControlsUit.GlyphButton { + id: reloadConnections; + width: reloadConnections.height; + glyph: hifi.glyphs.reload; + onClicked: { + connectionsLoading.visible = false; + connectionsLoading.visible = true; + pal.sendToScript({method: 'refreshConnections'}); + } + } + } + // "CONNECTIONS" text + RalewaySemiBold { + id: connectionsTabSelectorText; + text: "CONNECTIONS"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.fill: parent; + // Style + font.capitalization: Font.AllUppercase; + color: activeTab === "connectionsTab" ? hifi.colors.blueAccent : hifi.colors.baseGray; + // Alignment + horizontalAlignment: Text.AlignHLeft; + verticalAlignment: Text.AlignVCenter; + } + TextMetrics { + id: connectionsTabSelectorTextMetrics; + text: connectionsTabSelectorText.text; + } + + // This Rectangle refers to the [?] popup button next to "CONNECTIONS" + Rectangle { + color: connectionsTabSelector.color; + width: 20; + height: connectionsTabSelectorText.height - 2; + anchors.left: connectionsTabSelectorTextContainer.left; + anchors.top: connectionsTabSelectorTextContainer.top; + anchors.topMargin: 1; + anchors.leftMargin: connectionsTabSelectorTextMetrics.width + 42; + RalewayRegular { + id: connectionsHelpText; + text: "[?]"; + size: connectionsTabSelectorText.size + 6; + font.capitalization: Font.AllUppercase; + color: connectionsTabSelectorText.color; + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignVCenter; + anchors.fill: parent; + } + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + enabled: activeTab === "connectionsTab"; + onClicked: letterbox(hifi.glyphs.question, + "Connections and Friends", + "Purple borders around profile pictures are Connections.
" + + "When your availability is set to Everyone, Connections can see your username and location.

" + + "Green borders around profile pictures are Friends.
" + + "When your availability is set to Friends, only Friends can see your username and location."); + onEntered: connectionsHelpText.color = hifi.colors.blueHighlight; + onExited: connectionsHelpText.color = hifi.colors.blueAccent; + } + } } } } - // Rectangles used to cover up rounded edges on bottom of MyInfo Rectangle - Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.top: myInfo.bottom; - anchors.left: parent.left; - } - Rectangle { - color: pal.color; - width: palContainer.width; - height: 10; - anchors.bottom: table.top; - anchors.left: parent.left; + + /***************************************** + NEARBY TAB + *****************************************/ + Rectangle { + id: nearbyTab; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12 + (iAmAdmin ? -adminTab.anchors.topMargin : 0); + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; } + width: parent.width - 12; + visible: activeTab == "nearbyTab"; + // Rectangle that houses "ADMIN" string Rectangle { id: adminTab; // Size - width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 2; + width: 2*actionButtonWidth + hifi.dimensions.scrollbarBackgroundWidth + 6; height: 40; // Anchors - anchors.bottom: myInfo.bottom; - anchors.bottomMargin: -10; - anchors.right: myInfo.right; + anchors.top: parent.top; + anchors.topMargin: -30; + anchors.right: parent.right; // Properties visible: iAmAdmin; // Style color: hifi.colors.tableRowLightEven; - radius: hifi.dimensions.borderRadius; border.color: hifi.colors.lightGrayText; border.width: 2; // "ADMIN" text @@ -194,27 +362,24 @@ Rectangle { verticalAlignment: Text.AlignTop; } } - // This TableView refers to the table (below the current user's NameCard) - HifiControls.Table { - id: table; - // Size - height: palContainer.height - myInfo.height - 4; - width: palContainer.width - 4; + // This TableView refers to the Nearby Table (on the "Nearby" tab below the current user's NameCard) + HifiControlsUit.Table { + id: nearbyTable; + flickableItem.interactive: true; // Anchors - anchors.left: parent.left; - anchors.top: myInfo.bottom; + anchors.fill: parent; // Properties centerHeaderText: true; sortIndicatorVisible: true; headerVisible: true; - sortIndicatorColumn: settings.sortIndicatorColumn; - sortIndicatorOrder: settings.sortIndicatorOrder; + sortIndicatorColumn: settings.nearbySortIndicatorColumn; + sortIndicatorOrder: settings.nearbySortIndicatorOrder; onSortIndicatorColumnChanged: { - settings.sortIndicatorColumn = sortIndicatorColumn; + settings.nearbySortIndicatorColumn = sortIndicatorColumn; sortModel(); } onSortIndicatorOrderChanged: { - settings.sortIndicatorOrder = sortIndicatorOrder; + settings.nearbySortIndicatorOrder = sortIndicatorOrder; sortModel(); } @@ -229,8 +394,8 @@ Rectangle { TableViewColumn { id: displayNameHeader; role: "displayName"; - title: table.rowCount + (table.rowCount === 1 ? " NAME" : " NAMES"); - width: nameCardWidth; + title: nearbyTable.rowCount + (nearbyTable.rowCount === 1 ? " NAME" : " NAMES"); + width: nearbyNameCardWidth; movable: false; resizable: false; } @@ -258,16 +423,14 @@ Rectangle { resizable: false; } model: ListModel { - id: userModel; + id: nearbyUserModel; } - // This Rectangle refers to each Row in the table. + // This Rectangle refers to each Row in the nearbyTable. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: styleData.selected ? rowHeight : rowHeight - 15; - color: styleData.selected - ? hifi.colors.orangeHighlight - : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + height: rowHeight + (styleData.selected ? 15 : 0); + color: rowColor(styleData.selected, styleData.alternate); } // This Item refers to the contents of each Cell @@ -276,27 +439,31 @@ Rectangle { property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore"; property bool isButton: styleData.role === "mute" || styleData.role === "kick"; property bool isAvgAudio: styleData.role === "avgAudioLevel"; + opacity: !isButton ? (model && model.isPresent ? 1.0 : 0.4) : 1.0; // Admin actions shouldn't turn gray // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { id: nameCard; // Properties + profileUrl: (model && model.profileUrl) || ""; displayName: styleData.value; userName: model ? model.userName : ""; + connectionStatus: model ? model.connection : ""; audioLevel: model ? model.audioLevel : 0.0; avgAudioLevel: model ? model.avgAudioLevel : 0.0; visible: !isCheckBox && !isButton && !isAvgAudio; uuid: model ? model.sessionId : ""; selected: styleData.selected; isAdmin: model && model.admin; + isPresent: model && model.isPresent; // Size - width: nameCardWidth; + width: nearbyNameCardWidth; height: parent.height; // Anchors anchors.left: parent.left; } - HifiControls.GlyphButton { + HifiControlsUit.GlyphButton { function getGlyph() { var fileName = "vol_"; if (model && model.personalMute) { @@ -312,43 +479,44 @@ Rectangle { size: height; anchors.verticalCenter: parent.verticalCenter; anchors.horizontalCenter: parent.horizontalCenter; + enabled: (model ? !model["ignore"] && model["isPresent"] : true); onClicked: { // cannot change mute status when ignoring if (!model["ignore"]) { var newValue = !model["personalMute"]; - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming Users["personalMute"](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId); } } } - // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) + // This CheckBox belongs in the columns that contain the stateful action buttons ("Ignore" for now) // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox // will appear in the "hovered" state. Hovering over the checkbox will fix it. // Clicking on the sides of the sorting header doesn't cause this problem. // I'm guessing this is a QT bug and not anything I can fix. I spent too long trying to work around it... // I'm just going to leave the minor visual bug in. - HifiControls.CheckBox { + HifiControlsUit.CheckBox { id: actionCheckBox; visible: isCheckBox; anchors.centerIn: parent; checked: model ? model[styleData.role] : false; - // If this is a "Personal Mute" checkbox, disable the checkbox if the "Ignore" checkbox is checked. - enabled: !(styleData.role === "personalMute" && (model ? model["ignore"] : true)); + // If this is an "Ignore" checkbox, disable the checkbox if user isn't present. + enabled: styleData.role === "ignore" ? (model ? model["isPresent"] : true) : true; boxSize: 24; onClicked: { var newValue = !model[styleData.role]; - userModel.setProperty(model.userIndex, styleData.role, newValue); - userModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, styleData.role, newValue); + nearbyUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming Users[styleData.role](model.sessionId, newValue); UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); if (styleData.role === "ignore") { - userModel.setProperty(model.userIndex, "personalMute", newValue); - userModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming + nearbyUserModel.setProperty(model.userIndex, "personalMute", newValue); + nearbyUserModelData[model.userIndex]["personalMute"] = newValue; // Defensive programming if (newValue) { - ignored[model.sessionId] = userModelData[model.userIndex]; + ignored[model.sessionId] = nearbyUserModelData[model.userIndex]; } else { delete ignored[model.sessionId]; } @@ -362,7 +530,7 @@ Rectangle { } // This Button belongs in the columns that contain the stateless action buttons ("Silence" & "Ban" for now) - HifiControls.Button { + HifiControlsUit.Button { id: actionButton; color: 2; // Red visible: isButton; @@ -373,8 +541,8 @@ Rectangle { Users[styleData.role](model.sessionId); UserActivityLogger["palAction"](styleData.role, model.sessionId); if (styleData.role === "kick") { - userModelData.splice(model.userIndex, 1); - userModel.remove(model.userIndex); // after changing userModelData, b/c ListModel can frob the data + nearbyUserModelData.splice(model.userIndex, 1); + nearbyUserModel.remove(model.userIndex); // after changing nearbyUserModelData, b/c ListModel can frob the data } } // muted/error glyphs @@ -397,10 +565,10 @@ Rectangle { Rectangle { // Size width: 2; - height: table.height; + height: nearbyTable.height; // Anchors anchors.left: adminTab.left; - anchors.top: table.top; + anchors.top: nearbyTable.top; // Properties visible: iAmAdmin; color: hifi.colors.lightGrayText; @@ -415,10 +583,10 @@ Rectangle { color: hifi.colors.tableBackgroundLight; width: 20; height: hifi.dimensions.tableHeaderHeight - 2; - anchors.left: table.left; - anchors.top: table.top; + anchors.left: nearbyTable.left; + anchors.top: nearbyTable.top; anchors.topMargin: 1; - anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; + anchors.leftMargin: actionButtonWidth + nearbyNameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6; RalewayRegular { id: helpText; text: "[?]"; @@ -431,13 +599,16 @@ Rectangle { } MouseArea { anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Display Names", "Bold names in the list are avatar display names.
" + - "If a display name isn't set, a unique session display name is assigned." + - "

Administrators of this domain can also see the username or machine ID associated with each avatar present."); + "Purple borders around profile pictures are connections.
" + + "Green borders around profile pictures are friends.
" + + "(TEMPORARY LANGUAGE) In some situations, you can also see others' usernames.
" + + "If you can see someone's username, you can GoTo them by selecting them in the PAL, then clicking their name.
" + + "
If someone's display name isn't set, a unique session display name is assigned to them.
" + + "
Administrators of this domain can also see the username or machine ID associated with each avatar present."); onEntered: helpText.color = hifi.colors.baseGrayHighlight; onExited: helpText.color = hifi.colors.darkGray; } @@ -449,7 +620,7 @@ Rectangle { width: 20; height: 28; anchors.right: adminTab.right; - anchors.rightMargin: 10 + hifi.dimensions.scrollbarBackgroundWidth; + anchors.rightMargin: 12 + hifi.dimensions.scrollbarBackgroundWidth; anchors.top: adminTab.top; anchors.topMargin: 2; RalewayRegular { @@ -464,7 +635,6 @@ Rectangle { } MouseArea { anchors.fill: parent; - acceptedButtons: Qt.LeftButton; hoverEnabled: true; onClicked: letterbox(hifi.glyphs.question, "Admin Actions", @@ -474,20 +644,520 @@ Rectangle { onExited: adminHelpText.color = hifi.colors.redHighlight; } } + } // "Nearby" Tab - HifiControls.Keyboard { + + /***************************************** + CONNECTIONS TAB + *****************************************/ + Rectangle { + id: connectionsTab; + color: "white"; + // Anchors + anchors { + top: tabSelectorContainer.bottom; + topMargin: 12; + bottom: parent.bottom; + bottomMargin: 12; + horizontalCenter: parent.horizontalCenter; + } + width: parent.width - 12; + visible: activeTab == "connectionsTab"; + + AnimatedImage { + id: connectionsLoading; + source: "../../icons/profilePicLoading.gif" + width: 120; + height: width; + anchors.top: parent.top; + anchors.topMargin: 185; + anchors.horizontalCenter: parent.horizontalCenter; + visible: true; + onVisibleChanged: { + if (visible) { + connectionsTimeoutTimer.start(); + } else { + connectionsTimeoutTimer.stop(); + connectionsRefreshProblemText.visible = false; + } + } + } + + // "This is taking too long..." text + FiraSansSemiBold { + id: connectionsRefreshProblemText + // Properties + text: "This is taking longer than normal.\nIf you get stuck, try refreshing the Connections tab."; + // Anchors + anchors.top: connectionsLoading.bottom; + anchors.topMargin: 10; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignHCenter; + wrapMode: Text.WordWrap; + // Style + color: hifi.colors.darkGray; + } + + // This TableView refers to the Connections Table (on the "Connections" tab below the current user's NameCard) + HifiControlsUit.Table { + id: connectionsTable; + flickableItem.interactive: true; + visible: !connectionsLoading.visible; + // Anchors + anchors.fill: parent; + // Properties + centerHeaderText: true; + sortIndicatorVisible: true; + headerVisible: true; + sortIndicatorColumn: settings.connectionsSortIndicatorColumn; + sortIndicatorOrder: settings.connectionsSortIndicatorOrder; + onSortIndicatorColumnChanged: { + settings.connectionsSortIndicatorColumn = sortIndicatorColumn; + sortConnectionsModel(); + } + onSortIndicatorOrderChanged: { + settings.connectionsSortIndicatorOrder = sortIndicatorOrder; + sortConnectionsModel(); + } + + TableViewColumn { + id: connectionsUserNameHeader; + role: "userName"; + title: connectionsTable.rowCount + (connectionsTable.rowCount === 1 ? " NAME" : " NAMES"); + width: connectionsNameCardWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "placeName"; + title: "LOCATION"; + width: locationColumnWidth; + movable: false; + resizable: false; + } + TableViewColumn { + role: "friends"; + title: "FRIEND"; + width: actionButtonWidth; + movable: false; + resizable: false; + } + + model: ListModel { + id: connectionsUserModel; + } + + // This Rectangle refers to each Row in the connectionsTable. + rowDelegate: Rectangle { + // Size + height: rowHeight; + color: rowColor(styleData.selected, styleData.alternate); + } + + // This Item refers to the contents of each Cell + itemDelegate: Item { + id: connectionsItemCell; + + // This NameCard refers to the cell that contains a connection's UserName + NameCard { + id: connectionsNameCard; + // Properties + visible: styleData.role === "userName"; + profileUrl: (model && model.profileUrl) || ""; + displayName: ""; + userName: model ? model.userName : ""; + connectionStatus : model ? model.connection : ""; + selected: styleData.selected; + // Size + width: connectionsNameCardWidth; + height: parent.height; + // Anchors + anchors.left: parent.left; + } + + // LOCATION data + FiraSansRegular { + id: connectionsLocationData + // Properties + visible: styleData.role === "placeName"; + text: (model && model.placeName) || ""; + elide: Text.ElideRight; + // Size + width: parent.width; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + // Style + color: hifi.colors.blueAccent; + font.underline: true; + MouseArea { + anchors.fill: parent + hoverEnabled: enabled + enabled: connectionsNameCard.selected && pal.activeTab == "connectionsTab" + onClicked: { + AddressManager.goToUser(model.userName); + UserActivityLogger.palAction("go_to_user", model.userName); + } + onEntered: connectionsLocationData.color = hifi.colors.blueHighlight; + onExited: connectionsLocationData.color = hifi.colors.blueAccent; + } + } + + // "Friends" checkbox + HifiControlsUit.CheckBox { + id: friendsCheckBox; + visible: styleData.role === "friends" && model.userName !== myData.userName; + anchors.centerIn: parent; + checked: model ? (model["connection"] === "friend" ? true : false) : false; + boxSize: 24; + onClicked: { + var newValue = !(model["connection"] === "friend"); + connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue); + connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming + pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName}); + + UserActivityLogger["palAction"](newValue ? styleData.role : "un-" + styleData.role, model.sessionId); + + // http://doc.qt.io/qt-5/qtqml-syntax-propertybinding.html#creating-property-bindings-from-javascript + // I'm using an explicit binding here because clicking a checkbox breaks the implicit binding as set by + // "checked:" statement above. + checked = Qt.binding(function() { return (model["connection"] === "friend" ? true : false)}); + } + } + } + } + + // "Make a Connection" instructions + Rectangle { + id: connectionInstructions; + visible: connectionsTable.rowCount === 0 && !connectionsLoading.visible; + anchors.fill: connectionsTable; + anchors.topMargin: hifi.dimensions.tableHeaderHeight; + color: "white"; + + RalewayRegular { + id: makeAConnectionText; + // Properties + text: "Make a Connection"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 60; + anchors.left: parent.left; + anchors.right: parent.right; + // Text Size + size: 24; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.darkGray; + } + + Image { + id: connectionImage; + source: "../../icons/connection.svg"; + width: 150; + height: 150; + mipmap: true; + // Anchors + anchors.top: makeAConnectionText.bottom; + anchors.topMargin: 15; + anchors.horizontalCenter: parent.horizontalCenter; + } + + FontLoader { id: ralewayRegular; source: "../../fonts/Raleway-Regular.ttf"; } + Text { + id: connectionHelpText; + // Anchors + anchors.top: connectionImage.bottom; + anchors.topMargin: 15; + anchors.left: parent.left + anchors.leftMargin: 40; + anchors.right: parent.right + anchors.rightMargin: 10; + // Text alignment + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHLeft + // Style + font.pixelSize: 18; + font.family: ralewayRegular.name + color: hifi.colors.darkGray + wrapMode: Text.WordWrap + textFormat: Text.StyledText; + // Text + text: HMD.active ? + "When you meet someone you want to remember later, you can connect with a handshake:

" + + "1. Put your hand out onto their hand and squeeze your controller's grip button on its side.
" + + "2. Once the other person puts their hand onto yours, you'll see your connection form.
" + + "3. After about 3 seconds, you're connected!" + : + "When you meet someone you want to remember later, you can connect with a handshake:

" + + "1. Press and hold the 'x' key to extend your arm.
" + + "2. Once the other person puts their hand onto yours, you'll see your connection form.
" + + "3. After about 3 seconds, you're connected!"; + } + + } + } // "Connections" Tab + } // palTabContainer + + // This contains the current user's NameCard and will contain other information in the future + Rectangle { + id: myInfo; + // Size + width: pal.width; + height: myCardHeight; + // Style + color: pal.color; + // Anchors + anchors.top: pal.top; + anchors.topMargin: 10; + anchors.left: pal.left; + // This NameCard refers to the current user's NameCard (the one above the nearbyTable) + NameCard { + id: myCard; + // Properties + profileUrl: myData.profileUrl; + displayName: myData.displayName; + userName: myData.userName; + audioLevel: myData.audioLevel; + avgAudioLevel: myData.avgAudioLevel; + isMyCard: true; + isPresent: true; + // Size + width: myCardWidth; + height: parent.height; + // Anchors + anchors.top: parent.top + anchors.left: parent.left; + } + Item { + id: upperRightInfoContainer; + width: 160; + height: parent.height; + anchors.top: parent.top; + anchors.right: parent.right; + + RalewayRegular { + id: availabilityText; + text: "set availability"; + // Text size + size: hifi.fontSizes.tabularData; + // Anchors + anchors.top: availabilityComboBox.bottom; + anchors.horizontalCenter: parent.horizontalCenter; + // Style + color: hifi.colors.baseGrayHighlight; + // Alignment + horizontalAlignment: Text.AlignHCenter; + verticalAlignment: Text.AlignTop; + } + /*Rectangle { + id: availabilityComboBox; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + // Size + width: parent.width; + height: 40; + MouseArea { + anchors.fill: parent + onClicked: { + popupComboDialog("Set your list visibility", + ["Everyone", "Friends and Connections", "Friends Only", "Appear Offline"], + ["You will be invisible in everyone's 'People' list.\nAnyone will be able to jump to your location if the domain allows.", + "You will be visible in the 'People' list only for those with whom you are connected or friends.\nThey will be able to jump to your location if the domain allows.", + "You will be visible in the 'People' list only for those with whom you are friends.\nThey will be able to jump to your location if the domain allows.", + "You will not be visible in the 'People' list of any other users."], + ["all", "connections", "friends", "none"]); + } + } + }*/ + + HifiControlsUit.ComboBox { + function determineAvailabilityIndex() { + return ['all', 'connections', 'friends', 'none'].indexOf(GlobalServices.findableBy) + } + id: availabilityComboBox; + // Anchors + anchors.top: parent.top; + anchors.horizontalCenter: parent.horizontalCenter; + // Size + width: parent.width; + height: 40; + currentIndex: determineAvailabilityIndex(); + model: ListModel { + id: availabilityComboBoxListItems + ListElement { text: "Everyone"; value: "all"; } + ListElement { text: "All Connections"; value: "connections"; } + ListElement { text: "Friends Only"; value: "friends"; } + ListElement { text: "Appear Offline"; value: "none" } + } + onCurrentIndexChanged: { + GlobalServices.findableBy = availabilityComboBoxListItems.get(currentIndex).value; + UserActivityLogger.palAction("set_availability", availabilityComboBoxListItems.get(currentIndex).value); + print('Setting availability:', JSON.stringify(GlobalServices.findableBy)); + } + } + } + } + + HifiControlsUit.Keyboard { id: keyboard; - raised: myCard.currentlyEditingDisplayName && HMD.active; + raised: currentlyEditingDisplayName && HMD.mounted; numeric: parent.punctuationMode; anchors { bottom: parent.bottom; left: parent.left; right: parent.right; } - } - } + } // Keyboard - // Timer used when selecting table rows that aren't yet present in the model + Item { + id: webViewContainer; + anchors.fill: parent; + + Rectangle { + id: navigationContainer; + visible: userInfoViewer.visible; + height: 60; + anchors { + top: parent.top; + left: parent.left; + right: parent.right; + } + color: hifi.colors.faintGray; + + Item { + id: backButton + anchors { + top: parent.top; + left: parent.left; + } + height: parent.height - addressBar.height; + width: parent.width/2; + + FiraSansSemiBold { + // Properties + text: "BACK"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: backButtonMouseArea.containsMouse || !userInfoViewer.canGoBack ? hifi.colors.lightGray : hifi.colors.darkGray; + MouseArea { + id: backButtonMouseArea; + anchors.fill: parent + hoverEnabled: enabled + onClicked: { + if (userInfoViewer.canGoBack) { + userInfoViewer.goBack(); + } + } + } + } + } + + Item { + id: closeButtonContainer + anchors { + top: parent.top; + right: parent.right; + } + height: parent.height - addressBar.height; + width: parent.width/2; + + FiraSansSemiBold { + id: closeButton; + // Properties + text: "CLOSE"; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + // Text Size + size: 16; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter; + // Style + color: hifi.colors.redHighlight; + MouseArea { + anchors.fill: parent + hoverEnabled: enabled + onClicked: userInfoViewer.visible = false; + onEntered: closeButton.color = hifi.colors.redAccent; + onExited: closeButton.color = hifi.colors.redHighlight; + } + } + } + + Item { + id: addressBar + anchors { + top: closeButtonContainer.bottom; + left: parent.left; + right: parent.right; + } + height: 30; + width: parent.width; + + FiraSansRegular { + // Properties + text: userInfoViewer.url; + elide: Text.ElideRight; + // Anchors + anchors.fill: parent; + anchors.leftMargin: 5; + // Text Size + size: 14; + // Text Positioning + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft; + // Style + color: hifi.colors.lightGray; + } + } + } + + Rectangle { + id: webViewBackground; + color: "white"; + visible: userInfoViewer.visible; + anchors { + top: navigationContainer.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + } + + HifiControls.WebView { + id: userInfoViewer; + anchors { + top: navigationContainer.bottom; + bottom: parent.bottom; + left: parent.left; + right: parent.right; + } + visible: false; + } + } + + // Timer used when selecting nearbyTable rows that aren't yet present in the model // (i.e. when selecting avatars using edit.js or sphere overlays) Timer { property bool selected; // Selected or deselected? @@ -495,17 +1165,29 @@ Rectangle { id: selectionTimer; onTriggered: { if (selected) { - table.selection.clear(); // for now, no multi-select - table.selection.select(userIndex); - table.positionViewAtRow(userIndex, ListView.Beginning); + nearbyTable.selection.clear(); // for now, no multi-select + nearbyTable.selection.select(userIndex); + nearbyTable.positionViewAtRow(userIndex, ListView.Beginning); } else { - table.selection.deselect(userIndex); + nearbyTable.selection.deselect(userIndex); } } } - function findSessionIndex(sessionId, optionalData) { // no findIndex in .qml - var data = optionalData || userModelData, length = data.length; + // Timer used when refreshing the Connections tab + Timer { + id: connectionsTimeoutTimer; + interval: 3000; // 3 seconds + onTriggered: { + connectionsRefreshProblemText.visible = true; + } + } + + function rowColor(selected, alternate) { + return selected ? hifi.colors.orangeHighlight : alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd; + } + function findNearbySessionIndex(sessionId, optionalData) { // no findIndex in .qml + var data = optionalData || nearbyUserModelData, length = data.length; for (var i = 0; i < length; i++) { if (data[i].sessionId === sessionId) { return i; @@ -515,10 +1197,10 @@ Rectangle { } function fromScript(message) { switch (message.method) { - case 'users': + case 'nearbyUsers': var data = message.params; var index = -1; - index = findSessionIndex('', data); + index = findNearbySessionIndex('', data); if (index !== -1) { iAmAdmin = Users.canKick; myData = data[index]; @@ -526,39 +1208,48 @@ Rectangle { } else { console.log("This user's data was not found in the user list. PAL will not function properly."); } - userModelData = data; + nearbyUserModelData = data; for (var ignoredID in ignored) { - index = findSessionIndex(ignoredID); + index = findNearbySessionIndex(ignoredID); if (index === -1) { // Add back any missing ignored to the PAL, because they sometimes take a moment to show up. - userModelData.push(ignored[ignoredID]); + nearbyUserModelData.push(ignored[ignoredID]); } else { // Already appears in PAL; update properties of existing element in model data - userModelData[index] = ignored[ignoredID]; + nearbyUserModelData[index] = ignored[ignoredID]; } } sortModel(); + reloadNearby.color = 0; + break; + case 'connections': + var data = message.params; + console.log('Got connection data: ', JSON.stringify(data)); + connectionsUserModelData = data; + sortConnectionsModel(); + connectionsLoading.visible = false; + connectionsRefreshProblemText.visible = false; break; case 'select': var sessionIds = message.params[0]; var selected = message.params[1]; var alreadyRefreshed = message.params[2]; - var userIndex = findSessionIndex(sessionIds[0]); + var userIndex = findNearbySessionIndex(sessionIds[0]); if (sessionIds.length > 1) { letterbox("", "", 'Only one user can be selected at a time.'); } else if (userIndex < 0) { // If we've already refreshed the PAL and the avatar still isn't present in the model... if (alreadyRefreshed === true) { - letterbox('', '', 'The last editor of this object is either you or not among this list of users.'); + letterbox('', '', 'The user you attempted to select is no longer available.'); } else { pal.sendToScript({method: 'refresh', params: {selected: message.params}}); } } else { // If we've already refreshed the PAL and found the avatar in the model if (alreadyRefreshed === true) { - // Wait a little bit before trying to actually select the avatar in the table + // Wait a little bit before trying to actually select the avatar in the nearbyTable selectionTimer.interval = 250; } else { // If we've found the avatar in the model and didn't need to refresh, - // select the avatar in the table immediately + // select the avatar in the nearbyTable immediately selectionTimer.interval = 0; } selectionTimer.selected = selected; @@ -568,26 +1259,25 @@ Rectangle { break; // Received an "updateUsername()" request from the JS case 'updateUsername': - // The User ID (UUID) is the first parameter in the message. - var userId = message.params[0]; - // The text that goes in the userName field is the second parameter in the message. - var userName = message.params[1]; - var admin = message.params[2]; - // If the userId is empty, we're updating "myData". - if (!userId) { - myData.userName = userName; - myCard.userName = userName; // Defensive programming - } else { - // Get the index in userModel and userModelData associated with the passed UUID - var userIndex = findSessionIndex(userId); - if (userIndex != -1) { - // Set the userName appropriately - userModel.setProperty(userIndex, "userName", userName); - userModelData[userIndex].userName = userName; // Defensive programming - // Set the admin status appropriately - userModel.setProperty(userIndex, "admin", admin); - userModelData[userIndex].admin = admin; // Defensive programming + // Get the connection status + var connectionStatus = message.params.connection; + // If the connection status isn't "self"... + if (connectionStatus !== "self") { + // Get the index in nearbyUserModel and nearbyUserModelData associated with the passed UUID + var userIndex = findNearbySessionIndex(message.params.sessionId); + if (userIndex !== -1) { + ['userName', 'admin', 'connection', 'profileUrl', 'placeName'].forEach(function (name) { + var value = message.params[name]; + if (value === undefined) { + return; + } + nearbyUserModel.setProperty(userIndex, name, value); + nearbyUserModelData[userIndex][name] = value; // for refill after sort + }); } + } else if (message.params.profileUrl) { + myData.profileUrl = message.params.profileUrl; + myCard.profileUrl = message.params.profileUrl; } break; case 'updateAudioLevel': @@ -601,12 +1291,12 @@ Rectangle { myData.avgAudioLevel = avgAudioLevel; myCard.avgAudioLevel = avgAudioLevel; } else { - var userIndex = findSessionIndex(userId); + var userIndex = findNearbySessionIndex(userId); if (userIndex != -1) { - userModel.setProperty(userIndex, "audioLevel", audioLevel); - userModelData[userIndex].audioLevel = audioLevel; // Defensive programming - userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); - userModelData[userIndex].avgAudioLevel = avgAudioLevel; + nearbyUserModel.setProperty(userIndex, "audioLevel", audioLevel); + nearbyUserModelData[userIndex].audioLevel = audioLevel; // Defensive programming + nearbyUserModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + nearbyUserModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } @@ -618,18 +1308,35 @@ Rectangle { var sessionID = message.params[0]; delete ignored[sessionID]; break; + case 'palIsStale': + var sessionID = message.params[0]; + var reason = message.params[1]; + var userIndex = findNearbySessionIndex(sessionID); + if (userIndex != -1) { + if (!nearbyUserModelData[userIndex].ignore) { + if (reason !== 'avatarAdded') { + nearbyUserModel.setProperty(userIndex, "isPresent", false); + nearbyUserModelData[userIndex].isPresent = false; + nearbyTable.selection.deselect(userIndex); + } + reloadNearby.color = 2; + } + } else { + reloadNearby.color = 2; + } + break; default: console.log('Unrecognized message:', JSON.stringify(message)); } } function sortModel() { - var column = table.getColumn(table.sortIndicatorColumn); + var column = nearbyTable.getColumn(nearbyTable.sortIndicatorColumn); var sortProperty = column ? column.role : "displayName"; - var before = (table.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var before = (nearbyTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; var after = -1 * before; // get selection(s) before sorting - var selectedIDs = getSelectedSessionIDs(); - userModelData.sort(function (a, b) { + var selectedIDs = getSelectedNearbySessionIDs(); + nearbyUserModelData.sort(function (a, b) { var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); switch (true) { case (aValue < bValue): return before; @@ -637,39 +1344,77 @@ Rectangle { default: return 0; } }); - table.selection.clear(); + nearbyTable.selection.clear(); - userModel.clear(); + nearbyUserModel.clear(); var userIndex = 0; var newSelectedIndexes = []; - userModelData.forEach(function (datum) { + nearbyUserModelData.forEach(function (datum) { function init(property) { if (datum[property] === undefined) { - datum[property] = false; + // These properties must have values of type 'string'. + if (property === 'userName' || property === 'profileUrl' || property === 'placeName' || property === 'connection') { + datum[property] = ""; + // All other properties must have values of type 'bool'. + } else { + datum[property] = false; + } } } - ['personalMute', 'ignore', 'mute', 'kick'].forEach(init); + ['personalMute', 'ignore', 'mute', 'kick', 'admin', 'userName', 'profileUrl', 'placeName', 'connection'].forEach(init); datum.userIndex = userIndex++; - userModel.append(datum); + nearbyUserModel.append(datum); if (selectedIDs.indexOf(datum.sessionId) != -1) { newSelectedIndexes.push(datum.userIndex); } }); if (newSelectedIndexes.length > 0) { - table.selection.select(newSelectedIndexes); - table.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + nearbyTable.selection.select(newSelectedIndexes); + nearbyTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); + } + } + function sortConnectionsModel() { + var column = connectionsTable.getColumn(connectionsTable.sortIndicatorColumn); + var sortProperty = column ? column.role : "userName"; + var before = (connectionsTable.sortIndicatorOrder === Qt.AscendingOrder) ? -1 : 1; + var after = -1 * before; + // get selection(s) before sorting + var selectedIDs = getSelectedConnectionsUserNames(); + connectionsUserModelData.sort(function (a, b) { + var aValue = a[sortProperty].toString().toLowerCase(), bValue = b[sortProperty].toString().toLowerCase(); + switch (true) { + case (aValue < bValue): return before; + case (aValue > bValue): return after; + default: return 0; + } + }); + connectionsTable.selection.clear(); + + connectionsUserModel.clear(); + var userIndex = 0; + var newSelectedIndexes = []; + connectionsUserModelData.forEach(function (datum) { + datum.userIndex = userIndex++; + connectionsUserModel.append(datum); + if (selectedIDs.indexOf(datum.sessionId) != -1) { + newSelectedIndexes.push(datum.userIndex); + } + }); + if (newSelectedIndexes.length > 0) { + connectionsTable.selection.select(newSelectedIndexes); + connectionsTable.positionViewAtRow(newSelectedIndexes[0], ListView.Beginning); } } signal sendToScript(var message); function noticeSelection() { var userIds = []; - table.selection.forEach(function (userIndex) { - userIds.push(userModelData[userIndex].sessionId); + nearbyTable.selection.forEach(function (userIndex) { + userIds.push(nearbyUserModelData[userIndex].sessionId); }); pal.sendToScript({method: 'selected', params: userIds}); } Connections { - target: table.selection; + target: nearbyTable.selection; onSelectionChanged: pal.noticeSelection(); } } diff --git a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml index a5059b4b88..a58177640d 100644 --- a/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml +++ b/interface/resources/qml/hifi/tablet/TabletAddressDialog.qml @@ -27,7 +27,7 @@ StackView { initialItem: addressBarDialog width: parent.width height: parent.height - + property var eventBridge; property var allStories: []; property int cardWidth: 460; property int cardHeight: 320; @@ -59,6 +59,7 @@ StackView { if (0 !== targetString.indexOf('hifi://')) { var card = tabletStoryCard.createObject(); card.setUrl(addressBarDialog.metaverseServerUrl + targetString); + card.eventBridge = root.eventBridge; root.push(card); return; } diff --git a/interface/resources/qml/hifi/tablet/TabletStoryCard.qml b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml index ea6a23cb10..1d57c8a083 100644 --- a/interface/resources/qml/hifi/tablet/TabletStoryCard.qml +++ b/interface/resources/qml/hifi/tablet/TabletStoryCard.qml @@ -17,7 +17,8 @@ import "../../windows" import "../" import "../toolbars" import "../../styles-uit" as HifiStyles -import "../../controls-uit" as HifiControls +import "../../controls-uit" as HifiControlsUit +import "../../controls" as HifiControls Rectangle { @@ -26,104 +27,17 @@ Rectangle { width: parent.width height: parent.height property string address: "" - + property alias eventBridge: webview.eventBridge function setUrl(url) { cardRoot.address = url; webview.url = url; } - - function goBack() { - } - - function visit() { - } - - Rectangle { - id: header - anchors { - left: parent.left - right: parent.right - top: parent.top - } - - width: parent.width - height: 50 - color: hifi.colors.white - - Row { - anchors.fill: parent - spacing: 80 - - Item { - id: backButton - anchors { - top: parent.top - left: parent.left - leftMargin: 100 - } - height: parent.height - width: parent.height - - HifiStyles.FiraSansSemiBold { - text: "BACK" - elide: Text.ElideRight - anchors.fill: parent - size: 16 - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - color: hifi.colors.lightGray - - MouseArea { - id: backButtonMouseArea - anchors.fill: parent - hoverEnabled: enabled - - onClicked: { - webview.goBack(); - } - } - } - } - - Item { - id: closeButton - anchors { - top: parent.top - right: parent.right - rightMargin: 100 - } - height: parent.height - width: parent.height - - HifiStyles.FiraSansSemiBold { - text: "CLOSE" - elide: Text.ElideRight - anchors.fill: parent - size: 16 - - verticalAlignment: Text.AlignVCenter - horizontalAlignment: Text.AlignHCenter - - color: hifi.colors.lightGray - - MouseArea { - id: closeButtonMouseArea - anchors.fill: parent - hoverEnabled: enabled - - onClicked: root.pop(); - } - } - } - } - } - - HifiControls.WebView { + + HifiControls.TabletWebView { id: webview + parentStackItem: root anchors { - top: header.bottom + top: parent.top right: parent.right left: parent.left bottom: parent.bottom diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index 38534d4243..a9d5a0ed2f 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -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 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 9601d7bd50..33a9962bde 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -141,6 +141,7 @@ #include "LODManager.h" #include "ModelPackager.h" #include "networking/HFWebEngineProfile.h" +#include "networking/HFTabletWebEngineProfile.h" #include "scripting/TestScriptingInterface.h" #include "scripting/AccountScriptingInterface.h" #include "scripting/AssetMappingsScriptingInterface.h" @@ -179,6 +180,7 @@ #include "FrameTimingsScriptingInterface.h" #include #include +#include #include #include @@ -521,7 +523,9 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(nullptr, qApp->getOcteeSceneStats()); + return previousSessionCrashed; } @@ -548,7 +552,7 @@ const float DEFAULT_HMD_TABLET_SCALE_PERCENT = 100.0f; const float DEFAULT_DESKTOP_TABLET_SCALE_PERCENT = 75.0f; const bool DEFAULT_DESKTOP_TABLET_BECOMES_TOOLBAR = true; const bool DEFAULT_HMD_TABLET_BECOMES_TOOLBAR = false; -const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = true; +const bool DEFAULT_PREFER_AVATAR_FINGER_OVER_STYLUS = false; Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bool runServer, QString runServerPathOption) : QApplication(argc, argv), @@ -1929,6 +1933,7 @@ void Application::initializeUi() { qmlRegisterType("Hifi", 1, 0, "Preference"); qmlRegisterType("HFWebEngineProfile", 1, 0, "HFWebEngineProfile"); + qmlRegisterType("HFTabletWebEngineProfile", 1, 0, "HFTabletWebEngineProfile"); auto offscreenUi = DependencyManager::get(); offscreenUi->create(_glWidget->qglContext()); @@ -4562,6 +4567,8 @@ void Application::update(float deltaTime) { } AnimDebugDraw::getInstance().update(); + + DependencyManager::get()->update(); } void Application::sendAvatarViewFrustum() { @@ -5508,6 +5515,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("LimitlessSpeechRecognition", DependencyManager::get().data()); + if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { scriptEngine->registerGlobalObject("Steam", new SteamScriptingInterface(scriptEngine, steamClient.get())); } diff --git a/interface/src/DiscoverabilityManager.cpp b/interface/src/DiscoverabilityManager.cpp index 8fcc1e5477..f042330a46 100644 --- a/interface/src/DiscoverabilityManager.cpp +++ b/interface/src/DiscoverabilityManager.cpp @@ -40,9 +40,10 @@ void DiscoverabilityManager::updateLocation() { auto accountManager = DependencyManager::get(); auto addressManager = DependencyManager::get(); auto& domainHandler = DependencyManager::get()->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()->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(_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(discoverabilityMode)); - - if (static_cast(_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(); diff --git a/interface/src/DiscoverabilityManager.h b/interface/src/DiscoverabilityManager.h index 196b0cdf81..96190b25d9 100644 --- a/interface/src/DiscoverabilityManager.h +++ b/interface/src/DiscoverabilityManager.h @@ -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); diff --git a/interface/src/networking/HFTabletWebEngineProfile.cpp b/interface/src/networking/HFTabletWebEngineProfile.cpp new file mode 100644 index 0000000000..46634299bb --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineProfile.cpp @@ -0,0 +1,26 @@ +// +// HFTabletWebEngineProfile.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-03-31. +// 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 +// + +#include "HFTabletWebEngineProfile.h" +#include "HFTabletWebEngineRequestInterceptor.h" + +static const QString QML_WEB_ENGINE_NAME = "qmlTabletWebEngine"; + +HFTabletWebEngineProfile::HFTabletWebEngineProfile(QObject* parent) : QQuickWebEngineProfile(parent) { + + static const QString WEB_ENGINE_USER_AGENT = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + + setHttpUserAgent(WEB_ENGINE_USER_AGENT); + + auto requestInterceptor = new HFTabletWebEngineRequestInterceptor(this); + setRequestInterceptor(requestInterceptor); +} + diff --git a/interface/src/networking/HFTabletWebEngineProfile.h b/interface/src/networking/HFTabletWebEngineProfile.h new file mode 100644 index 0000000000..406cb1a19a --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineProfile.h @@ -0,0 +1,23 @@ +// +// HFTabletWebEngineProfile.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-03-31. +// 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 +// + + +#ifndef hifi_HFTabletWebEngineProfile_h +#define hifi_HFTabletWebEngineProfile_h + +#include + +class HFTabletWebEngineProfile : public QQuickWebEngineProfile { +public: + HFTabletWebEngineProfile(QObject* parent = Q_NULLPTR); +}; + +#endif // hifi_HFTabletWebEngineProfile_h diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp b/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp new file mode 100644 index 0000000000..7282fb5e3d --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineRequestInterceptor.cpp @@ -0,0 +1,42 @@ +// +// HFTabletWebEngineRequestInterceptor.cpp +// interface/src/networking +// +// Created by Dante Ruiz on 2017-3-31. +// 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 +// + +#include "HFTabletWebEngineRequestInterceptor.h" +#include +#include + +bool isTabletAuthableHighFidelityURL(const QUrl& url) { + static const QStringList HF_HOSTS = { + "highfidelity.com", "highfidelity.io", + "metaverse.highfidelity.com", "metaverse.highfidelity.io" + }; + + return url.scheme() == "https" && HF_HOSTS.contains(url.host()); +} + +void HFTabletWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo& info) { + // check if this is a request to a highfidelity URL + if (isTabletAuthableHighFidelityURL(info.requestUrl())) { + // if we have an access token, add it to the right HTTP header for authorization + auto accountManager = DependencyManager::get(); + + if (accountManager->hasValidAccessToken()) { + static const QString OAUTH_AUTHORIZATION_HEADER = "Authorization"; + + QString bearerTokenString = "Bearer " + accountManager->getAccountInfo().getAccessToken().token; + info.setHttpHeader(OAUTH_AUTHORIZATION_HEADER.toLocal8Bit(), bearerTokenString.toLocal8Bit()); + } + } + + static const QString USER_AGENT = "User-Agent"; + QString tokenString = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Mobile Safari/537.36"; + info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit()); +} diff --git a/interface/src/networking/HFTabletWebEngineRequestInterceptor.h b/interface/src/networking/HFTabletWebEngineRequestInterceptor.h new file mode 100644 index 0000000000..e38549937e --- /dev/null +++ b/interface/src/networking/HFTabletWebEngineRequestInterceptor.h @@ -0,0 +1,24 @@ +// +// HFTabletWebEngineRequestInterceptor.h +// interface/src/networking +// +// Created by Dante Ruiz on 2017-3-31. +// 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 +// + +#ifndef hifi_HFTabletWebEngineRequestInterceptor_h +#define hifi_HFTabletWebEngineRequestInterceptor_h + +#include + +class HFTabletWebEngineRequestInterceptor : public QWebEngineUrlRequestInterceptor { +public: + HFTabletWebEngineRequestInterceptor(QObject* parent) : QWebEngineUrlRequestInterceptor(parent) {}; + + virtual void interceptRequest(QWebEngineUrlRequestInfo& info) override; +}; + +#endif // hifi_HFWebEngineRequestInterceptor_h diff --git a/interface/src/networking/HFWebEngineRequestInterceptor.cpp b/interface/src/networking/HFWebEngineRequestInterceptor.cpp index 9c3f0b232e..f6b0914f08 100644 --- a/interface/src/networking/HFWebEngineRequestInterceptor.cpp +++ b/interface/src/networking/HFWebEngineRequestInterceptor.cpp @@ -10,6 +10,7 @@ // #include "HFWebEngineRequestInterceptor.h" +#include "NetworkingConstants.h" #include @@ -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) { diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.cpp b/interface/src/scripting/GlobalServicesScriptingInterface.cpp index d7e5bae3f8..f4a5ffb39c 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.cpp +++ b/interface/src/scripting/GlobalServicesScriptingInterface.cpp @@ -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(); - return findableByString(discoverabilityManager->getDiscoverabilityMode()); + return DiscoverabilityManager::findableByString(discoverabilityManager->getDiscoverabilityMode()); } void GlobalServicesScriptingInterface::setFindableBy(const QString& discoverabilityMode) { auto discoverabilityManager = DependencyManager::get(); - 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() : diff --git a/interface/src/scripting/GlobalServicesScriptingInterface.h b/interface/src/scripting/GlobalServicesScriptingInterface.h index 11d8735187..63294fc656 100644 --- a/interface/src/scripting/GlobalServicesScriptingInterface.h +++ b/interface/src/scripting/GlobalServicesScriptingInterface.h @@ -18,6 +18,7 @@ #include #include #include +#include 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; }; diff --git a/interface/src/scripting/HMDScriptingInterface.h b/interface/src/scripting/HMDScriptingInterface.h index 276e23d2d5..7ecafdcbcb 100644 --- a/interface/src/scripting/HMDScriptingInterface.h +++ b/interface/src/scripting/HMDScriptingInterface.h @@ -27,7 +27,7 @@ class HMDScriptingInterface : public AbstractHMDScriptingInterface, public Depen Q_OBJECT Q_PROPERTY(glm::vec3 position READ getPosition) Q_PROPERTY(glm::quat orientation READ getOrientation) - Q_PROPERTY(bool mounted READ isMounted) + Q_PROPERTY(bool mounted READ isMounted NOTIFY mountedChanged) Q_PROPERTY(bool showTablet READ getShouldShowTablet) Q_PROPERTY(QUuid tabletID READ getCurrentTabletFrameID WRITE setCurrentTabletFrameID) Q_PROPERTY(QUuid homeButtonID READ getCurrentHomeButtonID WRITE setCurrentHomeButtonID) @@ -80,6 +80,7 @@ public: signals: bool shouldShowHandControllersChanged(); + void mountedChanged(); public: HMDScriptingInterface(); diff --git a/interface/src/scripting/LimitlessConnection.cpp b/interface/src/scripting/LimitlessConnection.cpp new file mode 100644 index 0000000000..b9f4eacd4b --- /dev/null +++ b/interface/src/scripting/LimitlessConnection.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include "LimitlessConnection.h" +#include "LimitlessVoiceRecognitionScriptingInterface.h" + +LimitlessConnection::LimitlessConnection() : + _streamingAudioForTranscription(false) +{ +} + +void LimitlessConnection::startListening(QString authCode) { + _transcribeServerSocket.reset(new QTcpSocket(this)); + connect(_transcribeServerSocket.get(), &QTcpSocket::readyRead, this, + &LimitlessConnection::transcriptionReceived); + connect(_transcribeServerSocket.get(), &QTcpSocket::disconnected, this, [this](){stopListening();}); + + static const auto host = "gserv_devel.studiolimitless.com"; + _transcribeServerSocket->connectToHost(host, 1407); + _transcribeServerSocket->waitForConnected(); + QString requestHeader = QString::asprintf("Authorization: %s\r\nfs: %i\r\n", + authCode.toLocal8Bit().data(), AudioConstants::SAMPLE_RATE); + qCDebug(interfaceapp) << "Sending Limitless Audio Stream Request: " << requestHeader; + _transcribeServerSocket->write(requestHeader.toLocal8Bit()); + _transcribeServerSocket->waitForBytesWritten(); +} + +void LimitlessConnection::stopListening() { + emit onFinishedSpeaking(_currentTranscription); + _streamingAudioForTranscription = false; + _currentTranscription = ""; + if (!isConnected()) + return; + _transcribeServerSocket->close(); + disconnect(_transcribeServerSocket.get(), &QTcpSocket::readyRead, this, + &LimitlessConnection::transcriptionReceived); + _transcribeServerSocket.release()->deleteLater(); + disconnect(DependencyManager::get().data(), &AudioClient::inputReceived, this, + &LimitlessConnection::audioInputReceived); + qCDebug(interfaceapp) << "Connection to Limitless Voice Server closed."; +} + +void LimitlessConnection::audioInputReceived(const QByteArray& inputSamples) { + if (isConnected()) { + _transcribeServerSocket->write(inputSamples.data(), inputSamples.size()); + _transcribeServerSocket->waitForBytesWritten(); + } +} + +void LimitlessConnection::transcriptionReceived() { + while (_transcribeServerSocket && _transcribeServerSocket->bytesAvailable() > 0) { + const QByteArray data = _transcribeServerSocket->readAll(); + _serverDataBuffer.append(data); + int begin = _serverDataBuffer.indexOf('<'); + int end = _serverDataBuffer.indexOf('>'); + while (begin > -1 && end > -1) { + const int len = end - begin; + const QByteArray serverMessage = _serverDataBuffer.mid(begin+1, len-1); + if (serverMessage.contains("1407")) { + qCDebug(interfaceapp) << "Limitless Speech Server denied the request."; + // Don't spam the server with further false requests please. + DependencyManager::get()->setListeningToVoice(true); + stopListening(); + return; + } else if (serverMessage.contains("1408")) { + qCDebug(interfaceapp) << "Limitless Audio request authenticated!"; + _serverDataBuffer.clear(); + connect(DependencyManager::get().data(), &AudioClient::inputReceived, this, + &LimitlessConnection::audioInputReceived); + return; + } + QJsonObject json = QJsonDocument::fromJson(serverMessage.data()).object(); + _serverDataBuffer.remove(begin, len+1); + _currentTranscription = json["alternatives"].toArray()[0].toObject()["transcript"].toString(); + emit onReceivedTranscription(_currentTranscription); + if (json["isFinal"] == true) { + qCDebug(interfaceapp) << "Final transcription: " << _currentTranscription; + stopListening(); + return; + } + begin = _serverDataBuffer.indexOf('<'); + end = _serverDataBuffer.indexOf('>'); + } + } +} + +bool LimitlessConnection::isConnected() const { + return _transcribeServerSocket.get() && _transcribeServerSocket->isWritable() + && _transcribeServerSocket->state() != QAbstractSocket::SocketState::UnconnectedState; +} diff --git a/interface/src/scripting/LimitlessConnection.h b/interface/src/scripting/LimitlessConnection.h new file mode 100644 index 0000000000..ee049aff8e --- /dev/null +++ b/interface/src/scripting/LimitlessConnection.h @@ -0,0 +1,44 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/24/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_LimitlessConnection_h +#define hifi_LimitlessConnection_h + +#include +#include +#include + +class LimitlessConnection : public QObject { + Q_OBJECT +public: + LimitlessConnection(); + + Q_INVOKABLE void startListening(QString authCode); + Q_INVOKABLE void stopListening(); + + std::atomic _streamingAudioForTranscription; + +signals: + void onReceivedTranscription(QString speech); + void onFinishedSpeaking(QString speech); + +private: + void transcriptionReceived(); + void audioInputReceived(const QByteArray& inputSamples); + + bool isConnected() const; + + std::unique_ptr _transcribeServerSocket; + QByteArray _serverDataBuffer; + QString _currentTranscription; +}; + +#endif //hifi_LimitlessConnection_h diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp new file mode 100644 index 0000000000..1352630f84 --- /dev/null +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.cpp @@ -0,0 +1,64 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include +#include +#include +#include "LimitlessVoiceRecognitionScriptingInterface.h" + +const float LimitlessVoiceRecognitionScriptingInterface::_audioLevelThreshold = 0.33f; +const int LimitlessVoiceRecognitionScriptingInterface::_voiceTimeoutDuration = 2000; + +LimitlessVoiceRecognitionScriptingInterface::LimitlessVoiceRecognitionScriptingInterface() : + _shouldStartListeningForVoice(false) +{ + _voiceTimer.setSingleShot(true); + connect(&_voiceTimer, &QTimer::timeout, this, &LimitlessVoiceRecognitionScriptingInterface::voiceTimeout); + connect(&_connection, &LimitlessConnection::onReceivedTranscription, this, [this](QString transcription){emit onReceivedTranscription(transcription);}); + connect(&_connection, &LimitlessConnection::onFinishedSpeaking, this, [this](QString transcription){emit onFinishedSpeaking(transcription);}); + _connection.moveToThread(&_connectionThread); + _connectionThread.setObjectName("Limitless Connection"); + _connectionThread.start(); +} + +void LimitlessVoiceRecognitionScriptingInterface::update() { + const float audioLevel = AvatarInputs::getInstance()->loudnessToAudioLevel(DependencyManager::get()->getAudioAverageInputLoudness()); + + if (_shouldStartListeningForVoice) { + if (_connection._streamingAudioForTranscription) { + if (audioLevel > _audioLevelThreshold) { + if (_voiceTimer.isActive()) { + _voiceTimer.stop(); + } + } else if (!_voiceTimer.isActive()){ + _voiceTimer.start(_voiceTimeoutDuration); + } + } else if (audioLevel > _audioLevelThreshold) { + // to make sure invoke doesn't get called twice before the method actually gets called + _connection._streamingAudioForTranscription = true; + QMetaObject::invokeMethod(&_connection, "startListening", Q_ARG(QString, authCode)); + } + } +} + +void LimitlessVoiceRecognitionScriptingInterface::setListeningToVoice(bool listening) { + _shouldStartListeningForVoice = listening; +} + +void LimitlessVoiceRecognitionScriptingInterface::setAuthKey(QString key) { + authCode = key; +} + +void LimitlessVoiceRecognitionScriptingInterface::voiceTimeout() { + if (_connection._streamingAudioForTranscription) { + QMetaObject::invokeMethod(&_connection, "stopListening"); + } +} diff --git a/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h new file mode 100644 index 0000000000..d1b1139695 --- /dev/null +++ b/interface/src/scripting/LimitlessVoiceRecognitionScriptingInterface.h @@ -0,0 +1,50 @@ +// +// SpeechRecognitionScriptingInterface.h +// interface/src/scripting +// +// Created by Trevor Berninger on 3/20/17. +// Copyright 2017 Limitless ltd. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_SpeechRecognitionScriptingInterface_h +#define hifi_SpeechRecognitionScriptingInterface_h + +#include +#include +#include +#include "LimitlessConnection.h" + +class LimitlessVoiceRecognitionScriptingInterface : public QObject, public Dependency { + Q_OBJECT +public: + LimitlessVoiceRecognitionScriptingInterface(); + + void update(); + + QString authCode; + +public slots: + void setListeningToVoice(bool listening); + void setAuthKey(QString key); + +signals: + void onReceivedTranscription(QString speech); + void onFinishedSpeaking(QString speech); + +private: + + bool _shouldStartListeningForVoice; + static const float _audioLevelThreshold; + static const int _voiceTimeoutDuration; + + QTimer _voiceTimer; + QThread _connectionThread; + LimitlessConnection _connection; + + void voiceTimeout(); +}; + +#endif //hifi_SpeechRecognitionScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 9c1aedf7a0..39c2f2e402 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -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()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, + auto messageBox = DependencyManager::get()->createMessageBox(OffscreenUi::ICON_INFORMATION, title, text, static_cast>(buttons), static_cast(defaultButton)); connect(messageBox, SIGNAL(selected(int)), this, SLOT(onMessageBoxSelected(int))); diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 60d24d50df..b7bed7d85f 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -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 diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index f2d97a0137..7239e49d89 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -85,7 +85,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts - renderStatsAndLogs(renderArgs); // currently renders nothing }); renderArgs->_batch = nullptr; // so future users of renderArgs don't try to use our batch @@ -159,27 +158,6 @@ void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { qApp->getOverlays().renderHUD(renderArgs); } -void ApplicationOverlay::renderStatsAndLogs(RenderArgs* renderArgs) { - - // Display stats and log text onscreen - - // Determine whether to compute timing details - - /* - // Show on-screen msec timer - if (Menu::getInstance()->isOptionChecked(MenuOption::FrameTimer)) { - auto canvasSize = qApp->getCanvasSize(); - quint64 mSecsNow = floor(usecTimestampNow() / 1000.0 + 0.5); - QString frameTimer = QString("%1\n").arg((int)(mSecsNow % 1000)); - int timerBottom = - (Menu::getInstance()->isOptionChecked(MenuOption::Stats)) - ? 80 : 20; - drawText(canvasSize.x - 100, canvasSize.y - timerBottom, - 0.30f, 0.0f, 0, frameTimer.toUtf8().constData(), WHITE_TEXT); - } - */ -} - void ApplicationOverlay::renderDomainConnectionStatusBorder(RenderArgs* renderArgs) { auto geometryCache = DependencyManager::get(); static std::once_flag once; @@ -229,13 +207,13 @@ void ApplicationOverlay::buildFramebufferObject() { auto width = uiSize.x; auto height = uiSize.y; if (!_overlayFramebuffer->getDepthStencilBuffer()) { - auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, DEFAULT_SAMPLER)); + auto overlayDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(DEPTH_FORMAT, width, height, gpu::Texture::SINGLE_MIP, DEFAULT_SAMPLER)); _overlayFramebuffer->setDepthStencilBuffer(overlayDepthTexture, DEPTH_FORMAT); } if (!_overlayFramebuffer->getRenderBuffer(0)) { const gpu::Sampler OVERLAY_SAMPLER(gpu::Sampler::FILTER_MIN_MAG_LINEAR, gpu::Sampler::WRAP_CLAMP); - auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, OVERLAY_SAMPLER)); + auto colorBuffer = gpu::TexturePointer(gpu::Texture::createRenderBuffer(COLOR_FORMAT, width, height, gpu::Texture::SINGLE_MIP, OVERLAY_SAMPLER)); _overlayFramebuffer->setRenderBuffer(0, colorBuffer); } } diff --git a/interface/src/ui/AvatarInputs.cpp b/interface/src/ui/AvatarInputs.cpp index 7d63a85a27..341915e57f 100644 --- a/interface/src/ui/AvatarInputs.cpp +++ b/interface/src/ui/AvatarInputs.cpp @@ -62,24 +62,13 @@ AvatarInputs::AvatarInputs(QQuickItem* parent) : QQuickItem(parent) { } \ } -void AvatarInputs::update() { - if (!Menu::getInstance()) { - return; - } - AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); - AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); - AI_UPDATE(isHMD, qApp->isHMDMode()); - - AI_UPDATE_WRITABLE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); - - auto audioIO = DependencyManager::get(); +float AvatarInputs::loudnessToAudioLevel(float loudness) { const float AUDIO_METER_AVERAGING = 0.5; const float LOG2 = log(2.0f); const float METER_LOUDNESS_SCALE = 2.8f / 5.0f; const float LOG2_LOUDNESS_FLOOR = 11.0f; float audioLevel = 0.0f; - auto audio = DependencyManager::get(); - float loudness = audio->getLastInputLoudness() + 1.0f; + loudness += 1.0f; _trailingAudioLoudness = AUDIO_METER_AVERAGING * _trailingAudioLoudness + (1.0f - AUDIO_METER_AVERAGING) * loudness; @@ -93,6 +82,24 @@ void AvatarInputs::update() { if (audioLevel > 1.0f) { audioLevel = 1.0; } + return audioLevel; +} + +void AvatarInputs::update() { + if (!Menu::getInstance()) { + return; + } + + AI_UPDATE(cameraEnabled, !Menu::getInstance()->isOptionChecked(MenuOption::NoFaceTracking)); + AI_UPDATE(cameraMuted, Menu::getInstance()->isOptionChecked(MenuOption::MuteFaceTracking)); + AI_UPDATE(isHMD, qApp->isHMDMode()); + + AI_UPDATE_WRITABLE(showAudioTools, Menu::getInstance()->isOptionChecked(MenuOption::AudioTools)); + + auto audioIO = DependencyManager::get(); + + const float audioLevel = loudnessToAudioLevel(DependencyManager::get()->getLastInputLoudness()); + AI_UPDATE_FLOAT(audioLevel, audioLevel, 0.01f); AI_UPDATE(audioClipping, ((audioIO->getTimeSinceLastClip() > 0.0f) && (audioIO->getTimeSinceLastClip() < 1.0f))); AI_UPDATE(audioMuted, audioIO->isMuted()); diff --git a/interface/src/ui/AvatarInputs.h b/interface/src/ui/AvatarInputs.h index 0c4fc0f23c..34b2cbca8b 100644 --- a/interface/src/ui/AvatarInputs.h +++ b/interface/src/ui/AvatarInputs.h @@ -34,6 +34,7 @@ class AvatarInputs : public QQuickItem { public: static AvatarInputs* getInstance(); + float loudnessToAudioLevel(float loudness); AvatarInputs(QQuickItem* parent = nullptr); void update(); bool showAudioTools() const { return _showAudioTools; } diff --git a/interface/src/ui/overlays/Web3DOverlay.cpp b/interface/src/ui/overlays/Web3DOverlay.cpp index 8c1c54a585..563f35a820 100644 --- a/interface/src/ui/overlays/Web3DOverlay.cpp +++ b/interface/src/ui/overlays/Web3DOverlay.cpp @@ -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().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().data()); _webSurface->getRootContext()->setContextProperty("pathToFonts", "../../"); tabletScriptingInterface->setQmlTabletRoot("com.highfidelity.interface.tablet.system", _webSurface->getRootItem(), _webSurface.data()); @@ -355,35 +359,33 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { glm::vec2 windowPos = event.getPos2D() * (METERS_TO_INCHES * _dpi); QPointF windowPoint(windowPos.x, windowPos.y); - if (event.getButtons() == PointerEvent::NoButtons && event.getType() == PointerEvent::Move) { - // Forward a mouse move event to the Web surface. - QMouseEvent* mouseEvent = new QMouseEvent(QEvent::MouseMove, windowPoint, windowPoint, windowPoint, Qt::NoButton, Qt::NoButton, Qt::NoModifier); - QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); - return; - } - if (event.getType() == PointerEvent::Press && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = true; } else if (event.getType() == PointerEvent::Release && event.getButton() == PointerEvent::PrimaryButton) { this->_pressed = false; } - QEvent::Type type; + QEvent::Type touchType; Qt::TouchPointState touchPointState; + QEvent::Type mouseType; switch (event.getType()) { case PointerEvent::Press: - type = QEvent::TouchBegin; + touchType = QEvent::TouchBegin; touchPointState = Qt::TouchPointPressed; + mouseType = QEvent::MouseButtonPress; break; case PointerEvent::Release: - type = QEvent::TouchEnd; + touchType = QEvent::TouchEnd; touchPointState = Qt::TouchPointReleased; + mouseType = QEvent::MouseButtonRelease; break; case PointerEvent::Move: - default: - type = QEvent::TouchUpdate; + touchType = QEvent::TouchUpdate; touchPointState = Qt::TouchPointMoved; + mouseType = QEvent::MouseMove; break; + default: + return; } QTouchEvent::TouchPoint point; @@ -394,13 +396,30 @@ void Web3DOverlay::handlePointerEventAsTouch(const PointerEvent& event) { QList touchPoints; touchPoints.push_back(point); - QTouchEvent* touchEvent = new QTouchEvent(type, &_touchDevice, event.getKeyboardModifiers()); + QTouchEvent* touchEvent = new QTouchEvent(touchType, &_touchDevice, event.getKeyboardModifiers()); touchEvent->setWindow(_webSurface->getWindow()); touchEvent->setTarget(_webSurface->getRootItem()); touchEvent->setTouchPoints(touchPoints); touchEvent->setTouchPointStates(touchPointState); QCoreApplication::postEvent(_webSurface->getWindow(), touchEvent); + + // Send mouse events to the Web surface so that HTML dialog elements work with mouse press and hover. + // FIXME: Scroll bar dragging is a bit unstable in the tablet (content can jump up and down at times). + // This may be improved in Qt 5.8. Release notes: "Cleaned up touch and mouse event delivery". + + Qt::MouseButtons buttons = Qt::NoButton; + if (event.getButtons() & PointerEvent::PrimaryButton) { + buttons |= Qt::LeftButton; + } + + Qt::MouseButton button = Qt::NoButton; + if (event.getButton() == PointerEvent::PrimaryButton) { + button = Qt::LeftButton; + } + + QMouseEvent* mouseEvent = new QMouseEvent(mouseType, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); + QCoreApplication::postEvent(_webSurface->getWindow(), mouseEvent); } void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { @@ -422,11 +441,12 @@ void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { buttons |= Qt::LeftButton; } - QEvent::Type type; Qt::MouseButton button = Qt::NoButton; if (event.getButton() == PointerEvent::PrimaryButton) { button = Qt::LeftButton; } + + QEvent::Type type; switch (event.getType()) { case PointerEvent::Press: type = QEvent::MouseButtonPress; @@ -435,9 +455,10 @@ void Web3DOverlay::handlePointerEventAsMouse(const PointerEvent& event) { type = QEvent::MouseButtonRelease; break; case PointerEvent::Move: - default: type = QEvent::MouseMove; break; + default: + return; } QMouseEvent* mouseEvent = new QMouseEvent(type, windowPoint, windowPoint, windowPoint, button, buttons, Qt::NoModifier); diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index dc806a4115..a1ea103edb 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -1495,6 +1495,9 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC setAvatarEntityData(identity.avatarEntityData); identityChanged = true; } + // flag this avatar as non-stale by updating _averageBytesReceived + const int BOGUS_NUM_BYTES = 1; + _averageBytesReceived.updateAverage(BOGUS_NUM_BYTES); } QByteArray AvatarData::identityByteArray() const { diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index f0759aedbd..1327798a0a 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -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(); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp index 5a317f64bc..a3cf91fcd5 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.cpp @@ -356,15 +356,16 @@ void OpenGLDisplayPlugin::customizeContext() { cursorData.texture.reset( gpu::Texture::createStrict( - gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), - image.width(), image.height(), - gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), + image.width(), image.height(), + gpu::Texture::MAX_NUM_MIPS, + gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); cursorData.texture->setSource("cursor texture"); auto usage = gpu::Texture::Usage::Builder().withColor().withAlpha(); cursorData.texture->setUsage(usage.build()); cursorData.texture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); cursorData.texture->assignStoredMip(0, image.byteCount(), image.constBits()); - cursorData.texture->autoGenerateMips(-1); + cursorData.texture->setAutoGenerateMips(true); } } } diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp index c55d985a62..52c689ec00 100644 --- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp +++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp @@ -299,12 +299,13 @@ void HmdDisplayPlugin::internalPresent() { gpu::Texture::createStrict( gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA), image.width(), image.height(), + gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); _previewTexture->setSource("HMD Preview Texture"); _previewTexture->setUsage(gpu::Texture::Usage::Builder().withColor().build()); _previewTexture->setStoredMipFormat(gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA)); _previewTexture->assignStoredMip(0, image.byteCount(), image.constBits()); - _previewTexture->autoGenerateMips(-1); + _previewTexture->setAutoGenerateMips(true); } auto viewport = getViewportForSourceSize(uvec2(_previewTexture->getDimensions())); diff --git a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp index 8aab6abaa9..1dad72dbc1 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp +++ b/libraries/gpu-gl/src/gpu/gl/GLBackendPipeline.cpp @@ -229,7 +229,7 @@ void GLBackend::do_setResourceTexture(const Batch& batch, size_t paramOffset) { _resource._textures[slot] = resourceTexture; - _stats._RSAmountTextureMemoryBounded += object->size(); + _stats._RSAmountTextureMemoryBounded += (int) object->size(); } else { releaseResourceTexture(slot); diff --git a/libraries/gpu-gl/src/gpu/gl/GLTexture.h b/libraries/gpu-gl/src/gpu/gl/GLTexture.h index 1f91e17157..b47aa3e8dd 100644 --- a/libraries/gpu-gl/src/gpu/gl/GLTexture.h +++ b/libraries/gpu-gl/src/gpu/gl/GLTexture.h @@ -43,7 +43,7 @@ public: static const GLenum WRAP_MODES[Sampler::NUM_WRAP_MODES]; protected: - virtual uint32 size() const = 0; + virtual Size size() const = 0; virtual void generateMips() const = 0; GLTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); @@ -57,7 +57,7 @@ public: protected: GLExternalTexture(const std::weak_ptr& backend, const Texture& texture, GLuint id); void generateMips() const override {} - uint32 size() const override { return 0; } + Size size() const override { return 0; } }; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h index 6d2f91c436..93d65b74dd 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h +++ b/libraries/gpu-gl/src/gpu/gl41/GL41Backend.h @@ -40,30 +40,59 @@ public: class GL41Texture : public GLTexture { using Parent = GLTexture; - static GLuint allocate(); - - public: - ~GL41Texture(); - - private: - GL41Texture(const std::weak_ptr& backend, const Texture& buffer); - - void generateMips() const override; - uint32 size() const override; - friend class GL41Backend; - const Stamp _storageStamp; - mutable Stamp _contentStamp { 0 }; - mutable Stamp _samplerStamp { 0 }; - const uint32 _size; + static GLuint allocate(const Texture& texture); + protected: + GL41Texture(const std::weak_ptr& backend, const Texture& texture); + void generateMips() const override; + void copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const; + void copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const; + virtual void syncSampler() const; - - bool isOutdated() const; void withPreservedTexture(std::function f) const; - void syncContent() const; - void syncSampler() const; }; + // + // Textures that have fixed allocation sizes and cannot be managed at runtime + // + + class GL41FixedAllocationTexture : public GL41Texture { + using Parent = GL41Texture; + friend class GL41Backend; + + public: + GL41FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41FixedAllocationTexture(); + + protected: + Size size() const override { return _size; } + void allocateStorage() const; + void syncSampler() const override; + const Size _size { 0 }; + }; + + class GL41AttachmentTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41AttachmentTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41AttachmentTexture(); + }; + + class GL41StrictResourceTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture); + }; + + class GL41ResourceTexture : public GL41FixedAllocationTexture { + using Parent = GL41FixedAllocationTexture; + friend class GL41Backend; + protected: + GL41ResourceTexture(const std::weak_ptr& backend, const Texture& texture); + ~GL41ResourceTexture(); + }; protected: GLuint getFramebufferID(const FramebufferPointer& framebuffer) override; diff --git a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp index 46672b3b65..2056085091 100644 --- a/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl41/GL41BackendTexture.cpp @@ -19,20 +19,11 @@ using namespace gpu; using namespace gpu::gl; using namespace gpu::gl41; -using GL41TexelFormat = GLTexelFormat; -using GL41Texture = GL41Backend::GL41Texture; - -GLuint GL41Texture::allocate() { - Backend::incrementTextureGPUCount(); - GLuint result; - glGenTextures(1, &result); - return result; -} - GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { if (!texturePointer) { return nullptr; } + const Texture& texture = *texturePointer; if (TextureUsageType::EXTERNAL == texture.getUsageType()) { return Parent::syncGPUObject(texturePointer); @@ -43,90 +34,58 @@ GLTexture* GL41Backend::syncGPUObject(const TexturePointer& texturePointer) { return nullptr; } - // If the object hasn't been created, or the object definition is out of date, drop and re-create GL41Texture* object = Backend::getGPUObject(texture); - if (!object || object->_storageStamp < texture.getStamp()) { - // This automatically any previous texture - object = new GL41Texture(shared_from_this(), texture); - } + if (!object) { + switch (texture.getUsageType()) { + case TextureUsageType::RENDERBUFFER: + object = new GL41AttachmentTexture(shared_from_this(), texture); + break; - // FIXME internalize to GL41Texture 'sync' function - if (object->isOutdated()) { - object->withPreservedTexture([&] { - if (object->_contentStamp <= texture.getDataStamp()) { - // FIXME implement synchronous texture transfer here - object->syncContent(); + case TextureUsageType::STRICT_RESOURCE: + qCDebug(gpugllogging) << "Strict texture " << texture.source().c_str(); + object = new GL41StrictResourceTexture(shared_from_this(), texture); + break; + + case TextureUsageType::RESOURCE: { + qCDebug(gpugllogging) << "variable / Strict texture " << texture.source().c_str(); + object = new GL41ResourceTexture(shared_from_this(), texture); + break; } - if (object->_samplerStamp <= texture.getSamplerStamp()) { - object->syncSampler(); - } - }); + default: + Q_UNREACHABLE(); + } } return object; } -GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) - : GLTexture(backend, texture, allocate()), _storageStamp { texture.getStamp() }, _size(texture.evalTotalSize()) { +using GL41Texture = GL41Backend::GL41Texture; + +GL41Texture::GL41Texture(const std::weak_ptr& backend, const Texture& texture) + : GLTexture(backend, texture, allocate(texture)) { incrementTextureGPUCount(); - withPreservedTexture([&] { - GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); - auto numMips = _gpuObject.getNumMipLevels(); - for (uint16_t mipLevel = 0; mipLevel < numMips; ++mipLevel) { - // Get the mip level dimensions, accounting for the downgrade level - Vec3u dimensions = _gpuObject.evalMipDimensions(mipLevel); - uint8_t face = 0; - for (GLenum target : getFaceTargets(_target)) { - const Byte* mipData = nullptr; - if (_gpuObject.isStoredMipFaceAvailable(mipLevel, face)) { - auto mip = _gpuObject.accessStoredMipFace(mipLevel, face); - mipData = mip->readData(); - } - glTexImage2D(target, mipLevel, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, mipData); - (void)CHECK_GL_ERROR(); - ++face; - } - } - }); } -GL41Texture::~GL41Texture() { - +GLuint GL41Texture::allocate(const Texture& texture) { + GLuint result; + glGenTextures(1, &result); + return result; } -bool GL41Texture::isOutdated() const { - if (_samplerStamp <= _gpuObject.getSamplerStamp()) { - return true; - } - if (TextureUsageType::RESOURCE == _gpuObject.getUsageType() && _contentStamp <= _gpuObject.getDataStamp()) { - return true; - } - return false; -} void GL41Texture::withPreservedTexture(std::function f) const { - GLint boundTex = -1; - switch (_target) { - case GL_TEXTURE_2D: - glGetIntegerv(GL_TEXTURE_BINDING_2D, &boundTex); - break; - - case GL_TEXTURE_CUBE_MAP: - glGetIntegerv(GL_TEXTURE_BINDING_CUBE_MAP, &boundTex); - break; - - default: - qFatal("Unsupported texture type"); - } + const GLint TRANSFER_TEXTURE_UNIT = 32; + glActiveTexture(GL_TEXTURE0 + TRANSFER_TEXTURE_UNIT); + glBindTexture(_target, _texture); (void)CHECK_GL_ERROR(); - glBindTexture(_target, _texture); f(); - glBindTexture(_target, boundTex); + glBindTexture(_target, 0); (void)CHECK_GL_ERROR(); } + void GL41Texture::generateMips() const { withPreservedTexture([&] { glGenerateMipmap(_target); @@ -134,13 +93,35 @@ void GL41Texture::generateMips() const { (void)CHECK_GL_ERROR(); } -void GL41Texture::syncContent() const { - // FIXME actually copy the texture data - _contentStamp = _gpuObject.getDataStamp() + 1; +void GL41Texture::copyMipFaceLinesFromTexture(uint16_t mip, uint8_t face, const uvec3& size, uint32_t yOffset, GLenum format, GLenum type, const void* sourcePointer) const { + if (GL_TEXTURE_2D == _target) { + glTexSubImage2D(_target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else if (GL_TEXTURE_CUBE_MAP == _target) { + auto target = GLTexture::CUBE_FACE_LAYOUT[face]; + glTexSubImage2D(target, mip, 0, yOffset, size.x, size.y, format, type, sourcePointer); + } else { + assert(false); + } + (void)CHECK_GL_ERROR(); +} + +void GL41Texture::copyMipFaceFromTexture(uint16_t sourceMip, uint16_t targetMip, uint8_t face) const { + if (!_gpuObject.isStoredMipFaceAvailable(sourceMip)) { + return; + } + auto size = _gpuObject.evalMipDimensions(sourceMip); + auto mipData = _gpuObject.accessStoredMipFace(sourceMip, face); + if (mipData) { + GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat(), _gpuObject.getStoredMipFormat()); + copyMipFaceLinesFromTexture(targetMip, face, size, 0, texelFormat.format, texelFormat.type, mipData->readData()); + } else { + qCDebug(gpugllogging) << "Missing mipData level=" << sourceMip << " face=" << (int)face << " for texture " << _gpuObject.source().c_str(); + } } void GL41Texture::syncSampler() const { const Sampler& sampler = _gpuObject.getSampler(); + const auto& fm = FILTER_MODES[sampler.getFilter()]; glTexParameteri(_target, GL_TEXTURE_MIN_FILTER, fm.minFilter); glTexParameteri(_target, GL_TEXTURE_MAG_FILTER, fm.magFilter); @@ -158,12 +139,106 @@ void GL41Texture::syncSampler() const { glTexParameterfv(_target, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, (uint16)sampler.getMipOffset()); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); + glTexParameterf(_target, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); - _samplerStamp = _gpuObject.getSamplerStamp() + 1; } -uint32 GL41Texture::size() const { - return _size; +using GL41FixedAllocationTexture = GL41Backend::GL41FixedAllocationTexture; + +GL41FixedAllocationTexture::GL41FixedAllocationTexture(const std::weak_ptr& backend, const Texture& texture) : GL41Texture(backend, texture), _size(texture.evalTotalSize()) { + withPreservedTexture([&] { + allocateStorage(); + syncSampler(); + }); +} + +GL41FixedAllocationTexture::~GL41FixedAllocationTexture() { +} + +void GL41FixedAllocationTexture::allocateStorage() const { + const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); + const auto numMips = _gpuObject.getNumMips(); + + // glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + for (GLint level = 0; level < numMips; level++) { + Vec3u dimensions = _gpuObject.evalMipDimensions(level); + for (GLenum target : getFaceTargets(_target)) { + glTexImage2D(target, level, texelFormat.internalFormat, dimensions.x, dimensions.y, 0, texelFormat.format, texelFormat.type, nullptr); + } + } + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, 0); + glTexParameteri(_target, GL_TEXTURE_MAX_LEVEL, numMips - 1); +} + +void GL41FixedAllocationTexture::syncSampler() const { + Parent::syncSampler(); + const Sampler& sampler = _gpuObject.getSampler(); + auto baseMip = std::max(sampler.getMipOffset(), sampler.getMinMip()); + + glTexParameteri(_target, GL_TEXTURE_BASE_LEVEL, baseMip); + glTexParameterf(_target, GL_TEXTURE_MIN_LOD, (float)sampler.getMinMip()); + glTexParameterf(_target, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.0f : sampler.getMaxMip())); +} + +// Renderbuffer attachment textures +using GL41AttachmentTexture = GL41Backend::GL41AttachmentTexture; + +GL41AttachmentTexture::GL41AttachmentTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUFramebufferMemoryUsage(0, size()); +} + +GL41AttachmentTexture::~GL41AttachmentTexture() { + Backend::updateTextureGPUFramebufferMemoryUsage(size(), 0); +} + +// Strict resource textures +using GL41StrictResourceTexture = GL41Backend::GL41StrictResourceTexture; + +GL41StrictResourceTexture::GL41StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + withPreservedTexture([&] { + + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; sourceMip++) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; face++) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + }); + + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +// resource textures +using GL41ResourceTexture = GL41Backend::GL41ResourceTexture; + +GL41ResourceTexture::GL41ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL41FixedAllocationTexture(backend, texture) { + Backend::updateTextureGPUMemoryUsage(0, size()); + + withPreservedTexture([&] { + + auto mipLevels = _gpuObject.getNumMips(); + for (uint16_t sourceMip = 0; sourceMip < mipLevels; sourceMip++) { + uint16_t targetMip = sourceMip; + size_t maxFace = GLTexture::getFaceCount(_target); + for (uint8_t face = 0; face < maxFace; face++) { + copyMipFaceFromTexture(sourceMip, targetMip, face); + } + } + }); + + if (texture.isAutogenerateMips()) { + generateMips(); + } +} + +GL41ResourceTexture::~GL41ResourceTexture() { + Backend::updateTextureGPUMemoryUsage(size(), 0); } diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h index 6a9811b055..ca8028aff6 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h +++ b/libraries/gpu-gl/src/gpu/gl45/GL45Backend.h @@ -58,10 +58,10 @@ public: ~GL45FixedAllocationTexture(); protected: - uint32 size() const override { return _size; } + Size size() const override { return _size; } void allocateStorage() const; void syncSampler() const override; - const uint32 _size { 0 }; + const Size _size { 0 }; }; class GL45AttachmentTexture : public GL45FixedAllocationTexture { @@ -173,7 +173,7 @@ public: bool canDemote() const { return _allocatedMip < _maxAllocatedMip; } bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; } void executeNextTransfer(const TexturePointer& currentTexture); - uint32 size() const override { return _size; } + Size size() const override { return _size; } virtual void populateTransferQueue() = 0; virtual void promote() = 0; virtual void demote() = 0; @@ -188,7 +188,7 @@ public: // The highest (lowest resolution) mip that we will support, relative to the number // of mips in the gpu::Texture object uint16 _maxAllocatedMip { 0 }; - uint32 _size { 0 }; + Size _size { 0 }; // Contains a series of lambdas that when executed will transfer data to the GPU, modify // the _populatedMip and update the sampler in order to fully populate the allocated texture // until _populatedMip == _allocatedMip diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp index c82c13c57c..ab4153c04c 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendTexture.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include "../gl/GLTexelFormat.h" @@ -167,8 +166,10 @@ void GL45Texture::syncSampler() const { glTextureParameteri(_id, GL_TEXTURE_WRAP_S, WRAP_MODES[sampler.getWrapModeU()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_T, WRAP_MODES[sampler.getWrapModeV()]); glTextureParameteri(_id, GL_TEXTURE_WRAP_R, WRAP_MODES[sampler.getWrapModeW()]); + glTextureParameterf(_id, GL_TEXTURE_MAX_ANISOTROPY_EXT, sampler.getMaxAnisotropy()); glTextureParameterfv(_id, GL_TEXTURE_BORDER_COLOR, (const float*)&sampler.getBorderColor()); + glTextureParameterf(_id, GL_TEXTURE_MIN_LOD, sampler.getMinMip()); glTextureParameterf(_id, GL_TEXTURE_MAX_LOD, (sampler.getMaxMip() == Sampler::MAX_MIP_LEVEL ? 1000.f : sampler.getMaxMip())); } @@ -186,10 +187,12 @@ GL45FixedAllocationTexture::~GL45FixedAllocationTexture() { void GL45FixedAllocationTexture::allocateStorage() const { const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); const auto dimensions = _gpuObject.getDimensions(); - const auto mips = _gpuObject.getNumMipLevels(); + const auto mips = _gpuObject.getNumMips(); glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); + glTextureParameteri(_id, GL_TEXTURE_BASE_LEVEL, 0); + glTextureParameteri(_id, GL_TEXTURE_MAX_LEVEL, mips - 1); } void GL45FixedAllocationTexture::syncSampler() const { @@ -216,7 +219,7 @@ GL45AttachmentTexture::~GL45AttachmentTexture() { using GL45StrictResourceTexture = GL45Backend::GL45StrictResourceTexture; GL45StrictResourceTexture::GL45StrictResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45FixedAllocationTexture(backend, texture) { - auto mipLevels = _gpuObject.getNumMipLevels(); + auto mipLevels = _gpuObject.getNumMips(); for (uint16_t sourceMip = 0; sourceMip < mipLevels; ++sourceMip) { uint16_t targetMip = sourceMip; size_t maxFace = GLTexture::getFaceCount(_target); diff --git a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp index 4083f09251..a614d62221 100644 --- a/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp +++ b/libraries/gpu-gl/src/gpu/gl45/GL45BackendVariableTexture.cpp @@ -441,7 +441,7 @@ void GL45VariableAllocationTexture::executeNextTransfer(const TexturePointer& cu using GL45ResourceTexture = GL45Backend::GL45ResourceTexture; GL45ResourceTexture::GL45ResourceTexture(const std::weak_ptr& backend, const Texture& texture) : GL45VariableAllocationTexture(backend, texture) { - auto mipLevels = texture.evalNumMips(); + auto mipLevels = texture.getNumMips(); _allocatedMip = mipLevels; uvec3 mipDimensions; for (uint16_t mip = 0; mip < mipLevels; ++mip) { @@ -463,10 +463,10 @@ void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { _allocatedMip = allocatedMip; const GLTexelFormat texelFormat = GLTexelFormat::evalGLTexelFormat(_gpuObject.getTexelFormat()); const auto dimensions = _gpuObject.evalMipDimensions(_allocatedMip); - const auto totalMips = _gpuObject.getNumMipLevels(); + const auto totalMips = _gpuObject.getNumMips(); const auto mips = totalMips - _allocatedMip; glTextureStorage2D(_id, mips, texelFormat.internalFormat, dimensions.x, dimensions.y); - auto mipLevels = _gpuObject.getNumMipLevels(); + auto mipLevels = _gpuObject.getNumMips(); _size = 0; for (uint16_t mip = _allocatedMip; mip < mipLevels; ++mip) { _size += _gpuObject.evalMipSize(mip); @@ -476,7 +476,7 @@ void GL45ResourceTexture::allocateStorage(uint16 allocatedMip) { } void GL45ResourceTexture::copyMipsFromTexture() { - auto mipLevels = _gpuObject.getNumMipLevels(); + auto mipLevels = _gpuObject.getNumMips(); size_t maxFace = GLTexture::getFaceCount(_target); for (uint16_t sourceMip = _populatedMip; sourceMip < mipLevels; ++sourceMip) { uint16_t targetMip = sourceMip - _allocatedMip; @@ -495,13 +495,13 @@ void GL45ResourceTexture::promote() { PROFILE_RANGE(render_gpu_gl, __FUNCTION__); Q_ASSERT(_allocatedMip > 0); GLuint oldId = _id; - uint32_t oldSize = _size; + auto oldSize = _size; // create new texture const_cast(_id) = allocate(_gpuObject); uint16_t oldAllocatedMip = _allocatedMip; // allocate storage for new level allocateStorage(_allocatedMip - std::min(_allocatedMip, 2)); - uint16_t mips = _gpuObject.getNumMipLevels(); + uint16_t mips = _gpuObject.getNumMips(); // copy pre-existing mips for (uint16_t mip = _populatedMip; mip < mips; ++mip) { auto mipDimensions = _gpuObject.evalMipDimensions(mip); @@ -534,7 +534,7 @@ void GL45ResourceTexture::demote() { const_cast(_id) = allocate(_gpuObject); allocateStorage(_allocatedMip + 1); _populatedMip = std::max(_populatedMip, _allocatedMip); - uint16_t mips = _gpuObject.getNumMipLevels(); + uint16_t mips = _gpuObject.getNumMips(); // copy pre-existing mips for (uint16_t mip = _populatedMip; mip < mips; ++mip) { auto mipDimensions = _gpuObject.evalMipDimensions(mip); diff --git a/libraries/gpu/src/gpu/Context.cpp b/libraries/gpu/src/gpu/Context.cpp index cc570f696f..0030b2fa88 100644 --- a/libraries/gpu/src/gpu/Context.cpp +++ b/libraries/gpu/src/gpu/Context.cpp @@ -265,7 +265,7 @@ void Context::incrementBufferGPUCount() { auto total = ++_bufferGPUCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU buffers " << total; + // qCDebug(gpulogging) << "New max GPU buffers " << total; } } void Context::decrementBufferGPUCount() { @@ -299,7 +299,7 @@ void Context::incrementTextureGPUCount() { auto total = ++_textureGPUCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures " << total; + // qCDebug(gpulogging) << "New max GPU textures " << total; } } void Context::decrementTextureGPUCount() { @@ -311,7 +311,7 @@ void Context::incrementTextureGPUSparseCount() { auto total = ++_textureGPUSparseCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures " << total; + // qCDebug(gpulogging) << "New max GPU textures " << total; } } void Context::decrementTextureGPUSparseCount() { @@ -378,7 +378,7 @@ void Context::incrementTextureGPUTransferCount() { auto total = ++_textureGPUTransferCount; if (total > max.load()) { max = total; - qCDebug(gpulogging) << "New max GPU textures transfers" << total; + // qCDebug(gpulogging) << "New max GPU textures transfers" << total; } } diff --git a/libraries/gpu/src/gpu/Framebuffer.cpp b/libraries/gpu/src/gpu/Framebuffer.cpp index 0d3291a74d..b49c681889 100755 --- a/libraries/gpu/src/gpu/Framebuffer.cpp +++ b/libraries/gpu/src/gpu/Framebuffer.cpp @@ -32,7 +32,7 @@ Framebuffer* Framebuffer::create(const std::string& name) { Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); colorTexture->setSource("Framebuffer::colorTexture"); framebuffer->setRenderBuffer(0, colorTexture); @@ -43,8 +43,8 @@ Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBuf Framebuffer* Framebuffer::create(const std::string& name, const Format& colorBufferFormat, const Format& depthStencilBufferFormat, uint16 width, uint16 height) { auto framebuffer = Framebuffer::create(name); - auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); - auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto colorTexture = TexturePointer(Texture::createRenderBuffer(colorBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); + auto depthTexture = TexturePointer(Texture::createRenderBuffer(depthStencilBufferFormat, width, height, Texture::SINGLE_MIP, Sampler(Sampler::FILTER_MIN_MAG_POINT))); framebuffer->setRenderBuffer(0, colorTexture); framebuffer->setDepthStencilBuffer(depthTexture, depthStencilBufferFormat); diff --git a/libraries/gpu/src/gpu/Texture.cpp b/libraries/gpu/src/gpu/Texture.cpp index 38019c5a03..1e65972114 100755 --- a/libraries/gpu/src/gpu/Texture.cpp +++ b/libraries/gpu/src/gpu/Texture.cpp @@ -149,8 +149,14 @@ PixelsPointer MemoryStorage::getMipFace(uint16 level, uint8 face) const { return PixelsPointer(); } + Size MemoryStorage::getMipFaceSize(uint16 level, uint8 face) const { - return getMipFace(level, face)->getSize(); + PixelsPointer mipFace = getMipFace(level, face); + if (mipFace) { + return mipFace->getSize(); + } else { + return 0; + } } bool MemoryStorage::isMipAvailable(uint16 level, uint8 face) const { @@ -209,44 +215,43 @@ void Texture::MemoryStorage::assignMipFaceData(uint16 level, uint8 face, const s Texture* Texture::createExternal(const ExternalRecycler& recycler, const Sampler& sampler) { Texture* tex = new Texture(TextureUsageType::EXTERNAL); tex->_type = TEX_2D; - tex->_maxMip = 0; + tex->_maxMipLevel = 0; tex->_sampler = sampler; tex->setExternalRecycler(recycler); return tex; } -Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +Texture* Texture::createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RENDERBUFFER, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create1D(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, sampler); +Texture* Texture::create1D(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_1D, texelFormat, width, 1, 1, 1, 0, numMips, sampler); } -Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +Texture* Texture::create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler) { - return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, sampler); +Texture* Texture::createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::STRICT_RESOURCE, TEX_2D, texelFormat, width, height, 1, 1, 0, numMips, sampler); } -Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, sampler); +Texture* Texture::create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_3D, texelFormat, width, height, depth, 1, 0, numMips, sampler); } -Texture* Texture::createCube(const Element& texelFormat, uint16 width, const Sampler& sampler) { - return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, sampler); +Texture* Texture::createCube(const Element& texelFormat, uint16 width, uint16 numMips, const Sampler& sampler) { + return create(TextureUsageType::RESOURCE, TEX_CUBE, texelFormat, width, width, 1, 1, 0, numMips, sampler); } -Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler) +Texture* Texture::create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler) { Texture* tex = new Texture(usageType); tex->_storage.reset(new MemoryStorage()); tex->_type = type; tex->_storage->assignTexture(tex); - tex->_maxMip = 0; - tex->resize(type, texelFormat, width, height, depth, numSamples, numSlices); + tex->resize(type, texelFormat, width, height, depth, numSamples, numSlices, numMips); tex->_sampler = sampler; @@ -278,7 +283,7 @@ Texture::~Texture() { } } -Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices) { +Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips) { if (width && height && depth && numSamples) { bool changed = false; @@ -313,9 +318,19 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt _depth = depth; changed = true; } - + + if ((_maxMipLevel + 1) != numMips) { + _maxMipLevel = safeNumMips(numMips) - 1; + changed = true; + } + + if (texelFormat != _texelFormat) { + _texelFormat = texelFormat; + changed = true; + } + // Evaluate the new size with the new format - uint32_t size = NUM_FACES_PER_TYPE[_type] *_width * _height * _depth * _numSamples * texelFormat.getSize(); + Size size = NUM_FACES_PER_TYPE[_type] * _height * _depth * evalPaddedSize(_numSamples * _width * _texelFormat.getSize()); // If size change then we need to reset if (changed || (size != getSize())) { @@ -324,12 +339,6 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt _stamp++; } - // TexelFormat might have change, but it's mostly interpretation - if (texelFormat != _texelFormat) { - _texelFormat = texelFormat; - _stamp++; - } - // Here the Texture has been fully defined from the gpu point of view (size and format) _defined = true; } else { @@ -339,23 +348,6 @@ Texture::Size Texture::resize(Type type, const Element& texelFormat, uint16 widt return _size; } -Texture::Size Texture::resize1D(uint16 width, uint16 numSamples) { - return resize(TEX_1D, getTexelFormat(), width, 1, 1, numSamples, 0); -} -Texture::Size Texture::resize2D(uint16 width, uint16 height, uint16 numSamples) { - return resize(TEX_2D, getTexelFormat(), width, height, 1, numSamples, 0); -} -Texture::Size Texture::resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples) { - return resize(TEX_3D, getTexelFormat(), width, height, depth, numSamples, 0); -} -Texture::Size Texture::resizeCube(uint16 width, uint16 numSamples) { - return resize(TEX_CUBE, getTexelFormat(), width, 1, 1, numSamples, 0); -} - -Texture::Size Texture::reformat(const Element& texelFormat) { - return resize(_type, texelFormat, getWidth(), getHeight(), getDepth(), getNumSamples(), _numSlices); -} - bool Texture::isColorRenderTarget() const { return (_texelFormat.getSemantic() == gpu::RGBA); } @@ -364,7 +356,7 @@ bool Texture::isDepthStencilRenderTarget() const { return (_texelFormat.getSemantic() == gpu::DEPTH) || (_texelFormat.getSemantic() == gpu::DEPTH_STENCIL); } -uint16 Texture::evalDimNumMips(uint16 size) { +uint16 Texture::evalDimMaxNumMips(uint16 size) { double largerDim = size; double val = log(largerDim)/log(2.0); return 1 + (uint16) val; @@ -372,7 +364,7 @@ uint16 Texture::evalDimNumMips(uint16 size) { static const double LOG_2 = log(2.0); -uint16 Texture::evalNumMips(const Vec3u& dimensions) { +uint16 Texture::evalMaxNumMips(const Vec3u& dimensions) { double largerDim = glm::compMax(dimensions); double val = log(largerDim) / LOG_2; return 1 + (uint16)val; @@ -380,8 +372,34 @@ uint16 Texture::evalNumMips(const Vec3u& dimensions) { // The number mips that the texture could have if all existed // = log2(max(width, height, depth)) -uint16 Texture::evalNumMips() const { - return evalNumMips({ _width, _height, _depth }); +uint16 Texture::evalMaxNumMips() const { + return evalMaxNumMips({ _width, _height, _depth }); +} + +// Check a num of mips requested against the maximum possible specified +// if passing -1 then answer the max +// simply does (askedNumMips == 0 ? maxNumMips : (numstd::min(askedNumMips, maxNumMips)) +uint16 Texture::safeNumMips(uint16 askedNumMips, uint16 maxNumMips) { + if (askedNumMips > 0) { + return std::min(askedNumMips, maxNumMips); + } else { + return maxNumMips; + } +} + +// Same but applied to this texture's num max mips from evalNumMips() +uint16 Texture::safeNumMips(uint16 askedNumMips) const { + return safeNumMips(askedNumMips, evalMaxNumMips()); +} + +Size Texture::evalTotalSize(uint16 startingMip) const { + Size size = 0; + uint16 minMipLevel = std::max(getMinMip(), startingMip); + uint16 maxMipLevel = getMaxMip(); + for (uint16 level = minMipLevel; level <= maxMipLevel; level++) { + size += evalMipSize(level); + } + return size * getNumSlices(); } void Texture::setStoredMipFormat(const Element& format) { @@ -408,7 +426,7 @@ void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { if (_autoGenerateMips) { return; } - if (level >= evalNumMips()) { + if (level >= getNumMips()) { return; } } @@ -418,7 +436,6 @@ void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { auto size = storage->size(); if (storage->size() == expectedSize) { _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); _stamp++; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images @@ -426,7 +443,6 @@ void Texture::assignStoredMip(uint16 level, storage::StoragePointer& storage) { // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... _storage->assignMipData(level, storage); - _maxMip = std::max(_maxMip, level); _stamp++; } } @@ -437,7 +453,7 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin if (_autoGenerateMips) { return; } - if (level >= evalNumMips()) { + if (level >= getNumMips()) { return; } } @@ -447,7 +463,6 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin auto size = storage->size(); if (size == expectedSize) { _storage->assignMipFaceData(level, face, storage); - _maxMip = std::max(_maxMip, level); _stamp++; } else if (size > expectedSize) { // NOTE: We are facing this case sometime because apparently QImage (from where we get the bits) is generating images @@ -455,71 +470,36 @@ void Texture::assignStoredMipFace(uint16 level, uint8 face, storage::StoragePoin // We should probably consider something a bit more smart to get the correct result but for now (UI elements) // it seems to work... _storage->assignMipFaceData(level, face, storage); - _maxMip = std::max(_maxMip, level); _stamp++; } } - -uint16 Texture::autoGenerateMips(uint16 maxMip) { +void Texture::setAutoGenerateMips(bool enable) { bool changed = false; if (!_autoGenerateMips) { changed = true; _autoGenerateMips = true; } - auto newMaxMip = std::min((uint16)(evalNumMips() - 1), maxMip); - if (newMaxMip != _maxMip) { - changed = true; - _maxMip = newMaxMip;; - } - if (changed) { _stamp++; } - - return _maxMip; } -uint16 Texture::getStoredMipWidth(uint16 level) const { - if (!isStoredMipFaceAvailable(level)) { - return 0; +Size Texture::getStoredMipSize(uint16 level) const { + PixelsPointer mipFace = accessStoredMipFace(level); + Size size = 0; + if (mipFace && mipFace->getSize()) { + for (int face = 0; face < getNumFaces(); face++) { + size += getStoredMipFaceSize(level, face); + } } - return evalMipWidth(level); + return size; } -uint16 Texture::getStoredMipHeight(uint16 level) const { - if (!isStoredMipFaceAvailable(level)) { - return 0; - } - return evalMipHeight(level); -} - -uint16 Texture::getStoredMipDepth(uint16 level) const { - if (!isStoredMipFaceAvailable(level)) { - return 0; - } - return evalMipDepth(level); -} - -uint32 Texture::getStoredMipNumTexels(uint16 level) const { - if (!isStoredMipFaceAvailable(level)) { - return 0; - } - return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); -} - -uint32 Texture::getStoredMipSize(uint16 level) const { - if (!isStoredMipFaceAvailable(level)) { - return 0; - } - - return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level) * getTexelFormat().getSize(); -} - -gpu::Resource::Size Texture::getStoredSize() const { - auto size = 0; - for (int level = 0; level < evalNumMips(); ++level) { +Size Texture::getStoredSize() const { + Size size = 0; + for (int level = 0; level < getNumMips(); level++) { size += getStoredMipSize(level); } return size; @@ -937,7 +917,7 @@ bool TextureSource::isDefined() const { bool Texture::setMinMip(uint16 newMinMip) { uint16 oldMinMip = _minMip; - _minMip = std::min(std::max(_minMip, newMinMip), _maxMip); + _minMip = std::min(std::max(_minMip, newMinMip), getMaxMip()); return oldMinMip != _minMip; } diff --git a/libraries/gpu/src/gpu/Texture.h b/libraries/gpu/src/gpu/Texture.h index d6185df0d4..eab02141f0 100755 --- a/libraries/gpu/src/gpu/Texture.h +++ b/libraries/gpu/src/gpu/Texture.h @@ -232,9 +232,7 @@ public: bool operator!=(const Usage& usage) { return (_flags != usage._flags); } }; - using PixelsPointer = storage::StoragePointer; - - enum Type { + enum Type : uint8 { TEX_1D = 0, TEX_2D, TEX_3D, @@ -255,7 +253,13 @@ public: NUM_CUBE_FACES, // Not a valid vace index }; + // Lines of pixels are padded to be a multiple of "PACKING_SIZE" which is 4 bytes + static const uint32 PACKING_SIZE = 4; + static uint8 evalPaddingNumBytes(Size byteSize) { return (uint8) (3 - (byteSize + 3) % PACKING_SIZE); } + static Size evalPaddedSize(Size byteSize) { return byteSize + (Size) evalPaddingNumBytes(byteSize); } + + using PixelsPointer = storage::StoragePointer; class Storage { public: Storage() {} @@ -322,14 +326,19 @@ public: friend class Texture; }; - static Texture* create1D(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, const Sampler& sampler = Sampler()); - static Texture* createCube(const Element& texelFormat, uint16 width, const Sampler& sampler = Sampler()); - static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); - static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, const Sampler& sampler = Sampler()); + static const uint16 MAX_NUM_MIPS = 0; + static const uint16 SINGLE_MIP = 1; + static Texture* create1D(const Element& texelFormat, uint16 width, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* create2D(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* create3D(const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* createCube(const Element& texelFormat, uint16 width, uint16 numMips = 1, const Sampler& sampler = Sampler()); + static Texture* createRenderBuffer(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); + static Texture* createStrict(const Element& texelFormat, uint16 width, uint16 height, uint16 numMips = SINGLE_MIP, const Sampler& sampler = Sampler()); static Texture* createExternal(const ExternalRecycler& recycler, const Sampler& sampler = Sampler()); + // After the texture has been created, it should be defined + bool isDefined() const { return _defined; } + Texture(TextureUsageType usageType); Texture(const Texture& buf); // deep copy of the sysmem texture Texture& operator=(const Texture& buf); // deep copy of the sysmem texture @@ -339,20 +348,9 @@ public: Stamp getDataStamp() const { return _storage->getStamp(); } // The theoretical size in bytes of data stored in the texture + // For the master (level) first level of mip Size getSize() const override { return _size; } - // The actual size in bytes of data stored in the texture - Size getStoredSize() const; - - // Resize, unless auto mips mode would destroy all the sub mips - Size resize1D(uint16 width, uint16 numSamples); - Size resize2D(uint16 width, uint16 height, uint16 numSamples); - Size resize3D(uint16 width, uint16 height, uint16 depth, uint16 numSamples); - Size resizeCube(uint16 width, uint16 numSamples); - - // Reformat, unless auto mips mode would destroy all the sub mips - Size reformat(const Element& texelFormat); - // Size and format Type getType() const { return _type; } TextureUsageType getUsageType() const { return _usageType; } @@ -361,23 +359,18 @@ public: bool isDepthStencilRenderTarget() const; const Element& getTexelFormat() const { return _texelFormat; } - bool hasBorder() const { return false; } Vec3u getDimensions() const { return Vec3u(_width, _height, _depth); } uint16 getWidth() const { return _width; } uint16 getHeight() const { return _height; } uint16 getDepth() const { return _depth; } - uint32 getRowPitch() const { return getWidth() * getTexelFormat().getSize(); } - // The number of faces is mostly used for cube map, and maybe for stereo ? otherwise it's 1 // For cube maps, this means the pixels of the different faces are supposed to be packed back to back in a mip // as if the height was NUM_FACES time bigger. static uint8 NUM_FACES_PER_TYPE[NUM_TYPES]; uint8 getNumFaces() const { return NUM_FACES_PER_TYPE[getType()]; } - uint32 getNumTexels() const { return _width * _height * _depth * getNumFaces(); } - // The texture is an array if the _numSlices is not 0. // otherwise, if _numSLices is 0, then the texture is NOT an array // The number of slices returned is 1 at the minimum (if not an array) or the actual _numSlices. @@ -385,79 +378,74 @@ public: uint16 getNumSlices() const { return (isArray() ? _numSlices : 1); } uint16 getNumSamples() const { return _numSamples; } - - // NumSamples can only have certain values based on the hw static uint16 evalNumSamplesUsed(uint16 numSamplesTried); + // max mip is in the range [ 0 if no sub mips, log2(max(width, height, depth))] + // It is defined at creation time (immutable) + uint16 getMaxMip() const { return _maxMipLevel; } + uint16 getNumMips() const { return _maxMipLevel + 1; } + // Mips size evaluation // The number mips that a dimension could haves // = 1 + log2(size) - static uint16 evalDimNumMips(uint16 size); + static uint16 evalDimMaxNumMips(uint16 size); // The number mips that the texture could have if all existed // = 1 + log2(max(width, height, depth)) - uint16 evalNumMips() const; + uint16 evalMaxNumMips() const; + static uint16 evalMaxNumMips(const Vec3u& dimensions); - static uint16 evalNumMips(const Vec3u& dimensions); + // Check a num of mips requested against the maximum possible specified + // if passing -1 then answer the max + // simply does (askedNumMips == -1 ? maxMips : (numstd::min(askedNumMips, max)) + static uint16 safeNumMips(uint16 askedNumMips, uint16 maxMips); + + // Same but applied to this texture's num max mips from evalNumMips() + uint16 safeNumMips(uint16 askedNumMips) const; // Eval the size that the mips level SHOULD have // not the one stored in the Texture - static const uint MIN_DIMENSION = 1; Vec3u evalMipDimensions(uint16 level) const; uint16 evalMipWidth(uint16 level) const { return std::max(_width >> level, 1); } uint16 evalMipHeight(uint16 level) const { return std::max(_height >> level, 1); } uint16 evalMipDepth(uint16 level) const { return std::max(_depth >> level, 1); } + // The size of a face is a multiple of the padded line = (width * texelFormat_size + alignment padding) + Size evalMipLineSize(uint16 level) const { return evalPaddedSize(evalMipWidth(level) * getTexelFormat().getSize()); } + // Size for each face of a mip at a particular level uint32 evalMipFaceNumTexels(uint16 level) const { return evalMipWidth(level) * evalMipHeight(level) * evalMipDepth(level); } - uint32 evalMipFaceSize(uint16 level) const { return evalMipFaceNumTexels(level) * getTexelFormat().getSize(); } + Size evalMipFaceSize(uint16 level) const { return evalMipLineSize(level) * evalMipHeight(level) * evalMipDepth(level); } // Total size for the mip uint32 evalMipNumTexels(uint16 level) const { return evalMipFaceNumTexels(level) * getNumFaces(); } - uint32 evalMipSize(uint16 level) const { return evalMipNumTexels(level) * getTexelFormat().getSize(); } + Size evalMipSize(uint16 level) const { return evalMipFaceSize(level) * getNumFaces(); } - uint32 evalStoredMipFaceSize(uint16 level, const Element& format) const { return evalMipFaceNumTexels(level) * format.getSize(); } - uint32 evalStoredMipSize(uint16 level, const Element& format) const { return evalMipNumTexels(level) * format.getSize(); } + // Total size for all the mips of the texture + Size evalTotalSize(uint16 startingMip = 0) const; - uint32 evalTotalSize(uint16 startingMip = 0) const { - uint32 size = 0; - uint16 minMipLevel = std::max(getMinMip(), startingMip); - uint16 maxMipLevel = getMaxMip(); - for (uint16 l = minMipLevel; l <= maxMipLevel; l++) { - size += evalMipSize(l); - } - return size * getNumSlices(); - } - - // max mip is in the range [ 0 if no sub mips, log2(max(width, height, depth))] - // if autoGenerateMip is on => will provide the maxMIp level specified - // else provide the deepest mip level provided through assignMip - uint16 getMaxMip() const { return _maxMip; } - uint16 getMinMip() const { return _minMip; } - uint16 getNumMipLevels() const { return _maxMip + 1; } - uint16 usedMipLevels() const { return (_maxMip - _minMip) + 1; } + // Compute the theorical size of the texture elements storage depending on the specified format + Size evalStoredMipLineSize(uint16 level, const Element& format) const { return evalPaddedSize(evalMipWidth(level) * format.getSize()); } + Size evalStoredMipFaceSize(uint16 level, const Element& format) const { return evalMipFaceNumTexels(level) * format.getSize(); } + Size evalStoredMipSize(uint16 level, const Element& format) const { return evalMipNumTexels(level) * format.getSize(); } + // For convenience assign a source name const std::string& source() const { return _source; } void setSource(const std::string& source) { _source = source; } + + // Potentially change the minimum mip (mostly for debugging purpose) bool setMinMip(uint16 newMinMip); bool incremementMinMip(uint16 count = 1); + uint16 getMinMip() const { return _minMip; } + uint16 usedMipLevels() const { return (getNumMips() - _minMip); } - // Generate the mips automatically - // But the sysmem version is not available + // Generate the sub mips automatically for the texture + // If the storage version is not available (from CPU memory) // Only works for the standard formats - // Specify the maximum Mip level available - // 0 is the default one - // 1 is the first level - // ... - // nbMips - 1 is the last mip level - // - // If -1 then all the mips are generated - // - // Return the totalnumber of mips that will be available - uint16 autoGenerateMips(uint16 maxMip); + void setAutoGenerateMips(bool enable); bool isAutogenerateMips() const { return _autoGenerateMips; } // Managing Storage and mips @@ -471,30 +459,22 @@ public: // in case autoGen is on, this doesn't allocate // Explicitely assign mip data for a certain level // If Bytes is NULL then simply allocate the space so mip sysmem can be accessed - void assignStoredMip(uint16 level, Size size, const Byte* bytes); void assignStoredMipFace(uint16 level, uint8 face, Size size, const Byte* bytes); void assignStoredMip(uint16 level, storage::StoragePointer& storage); void assignStoredMipFace(uint16 level, uint8 face, storage::StoragePointer& storage); - // Access the the sub mips - bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } + // Access the stored mips and faces const PixelsPointer accessStoredMipFace(uint16 level, uint8 face = 0) const { return _storage->getMipFace(level, face); } + bool isStoredMipFaceAvailable(uint16 level, uint8 face = 0) const { return _storage->isMipAvailable(level, face); } Size getStoredMipFaceSize(uint16 level, uint8 face = 0) const { return _storage->getMipFaceSize(level, face); } + Size getStoredMipSize(uint16 level) const; + Size getStoredSize() const; void setStorage(std::unique_ptr& newStorage); void setKtxBacking(const std::string& filename); - // access sizes for the stored mips - uint16 getStoredMipWidth(uint16 level) const; - uint16 getStoredMipHeight(uint16 level) const; - uint16 getStoredMipDepth(uint16 level) const; - uint32 getStoredMipNumTexels(uint16 level) const; - uint32 getStoredMipSize(uint16 level) const; - - bool isDefined() const { return _defined; } - // Usage is a a set of flags providing Semantic about the usage of the Texture. void setUsage(const Usage& usage) { _usage = usage; } Usage getUsage() const { return _usage; } @@ -520,7 +500,7 @@ public: ExternalUpdates getUpdates() const; - // Textures can be serialized directly to ktx data file, here is how + // Textures can be serialized directly to ktx data file, here is how static ktx::KTXUniquePointer serialize(const Texture& texture); static Texture* unserialize(const std::string& ktxFile, TextureUsageType usageType = TextureUsageType::RESOURCE, Usage usage = Usage(), const Sampler::Desc& sampler = Sampler::Desc()); static bool evalKTXFormat(const Element& mipFormat, const Element& texelFormat, ktx::Header& header); @@ -545,7 +525,7 @@ protected: Sampler _sampler; Stamp _samplerStamp { 0 }; - uint32 _size { 0 }; + Size _size { 0 }; Element _texelFormat; uint16 _width { 1 }; @@ -553,9 +533,14 @@ protected: uint16 _depth { 1 }; uint16 _numSamples { 1 }; - uint16 _numSlices { 0 }; // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 - uint16 _maxMip { 0 }; + // if _numSlices is 0, the texture is not an "Array", the getNumSlices reported is 1 + uint16 _numSlices { 0 }; + + // valid _maxMipLevel is in the range [ 0 if no sub mips, log2(max(width, height, depth) ] + // The num of mips returned is _maxMipLevel + 1 + uint16 _maxMipLevel { 0 }; + uint16 _minMip { 0 }; Type _type { TEX_1D }; @@ -567,9 +552,9 @@ protected: bool _isIrradianceValid = false; bool _defined = false; - static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, const Sampler& sampler); + static Texture* create(TextureUsageType usageType, Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips, const Sampler& sampler); - Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices); + Size resize(Type type, const Element& texelFormat, uint16 width, uint16 height, uint16 depth, uint16 numSamples, uint16 numSlices, uint16 numMips); }; typedef std::shared_ptr TexturePointer; diff --git a/libraries/gpu/src/gpu/Texture_ktx.cpp b/libraries/gpu/src/gpu/Texture_ktx.cpp index 913ced8141..8befdf3e16 100644 --- a/libraries/gpu/src/gpu/Texture_ktx.cpp +++ b/libraries/gpu/src/gpu/Texture_ktx.cpp @@ -128,7 +128,7 @@ ktx::KTXUniquePointer Texture::serialize(const Texture& texture) { } // Number level of mips coming - header.numberOfMipmapLevels = texture.getNumMipLevels(); + header.numberOfMipmapLevels = texture.getNumMips(); ktx::Images images; for (uint32_t level = 0; level < header.numberOfMipmapLevels; level++) { @@ -224,6 +224,7 @@ Texture* Texture::unserialize(const std::string& ktxfile, TextureUsageType usage header.getPixelDepth(), 1, // num Samples header.getNumberOfSlices(), + header.getNumberOfLevels(), (isGPUKTXPayload ? gpuktxKeyValue._samplerDesc : sampler)); tex->setUsage((isGPUKTXPayload ? gpuktxKeyValue._usage : usage)); diff --git a/libraries/model/src/model/Light.cpp b/libraries/model/src/model/Light.cpp index 4ac0573cf6..11b13606b8 100755 --- a/libraries/model/src/model/Light.cpp +++ b/libraries/model/src/model/Light.cpp @@ -148,7 +148,7 @@ void Light::setAmbientSpherePreset(gpu::SphericalHarmonics::Preset preset) { void Light::setAmbientMap(gpu::TexturePointer ambientMap) { _ambientMap = ambientMap; if (ambientMap) { - setAmbientMapNumMips(_ambientMap->evalNumMips()); + setAmbientMapNumMips(_ambientMap->getNumMips()); } else { setAmbientMapNumMips(0); } diff --git a/libraries/model/src/model/TextureMap.cpp b/libraries/model/src/model/TextureMap.cpp index d07eae2166..e619a2d70f 100755 --- a/libraries/model/src/model/TextureMap.cpp +++ b/libraries/model/src/model/TextureMap.cpp @@ -210,7 +210,7 @@ const QImage& image, bool isLinear, bool doCompress) { void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateMips"); - auto numMips = texture->evalNumMips(); + auto numMips = texture->getNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); if (fastResize) { @@ -230,7 +230,7 @@ void generateMips(gpu::Texture* texture, QImage& image, bool fastResize) { void generateFaceMips(gpu::Texture* texture, QImage& image, uint8 face) { #if CPU_MIPMAPS PROFILE_RANGE(resource_parse, "generateFaceMips"); - auto numMips = texture->evalNumMips(); + auto numMips = texture->getNumMips(); for (uint16 level = 1; level < numMips; ++level) { QSize mipSize(texture->evalMipWidth(level), texture->evalMipHeight(level)); QImage mipImage = image.scaled(mipSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); @@ -255,9 +255,9 @@ gpu::Texture* TextureUsage::process2DTextureColorFromImage(const QImage& srcImag defineColorTexelFormats(formatGPU, formatMip, image, isLinear, doCompress); if (isStrict) { - theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::createStrict(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); } else { - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); } theTexture->setSource(srcImageName); auto usage = gpu::Texture::Usage::Builder().withColor(); @@ -317,7 +317,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromNormalImage(const QImage& src gpu::Element formatMip = gpu::Element::COLOR_BGRA_32; gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); @@ -345,8 +345,8 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm PROFILE_RANGE(resource_parse, "createNormalTextureFromBumpImage"); QImage image = processSourceImage(srcImage, false); - if (image.format() != QImage::Format_RGB888) { - image = image.convertToFormat(QImage::Format_RGB888); + if (image.format() != QImage::Format_Grayscale8) { + image = image.convertToFormat(QImage::Format_Grayscale8); } // PR 5540 by AlessandroSigna integrated here as a specialized TextureLoader for bumpmaps @@ -395,7 +395,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm glm::normalize(v); // convert to rgb from the value obtained computing the filter - QRgb qRgbValue = qRgba(mapComponent(v.x), mapComponent(v.y), mapComponent(v.z), 1.0); + QRgb qRgbValue = qRgba(mapComponent(v.z), mapComponent(v.y), mapComponent(v.x), 1.0); result.setPixel(i, j, qRgbValue); } } @@ -407,7 +407,7 @@ gpu::Texture* TextureUsage::createNormalTextureFromBumpImage(const QImage& srcIm gpu::Element formatGPU = gpu::Element::COLOR_RGBA_32; - theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, result.width(), result.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, result.byteCount(), result.constBits()); @@ -443,7 +443,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromImage(const QImage& srcIma #endif gpu::Element formatMip = gpu::Element::COLOR_R_8; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); @@ -483,7 +483,7 @@ gpu::Texture* TextureUsage::createRoughnessTextureFromGlossImage(const QImage& s #endif gpu::Element formatMip = gpu::Element::COLOR_R_8; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); @@ -520,7 +520,7 @@ gpu::Texture* TextureUsage::createMetallicTextureFromImage(const QImage& srcImag #endif gpu::Element formatMip = gpu::Element::COLOR_R_8; - theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); + theTexture = (gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR))); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); theTexture->assignStoredMip(0, image.byteCount(), image.constBits()); @@ -836,7 +836,7 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm // If the 6 faces have been created go on and define the true Texture if (faces.size() == gpu::Texture::NUM_FACES_PER_TYPE[gpu::Texture::TEX_CUBE]) { - theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); + theTexture = gpu::Texture::createCube(formatGPU, faces[0].width(), gpu::Texture::MAX_NUM_MIPS, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP)); theTexture->setSource(srcImageName); theTexture->setStoredMipFormat(formatMip); int f = 0; @@ -848,11 +848,6 @@ gpu::Texture* TextureUsage::processCubeTextureColorFromImage(const QImage& srcIm f++; } - if (generateMips) { - PROFILE_RANGE(resource_parse, "generateMips"); - theTexture->autoGenerateMips(-1); - } - // Generate irradiance while we are at it if (generateIrradiance) { PROFILE_RANGE(resource_parse, "generateIrradiance"); diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h index c7d283ad02..83eedfc82f 100644 --- a/libraries/networking/src/AddressManager.h +++ b/libraries/networking/src/AddressManager.h @@ -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; @@ -123,6 +123,8 @@ signals: void goBackPossible(bool isPossible); void goForwardPossible(bool isPossible); + void metaverseServerUrlChanged(); + protected: AddressManager(); private slots: diff --git a/libraries/procedural/src/procedural/Procedural.cpp b/libraries/procedural/src/procedural/Procedural.cpp index ac6163c227..e4ce3c691a 100644 --- a/libraries/procedural/src/procedural/Procedural.cpp +++ b/libraries/procedural/src/procedural/Procedural.cpp @@ -325,7 +325,7 @@ void Procedural::prepare(gpu::Batch& batch, const glm::vec3& position, const glm auto gpuTexture = _channels[i]->getGPUTexture(); if (gpuTexture) { gpuTexture->setSampler(sampler); - gpuTexture->autoGenerateMips(-1); + gpuTexture->setAutoGenerateMips(true); } batch.setResourceTexture((gpu::uint32)i, gpuTexture); } diff --git a/libraries/render-utils/src/AmbientOcclusionEffect.cpp b/libraries/render-utils/src/AmbientOcclusionEffect.cpp index 3bacf85273..678d8b1baf 100644 --- a/libraries/render-utils/src/AmbientOcclusionEffect.cpp +++ b/libraries/render-utils/src/AmbientOcclusionEffect.cpp @@ -74,11 +74,11 @@ void AmbientOcclusionFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _occlusionTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _occlusionTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _occlusionFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusion")); _occlusionFramebuffer->setRenderBuffer(0, _occlusionTexture); - _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _occlusionBlurredTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _occlusionBlurredFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("occlusionBlurred")); _occlusionBlurredFramebuffer->setRenderBuffer(0, _occlusionBlurredTexture); } diff --git a/libraries/render-utils/src/AntialiasingEffect.cpp b/libraries/render-utils/src/AntialiasingEffect.cpp index f95d45de04..f11d62acbe 100644 --- a/libraries/render-utils/src/AntialiasingEffect.cpp +++ b/libraries/render-utils/src/AntialiasingEffect.cpp @@ -52,7 +52,7 @@ const gpu::PipelinePointer& Antialiasing::getAntialiasingPipeline() { _antialiasingBuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("antialiasing")); auto format = gpu::Element::COLOR_SRGBA_32; // DependencyManager::get()->getLightingTexture()->getTexelFormat(); auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, defaultSampler)); + _antialiasingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(format, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _antialiasingBuffer->setRenderBuffer(0, _antialiasingTexture); } diff --git a/libraries/render-utils/src/DeferredFramebuffer.cpp b/libraries/render-utils/src/DeferredFramebuffer.cpp index 40c22beba4..52329931d0 100644 --- a/libraries/render-utils/src/DeferredFramebuffer.cpp +++ b/libraries/render-utils/src/DeferredFramebuffer.cpp @@ -53,9 +53,9 @@ void DeferredFramebuffer::allocate() { auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); - _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, defaultSampler)); - _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, defaultSampler)); + _deferredColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _deferredNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(linearFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); + _deferredSpecularTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _deferredFramebuffer->setRenderBuffer(0, _deferredColorTexture); _deferredFramebuffer->setRenderBuffer(1, _deferredNormalTexture); @@ -65,7 +65,7 @@ void DeferredFramebuffer::allocate() { auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format if (!_primaryDepthTexture) { - _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, defaultSampler)); + _primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); } _deferredFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); @@ -75,7 +75,7 @@ void DeferredFramebuffer::allocate() { auto smoothSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR); - _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, defaultSampler)); + _lightingTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::R11G11B10), width, height, gpu::Texture::SINGLE_MIP, defaultSampler)); _lightingFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("lighting")); _lightingFramebuffer->setRenderBuffer(0, _lightingTexture); _lightingFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, depthFormat); diff --git a/libraries/render-utils/src/DeferredLightingEffect.cpp b/libraries/render-utils/src/DeferredLightingEffect.cpp index ce340583ee..dc1822c0f5 100644 --- a/libraries/render-utils/src/DeferredLightingEffect.cpp +++ b/libraries/render-utils/src/DeferredLightingEffect.cpp @@ -496,14 +496,14 @@ void PreparePrimaryFramebuffer::run(const SceneContextPointer& sceneContext, con auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryColorTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _primaryFramebuffer->setRenderBuffer(0, primaryColorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto primaryDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _primaryFramebuffer->setDepthStencilBuffer(primaryDepthTexture, depthFormat); } diff --git a/libraries/render-utils/src/MeshPartPayload.cpp b/libraries/render-utils/src/MeshPartPayload.cpp index 41a1bb4c74..51ce0fffa7 100644 --- a/libraries/render-utils/src/MeshPartPayload.cpp +++ b/libraries/render-utils/src/MeshPartPayload.cpp @@ -168,8 +168,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getGrayTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::ALBEDO, textureCache->getWhiteTexture()); } // Roughness map @@ -182,8 +180,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::ROUGHNESS, textureCache->getWhiteTexture()); } // Normal map @@ -196,8 +192,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, textureCache->getBlueTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::NORMAL, nullptr); } // Metallic map @@ -210,8 +204,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, textureCache->getBlackTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::METALLIC, nullptr); } // Occlusion map @@ -224,8 +216,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::OCCLUSION, nullptr); } // Scattering map @@ -238,8 +228,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, textureCache->getWhiteTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::SCATTERING, nullptr); } // Emissive / Lightmap @@ -259,8 +247,6 @@ void MeshPartPayload::bindMaterial(gpu::Batch& batch, const ShapePipeline::Locat } else { batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, textureCache->getBlackTexture()); } - } else { - batch.setResourceTexture(ShapePipeline::Slot::MAP::EMISSIVE_LIGHTMAP, nullptr); } } diff --git a/libraries/render-utils/src/RenderForwardTask.cpp b/libraries/render-utils/src/RenderForwardTask.cpp index 45a32c1aaf..49090c2f5f 100755 --- a/libraries/render-utils/src/RenderForwardTask.cpp +++ b/libraries/render-utils/src/RenderForwardTask.cpp @@ -73,11 +73,11 @@ void PrepareFramebuffer::run(const SceneContextPointer& sceneContext, const Rend auto colorFormat = gpu::Element::COLOR_SRGBA_32; auto defaultSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT); - auto colorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, defaultSampler)); + auto colorTexture = gpu::TexturePointer(gpu::Texture::create2D(colorFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _framebuffer->setRenderBuffer(0, colorTexture); auto depthFormat = gpu::Element(gpu::SCALAR, gpu::UINT32, gpu::DEPTH_STENCIL); // Depth24_Stencil8 texel format - auto depthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, defaultSampler)); + auto depthTexture = gpu::TexturePointer(gpu::Texture::create2D(depthFormat, frameSize.x, frameSize.y, gpu::Texture::SINGLE_MIP, defaultSampler)); _framebuffer->setDepthStencilBuffer(depthTexture, depthFormat); } diff --git a/libraries/render-utils/src/SubsurfaceScattering.cpp b/libraries/render-utils/src/SubsurfaceScattering.cpp index 25a01bff1b..a57657a353 100644 --- a/libraries/render-utils/src/SubsurfaceScattering.cpp +++ b/libraries/render-utils/src/SubsurfaceScattering.cpp @@ -414,7 +414,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringProfile(Rend const int PROFILE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto profileMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, PROFILE_RESOLUTION, 1, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); profileMap->setSource("Generated Scattering Profile"); diffuseProfileGPU(profileMap, args); return profileMap; @@ -425,7 +425,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin const int TABLE_RESOLUTION = 512; // const auto pixelFormat = gpu::Element::COLOR_SRGBA_32; const auto pixelFormat = gpu::Element::COLOR_R11G11B10; - auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto scatteringLUT = gpu::TexturePointer(gpu::Texture::createRenderBuffer(pixelFormat, TABLE_RESOLUTION, TABLE_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); //diffuseScatter(scatteringLUT); scatteringLUT->setSource("Generated pre-integrated scattering"); diffuseScatterGPU(profile, scatteringLUT, args); @@ -434,7 +434,7 @@ gpu::TexturePointer SubsurfaceScatteringResource::generatePreIntegratedScatterin gpu::TexturePointer SubsurfaceScatteringResource::generateScatteringSpecularBeckmann(RenderArgs* args) { const int SPECULAR_RESOLUTION = 256; - auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32 /*gpu::Element(gpu::SCALAR, gpu::HALF, gpu::RGB)*/, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); + auto beckmannMap = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, SPECULAR_RESOLUTION, SPECULAR_RESOLUTION, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_MIP_LINEAR, gpu::Sampler::WRAP_CLAMP))); beckmannMap->setSource("Generated beckmannMap"); computeSpecularBeckmannGPU(beckmannMap, args); return beckmannMap; diff --git a/libraries/render-utils/src/SurfaceGeometryPass.cpp b/libraries/render-utils/src/SurfaceGeometryPass.cpp index f713e3ce75..a4a83bb6c5 100644 --- a/libraries/render-utils/src/SurfaceGeometryPass.cpp +++ b/libraries/render-utils/src/SurfaceGeometryPass.cpp @@ -72,18 +72,18 @@ void LinearDepthFramebuffer::allocate() { auto height = _frameSize.y; // For Linear Depth: - _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, + _linearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _linearDepthFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("linearDepth")); _linearDepthFramebuffer->setRenderBuffer(0, _linearDepthTexture); _linearDepthFramebuffer->setDepthStencilBuffer(_primaryDepthTexture, _primaryDepthTexture->getTexelFormat()); // For Downsampling: - _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, + const uint16_t HALF_LINEAR_DEPTH_MAX_MIP_LEVEL = 5; + _halfLinearDepthTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element(gpu::SCALAR, gpu::FLOAT, gpu::RED), _halfFrameSize.x, _halfFrameSize.y, HALF_LINEAR_DEPTH_MAX_MIP_LEVEL, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); - _halfLinearDepthTexture->autoGenerateMips(5); - _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, + _halfNormalTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _halfFrameSize.x, _halfFrameSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _downsampleFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("halfLinearDepth")); @@ -304,15 +304,15 @@ void SurfaceGeometryFramebuffer::allocate() { auto width = _frameSize.x; auto height = _frameSize.y; - _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _curvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _curvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::curvature")); _curvatureFramebuffer->setRenderBuffer(0, _curvatureTexture); - _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _lowCurvatureTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _lowCurvatureFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::lowCurvature")); _lowCurvatureFramebuffer->setRenderBuffer(0, _lowCurvatureTexture); - _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); + _blurringTexture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, width, height, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT))); _blurringFramebuffer = gpu::FramebufferPointer(gpu::Framebuffer::create("surfaceGeometry::blurring")); _blurringFramebuffer->setRenderBuffer(0, _blurringTexture); } diff --git a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf index 46554dd7f8..205dad124e 100644 --- a/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf +++ b/libraries/render-utils/src/surfaceGeometry_downsampleDepthNormal.slf @@ -21,17 +21,17 @@ out vec4 outLinearDepth; out vec4 outNormal; void main(void) { - // Gather 2 by 2 quads from texture + // Gather 2 by 2 quads from texture and downsample // Try different filters for Z - //vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); - //float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); - float Zeye = texture(linearDepthMap, varTexCoord0).x; + vec4 Zeyes = textureGather(linearDepthMap, varTexCoord0, 0); + // float Zeye = texture(linearDepthMap, varTexCoord0).x; vec4 rawNormalsX = textureGather(normalMap, varTexCoord0, 0); vec4 rawNormalsY = textureGather(normalMap, varTexCoord0, 1); vec4 rawNormalsZ = textureGather(normalMap, varTexCoord0, 2); + float Zeye = min(min(Zeyes.x, Zeyes.y), min(Zeyes.z, Zeyes.w)); vec3 normal = vec3(0.0); normal += unpackNormal(vec3(rawNormalsX[0], rawNormalsY[0], rawNormalsZ[0])); diff --git a/libraries/render-utils/src/text/Font.cpp b/libraries/render-utils/src/text/Font.cpp index c405f6d6ae..00fcabd7da 100644 --- a/libraries/render-utils/src/text/Font.cpp +++ b/libraries/render-utils/src/text/Font.cpp @@ -207,7 +207,7 @@ void Font::read(QIODevice& in) { formatGPU = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::RGBA); formatMip = gpu::Element(gpu::VEC4, gpu::NUINT8, gpu::BGRA); } - _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), + _texture = gpu::TexturePointer(gpu::Texture::create2D(formatGPU, image.width(), image.height(), gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_POINT_MAG_LINEAR))); _texture->setStoredMipFormat(formatMip); _texture->assignStoredMip(0, image.byteCount(), image.constBits()); diff --git a/libraries/render/src/render/BlurTask.cpp b/libraries/render/src/render/BlurTask.cpp index f8b5546b92..b0329b22a5 100644 --- a/libraries/render/src/render/BlurTask.cpp +++ b/libraries/render/src/render/BlurTask.cpp @@ -108,7 +108,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra // _blurredFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); //} auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); _blurredFramebuffer->setRenderBuffer(0, blurringTarget); } @@ -131,7 +131,7 @@ bool BlurInOutResource::updateResources(const gpu::FramebufferPointer& sourceFra _outputFramebuffer->setDepthStencilBuffer(sourceFramebuffer->getDepthStencilBuffer(), sourceFramebuffer->getDepthStencilBufferFormat()); }*/ auto blurringSampler = gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_LINEAR_MIP_POINT); - auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), blurringSampler)); + auto blurringTarget = gpu::TexturePointer(gpu::Texture::create2D(sourceFramebuffer->getRenderBuffer(0)->getTexelFormat(), sourceFramebuffer->getWidth(), sourceFramebuffer->getHeight(), gpu::Texture::SINGLE_MIP, blurringSampler)); _outputFramebuffer->setRenderBuffer(0, blurringTarget); } diff --git a/libraries/script-engine/src/TabletScriptingInterface.cpp b/libraries/script-engine/src/TabletScriptingInterface.cpp index 636edb1182..91a1b1b767 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.cpp +++ b/libraries/script-engine/src/TabletScriptingInterface.cpp @@ -251,7 +251,7 @@ static QString getUsername() { } void TabletProxy::initialScreen(const QVariant& url) { - if (getQmlTablet()) { + if (_qmlTabletRoot) { pushOntoStack(url); } else { _initialScreen = true; diff --git a/libraries/script-engine/src/TabletScriptingInterface.h b/libraries/script-engine/src/TabletScriptingInterface.h index 35b4f6d142..ef9edfb296 100644 --- a/libraries/script-engine/src/TabletScriptingInterface.h +++ b/libraries/script-engine/src/TabletScriptingInterface.h @@ -86,6 +86,7 @@ class TabletProxy : public QObject { Q_PROPERTY(QString name READ getName) Q_PROPERTY(bool toolbarMode READ getToolbarMode WRITE setToolbarMode) Q_PROPERTY(bool landscape READ getLandscape WRITE setLandscape) + Q_PROPERTY(bool tabletShown MEMBER _tabletShown NOTIFY tabletShownChanged) public: TabletProxy(QString name); @@ -215,6 +216,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(); @@ -233,6 +241,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 }; diff --git a/libraries/script-engine/src/XMLHttpRequestClass.cpp b/libraries/script-engine/src/XMLHttpRequestClass.cpp index 4e528ec52c..1d3c8fda32 100644 --- a/libraries/script-engine/src/XMLHttpRequestClass.cpp +++ b/libraries/script-engine/src/XMLHttpRequestClass.cpp @@ -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(); - 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()); diff --git a/plugins/openvr/src/OpenVrDisplayPlugin.cpp b/plugins/openvr/src/OpenVrDisplayPlugin.cpp index 46c2cf3ff2..9f95e64361 100644 --- a/plugins/openvr/src/OpenVrDisplayPlugin.cpp +++ b/plugins/openvr/src/OpenVrDisplayPlugin.cpp @@ -494,7 +494,7 @@ void OpenVrDisplayPlugin::customizeContext() { _compositeInfos[0].texture = _compositeFramebuffer->getRenderBuffer(0); for (size_t i = 0; i < COMPOSITING_BUFFER_SIZE; ++i) { if (0 != i) { - _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); + _compositeInfos[i].texture = gpu::TexturePointer(gpu::Texture::createRenderBuffer(gpu::Element::COLOR_RGBA_32, _renderTargetSize.x, _renderTargetSize.y, gpu::Texture::SINGLE_MIP, gpu::Sampler(gpu::Sampler::FILTER_MIN_MAG_POINT))); } _compositeInfos[i].textureID = getGLBackend()->getTextureID(_compositeInfos[i].texture); } diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 109b92d33a..81ce72d901 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -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", diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js index 9a6760a37b..e1b432d09f 100644 --- a/scripts/system/controllers/handControllerGrab.js +++ b/scripts/system/controllers/handControllerGrab.js @@ -1181,7 +1181,7 @@ function MyController(hand) { this.updateStylusTip(); - var DEFAULT_USE_FINGER_AS_STYLUS = true; + var DEFAULT_USE_FINGER_AS_STYLUS = false; var USE_FINGER_AS_STYLUS = Settings.getValue("preferAvatarFingerOverStylus"); if (USE_FINGER_AS_STYLUS === "") { USE_FINGER_AS_STYLUS = DEFAULT_USE_FINGER_AS_STYLUS; diff --git a/scripts/system/html/css/marketplaces.css b/scripts/system/html/css/marketplaces.css index bb57bea3bc..04c132eab1 100644 --- a/scripts/system/html/css/marketplaces.css +++ b/scripts/system/html/css/marketplaces.css @@ -5,6 +5,93 @@ // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html */ + +/* + CSS rules copied from edit-style.css. + Edit-style.css is not used in its entirety because don't want custom scrollbars; default scrollbar styling is used in order + to match other marketplace pages. +*/ + +@font-face { + font-family: Raleway-Regular; + src: url(../../../../resources/fonts/Raleway-Regular.ttf), /* Windows production */ + url(../../../../fonts/Raleway-Regular.ttf), /* OSX production */ + url(../../../../interface/resources/fonts/Raleway-Regular.ttf); /* Development, running script in /HiFi/examples */ +} + +@font-face { + font-family: Raleway-Bold; + src: url(../../../../resources/fonts/Raleway-Bold.ttf), + url(../../../../fonts/Raleway-Bold.ttf), + url(../../../../interface/resources/fonts/Raleway-Bold.ttf); +} + +@font-face { + font-family: Raleway-SemiBold; + src: url(../../../../resources/fonts/Raleway-SemiBold.ttf), + url(../../../../fonts/Raleway-SemiBold.ttf), + url(../../../../interface/resources/fonts/Raleway-SemiBold.ttf); +} + +@font-face { + font-family: FiraSans-SemiBold; + src: url(../../../../resources/fonts/FiraSans-SemiBold.ttf), + url(../../../../fonts/FiraSans-SemiBold.ttf), + url(../../../../interface/resources/fonts/FiraSans-SemiBold.ttf); +} + +* { + margin: 0; + padding: 0; +} + +body { + padding: 21px 21px 21px 21px; + + color: #afafaf; + background-color: #404040; + font-family: Raleway-Regular; + font-size: 15px; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + overflow-x: hidden; + overflow-y: auto; +} + +input[type=button] { + font-family: Raleway-Bold; + font-size: 13px; + text-transform: uppercase; + vertical-align: top; + height: 28px; + min-width: 120px; + padding: 0px 18px; + margin-right: 6px; + border-radius: 5px; + border: none; + color: #fff; + background-color: #000; + background: linear-gradient(#343434 20%, #000 100%); + cursor: pointer; +} + +input[type=button].blue { + color: #fff; + background-color: #1080b8; + background: linear-gradient(#00b4ef 20%, #1080b8 100%); +} + + +/* + Marketplaces-specific CSS. +*/ + body { background: white; padding: 0 0 0 0; diff --git a/scripts/system/html/js/marketplacesInjectNoScrollbar.js b/scripts/system/html/js/marketplacesInjectNoScrollbar.js deleted file mode 100644 index 86ec1b944a..0000000000 --- a/scripts/system/html/js/marketplacesInjectNoScrollbar.js +++ /dev/null @@ -1,349 +0,0 @@ -// -// marketplacesInject.js -// -// Created by David Rowe on 12 Nov 2016. -// Copyright 2016 High Fidelity, Inc. -// -// Injected into marketplace Web pages. -// -// Distributed under the Apache License, Version 2.0. -// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html -// - -(function () { - - // Event bridge messages. - var CLARA_IO_DOWNLOAD = "CLARA.IO DOWNLOAD"; - var CLARA_IO_STATUS = "CLARA.IO STATUS"; - var CLARA_IO_CANCEL_DOWNLOAD = "CLARA.IO CANCEL DOWNLOAD"; - var CLARA_IO_CANCELLED_DOWNLOAD = "CLARA.IO CANCELLED DOWNLOAD"; - var GOTO_DIRECTORY = "GOTO_DIRECTORY"; - var QUERY_CAN_WRITE_ASSETS = "QUERY_CAN_WRITE_ASSETS"; - var CAN_WRITE_ASSETS = "CAN_WRITE_ASSETS"; - var WARN_USER_NO_PERMISSIONS = "WARN_USER_NO_PERMISSIONS"; - - var canWriteAssets = false; - var xmlHttpRequest = null; - var isPreparing = false; // Explicitly track download request status. - - function injectCommonCode(isDirectoryPage) { - - // Supporting styles from marketplaces.css. - // Glyph font family, size, and spacing adjusted because HiFi-Glyphs cannot be used cross-domain. - $("head").append( - '' - ); - - // Supporting styles from edit-style.css. - // Font family, size, and position adjusted because Raleway-Bold cannot be used cross-domain. - $("head").append( - '' - ); - - // Footer. - var isInitialHiFiPage = location.href === "https://metaverse.highfidelity.com/marketplace?"; - $("body").append( - '
' + - (!isInitialHiFiPage ? '' : '') + - (isInitialHiFiPage ? '🛈 See also other marketplaces.' : '') + - (!isDirectoryPage ? '' : '') + - (isDirectoryPage ? '🛈 Select a marketplace to explore.' : '') + - '
' - ); - - // Footer actions. - $("#back-button").on("click", function () { - window.history.back(); - }); - $("#all-markets").on("click", function () { - EventBridge.emitWebEvent(GOTO_DIRECTORY); - }); - } - - function injectDirectoryCode() { - - // Remove e-mail hyperlink. - var letUsKnow = $("#letUsKnow"); - letUsKnow.replaceWith(letUsKnow.html()); - - // Add button links. - $('#exploreClaraMarketplace').on('click', function () { - window.location = "https://clara.io/library?gameCheck=true&public=true"; - }); - $('#exploreHifiMarketplace').on('click', function () { - window.location = "http://www.highfidelity.com/marketplace"; - }); - } - - function injectHiFiCode() { - // Nothing to do. - } - - function updateClaraCode() { - // Have to repeatedly update Clara page because its content can change dynamically without location.href changing. - - // Clara library page. - if (location.href.indexOf("clara.io/library") !== -1) { - // Make entries navigate to "Image" view instead of default "Real Time" view. - var elements = $("a.thumbnail"); - for (var i = 0, length = elements.length; i < length; i++) { - var value = elements[i].getAttribute("href"); - if (value.slice(-6) !== "/image") { - elements[i].setAttribute("href", value + "/image"); - } - } - } - - // Clara item page. - if (location.href.indexOf("clara.io/view/") !== -1) { - // Make site navigation links retain gameCheck etc. parameters. - var element = $("a[href^=\'/library\']")[0]; - var parameters = "?gameCheck=true&public=true"; - var href = element.getAttribute("href"); - if (href.slice(-parameters.length) !== parameters) { - element.setAttribute("href", href + parameters); - } - - // Remove unwanted buttons and replace download options with a single "Download to High Fidelity" button. - var buttons = $("a.embed-button").parent("div"); - var downloadFBX; - if (buttons.find("div.btn-group").length > 0) { - buttons.children(".btn-primary, .btn-group , .embed-button").each(function () { this.remove(); }); - if ($("#hifi-download-container").length === 0) { // Button hasn't been moved already. - downloadFBX = $(' Download to High Fidelity'); - buttons.prepend(downloadFBX); - downloadFBX[0].addEventListener("click", startAutoDownload); - } - } - - // Move the "Download to High Fidelity" button to be more visible on tablet. - if ($("#hifi-download-container").length === 0 && window.innerWidth < 700) { - var downloadContainer = $('
'); - $(".top-title .col-sm-4").append(downloadContainer); - downloadContainer.append(downloadFBX); - } - - // Automatic download to High Fidelity. - function startAutoDownload() { - - // One file request at a time. - if (isPreparing) { - console.log("WARNIKNG: Clara.io FBX: Prepare only one download at a time"); - return; - } - - // User must be able to write to Asset Server. - if (!canWriteAssets) { - console.log("ERROR: Clara.io FBX: File download cancelled because no permissions to write to Asset Server"); - EventBridge.emitWebEvent(WARN_USER_NO_PERMISSIONS); - return; - } - - // User must be logged in. - var loginButton = $("#topnav a[href='/signup']"); - if (loginButton.length > 0) { - loginButton[0].click(); - return; - } - - // Obtain zip file to download for requested asset. - // Reference: https://clara.io/learn/sdk/api/export - - //var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?zip=true¢erScene=true&alignSceneGround=true&fbxUnit=Meter&fbxVersion=7&fbxEmbedTextures=true&imageFormat=WebGL"; - // 13 Jan 2017: Specify FBX version 5 and remove some options in order to make Clara.io site more likely to - // be successful in generating zip files. - var XMLHTTPREQUEST_URL = "https://clara.io/api/scenes/{uuid}/export/fbx?fbxUnit=Meter&fbxVersion=5&fbxEmbedTextures=true&imageFormat=WebGL"; - - var uuid = location.href.match(/\/view\/([a-z0-9\-]*)/)[1]; - var url = XMLHTTPREQUEST_URL.replace("{uuid}", uuid); - - xmlHttpRequest = new XMLHttpRequest(); - var responseTextIndex = 0; - var zipFileURL = ""; - - xmlHttpRequest.onreadystatechange = function () { - // Messages are appended to responseText; process the new ones. - var message = this.responseText.slice(responseTextIndex); - var statusMessage = ""; - - if (isPreparing) { // Ignore messages in flight after finished/cancelled. - var lines = message.split(/[\n\r]+/); - - for (var i = 0, length = lines.length; i < length; i++) { - if (lines[i].slice(0, 5) === "data:") { - // Parse line. - var data; - try { - data = JSON.parse(lines[i].slice(5)); - } - catch (e) { - data = {}; - } - - // Extract status message. - if (data.hasOwnProperty("message") && data.message !== null) { - statusMessage = data.message; - console.log("Clara.io FBX: " + statusMessage); - } - - // Extract zip file URL. - if (data.hasOwnProperty("files") && data.files.length > 0) { - zipFileURL = data.files[0].url; - if (zipFileURL.slice(-4) !== ".zip") { - console.log(JSON.stringify(data)); // Data for debugging. - } - } - } - } - - if (statusMessage !== "") { - // Update the UI with the most recent status message. - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } - } - - responseTextIndex = this.responseText.length; - }; - - // Note: onprogress doesn't have computable total length so can't use it to determine % complete. - - xmlHttpRequest.onload = function () { - var statusMessage = ""; - - if (!isPreparing) { - return; - } - - isPreparing = false; - - var HTTP_OK = 200; - if (this.status !== HTTP_OK) { - statusMessage = "Zip file request terminated with " + this.status + " " + this.statusText; - console.log("ERROR: Clara.io FBX: " + statusMessage); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else if (zipFileURL.slice(-4) !== ".zip") { - statusMessage = "Error creating zip file for download."; - console.log("ERROR: Clara.io FBX: " + statusMessage + ": " + zipFileURL); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " " + statusMessage); - } else { - EventBridge.emitWebEvent(CLARA_IO_DOWNLOAD + " " + zipFileURL); - console.log("Clara.io FBX: File download initiated for " + zipFileURL); - } - - xmlHttpRequest = null; - } - - isPreparing = true; - - console.log("Clara.io FBX: Request zip file for " + uuid); - EventBridge.emitWebEvent(CLARA_IO_STATUS + " Initiating download"); - - xmlHttpRequest.open("POST", url, true); - xmlHttpRequest.setRequestHeader("Accept", "text/event-stream"); - xmlHttpRequest.send(); - } - } - } - - function injectClaraCode() { - - // Make space for marketplaces footer in Clara pages. - $("head").append( - '' - ); - - // Condense space. - $("head").append( - '' - ); - - // Move "Download to High Fidelity" button. - $("head").append( - '' - ); - - // Update code injected per page displayed. - var updateClaraCodeInterval = undefined; - updateClaraCode(); - updateClaraCodeInterval = setInterval(function () { - updateClaraCode(); - }, 1000); - - window.addEventListener("unload", function () { - clearInterval(updateClaraCodeInterval); - updateClaraCodeInterval = undefined; - }); - - EventBridge.emitWebEvent(QUERY_CAN_WRITE_ASSETS); - } - - function cancelClaraDownload() { - isPreparing = false; - - if (xmlHttpRequest) { - xmlHttpRequest.abort(); - xmlHttpRequest = null; - console.log("Clara.io FBX: File download cancelled"); - EventBridge.emitWebEvent(CLARA_IO_CANCELLED_DOWNLOAD); - } - } - - function onLoad() { - - EventBridge.scriptEventReceived.connect(function (message) { - if (message.slice(0, CAN_WRITE_ASSETS.length) === CAN_WRITE_ASSETS) { - canWriteAssets = message.slice(-4) === "true"; - } - - if (message.slice(0, CLARA_IO_CANCEL_DOWNLOAD.length) === CLARA_IO_CANCEL_DOWNLOAD) { - cancelClaraDownload(); - } - }); - - var DIRECTORY = 0; - var HIFI = 1; - var CLARA = 2; - var pageType = DIRECTORY; - - if (location.href.indexOf("highfidelity.com/") !== -1) { pageType = HIFI; } - if (location.href.indexOf("clara.io/") !== -1) { pageType = CLARA; } - - injectCommonCode(pageType === DIRECTORY); - switch (pageType) { - case DIRECTORY: - injectDirectoryCode(); - break; - case HIFI: - injectHiFiCode(); - break; - case CLARA: - injectClaraCode(); - break; - } - } - - // Load / unload. - window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). - -}()); diff --git a/scripts/system/html/marketplaces.html b/scripts/system/html/marketplaces.html index 976c0f294f..6051a9df96 100644 --- a/scripts/system/html/marketplaces.html +++ b/scripts/system/html/marketplaces.html @@ -10,7 +10,6 @@ Marketplaces - diff --git a/scripts/system/makeUserConnection.js b/scripts/system/makeUserConnection.js new file mode 100644 index 0000000000..e70e0a2ea2 --- /dev/null +++ b/scripts/system/makeUserConnection.js @@ -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 + diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index c24034b38e..4d26bcadb6 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,7 +19,6 @@ var MARKETPLACE_URL = "https://metaverse.highfidelity.com/marketplace"; var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); -var MARKETPLACES_INJECT_NO_SCROLLBAR_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInjectNoScrollbar.js"); var HOME_BUTTON_TEXTURE = "http://hifi-content.s3.amazonaws.com/alan/dev/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; // var HOME_BUTTON_TEXTURE = Script.resourcesPath() + "meshes/tablet-with-home-button.fbx/tablet-with-home-button.fbm/button-root.png"; @@ -61,12 +60,7 @@ function showMarketplace() { shouldActivateButton = true; - // by default the marketplace should NOT have a scrollbar, except when tablet is in toolbar mode. - var injectURL = MARKETPLACES_INJECT_NO_SCROLLBAR_SCRIPT_URL; - if (tablet.toolbarMode) { - injectURL = MARKETPLACES_INJECT_SCRIPT_URL; - } - tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, injectURL); + tablet.gotoWebScreen(MARKETPLACE_URL_INITIAL, MARKETPLACES_INJECT_SCRIPT_URL); onMarketplaceScreen = true; tablet.webEventReceived.connect(function (message) { diff --git a/scripts/system/notifications.js b/scripts/system/notifications.js index b527348733..3989e8e372 100644 --- a/scripts/system/notifications.js +++ b/scripts/system/notifications.js @@ -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); diff --git a/scripts/system/pal.js b/scripts/system/pal.js index d85fbce19a..b39c993894 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -1,6 +1,6 @@ "use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/*jslint vars:true, plusplus:true, forin:true*/ +/*global Tablet, Settings, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, HMD, Controller, Account, UserActivityLogger, Messages, Window, XMLHttpRequest, print, location, getControllerWorldLocation*/ /* 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(); } diff --git a/scripts/system/selectAudioDevice.js b/scripts/system/selectAudioDevice.js index f5929ce151..2dd426932f 100644 --- a/scripts/system/selectAudioDevice.js +++ b/scripts/system/selectAudioDevice.js @@ -15,95 +15,94 @@ (function() { // BEGIN LOCAL_SCOPE -if (typeof String.prototype.startsWith != 'function') { - String.prototype.startsWith = function (str){ - return this.slice(0, str.length) == str; - }; -} - -if (typeof String.prototype.endsWith != 'function') { - String.prototype.endsWith = function (str){ - return this.slice(-str.length) == str; - }; -} - -if (typeof String.prototype.trimStartsWith != 'function') { - String.prototype.trimStartsWith = function (str){ - if (this.startsWith(str)) { - return this.substr(str.length); - } - return this; - }; -} - -if (typeof String.prototype.trimEndsWith != 'function') { - String.prototype.trimEndsWith = function (str){ - if (this.endsWith(str)) { - return this.substr(0,this.length - str.length); - } - return this; - }; +const INPUT = "Input"; +const OUTPUT = "Output"; +function parseMenuItem(item) { + const USE = "Use "; + const FOR_INPUT = " for " + INPUT; + const FOR_OUTPUT = " for " + OUTPUT; + if (item.slice(0, USE.length) == USE) { + if (item.slice(-FOR_INPUT.length) == FOR_INPUT) { + return { device: item.slice(USE.length, -FOR_INPUT.length), mode: INPUT }; + } else if (item.slice(-FOR_OUTPUT.length) == FOR_OUTPUT) { + return { device: item.slice(USE.length, -FOR_OUTPUT.length), mode: OUTPUT }; + } + } } +// +// VAR DEFINITIONS +// +var debugPrintStatements = true; const INPUT_DEVICE_SETTING = "audio_input_device"; const OUTPUT_DEVICE_SETTING = "audio_output_device"; +var audioDevicesList = []; +var wasHmdActive = false; // assume it's not active to start +var switchedAudioInputToHMD = false; +var switchedAudioOutputToHMD = false; +var previousSelectedInputAudioDevice = ""; +var previousSelectedOutputAudioDevice = ""; +var skipMenuEvents = true; -var selectedInputMenu = ""; -var selectedOutputMenu = ""; - - var audioDevicesList = []; -function setupAudioMenus() { - removeAudioMenus(); - Menu.addSeparator("Audio", "Input Audio Device"); - - var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); - var inputDevices = AudioDevice.getInputDevices(); - var selectedInputDevice = AudioDevice.getInputDevice(); - if (inputDevices.indexOf(inputDeviceSetting) != -1 && selectedInputDevice != inputDeviceSetting) { - if (AudioDevice.setInputDevice(inputDeviceSetting)) { - selectedInputDevice = inputDeviceSetting; - } +// +// BEGIN FUNCTION DEFINITIONS +// +function debug() { + if (debugPrintStatements) { + print.apply(null, [].concat.apply(["selectAudioDevice.js:"], [].map.call(arguments, JSON.stringify))); } - print("audio input devices: " + inputDevices); - for(var i = 0; i < inputDevices.length; i++) { - var thisDeviceSelected = (inputDevices[i] == selectedInputDevice); - var menuItem = "Use " + inputDevices[i] + " for Input"; +} + + +function setupAudioMenus() { + // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches + skipMenuEvents = true; + Script.setTimeout(function() { skipMenuEvents = false; }, 200); + + removeAudioMenus(); + + // Setup audio input devices + Menu.addSeparator("Audio", "Input Audio Device"); + var inputDevices = AudioDevice.getInputDevices(); + for (var i = 0; i < inputDevices.length; i++) { + var audioDeviceMenuString = "Use " + inputDevices[i] + " for Input"; Menu.addMenuItem({ menuName: "Audio", - menuItemName: menuItem, + menuItemName: audioDeviceMenuString, isCheckable: true, - isChecked: thisDeviceSelected + isChecked: inputDevices[i] == AudioDevice.getInputDevice() }); - audioDevicesList.push(menuItem); - if (thisDeviceSelected) { - selectedInputMenu = menuItem; - } + audioDevicesList.push(audioDeviceMenuString); } + // Setup audio output devices Menu.addSeparator("Audio", "Output Audio Device"); + var outputDevices = AudioDevice.getOutputDevices(); + for (var i = 0; i < outputDevices.length; i++) { + var audioDeviceMenuString = "Use " + outputDevices[i] + " for Output"; + Menu.addMenuItem({ + menuName: "Audio", + menuItemName: audioDeviceMenuString, + isCheckable: true, + isChecked: outputDevices[i] == AudioDevice.getOutputDevice() + }); + audioDevicesList.push(audioDeviceMenuString); + } +} + +function checkDeviceMismatch() { + var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING); + var interfaceInputDevice = AudioDevice.getInputDevice(); + if (interfaceInputDevice != inputDeviceSetting) { + debug("Input Setting & Device mismatch! Input SETTING: " + inputDeviceSetting + "Input DEVICE IN USE: " + interfaceInputDevice); + switchAudioDevice("Use " + inputDeviceSetting + " for Input"); + } var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING); - var outputDevices = AudioDevice.getOutputDevices(); - var selectedOutputDevice = AudioDevice.getOutputDevice(); - if (outputDevices.indexOf(outputDeviceSetting) != -1 && selectedOutputDevice != outputDeviceSetting) { - if (AudioDevice.setOutputDevice(outputDeviceSetting)) { - selectedOutputDevice = outputDeviceSetting; - } - } - print("audio output devices: " + outputDevices); - for (var i = 0; i < outputDevices.length; i++) { - var thisDeviceSelected = (outputDevices[i] == selectedOutputDevice); - var menuItem = "Use " + outputDevices[i] + " for Output"; - Menu.addMenuItem({ - menuName: "Audio", - menuItemName: menuItem, - isCheckable: true, - isChecked: thisDeviceSelected - }); - audioDevicesList.push(menuItem); - if (thisDeviceSelected) { - selectedOutputMenu = menuItem; - } + var interfaceOutputDevice = AudioDevice.getOutputDevice(); + if (interfaceOutputDevice != outputDeviceSetting) { + debug("Output Setting & Device mismatch! Output SETTING: " + outputDeviceSetting + "Output DEVICE IN USE: " + interfaceOutputDevice); + switchAudioDevice("Use " + outputDeviceSetting + " for Output"); } } @@ -112,130 +111,170 @@ function removeAudioMenus() { Menu.removeSeparator("Audio", "Output Audio Device"); for (var index = 0; index < audioDevicesList.length; index++) { - Menu.removeMenuItem("Audio", audioDevicesList[index]); + if (Menu.menuItemExists("Audio", audioDevicesList[index])) { + Menu.removeMenuItem("Audio", audioDevicesList[index]); + } } + Menu.removeMenu("Audio > Devices"); + audioDevicesList = []; } function onDevicechanged() { - print("audio devices changed, removing Audio > Devices menu..."); - Menu.removeMenu("Audio > Devices"); - print("now setting up Audio > Devices menu"); + debug("System audio devices changed. Removing and replacing Audio Menus..."); setupAudioMenus(); + checkDeviceMismatch(); } -// Have a small delay before the menu's get setup and the audio devices can switch to the last selected ones -Script.setTimeout(function () { - print("connecting deviceChanged"); - AudioDevice.deviceChanged.connect(onDevicechanged); - print("setting up Audio > Devices menu for first time"); - setupAudioMenus(); -}, 5000); - -function scriptEnding() { - Menu.removeMenu("Audio > Devices"); +function onMenuEvent(audioDeviceMenuString) { + if (!skipMenuEvents) { + switchAudioDevice(audioDeviceMenuString); + } } -Script.scriptEnding.connect(scriptEnding); +function switchAudioDevice(audioDeviceMenuString) { + // if the device is not plugged in, short-circuit + if (!~audioDevicesList.indexOf(audioDeviceMenuString)) { + return; + } -function menuItemEvent(menuItem) { - if (menuItem.startsWith("Use ")) { - if (menuItem.endsWith(" for Output")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Output"); - print("output audio selection..." + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedOutputMenu, false); - selectedOutputMenu = menuItem; - Menu.setIsOptionChecked(selectedOutputMenu, true); - if (AudioDevice.setOutputDevice(selectedDevice)) { - Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); - } - Menu.menuItemEvent.connect(menuItemEvent); - } else if (menuItem.endsWith(" for Input")) { - var selectedDevice = menuItem.trimStartsWith("Use ").trimEndsWith(" for Input"); - print("input audio selection..." + selectedDevice); - Menu.menuItemEvent.disconnect(menuItemEvent); - Menu.setIsOptionChecked(selectedInputMenu, false); - selectedInputMenu = menuItem; - Menu.setIsOptionChecked(selectedInputMenu, true); + var selection = parseMenuItem(audioDeviceMenuString); + if (!selection) { + debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'"); + return; + } + + // menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches + skipMenuEvents = true; + Script.setTimeout(function() { skipMenuEvents = false; }, 200); + + var selectedDevice = selection.device; + if (selection.mode == INPUT) { + var currentInputDevice = AudioDevice.getInputDevice(); + if (selectedDevice != currentInputDevice) { + debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false); if (AudioDevice.setInputDevice(selectedDevice)) { Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio input device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); } - Menu.menuItemEvent.connect(menuItemEvent); + } else { + debug("Selected input device is the same as the current input device!") + Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) + } + } else if (selection.mode == OUTPUT) { + var currentOutputDevice = AudioDevice.getOutputDevice(); + if (selectedDevice != currentOutputDevice) { + debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice); + Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false); + if (AudioDevice.setOutputDevice(selectedDevice)) { + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + } else { + debug("Error setting audio output device!") + Menu.setIsOptionChecked(audioDeviceMenuString, false); + } + } else { + debug("Selected output device is the same as the current output device!") + Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice); + Menu.setIsOptionChecked(audioDeviceMenuString, true); + AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue) } } } -Menu.menuItemEvent.connect(menuItemEvent); +function restoreAudio() { + if (switchedAudioInputToHMD) { + debug("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice); + switchAudioDevice("Use " + previousSelectedInputAudioDevice + " for Input"); + switchedAudioInputToHMD = false; + } + if (switchedAudioOutputToHMD) { + debug("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice); + switchAudioDevice("Use " + previousSelectedOutputAudioDevice + " for Output"); + switchedAudioOutputToHMD = false; + } +} // Some HMDs (like Oculus CV1) have a built in audio device. If they // do, then this function will handle switching to that device automatically // when you goActive with the HMD active. -var wasHmdMounted = false; // assume it's un-mounted to start -var switchedAudioInputToHMD = false; -var switchedAudioOutputToHMD = false; -var previousSelectedInputAudioDevice = ""; -var previousSelectedOutputAudioDevice = ""; - -function restoreAudio() { - if (switchedAudioInputToHMD) { - print("switching back from HMD preferred audio input to:" + previousSelectedInputAudioDevice); - menuItemEvent("Use " + previousSelectedInputAudioDevice + " for Input"); - } - if (switchedAudioOutputToHMD) { - print("switching back from HMD preferred audio output to:" + previousSelectedOutputAudioDevice); - menuItemEvent("Use " + previousSelectedOutputAudioDevice + " for Output"); - } -} - function checkHMDAudio() { - // Mounted state is changing... handle switching - if (HMD.mounted != wasHmdMounted) { - print("HMD mounted changed..."); + // HMD Active state is changing; handle switching + if (HMD.active != wasHmdActive) { + debug("HMD Active state changed!"); - // We're putting the HMD on... switch to those devices - if (HMD.mounted) { - print("NOW mounted..."); + // We're putting the HMD on; switch to those devices + if (HMD.active) { + debug("HMD is now Active."); var hmdPreferredAudioInput = HMD.preferredAudioInput(); var hmdPreferredAudioOutput = HMD.preferredAudioOutput(); - print("hmdPreferredAudioInput:" + hmdPreferredAudioInput); - print("hmdPreferredAudioOutput:" + hmdPreferredAudioOutput); + debug("hmdPreferredAudioInput: " + hmdPreferredAudioInput); + debug("hmdPreferredAudioOutput: " + hmdPreferredAudioOutput); - - var hmdHasPreferredAudio = (hmdPreferredAudioInput !== "") || (hmdPreferredAudioOutput !== ""); - if (hmdHasPreferredAudio) { - print("HMD has preferred audio!"); + if (hmdPreferredAudioInput !== "") { + debug("HMD has preferred audio input device."); previousSelectedInputAudioDevice = Settings.getValue(INPUT_DEVICE_SETTING); - previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); - print("previousSelectedInputAudioDevice:" + previousSelectedInputAudioDevice); - print("previousSelectedOutputAudioDevice:" + previousSelectedOutputAudioDevice); - if (hmdPreferredAudioInput != previousSelectedInputAudioDevice && hmdPreferredAudioInput !== "") { - print("switching to HMD preferred audio input to:" + hmdPreferredAudioInput); + debug("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice); + if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) { switchedAudioInputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioInput + " for Input"); + switchAudioDevice("Use " + hmdPreferredAudioInput + " for Input"); } - if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice && hmdPreferredAudioOutput !== "") { - print("switching to HMD preferred audio output to:" + hmdPreferredAudioOutput); + } + if (hmdPreferredAudioOutput !== "") { + debug("HMD has preferred audio output device."); + previousSelectedOutputAudioDevice = Settings.getValue(OUTPUT_DEVICE_SETTING); + debug("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice); + if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) { switchedAudioOutputToHMD = true; - menuItemEvent("Use " + hmdPreferredAudioOutput + " for Output"); + switchAudioDevice("Use " + hmdPreferredAudioOutput + " for Output"); } } } else { - print("HMD NOW un-mounted..."); + debug("HMD no longer active. Restoring audio I/O devices..."); restoreAudio(); } } - wasHmdMounted = HMD.mounted; + wasHmdActive = HMD.active; } -Script.update.connect(checkHMDAudio); +// +// END FUNCTION DEFINITIONS +// +// +// BEGIN SCRIPT BODY +// +// Wait for the C++ systems to fire up before trying to do anything with audio devices +Script.setTimeout(function () { + debug("Connecting deviceChanged(), displayModeChanged(), and switchAudioDevice()..."); + AudioDevice.deviceChanged.connect(onDevicechanged); + HMD.displayModeChanged.connect(checkHMDAudio); + Menu.menuItemEvent.connect(onMenuEvent); + debug("Setting up Audio I/O menu for the first time..."); + setupAudioMenus(); + checkDeviceMismatch(); + debug("Checking HMD audio status...") + checkHMDAudio(); +}, 3000); + +debug("Connecting scriptEnding()"); Script.scriptEnding.connect(function () { restoreAudio(); removeAudioMenus(); - Menu.menuItemEvent.disconnect(menuItemEvent); - Script.update.disconnect(checkHMDAudio); + Menu.menuItemEvent.disconnect(onMenuEvent); + HMD.displayModeChanged.disconnect(checkHMDAudio); + AudioDevice.deviceChanged.disconnect(onDevicechanged); }); +// +// END SCRIPT BODY +// + }()); // END LOCAL_SCOPE diff --git a/scripts/system/tablet-ui/tabletUI.js b/scripts/system/tablet-ui/tabletUI.js index c9ea1170bc..174153cad3 100644 --- a/scripts/system/tablet-ui/tabletUI.js +++ b/scripts/system/tablet-ui/tabletUI.js @@ -16,8 +16,6 @@ MyAvatar, Menu */ (function() { // BEGIN LOCAL_SCOPE - var _this = this; - var tabletShown = false; var tabletRezzed = false; var activeHand = null; var DEFAULT_WIDTH = 0.4375; @@ -94,7 +92,7 @@ } function showTabletUI() { - tabletShown = true; + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = true; if (!tabletRezzed || !tabletIsValid()) { closeTabletUI(); @@ -116,7 +114,7 @@ } function hideTabletUI() { - tabletShown = false; + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; if (!UIWebTablet) { return; } @@ -132,7 +130,7 @@ } function closeTabletUI() { - tabletShown = false; + Tablet.getTablet("com.highfidelity.interface.tablet.system").tabletShown = false; if (UIWebTablet) { if (UIWebTablet.onClose) { UIWebTablet.onClose(); @@ -159,6 +157,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 landscape = Tablet.getTablet("com.highfidelity.interface.tablet.system").landscape;