diff --git a/interface/resources/qml/ToolWindow.qml b/interface/resources/qml/ToolWindow.qml deleted file mode 100644 index b1120058f9..0000000000 --- a/interface/resources/qml/ToolWindow.qml +++ /dev/null @@ -1,256 +0,0 @@ -// -// ToolWindow.qml -// -// Created by Bradley Austin Davis on 12 Jan 2016 -// Copyright 2016 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 QtQuick.Controls.Styles 1.4 -import QtWebEngine 1.1 -import QtWebChannel 1.0 -import Qt.labs.settings 1.0 - -import "windows" -import "controls-uit" -import "styles-uit" - - -ScrollingWindow { - id: toolWindow - resizable: true - objectName: "ToolWindow" - destroyOnCloseButton: false - destroyOnHidden: false - closable: true - shown: false - title: "Edit" - property alias tabView: tabView - implicitWidth: 520; implicitHeight: 695 - minSize: Qt.vector2d(456, 500) - - HifiConstants { id: hifi } - - onParentChanged: { - if (parent) { - x = 120; - y = 120; - } - } - - onShownChanged: { - keyboardEnabled = HMD.active; - } - - Settings { - category: "ToolWindow.Position" - property alias x: toolWindow.x - property alias y: toolWindow.y - } - - Item { - id: toolWindowTabViewItem - height: pane.scrollHeight - width: pane.contentWidth - anchors.left: parent.left - anchors.top: parent.top - - TabView { - id: tabView - width: pane.contentWidth - // Pane height so that don't use Window's scrollbars otherwise tabs may be scrolled out of view. - height: pane.scrollHeight - property int tabCount: 0 - - Repeater { - model: 4 - Tab { - // Force loading of the content even if the tab is not visible - // (required for letting the C++ code access the webview) - active: true - enabled: false - property string originalUrl: "" - - WebView { - id: webView - anchors.fill: parent - enabled: false - Component.onCompleted: { - webChannel.registerObject("eventBridge", eventBridge); - webChannel.registerObject("eventBridgeWrapper", eventBridgeWrapper); - } - - onEnabledChanged: toolWindow.updateVisiblity() - } - } - } - - style: TabViewStyle { - - frame: Rectangle { // Background shown before content loads. - anchors.fill: parent - color: hifi.colors.baseGray - } - - frameOverlap: 0 - - tab: Rectangle { - implicitWidth: text.width - implicitHeight: 3 * text.height - color: styleData.selected ? hifi.colors.black : hifi.colors.tabBackgroundDark - - RalewayRegular { - id: text - text: styleData.title - font.capitalization: Font.AllUppercase - size: hifi.fontSizes.tabName - width: tabView.tabCount > 1 ? styleData.availableWidth / tabView.tabCount : implicitWidth + 2 * hifi.dimensions.contentSpacing.x - elide: Text.ElideRight - color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.lightGrayText - horizontalAlignment: Text.AlignHCenter - anchors.centerIn: parent - } - - Rectangle { // Separator. - width: 1 - height: parent.height - color: hifi.colors.black - anchors.left: parent.left - anchors.top: parent.top - visible: styleData.index > 0 - - Rectangle { - width: 1 - height: 1 - color: hifi.colors.baseGray - anchors.left: parent.left - anchors.bottom: parent.bottom - } - } - - Rectangle { // Active underline. - width: parent.width - (styleData.index > 0 ? 1 : 0) - height: 1 - anchors.right: parent.right - anchors.bottom: parent.bottom - color: styleData.selected ? hifi.colors.primaryHighlight : hifi.colors.baseGray - } - } - - tabOverlap: 0 - } - } - } - - function updateVisiblity() { - if (visible) { - for (var i = 0; i < tabView.count; ++i) { - if (tabView.getTab(i).enabled) { - return; - } - } - shown = false; - } - } - - function findIndexForUrl(source) { - for (var i = 0; i < tabView.count; ++i) { - var tab = tabView.getTab(i); - if (tab.originalUrl === source) { - return i; - } - } - return -1; - } - - function findTabForUrl(source) { - var index = findIndexForUrl(source); - if (index < 0) { - return; - } - return tabView.getTab(index); - } - - function showTabForUrl(source, newVisible) { - var index = findIndexForUrl(source); - if (index < 0) { - return; - } - - var tab = tabView.getTab(index); - if (newVisible) { - toolWindow.shown = true - tab.enabled = true - } else { - tab.enabled = false; - updateVisiblity(); - } - } - - function findFreeTab() { - for (var i = 0; i < tabView.count; ++i) { - var tab = tabView.getTab(i); - if (tab && (!tab.originalUrl || tab.originalUrl === "")) { - return i; - } - } - return -1; - } - - function removeTabForUrl(source) { - var index = findIndexForUrl(source); - if (index < 0) { - return; - } - - var tab = tabView.getTab(index); - tab.title = ""; - tab.enabled = false; - tab.originalUrl = ""; - tab.item.url = "about:blank"; - tab.item.enabled = false; - tabView.tabCount--; - } - - function addWebTab(properties) { - if (!properties.source) { - console.warn("Attempted to open Web Tool Pane without URL"); - return; - } - - var existingTabIndex = findIndexForUrl(properties.source); - if (existingTabIndex >= 0) { - var tab = tabView.getTab(existingTabIndex); - return tab.item; - } - - var freeTabIndex = findFreeTab(); - if (freeTabIndex === -1) { - console.warn("Unable to add new tab"); - return; - } - - if (properties.width) { - tabView.width = Math.min(Math.max(tabView.width, properties.width), toolWindow.maxSize.x); - } - - if (properties.height) { - tabView.height = Math.min(Math.max(tabView.height, properties.height), toolWindow.maxSize.y); - } - - var tab = tabView.getTab(freeTabIndex); - tab.title = properties.title || "Unknown"; - tab.enabled = true; - tab.originalUrl = properties.source; - - var result = tab.item; - result.enabled = true; - tabView.tabCount++; - result.url = properties.source; - return result; - } -} diff --git a/interface/resources/qml/controls-uit/TextField.qml b/interface/resources/qml/controls-uit/TextField.qml index e636bfc27f..b942c8b4ab 100644 --- a/interface/resources/qml/controls-uit/TextField.qml +++ b/interface/resources/qml/controls-uit/TextField.qml @@ -27,10 +27,12 @@ TextField { property bool hasRoundedBorder: false property bool error: false; property bool hasClearButton: false; + property string leftPlaceholderGlyph: ""; placeholderText: textField.placeholderText FontLoader { id: firaSansSemiBold; source: "../../fonts/FiraSans-SemiBold.ttf"; } + FontLoader { id: hifiGlyphs; source: "../../fonts/hifi-glyphs.ttf"; } font.family: firaSansSemiBold.name font.pixelSize: hifi.fontSizes.textFieldInput font.italic: textField.text == "" @@ -54,6 +56,7 @@ TextField { } style: TextFieldStyle { + id: style; textColor: { if (isLightColorScheme) { if (textField.activeFocus) { @@ -102,6 +105,16 @@ TextField { border.width: textField.activeFocus || hasRoundedBorder || textField.error ? 1 : 0 radius: isSearchField ? textField.height / 2 : (hasRoundedBorder ? 4 : 0) + HiFiGlyphs { + text: textField.leftPlaceholderGlyph; + color: textColor; + size: hifi.fontSizes.textFieldSearchIcon; + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; + anchors.leftMargin: hifi.dimensions.textPadding - 2; + visible: text; + } + HiFiGlyphs { text: hifi.glyphs.search color: textColor @@ -132,7 +145,7 @@ TextField { placeholderTextColor: isFaintGrayColorScheme ? hifi.colors.lightGrayText : hifi.colors.lightGray selectedTextColor: hifi.colors.black selectionColor: hifi.colors.primaryHighlight - padding.left: (isSearchField ? textField.height - 2 : 0) + hifi.dimensions.textPadding + padding.left: ((isSearchField || textField.leftPlaceholderGlyph !== "") ? textField.height - 2 : 0) + hifi.dimensions.textPadding padding.right: (hasClearButton ? textField.height - 2 : 0) + hifi.dimensions.textPadding } diff --git a/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml deleted file mode 100644 index 11a6c99b0c..0000000000 --- a/interface/resources/qml/hifi/commerce/wallet/SendMoney.qml +++ /dev/null @@ -1,73 +0,0 @@ -// -// SendMoney.qml -// qml/hifi/commerce/wallet -// -// SendMoney -// -// Created by Zach Fox on 2017-08-18 -// 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 Hifi 1.0 as Hifi -import QtQuick 2.5 -import QtQuick.Controls 1.4 -import "../../../styles-uit" -import "../../../controls-uit" as HifiControlsUit -import "../../../controls" as HifiControls - -// references XXX from root context - -Item { - HifiConstants { id: hifi; } - - id: root; - - Connections { - target: Commerce; - } - - // "Unavailable" - RalewayRegular { - text: "You currently cannot send money to other High Fidelity users."; - // Anchors - anchors.fill: parent; - // Text size - size: 24; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; - } - - // - // FUNCTION DEFINITIONS START - // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript. - // Messages are in format "{method, params}", like json-rpc. - // - // Description: - // Called when a message is received from a script. - // - function fromScript(message) { - switch (message.method) { - default: - console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); - } - } - signal sendSignalToWallet(var msg); - // - // FUNCTION DEFINITIONS END - // -} diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index ef2b007dc8..da67569bc3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -19,8 +19,7 @@ import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../common" as HifiCommerceCommon - -// references XXX from root context +import "./sendMoney" Rectangle { HifiConstants { id: hifi; } @@ -316,18 +315,29 @@ Rectangle { Connections { onSendSignalToWallet: { - sendToScript(msg); + if (msg.method === 'transactionHistory_usernameLinkClicked') { + userInfoViewer.url = msg.usernameLink; + userInfoViewer.visible = true; + } else { + sendToScript(msg); + } } } } SendMoney { id: sendMoney; + z: 997; visible: root.activeView === "sendMoney"; - anchors.top: titleBarContainer.bottom; - anchors.bottom: tabButtonsContainer.top; - anchors.left: parent.left; - anchors.right: parent.right; + anchors.fill: parent; + parentAppTitleBarHeight: titleBarContainer.height; + parentAppNavBarHeight: tabButtonsContainer.height; + + Connections { + onSendSignalToWallet: { + sendToScript(msg); + } + } } Security { @@ -497,7 +507,7 @@ Rectangle { Rectangle { id: sendMoneyButtonContainer; visible: !walletSetup.visible; - color: hifi.colors.black; + color: root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; anchors.top: parent.top; anchors.left: exchangeMoneyButtonContainer.right; anchors.bottom: parent.bottom; @@ -513,7 +523,7 @@ Rectangle { anchors.top: parent.top; anchors.topMargin: -2; // Style - color: hifi.colors.lightGray50; + color: root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; } RalewaySemiBold { @@ -528,12 +538,24 @@ Rectangle { anchors.right: parent.right; anchors.rightMargin: 4; // Style - color: hifi.colors.lightGray50; + color: root.activeView === "sendMoney" || sendMoneyTabMouseArea.containsMouse ? hifi.colors.white : hifi.colors.blueHighlight; wrapMode: Text.WordWrap; // Alignment horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignTop; } + + MouseArea { + id: sendMoneyTabMouseArea; + anchors.fill: parent; + hoverEnabled: enabled; + onClicked: { + root.activeView = "sendMoney"; + tabButtonsContainer.resetTabButtonColors(); + } + onEntered: parent.color = hifi.colors.blueHighlight; + onExited: parent.color = root.activeView === "sendMoney" ? hifi.colors.blueAccent : hifi.colors.black; + } } // "SECURITY" tab button @@ -665,9 +687,16 @@ Rectangle { // TAB BUTTONS END // + HifiControls.TabletWebView { + id: userInfoViewer; + z: 998; + anchors.fill: parent; + visible: false; + } + Item { id: keyboardContainer; - z: 998; + z: 999; visible: keyboard.raised; property bool punctuationMode: false; anchors { @@ -713,6 +742,13 @@ Rectangle { case 'inspectionCertificate_resetCert': // NOP break; + case 'updateConnections': + sendMoney.updateConnections(message.connections); + break; + case 'selectRecipient': + case 'updateSelectedRecipientUsername': + sendMoney.fromScript(message); + break; default: console.log('Unrecognized message from wallet.js:', JSON.stringify(message)); } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 780e08caf8..81d38bc0dd 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -19,8 +19,6 @@ import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls -// references XXX from root context - Item { HifiConstants { id: hifi; } @@ -32,6 +30,20 @@ Item { property int currentHistoryPage: 1; property var pagesAlreadyAdded: new Array(); + onVisibleChanged: { + if (visible) { + transactionHistoryModel.clear(); + Commerce.balance(); + initialHistoryReceived = false; + root.currentHistoryPage = 1; + root.noMoreHistoryData = false; + root.historyRequestPending = true; + Commerce.history(root.currentHistoryPage); + } else { + refreshTimer.stop(); + } + } + Connections { target: Commerce; @@ -189,20 +201,6 @@ Item { color: hifi.colors.white; // Alignment verticalAlignment: Text.AlignVCenter; - - onVisibleChanged: { - if (visible) { - transactionHistoryModel.clear(); - Commerce.balance(); - initialHistoryReceived = false; - root.currentHistoryPage = 1; - root.noMoreHistoryData = false; - root.historyRequestPending = true; - Commerce.history(root.currentHistoryPage); - } else { - refreshTimer.stop(); - } - } } // "balance" text below field @@ -384,8 +382,8 @@ Item { height: visible ? parent.height : 0; AnonymousProRegular { - id: dateText; - text: model.created_at ? getFormattedDate(model.created_at * 1000) : ""; + id: hfcText; + text: model.hfc_text || ''; // Style size: 18; anchors.left: parent.left; @@ -393,28 +391,33 @@ Item { anchors.topMargin: 15; width: 118; height: paintedHeight; - color: hifi.colors.blueAccent; wrapMode: Text.WordWrap; + font.bold: true; // Alignment horizontalAlignment: Text.AlignRight; } AnonymousProRegular { id: transactionText; - text: model.text ? (model.status === "invalidated" ? ("INVALIDATED: " + model.text) : model.text) : ""; + text: model.transaction_text ? (model.status === "invalidated" ? ("INVALIDATED: " + model.transaction_text) : model.transaction_text) : ""; size: 18; anchors.top: parent.top; anchors.topMargin: 15; - anchors.left: dateText.right; + anchors.left: hfcText.right; anchors.leftMargin: 20; anchors.right: parent.right; height: paintedHeight; color: model.status === "invalidated" ? hifi.colors.redAccent : hifi.colors.baseGrayHighlight; + linkColor: hifi.colors.blueAccent; wrapMode: Text.WordWrap; font.strikeout: model.status === "invalidated"; onLinkActivated: { - sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + if (link.indexOf("users/") !== -1) { + sendSignalToWallet({method: 'transactionHistory_usernameLinkClicked', usernameLink: link}); + } else { + sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/ConnectionItem.qml b/interface/resources/qml/hifi/commerce/wallet/sendMoney/ConnectionItem.qml new file mode 100644 index 0000000000..c2d9ef3b19 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/sendMoney/ConnectionItem.qml @@ -0,0 +1,128 @@ +// +// ConnectionItem.qml +// qml/hifi/commerce/wallet/sendMoney +// +// ConnectionItem +// +// Created by Zach Fox on 2018-01-09 +// Copyright 2018 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 Hifi 1.0 as Hifi +import QtQuick 2.5 +import QtGraphicalEffects 1.0 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import "../../../../styles-uit" +import "../../../../controls-uit" as HifiControlsUit +import "../../../../controls" as HifiControls +import "../../wallet" as HifiWallet + +Item { + HifiConstants { id: hifi; } + + id: root; + property bool isSelected: false; + property string userName; + property string profilePicUrl; + + height: 65; + width: parent.width; + + Rectangle { + id: mainContainer; + // Style + color: root.isSelected ? hifi.colors.faintGray : hifi.colors.white; + // Size + anchors.left: parent.left; + anchors.right: parent.right; + anchors.top: parent.top; + height: root.height; + + Item { + id: avatarImage; + visible: profileUrl !== "" && userName !== ""; + // Size + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.leftMargin: 36; + height: root.height - 15; + width: visible ? height : 0; + clip: true; + Image { + id: userImage; + source: root.profilePicUrl !== "" ? ((0 === root.profilePicUrl.indexOf("http")) ? + root.profilePicUrl : (Account.metaverseServerURL + root.profilePicUrl)) : ""; + 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; + } + } + + RalewaySemiBold { + id: userName; + anchors.left: avatarImage.right; + anchors.leftMargin: 16; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + anchors.right: chooseButton.visible ? chooseButton.left : parent.right; + anchors.rightMargin: chooseButton.visible ? 10 : 0; + // Text size + size: 20; + // Style + color: hifi.colors.baseGray; + text: root.userName; + elide: Text.ElideRight; + // Alignment + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignVCenter; + } + + // "Choose" button + HifiControlsUit.Button { + id: chooseButton; + visible: root.isSelected; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.verticalCenter: parent.verticalCenter; + anchors.right: parent.right; + anchors.rightMargin: 24; + height: root.height - 20; + width: 110; + text: "CHOOSE"; + onClicked: { + var msg = { method: 'chooseConnection', userName: root.userName, profilePicUrl: root.profilePicUrl }; + sendToSendMoney(msg); + } + } + } + + // + // FUNCTION DEFINITIONS START + // + signal sendToSendMoney(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/RecipientDisplay.qml b/interface/resources/qml/hifi/commerce/wallet/sendMoney/RecipientDisplay.qml new file mode 100644 index 0000000000..267cf0ed51 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/sendMoney/RecipientDisplay.qml @@ -0,0 +1,116 @@ +// +// RecipientDisplay.qml +// qml/hifi/commerce/wallet/sendMoney +// +// RecipientDisplay +// +// Created by Zach Fox on 2018-01-11 +// Copyright 2018 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 Hifi 1.0 as Hifi +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import "../../../../styles-uit" +import "../../../../controls-uit" as HifiControlsUit +import "../../../../controls" as HifiControls +import "../../common" as HifiCommerceCommon + +Item { + HifiConstants { id: hifi; } + + id: root; + + property bool isDisplayingNearby; // as opposed to 'connections' + property string displayName; + property string userName; + property string profilePic; + + Item { + visible: root.isDisplayingNearby; + anchors.fill: parent; + + RalewaySemiBold { + id: recipientDisplayName; + text: root.displayName; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.rightMargin: 12; + height: parent.height/2; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignBottom; + elide: Text.ElideRight; + } + + RalewaySemiBold { + text: root.userName; + // Anchors + anchors.bottom: parent.bottom; + anchors.left: recipientDisplayName.anchors.left; + anchors.leftMargin: recipientDisplayName.anchors.leftMargin; + anchors.right: recipientDisplayName.anchors.right; + anchors.rightMargin: recipientDisplayName.anchors.rightMargin; + height: parent.height/2; + // Text size + size: 16; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignTop; + elide: Text.ElideRight; + } + } + + Item { + visible: !root.isDisplayingNearby; + anchors.fill: parent; + + Image { + id: userImage; + source: root.profilePic; + mipmap: true; + // Anchors + anchors.left: parent.left; + anchors.verticalCenter: parent.verticalCenter; + height: parent.height - 36; + width: height; + 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; + } + } + } + } + + RalewaySemiBold { + text: root.userName; + // Anchors + anchors.left: userImage.right; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + height: parent.height - 4; + // Text size + size: 16; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + elide: Text.ElideRight; + } + } +} diff --git a/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml new file mode 100644 index 0000000000..9164ba7a8c --- /dev/null +++ b/interface/resources/qml/hifi/commerce/wallet/sendMoney/SendMoney.qml @@ -0,0 +1,1502 @@ +// +// SendMoney.qml +// qml/hifi/commerce/wallet/sendMoney +// +// SendMoney +// +// Created by Zach Fox on 2018-01-09 +// Copyright 2018 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 Hifi 1.0 as Hifi +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtGraphicalEffects 1.0 +import "../../../../styles-uit" +import "../../../../controls-uit" as HifiControlsUit +import "../../../../controls" as HifiControls +import "../../common" as HifiCommerceCommon + +Item { + HifiConstants { id: hifi; } + + id: root; + + property int parentAppTitleBarHeight; + property int parentAppNavBarHeight; + property string currentActiveView: "sendMoneyHome"; + property string nextActiveView: ""; + property bool isCurrentlyFullScreen: chooseRecipientConnection.visible || + chooseRecipientNearby.visible || sendMoneyStep.visible || paymentSuccess.visible || paymentFailure.visible; + property bool isCurrentlySendingMoney: false; + + // This object is always used in a popup or full-screen Wallet section. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup/section. + MouseArea { + x: 0; + y: root.isCurrentlyFullScreen ? 0 : root.parentAppTitleBarHeight; + width: parent.width; + height: root.isCurrentlyFullScreen ? parent.height : parent.height - root.parentAppTitleBarHeight - root.parentAppNavBarHeight; + propagateComposedEvents: false; + } + + Connections { + target: Commerce; + + onBalanceResult : { + balanceText.text = result.data.balance; + } + + onTransferHfcToNodeResult: { + root.isCurrentlySendingMoney = false; + + if (result.status === 'success') { + root.nextActiveView = 'paymentSuccess'; + } else { + root.nextActiveView = 'paymentFailure'; + } + } + + onTransferHfcToUsernameResult: { + root.isCurrentlySendingMoney = false; + + if (result.status === 'success') { + root.nextActiveView = 'paymentSuccess'; + } else { + root.nextActiveView = 'paymentFailure'; + } + } + } + + Connections { + target: GlobalServices + onMyUsernameChanged: { + usernameText.text = Account.username; + } + } + + Component.onCompleted: { + Commerce.balance(); + } + + onNextActiveViewChanged: { + if (root.currentActiveView === 'chooseRecipientNearby') { + sendSignalToWallet({method: 'disable_ChooseRecipientNearbyMode'}); + } + + root.currentActiveView = root.nextActiveView; + + if (root.currentActiveView === 'chooseRecipientConnection') { + // Refresh connections model + connectionsLoading.visible = false; + connectionsLoading.visible = true; + sendSignalToWallet({method: 'refreshConnections'}); + } else if (root.currentActiveView === 'sendMoneyHome') { + Commerce.balance(); + } else if (root.currentActiveView === 'chooseRecipientNearby') { + sendSignalToWallet({method: 'enable_ChooseRecipientNearbyMode'}); + } + } + + // Send Money Home BEGIN + Item { + id: sendMoneyHome; + visible: root.currentActiveView === "sendMoneyHome"; + anchors.fill: parent; + anchors.topMargin: root.parentAppTitleBarHeight; + anchors.bottomMargin: root.parentAppNavBarHeight; + + // Username Text + RalewayRegular { + id: usernameText; + text: Account.username; + // Text size + size: 24; + // Style + color: hifi.colors.white; + elide: Text.ElideRight; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; + } + + // HFC Balance Container + Item { + id: hfcBalanceContainer; + // Anchors + anchors.top: parent.top; + anchors.right: parent.right; + anchors.leftMargin: 20; + width: parent.width/2; + height: 80; + + // "HFC" balance label + HiFiGlyphs { + id: balanceLabel; + text: hifi.glyphs.hfc; + // Size + size: 40; + // Anchors + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + // Style + color: hifi.colors.white; + } + + // Balance Text + FiraSansRegular { + id: balanceText; + text: "--"; + // Text size + size: 28; + // Anchors + anchors.top: balanceLabel.top; + anchors.bottom: balanceLabel.bottom; + anchors.left: balanceLabel.right; + anchors.leftMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 4; + // Style + color: hifi.colors.white; + // Alignment + verticalAlignment: Text.AlignVCenter; + } + + // "balance" text below field + RalewayRegular { + text: "BALANCE (HFC)"; + // Text size + size: 14; + // Anchors + anchors.top: balanceLabel.top; + anchors.topMargin: balanceText.paintedHeight + 20; + anchors.bottom: balanceLabel.bottom; + anchors.left: balanceText.left; + anchors.right: balanceText.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + } + } + + // Send Money + Rectangle { + id: sendMoneyContainer; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + height: 440; + + RalewaySemiBold { + id: sendMoneyText; + text: "Send Money To:"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + Item { + id: connectionButton; + // Anchors + anchors.top: sendMoneyText.bottom; + anchors.topMargin: 40; + anchors.left: parent.left; + anchors.leftMargin: 75; + height: 95; + width: 95; + + Image { + anchors.top: parent.top; + source: "../images/wallet-bg.jpg"; + height: 60; + width: parent.width; + fillMode: Image.PreserveAspectFit; + horizontalAlignment: Image.AlignHCenter; + verticalAlignment: Image.AlignTop; + } + + RalewaySemiBold { + text: "Connection"; + // Anchors + anchors.bottom: parent.bottom; + height: 15; + width: parent.width; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + } + + MouseArea { + anchors.fill: parent; + onClicked: { + root.nextActiveView = "chooseRecipientConnection"; + } + } + } + + Item { + id: nearbyButton; + // Anchors + anchors.top: sendMoneyText.bottom; + anchors.topMargin: connectionButton.anchors.topMargin; + anchors.right: parent.right; + anchors.rightMargin: connectionButton.anchors.leftMargin; + height: connectionButton.height; + width: connectionButton.width; + + Image { + anchors.top: parent.top; + source: "../images/wallet-bg.jpg"; + height: 60; + width: parent.width; + fillMode: Image.PreserveAspectFit; + horizontalAlignment: Image.AlignHCenter; + verticalAlignment: Image.AlignTop; + } + + RalewaySemiBold { + text: "Someone Nearby"; + // Anchors + anchors.bottom: parent.bottom; + height: 15; + width: parent.width; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + } + + MouseArea { + anchors.fill: parent; + onClicked: { + root.nextActiveView = "chooseRecipientNearby"; + } + } + } + } + } + // Send Money Home END + + // Choose Recipient Connection BEGIN + Rectangle { + id: chooseRecipientConnection; + visible: root.currentActiveView === "chooseRecipientConnection"; + anchors.fill: parent; + color: "#AAAAAA"; + + ListModel { + id: connectionsModel; + } + ListModel { + id: filteredConnectionsModel; + } + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 30; + height: parent.height - 30; + + RalewaySemiBold { + id: chooseRecipientText_connections; + text: "Choose Recipient:"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + HiFiGlyphs { + id: closeGlyphButton_connections; + text: hifi.glyphs.close; + size: 26; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + root.nextActiveView = "sendMoneyHome"; + } + } + } + + // + // FILTER BAR START + // + Item { + id: filterBarContainer; + // Size + height: 40; + // Anchors + anchors.left: parent.left; + anchors.leftMargin: 16; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.top: chooseRecipientText_connections.bottom; + anchors.topMargin: 12; + + HifiControlsUit.TextField { + id: filterBar; + colorScheme: hifi.colorSchemes.faintGray; + hasClearButton: true; + hasRoundedBorder: true; + anchors.fill: parent; + placeholderText: "filter recipients"; + + onTextChanged: { + buildFilteredConnectionsModel(); + } + + onAccepted: { + focus = false; + } + } + } + // + // FILTER BAR END + // + + Item { + id: connectionsContainer; + anchors.top: filterBarContainer.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + + AnimatedImage { + id: connectionsLoading; + source: "../../../../../icons/profilePicLoading.gif" + width: 120; + height: width; + anchors.top: parent.top; + anchors.topMargin: 185; + anchors.horizontalCenter: parent.horizontalCenter; + } + + ListView { + id: connectionsList; + ScrollBar.vertical: ScrollBar { + policy: connectionsList.contentHeight > parent.parent.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded; + parent: connectionsList.parent; + anchors.top: connectionsList.top; + anchors.right: connectionsList.right; + anchors.bottom: connectionsList.bottom; + width: 20; + } + visible: !connectionsLoading.visible; + clip: true; + model: filteredConnectionsModel; + snapMode: ListView.SnapToItem; + // Anchors + anchors.fill: parent; + delegate: ConnectionItem { + isSelected: connectionsList.currentIndex === index; + userName: model.userName; + profilePicUrl: model.profileUrl; + anchors.topMargin: 6; + anchors.bottomMargin: 6; + + Connections { + onSendToSendMoney: { + sendMoneyStep.referrer = "connections"; + sendMoneyStep.selectedRecipientNodeID = ''; + sendMoneyStep.selectedRecipientDisplayName = 'connection'; + sendMoneyStep.selectedRecipientUserName = msg.userName; + sendMoneyStep.selectedRecipientProfilePic = msg.profilePicUrl; + + root.nextActiveView = "sendMoneyStep"; + } + } + + MouseArea { + enabled: connectionsList.currentIndex !== index; + anchors.fill: parent; + propagateComposedEvents: false; + onClicked: { + connectionsList.currentIndex = index; + } + } + } + } + } + } + } + // Choose Recipient Connection END + + // Choose Recipient Nearby BEGIN + Rectangle { + id: chooseRecipientNearby; + + property string selectedRecipient; + + visible: root.currentActiveView === "chooseRecipientNearby"; + anchors.fill: parent; + color: "#AAAAAA"; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 30; + height: parent.height - 30; + + RalewaySemiBold { + text: "Choose Recipient:"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + HiFiGlyphs { + id: closeGlyphButton_nearby; + text: hifi.glyphs.close; + size: 26; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + } + + Item { + id: selectionInstructionsContainer; + visible: chooseRecipientNearby.selectedRecipient === ""; + anchors.fill: parent; + + RalewaySemiBold { + id: selectionInstructions_deselected; + text: "Click/trigger on an avatar nearby to select them..."; + // Anchors + anchors.bottom: parent.bottom; + anchors.bottomMargin: 200; + anchors.left: parent.left; + anchors.leftMargin: 58; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + height: paintedHeight; + // Text size + size: 20; + // Style + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + wrapMode: Text.Wrap; + } + } + + Item { + id: selectionMadeContainer; + visible: !selectionInstructionsContainer.visible; + anchors.fill: parent; + + RalewaySemiBold { + id: sendToText; + text: "Send To:"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 120; + anchors.left: parent.left; + anchors.leftMargin: 12; + width: paintedWidth; + height: paintedHeight; + // Text size + size: 20; + // Style + color: hifi.colors.baseGray; + } + + RalewaySemiBold { + id: avatarDisplayName; + text: '"' + AvatarList.getAvatar(chooseRecipientNearby.selectedRecipient).sessionDisplayName + '"'; + // Anchors + anchors.top: sendToText.bottom; + anchors.topMargin: 60; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + // Text size + size: 22; + // Style + horizontalAlignment: Text.AlignHCenter; + color: hifi.colors.baseGray; + } + + RalewaySemiBold { + id: avatarNodeID; + text: chooseRecipientNearby.selectedRecipient; + // Anchors + anchors.top: avatarDisplayName.bottom; + anchors.topMargin: 6; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + // Text size + size: 14; + // Style + horizontalAlignment: Text.AlignHCenter; + color: hifi.colors.lightGrayText; + } + + RalewaySemiBold { + id: avatarUserName; + text: sendMoneyStep.selectedRecipientUserName; + // Anchors + anchors.top: avatarNodeID.bottom; + anchors.topMargin: 12; + anchors.left: parent.left; + anchors.leftMargin: 30; + anchors.right: parent.right; + anchors.rightMargin: 30; + height: paintedHeight; + // Text size + size: 22; + // Style + horizontalAlignment: Text.AlignHCenter; + color: hifi.colors.baseGray; + } + + RalewaySemiBold { + id: selectionInstructions_selected; + text: "Click/trigger on another avatar nearby to select them...\n\nor press 'Next' to continue."; + // Anchors + anchors.bottom: parent.bottom; + anchors.bottomMargin: 200; + anchors.left: parent.left; + anchors.leftMargin: 58; + anchors.right: parent.right; + anchors.rightMargin: anchors.leftMargin; + height: paintedHeight; + // Text size + size: 20; + // Style + color: hifi.colors.baseGray; + horizontalAlignment: Text.AlignHCenter; + wrapMode: Text.Wrap; + } + } + + // "Cancel" button + HifiControlsUit.Button { + id: cancelButton; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.dark; + anchors.left: parent.left; + anchors.leftMargin: 60; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 80; + height: 50; + width: 120; + text: "Cancel"; + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + + // "Next" button + HifiControlsUit.Button { + id: nextButton; + enabled: chooseRecipientNearby.selectedRecipient !== ""; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.rightMargin: 60; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 80; + height: 50; + width: 120; + text: "Next"; + onClicked: { + sendMoneyStep.referrer = "nearby"; + sendMoneyStep.selectedRecipientNodeID = chooseRecipientNearby.selectedRecipient; + chooseRecipientNearby.selectedRecipient = ""; + + root.nextActiveView = "sendMoneyStep"; + } + } + } + } + // Choose Recipient Nearby END + + // Send Money Screen BEGIN + Rectangle { + id: sendMoneyStep; + z: 997; + + property string referrer; // either "connections" or "nearby" + property string selectedRecipientNodeID; + property string selectedRecipientDisplayName; + property string selectedRecipientUserName; + property string selectedRecipientProfilePic; + + visible: root.currentActiveView === "sendMoneyStep"; + anchors.fill: parent; + color: "#AAAAAA"; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 30; + height: parent.height - 30; + + RalewaySemiBold { + id: sendMoneyText_sendMoneyStep; + text: "Send Money To:"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + Item { + id: sendToContainer; + anchors.top: sendMoneyText_sendMoneyStep.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: sendToText_sendMoneyStep; + text: "Send To:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + RecipientDisplay { + anchors.top: parent.top; + anchors.left: sendToText_sendMoneyStep.right; + anchors.right: changeButton.left; + anchors.rightMargin: 12; + height: parent.height; + + displayName: sendMoneyStep.selectedRecipientDisplayName; + userName: sendMoneyStep.selectedRecipientUserName; + profilePic: sendMoneyStep.selectedRecipientProfilePic !== "" ? ((0 === sendMoneyStep.selectedRecipientProfilePic.indexOf("http")) ? + sendMoneyStep.selectedRecipientProfilePic : (Account.metaverseServerURL + sendMoneyStep.selectedRecipientProfilePic)) : ""; + isDisplayingNearby: sendMoneyStep.referrer === "nearby"; + } + + // "CHANGE" button + HifiControlsUit.Button { + id: changeButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + height: 35; + width: 120; + text: "CHANGE"; + onClicked: { + if (sendMoneyStep.referrer === "connections") { + root.nextActiveView = "chooseRecipientConnection"; + } else if (sendMoneyStep.referrer === "nearby") { + root.nextActiveView = "chooseRecipientNearby"; + } + resetSendMoneyData(); + } + } + } + + Item { + id: amountContainer; + anchors.top: sendToContainer.bottom; + anchors.topMargin: 2; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: amountText; + text: "Amount:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + HifiControlsUit.TextField { + id: amountTextField; + colorScheme: hifi.colorSchemes.light; + inputMethodHints: Qt.ImhDigitsOnly; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: amountText.right; + anchors.right: parent.right; + height: 50; + // Style + leftPlaceholderGlyph: hifi.glyphs.hfc; + activeFocusOnPress: true; + activeFocusOnTab: true; + + validator: IntValidator { bottom: 0; } + + onAccepted: { + optionalMessage.focus = true; + } + } + + RalewaySemiBold { + id: amountTextFieldError; + // Anchors + anchors.top: amountTextField.bottom; + anchors.topMargin: 2; + anchors.left: amountTextField.left; + anchors.right: amountTextField.right; + height: 40; + // Text size + size: 16; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignRight; + } + } + + Item { + id: messageContainer; + anchors.top: amountContainer.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 140; + + FontLoader { id: firaSansSemiBold; source: "../../../../../fonts/FiraSans-SemiBold.ttf"; } + TextArea { + id: optionalMessage; + property int maximumLength: 70; + property string previousText: text; + placeholderText: "Optional Message"; + font.family: firaSansSemiBold.name; + font.pixelSize: 20; + // Anchors + anchors.fill: parent; + // Style + background: Rectangle { + anchors.fill: parent; + color: optionalMessage.activeFocus ? hifi.colors.white : hifi.colors.textFieldLightBackground; + border.width: optionalMessage.activeFocus ? 1 : 0; + border.color: optionalMessage.activeFocus ? hifi.colors.primaryHighlight : hifi.colors.textFieldLightBackground; + } + color: hifi.colors.black; + textFormat: TextEdit.PlainText; + wrapMode: TextEdit.Wrap; + activeFocusOnPress: true; + activeFocusOnTab: true; + // Workaround for no max length on TextAreas + onTextChanged: { + if (text.length > maximumLength) { + var cursor = cursorPosition; + text = previousText; + if (cursor > text.length) { + cursorPosition = text.length; + } else { + cursorPosition = cursor-1; + } + } + previousText = text; + } + } + RalewaySemiBold { + id: optionalMessageCharacterCount; + text: optionalMessage.text.length + "/" + optionalMessage.maximumLength; + // Anchors + anchors.top: optionalMessage.bottom; + anchors.topMargin: 2; + anchors.right: optionalMessage.right; + height: paintedHeight; + // Text size + size: 16; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignTop; + horizontalAlignment: Text.AlignRight; + } + } + + Item { + id: bottomBarContainer; + anchors.top: messageContainer.bottom; + anchors.topMargin: 30; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + HifiControlsUit.CheckBox { + id: sendPubliclyCheckbox; + visible: false; // FIXME ONCE PARTICLE EFFECTS ARE IN + text: "Send Publicly" + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: parent.left; + anchors.right: cancelButton_sendMoneyStep.left; + anchors.rightMargin: 16; + boxSize: 24; + } + + // "CANCEL" button + HifiControlsUit.Button { + id: cancelButton_sendMoneyStep; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.dark; + anchors.right: sendButton.left; + anchors.rightMargin: 16; + anchors.verticalCenter: parent.verticalCenter; + height: 35; + width: 100; + text: "CANCEL"; + onClicked: { + resetSendMoneyData(); + root.nextActiveView = "sendMoneyHome"; + } + } + + // "SEND" button + HifiControlsUit.Button { + id: sendButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.verticalCenter: parent.verticalCenter; + height: 35; + width: 100; + text: "SEND"; + onClicked: { + if (parseInt(amountTextField.text) > parseInt(balanceText.text)) { + amountTextField.focus = true; + amountTextField.error = true; + amountTextFieldError.text = "amount exceeds available funds"; + } else if (amountTextField.text === "" || parseInt(amountTextField.text) < 1) { + amountTextField.focus = true; + amountTextField.error = true; + amountTextFieldError.text = "invalid amount"; + } else { + amountTextFieldError.text = ""; + amountTextField.error = false; + root.isCurrentlySendingMoney = true; + amountTextField.focus = false; + optionalMessage.focus = false; + if (sendMoneyStep.referrer === "connections") { + Commerce.transferHfcToUsername(sendMoneyStep.selectedRecipientUserName, parseInt(amountTextField.text), optionalMessage.text); + } else if (sendMoneyStep.referrer === "nearby") { + Commerce.transferHfcToNode(sendMoneyStep.selectedRecipientNodeID, parseInt(amountTextField.text), optionalMessage.text); + } + } + } + } + } + } + } + // Send Money Screen END + + // Sending Money Overlay START + Rectangle { + id: sendingMoneyOverlay; + z: 998; + + visible: root.isCurrentlySendingMoney; + anchors.fill: parent; + color: Qt.rgba(0.0, 0.0, 0.0, 0.5); + + // This object is always used in a popup or full-screen Wallet section. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup/section. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + AnimatedImage { + id: sendingMoneyImage; + source: "../../../../../icons/profilePicLoading.gif" + width: 160; + height: width; + anchors.top: parent.top; + anchors.topMargin: 185; + anchors.horizontalCenter: parent.horizontalCenter; + } + + RalewaySemiBold { + text: "Sending"; + // Anchors + anchors.top: sendingMoneyImage.bottom; + anchors.topMargin: 22; + anchors.horizontalCenter: parent.horizontalCenter; + width: paintedWidth; + // Text size + size: 24; + // Style + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + } + } + // Sending Money Overlay END + + // Payment Success BEGIN + Rectangle { + id: paymentSuccess; + + visible: root.currentActiveView === "paymentSuccess"; + anchors.fill: parent; + color: "#AAAAAA"; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 30; + height: parent.height - 30; + + RalewaySemiBold { + id: paymentSentText; + text: "Payment Sent"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + HiFiGlyphs { + id: closeGlyphButton_paymentSuccess; + text: hifi.glyphs.close; + size: 26; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + } + + Item { + id: sendToContainer_paymentSuccess; + anchors.top: paymentSentText.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: sendToText_paymentSuccess; + text: "Sent To:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + RecipientDisplay { + anchors.top: parent.top; + anchors.left: sendToText_paymentSuccess.right; + anchors.right: parent.right; + height: parent.height; + + displayName: sendMoneyStep.selectedRecipientDisplayName; + userName: sendMoneyStep.selectedRecipientUserName; + profilePic: sendMoneyStep.selectedRecipientProfilePic !== "" ? ((0 === sendMoneyStep.selectedRecipientProfilePic.indexOf("http")) ? + sendMoneyStep.selectedRecipientProfilePic : (Account.metaverseServerURL + sendMoneyStep.selectedRecipientProfilePic)) : ""; + isDisplayingNearby: sendMoneyStep.referrer === "nearby"; + } + } + + Item { + id: amountContainer_paymentSuccess; + anchors.top: sendToContainer_paymentSuccess.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: amountText_paymentSuccess; + text: "Amount:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + // "HFC" balance label + HiFiGlyphs { + id: amountSentLabel; + text: hifi.glyphs.hfc; + // Size + size: 32; + // Anchors + anchors.left: amountText_paymentSuccess.right; + anchors.verticalCenter: parent.verticalCenter; + height: 50; + // Style + color: hifi.colors.baseGray; + } + + RalewaySemiBold { + id: amountSentText; + text: amountTextField.text; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: amountSentLabel.right; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: 50; + // Style + size: 22; + color: hifi.colors.baseGray; + } + } + + RalewaySemiBold { + id: optionalMessage_paymentSuccess; + text: optionalMessage.text; + // Anchors + anchors.top: amountContainer_paymentSuccess.bottom; + anchors.left: parent.left; + anchors.leftMargin: 110; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.bottom: closeButton.top; + anchors.bottomMargin: 40; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + wrapMode: Text.Wrap; + verticalAlignment: Text.AlignTop; + } + + // "Close" button + HifiControlsUit.Button { + id: closeButton; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 80; + height: 50; + width: 120; + text: "Close"; + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + } + } + // Payment Success END + + // Payment Failure BEGIN + Rectangle { + id: paymentFailure; + + visible: root.currentActiveView === "paymentFailure"; + anchors.fill: parent; + color: "#AAAAAA"; + + Rectangle { + anchors.centerIn: parent; + width: parent.width - 30; + height: parent.height - 30; + + RalewaySemiBold { + id: paymentFailureText; + text: "Payment Failed"; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 26; + anchors.left: parent.left; + anchors.leftMargin: 20; + width: paintedWidth; + height: 30; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + } + + HiFiGlyphs { + id: closeGlyphButton_paymentFailure; + text: hifi.glyphs.close; + size: 26; + anchors.top: parent.top; + anchors.topMargin: 10; + anchors.right: parent.right; + anchors.rightMargin: 10; + MouseArea { + anchors.fill: parent; + hoverEnabled: true; + onEntered: { + parent.text = hifi.glyphs.closeInverted; + } + onExited: { + parent.text = hifi.glyphs.close; + } + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + } + + RalewaySemiBold { + id: paymentFailureDetailText; + text: "The recipient you specified was unable to receive your payment."; + anchors.top: paymentFailureText.bottom; + anchors.topMargin: 20; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + wrapMode: Text.Wrap; + } + + Item { + id: sendToContainer_paymentFailure; + anchors.top: paymentFailureDetailText.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: sentToText_paymentFailure; + text: "Sent To:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + RecipientDisplay { + anchors.top: parent.top; + anchors.left: sentToText_paymentFailure.right; + anchors.right: parent.right; + height: parent.height; + + displayName: sendMoneyStep.selectedRecipientDisplayName; + userName: sendMoneyStep.selectedRecipientUserName; + profilePic: sendMoneyStep.selectedRecipientProfilePic !== "" ? ((0 === sendMoneyStep.selectedRecipientProfilePic.indexOf("http")) ? + sendMoneyStep.selectedRecipientProfilePic : (Account.metaverseServerURL + sendMoneyStep.selectedRecipientProfilePic)) : ""; + isDisplayingNearby: sendMoneyStep.referrer === "nearby"; + } + } + + Item { + id: amountContainer_paymentFailure; + anchors.top: sendToContainer_paymentFailure.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 80; + + RalewaySemiBold { + id: amountText_paymentFailure; + text: "Amount:"; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: 90; + // Text size + size: 18; + // Style + color: hifi.colors.baseGray; + verticalAlignment: Text.AlignVCenter; + } + + // "HFC" balance label + HiFiGlyphs { + id: amountSentLabel_paymentFailure; + text: hifi.glyphs.hfc; + // Size + size: 32; + // Anchors + anchors.left: amountText_paymentFailure.right; + anchors.verticalCenter: parent.verticalCenter; + height: 50; + // Style + color: hifi.colors.baseGray; + } + + RalewaySemiBold { + id: amountSentText_paymentFailure; + text: amountTextField.text; + // Anchors + anchors.verticalCenter: parent.verticalCenter; + anchors.left: amountSentLabel_paymentFailure.right; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: 50; + // Style + size: 22; + color: hifi.colors.baseGray; + } + } + + RalewaySemiBold { + id: optionalMessage_paymentFailuire; + text: optionalMessage.text; + // Anchors + anchors.top: amountContainer_paymentFailure.bottom; + anchors.left: parent.left; + anchors.leftMargin: 110; + anchors.right: parent.right; + anchors.rightMargin: 16; + anchors.bottom: closeButton_paymentFailure.top; + anchors.bottomMargin: 40; + // Text size + size: 22; + // Style + color: hifi.colors.baseGray; + wrapMode: Text.Wrap; + verticalAlignment: Text.AlignTop; + } + + // "Close" button + HifiControlsUit.Button { + id: closeButton_paymentFailure; + color: hifi.buttons.noneBorderless; + colorScheme: hifi.colorSchemes.dark; + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 80; + height: 50; + width: 120; + text: "Cancel"; + onClicked: { + root.nextActiveView = "sendMoneyHome"; + resetSendMoneyData(); + } + } + + // "Retry" button + HifiControlsUit.Button { + id: retryButton_paymentFailure; + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.rightMargin: 12; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 80; + height: 50; + width: 120; + text: "Retry"; + onClicked: { + root.isCurrentlySendingMoney = true; + if (sendMoneyStep.referrer === "connections") { + Commerce.transferHfcToUsername(sendMoneyStep.selectedRecipientUserName, parseInt(amountTextField.text), optionalMessage.text); + } else if (sendMoneyStep.referrer === "nearby") { + Commerce.transferHfcToNode(sendMoneyStep.selectedRecipientNodeID, parseInt(amountTextField.text), optionalMessage.text); + } + } + } + } + } + // Payment Failure END + + + // + // FUNCTION DEFINITIONS START + // + + function updateConnections(connections) { + connectionsModel.clear(); + connectionsModel.append(connections); + buildFilteredConnectionsModel(); + connectionsLoading.visible = false; + } + + function buildFilteredConnectionsModel() { + filteredConnectionsModel.clear(); + for (var i = 0; i < connectionsModel.count; i++) { + if (connectionsModel.get(i).userName.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { + filteredConnectionsModel.append(connectionsModel.get(i)); + } + } + } + + function resetSendMoneyData() { + amountTextField.focus = false; + optionalMessage.focus = false; + amountTextFieldError.text = ""; + amountTextField.error = false; + chooseRecipientNearby.selectedRecipient = ""; + sendMoneyStep.selectedRecipientNodeID = ""; + sendMoneyStep.selectedRecipientDisplayName = ""; + sendMoneyStep.selectedRecipientUserName = ""; + sendMoneyStep.selectedRecipientProfilePic = ""; + amountTextField.text = ""; + optionalMessage.text = ""; + } + + // + // Function Name: fromScript() + // + // Relevant Variables: + // None + // + // Arguments: + // message: The message sent from the JavaScript. + // Messages are in format "{method, params}", like json-rpc. + // + // Description: + // Called when a message is received from a script. + // + function fromScript(message) { + switch (message.method) { + case 'selectRecipient': + if (message.isSelected) { + chooseRecipientNearby.selectedRecipient = message.id[0]; + sendMoneyStep.selectedRecipientDisplayName = message.displayName; + sendMoneyStep.selectedRecipientUserName = message.userName; + } else { + chooseRecipientNearby.selectedRecipient = ""; + sendMoneyStep.selectedRecipientDisplayName = ''; + sendMoneyStep.selectedRecipientUserName = ''; + } + break; + case 'updateSelectedRecipientUsername': + sendMoneyStep.selectedRecipientUserName = message.userName; + break; + default: + console.log('SendMoney: Unrecognized message from wallet.js:', JSON.stringify(message)); + } + } + signal sendSignalToWallet(var msg); + // + // FUNCTION DEFINITIONS END + // +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index bb4e9af2f7..7ba93675a8 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2351,7 +2351,7 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/wallet/SecurityImageChange.qml" }, QUrl{ "hifi/commerce/wallet/SecurityImageModel.qml" }, QUrl{ "hifi/commerce/wallet/SecurityImageSelection.qml" }, - QUrl{ "hifi/commerce/wallet/SendMoney.qml" }, + QUrl{ "hifi/commerce/wallet/sendMoney/SendMoney.qml" }, QUrl{ "hifi/commerce/wallet/Wallet.qml" }, QUrl{ "hifi/commerce/wallet/WalletHome.qml" }, QUrl{ "hifi/commerce/wallet/WalletSetup.qml" }, diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 3072ecf240..2a33ee6f7b 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -574,8 +574,6 @@ Menu::Menu() { avatar.get(), SLOT(setEnableMeshVisible(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::TurnWithHead, 0, false); - addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::UseAnimPreAndPostRotations, 0, true, - avatar.get(), SLOT(setUseAnimPreAndPostRotations(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableInverseKinematics, 0, true, avatar.get(), SLOT(setEnableInverseKinematics(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::RenderSensorToWorldMatrix, 0, false, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 854f8d8c2b..11a27ff7f5 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -194,7 +194,6 @@ namespace MenuOption { const QString TurnWithHead = "Turn using Head"; const QString UseAudioForMouth = "Use Audio for Mouth"; const QString UseCamera = "Use Camera"; - const QString UseAnimPreAndPostRotations = "Use Anim Pre and Post Rotations"; const QString VelocityFilter = "Velocity Filter"; const QString VisibleToEveryone = "Everyone"; const QString VisibleToFriends = "Friends"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index e45228bc6f..c9ae9e5d6a 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -1062,11 +1062,6 @@ void MyAvatar::setEnableMeshVisible(bool isEnabled) { _skeletonModel->setVisibleInScene(isEnabled, qApp->getMain3DScene()); } -void MyAvatar::setUseAnimPreAndPostRotations(bool isEnabled) { - AnimClip::usePreAndPostPoseFromAnim = isEnabled; - reset(true); -} - void MyAvatar::setEnableInverseKinematics(bool isEnabled) { _skeletonModel->getRig().setEnableInverseKinematics(isEnabled); } @@ -1930,7 +1925,7 @@ void MyAvatar::preDisplaySide(RenderArgs* renderArgs) { _prevShouldDrawHead = shouldDrawHead; } -const float RENDER_HEAD_CUTOFF_DISTANCE = 0.3f; +const float RENDER_HEAD_CUTOFF_DISTANCE = 0.47f; bool MyAvatar::cameraInsideHead(const glm::vec3& cameraPosition) const { return glm::length(cameraPosition - getHeadPosition()) < (RENDER_HEAD_CUTOFF_DISTANCE * getModelScale()); @@ -2101,7 +2096,7 @@ void MyAvatar::updateActionMotor(float deltaTime) { _actionMotorVelocity = motorSpeed * direction; } else { // we're interacting with a floor --> simple horizontal speed and exponential decay - _actionMotorVelocity = getSensorToWorldScale() * DEFAULT_AVATAR_MAX_WALKING_SPEED * direction; + _actionMotorVelocity = getSensorToWorldScale() * _walkSpeed.get() * direction; } float boomChange = getDriveKey(ZOOM); @@ -2693,6 +2688,14 @@ float MyAvatar::getUserEyeHeight() const { return userHeight - userHeight * ratio; } +float MyAvatar::getWalkSpeed() const { + return _walkSpeed.get(); +} + +void MyAvatar::setWalkSpeed(float value) { + _walkSpeed.set(value); +} + glm::vec3 MyAvatar::getPositionForAudio() { switch (_audioListenerMode) { case AudioListenerMode::FROM_HEAD: diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index ab74460d4e..58b49b61ff 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -163,6 +163,8 @@ class MyAvatar : public Avatar { Q_PROPERTY(QUuid SELF_ID READ getSelfID CONSTANT) + Q_PROPERTY(float walkSpeed READ getWalkSpeed WRITE setWalkSpeed); + const QString DOMINANT_LEFT_HAND = "left"; const QString DOMINANT_RIGHT_HAND = "right"; @@ -557,6 +559,9 @@ public: const QUuid& getSelfID() const { return AVATAR_SELF_ID; } + void setWalkSpeed(float value); + float getWalkSpeed() const; + public slots: void increaseSize(); void decreaseSize(); @@ -594,7 +599,6 @@ public slots: bool getEnableMeshVisible() const { return _skeletonModel->isVisible(); } void setEnableMeshVisible(bool isEnabled); - void setUseAnimPreAndPostRotations(bool isEnabled); void setEnableInverseKinematics(bool isEnabled); QUrl getAnimGraphOverrideUrl() const; // thread-safe @@ -841,6 +845,9 @@ private: // height of user in sensor space, when standing erect. ThreadSafeValueCache _userHeight { DEFAULT_AVATAR_HEIGHT }; + + // max unscaled forward movement speed + ThreadSafeValueCache _walkSpeed { DEFAULT_AVATAR_MAX_WALKING_SPEED }; }; QScriptValue audioListenModeToScriptValue(QScriptEngine* engine, const AudioListenerMode& audioListenerMode); diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index 15830636f0..50ea6629cf 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -46,6 +46,8 @@ Handler(buy) Handler(receiveAt) Handler(balance) Handler(inventory) +Handler(transferHfcToNode) +Handler(transferHfcToUsername) void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) { auto accountManager = DependencyManager::get(); @@ -116,23 +118,60 @@ void Ledger::inventory(const QStringList& keys) { keysQuery("inventory", "inventorySuccess", "inventoryFailure"); } -QString amountString(const QString& label, const QString&color, const QJsonValue& moneyValue, const QJsonValue& certsValue) { - int money = moneyValue.toInt(); - int certs = certsValue.toInt(); - if (money <= 0 && certs <= 0) { - return QString(); +QString hfcString(const QJsonValue& sentValue, const QJsonValue& receivedValue) { + int sent = sentValue.toInt(); + int received = receivedValue.toInt(); + if (sent <= 0 && received <= 0) { + return QString("-"); } - QString result(QString(" %2").arg(color, label)); - if (money > 0) { - result += QString(" %1 HFC").arg(money); - } - if (certs > 0) { - if (money > 0) { - result += QString(","); + QString result; + if (sent > 0) { + result += QString("-%1 HFC").arg(sent); + if (received > 0) { + result += QString("
"); } - result += QString((certs == 1) ? " %1 certificate" : " %1 certificates").arg(certs); } - return result + QString("
"); + if (received > 0) { + result += QString("%1 HFC").arg(received); + } + return result; +} +static const QString USER_PAGE_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/users/"; +QString userLink(const QString& username) { + if (username.isEmpty()) { + return QString("someone"); + } + return QString("%2").arg(USER_PAGE_BASE_URL, username); +} + +QString transactionString(const QJsonObject& valueObject) { + int sentCerts = valueObject["sent_certs"].toInt(); + int receivedCerts = valueObject["received_certs"].toInt(); + int sent = valueObject["sent_money"].toInt(); + int dateInteger = valueObject["created_at"].toInt(); + QString message = valueObject["message"].toString(); + QDateTime createdAt(QDateTime::fromSecsSinceEpoch(dateInteger, Qt::UTC)); + QString result; + + if (sentCerts <= 0 && receivedCerts <= 0) { + // this is an hfc transfer. + if (sent > 0) { + QString recipient = userLink(valueObject["recipient_name"].toString()); + result += QString("Money sent to %1").arg(recipient); + } else { + QString sender = userLink(valueObject["sender_name"].toString()); + result += QString("Money from %1").arg(sender); + } + if (!message.isEmpty()) { + result += QString("
with memo: \"%1\"").arg(message); + } + } else { + result += valueObject["message"].toString(); + } + // no matter what we append a smaller date to the bottom of this... + + result += QString("
%1").arg(createdAt.toLocalTime().toString(Qt::DefaultLocaleShortDate)); + return result; } static const QString MARKETPLACE_ITEMS_BASE_URL = NetworkingConstants::METAVERSE_SERVER_URL().toString() + "/marketplace/items/"; @@ -155,16 +194,13 @@ void Ledger::historySuccess(QNetworkReply& reply) { // TODO: do this with 0 copies if possible for (auto it = historyArray.begin(); it != historyArray.end(); it++) { + // We have 2 text fields to synthesize, the one on the left is a listing + // of the HFC in/out of your wallet. The one on the right contains an explaination + // of the transaction. That could be just the memo (if it is a regular purchase), or + // more text (plus the optional memo) if an hfc transfer auto valueObject = (*it).toObject(); - QString sent = amountString("sent", "EA4C5F", valueObject["sent_money"], valueObject["sent_certs"]); - QString received = amountString("received", "1FC6A6", valueObject["received_money"], valueObject["received_certs"]); - - // turns out on my machine, toLocalTime convert to some weird timezone, yet the - // systemTimeZone is correct. To avoid a strange bug with other's systems too, lets - // be explicit - QDateTime createdAt = QDateTime::fromSecsSinceEpoch(valueObject["created_at"].toInt(), Qt::UTC); - QDateTime localCreatedAt = createdAt.toTimeZone(QTimeZone::systemTimeZone()); - valueObject["text"] = QString("%1%2%3").arg(valueObject["message"].toString(), sent, received); + valueObject["hfc_text"] = hfcString(valueObject["sent_money"], valueObject["received_money"]); + valueObject["transaction_text"] = transactionString(valueObject); newHistoryArray.push_back(valueObject); } // now copy the rest of the json -- this is inefficient @@ -198,11 +234,17 @@ void Ledger::accountSuccess(QNetworkReply& reply) { auto salt = QByteArray::fromBase64(data["salt"].toString().toUtf8()); auto iv = QByteArray::fromBase64(data["iv"].toString().toUtf8()); auto ckey = QByteArray::fromBase64(data["ckey"].toString().toUtf8()); + QString remotePublicKey = data["public_key"].toString(); wallet->setSalt(salt); wallet->setIv(iv); wallet->setCKey(ckey); + QStringList localPublicKeys = wallet->listPublicKeys(); + if (remotePublicKey.isEmpty() && !localPublicKeys.isEmpty()) { + receiveAt(localPublicKeys.first(), ""); + } + // none of the hfc account info should be emitted emit accountResult(QJsonObject{ {"status", "success"} }); } @@ -261,3 +303,25 @@ void Ledger::certificateInfo(const QString& certificateId) { request["certificate_id"] = certificateId; send(endpoint, "certificateInfoSuccess", "certificateInfoFailure", QNetworkAccessManager::PutOperation, AccountManagerAuth::None, request); } + +void Ledger::transferHfcToNode(const QString& hfc_key, const QString& nodeID, const int& amount, const QString& optionalMessage) { + QJsonObject transaction; + transaction["public_key"] = hfc_key; + transaction["node_id"] = nodeID; + transaction["quantity"] = amount; + transaction["message"] = optionalMessage; + QJsonDocument transactionDoc{ transaction }; + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + signedSend("transaction", transactionString, hfc_key, "transfer_hfc_to_node", "transferHfcToNodeSuccess", "transferHfcToNodeFailure"); +} + +void Ledger::transferHfcToUsername(const QString& hfc_key, const QString& username, const int& amount, const QString& optionalMessage) { + QJsonObject transaction; + transaction["public_key"] = hfc_key; + transaction["username"] = username; + transaction["quantity"] = amount; + transaction["message"] = optionalMessage; + QJsonDocument transactionDoc{ transaction }; + auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); + signedSend("transaction", transactionString, hfc_key, "transfer_hfc_to_user", "transferHfcToUsernameSuccess", "transferHfcToUsernameFailure"); +} diff --git a/interface/src/commerce/Ledger.h b/interface/src/commerce/Ledger.h index 318006b645..1b56c893ab 100644 --- a/interface/src/commerce/Ledger.h +++ b/interface/src/commerce/Ledger.h @@ -33,6 +33,8 @@ public: void account(); void updateLocation(const QString& asset_id, const QString location, const bool controlledFailure = false); void certificateInfo(const QString& certificateId); + void transferHfcToNode(const QString& hfc_key, const QString& nodeID, const int& amount, const QString& optionalMessage); + void transferHfcToUsername(const QString& hfc_key, const QString& username, const int& amount, const QString& optionalMessage); enum CertificateStatus { CERTIFICATE_STATUS_UNKNOWN = 0, @@ -51,6 +53,8 @@ signals: void accountResult(QJsonObject result); void locationUpdateResult(QJsonObject result); void certificateInfoResult(QJsonObject result); + void transferHfcToNodeResult(QJsonObject result); + void transferHfcToUsernameResult(QJsonObject result); void updateCertificateStatus(const QString& certID, uint certStatus); @@ -71,6 +75,10 @@ public slots: void updateLocationFailure(QNetworkReply& reply); void certificateInfoSuccess(QNetworkReply& reply); void certificateInfoFailure(QNetworkReply& reply); + void transferHfcToNodeSuccess(QNetworkReply& reply); + void transferHfcToNodeFailure(QNetworkReply& reply); + void transferHfcToUsernameSuccess(QNetworkReply& reply); + void transferHfcToUsernameFailure(QNetworkReply& reply); private: QJsonObject apiResponse(const QString& label, QNetworkReply& reply); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 58548eb3ef..c9caa393ce 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -29,6 +29,8 @@ QmlCommerce::QmlCommerce() { connect(wallet.data(), &Wallet::walletStatusResult, this, &QmlCommerce::walletStatusResult); connect(ledger.data(), &Ledger::certificateInfoResult, this, &QmlCommerce::certificateInfoResult); connect(ledger.data(), &Ledger::updateCertificateStatus, this, &QmlCommerce::updateCertificateStatus); + connect(ledger.data(), &Ledger::transferHfcToNodeResult, this, &QmlCommerce::transferHfcToNodeResult); + connect(ledger.data(), &Ledger::transferHfcToUsernameResult, this, &QmlCommerce::transferHfcToUsernameResult); auto accountManager = DependencyManager::get(); connect(accountManager.data(), &AccountManager::usernameChanged, this, [&]() { @@ -137,3 +139,27 @@ void QmlCommerce::certificateInfo(const QString& certificateId) { auto ledger = DependencyManager::get(); ledger->certificateInfo(certificateId); } + +void QmlCommerce::transferHfcToNode(const QString& nodeID, const int& amount, const QString& optionalMessage) { + auto ledger = DependencyManager::get(); + auto wallet = DependencyManager::get(); + QStringList keys = wallet->listPublicKeys(); + if (keys.count() == 0) { + QJsonObject result{ { "status", "fail" },{ "message", "Uninitialized Wallet." } }; + return emit buyResult(result); + } + QString key = keys[0]; + ledger->transferHfcToNode(key, nodeID, amount, optionalMessage); +} + +void QmlCommerce::transferHfcToUsername(const QString& username, const int& amount, const QString& optionalMessage) { + auto ledger = DependencyManager::get(); + auto wallet = DependencyManager::get(); + QStringList keys = wallet->listPublicKeys(); + if (keys.count() == 0) { + QJsonObject result{ { "status", "fail" },{ "message", "Uninitialized Wallet." } }; + return emit buyResult(result); + } + QString key = keys[0]; + ledger->transferHfcToUsername(key, username, amount, optionalMessage); +} diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index c04ede86c4..b261ee6de6 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -45,6 +45,9 @@ signals: void updateCertificateStatus(const QString& certID, uint certStatus); + void transferHfcToNodeResult(QJsonObject result); + void transferHfcToUsernameResult(QJsonObject result); + protected: Q_INVOKABLE void getWalletStatus(); @@ -65,6 +68,9 @@ protected: Q_INVOKABLE void account(); Q_INVOKABLE void certificateInfo(const QString& certificateId); + + Q_INVOKABLE void transferHfcToNode(const QString& nodeID, const int& amount, const QString& optionalMessage); + Q_INVOKABLE void transferHfcToUsername(const QString& username, const int& amount, const QString& optionalMessage); }; #endif // hifi_QmlCommerce_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index e36b84ac96..e50eb06355 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -411,6 +411,11 @@ int WindowScriptingInterface::getY() { } void WindowScriptingInterface::copyToClipboard(const QString& text) { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyToClipboard", Q_ARG(QString, text)); + return; + } + qDebug() << "Copying"; QApplication::clipboard()->setText(text); } diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 1118e21c91..2ead4558fe 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -13,8 +13,6 @@ #include "AnimationLogging.h" #include "AnimUtil.h" -bool AnimClip::usePreAndPostPoseFromAnim = true; - AnimClip::AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag) : AnimNode(AnimNode::Type::Clip, id), _startFrame(startFrame), @@ -138,14 +136,8 @@ void AnimClip::copyFromNetworkAnim() { if (skeletonJoint >= 0 && skeletonJoint < skeletonJointCount) { AnimPose preRot, postRot; - if (usePreAndPostPoseFromAnim) { - preRot = animSkeleton.getPreRotationPose(animJoint); - postRot = animSkeleton.getPostRotationPose(animJoint); - } else { - // In order to support Blender, which does not have preRotation FBX support, we use the models defaultPose as the reference frame for the animations. - preRot = AnimPose(glm::vec3(1.0f), _skeleton->getRelativeBindPose(skeletonJoint).rot(), glm::vec3()); - postRot = AnimPose::identity; - } + preRot = animSkeleton.getPreRotationPose(animJoint); + postRot = animSkeleton.getPostRotationPose(animJoint); // cancel out scale preRot.scale() = glm::vec3(1.0f); diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index c7e7ebf3ee..717972ca26 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -25,8 +25,6 @@ class AnimClip : public AnimNode { public: friend class AnimTests; - static bool usePreAndPostPoseFromAnim; - AnimClip(const QString& id, const QString& url, float startFrame, float endFrame, float timeScale, bool loopFlag, bool mirrorFlag); virtual ~AnimClip() override; diff --git a/libraries/animation/src/AnimInverseKinematics.cpp b/libraries/animation/src/AnimInverseKinematics.cpp index c8388c3b12..e9f9f8818f 100644 --- a/libraries/animation/src/AnimInverseKinematics.cpp +++ b/libraries/animation/src/AnimInverseKinematics.cpp @@ -1255,7 +1255,7 @@ void AnimInverseKinematics::initConstraints() { // / / // O--O O--O - loadDefaultPoses(_skeleton->getRelativeBindPoses()); + loadDefaultPoses(_skeleton->getRelativeDefaultPoses()); int numJoints = (int)_defaultRelativePoses.size(); diff --git a/libraries/animation/src/AnimManipulator.cpp b/libraries/animation/src/AnimManipulator.cpp index 070949ab3b..46b3cf1c28 100644 --- a/libraries/animation/src/AnimManipulator.cpp +++ b/libraries/animation/src/AnimManipulator.cpp @@ -33,7 +33,7 @@ AnimManipulator::~AnimManipulator() { } const AnimPoseVec& AnimManipulator::evaluate(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut) { - return overlay(animVars, context, dt, triggersOut, _skeleton->getRelativeBindPoses()); + return overlay(animVars, context, dt, triggersOut, _skeleton->getRelativeDefaultPoses()); } const AnimPoseVec& AnimManipulator::overlay(const AnimVariantMap& animVars, const AnimContext& context, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index 804ffb0583..2bce16c5ca 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -56,14 +56,6 @@ int AnimSkeleton::getChainDepth(int jointIndex) const { } } -const AnimPose& AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { - return _absoluteBindPoses[jointIndex]; -} - -const AnimPose& AnimSkeleton::getRelativeBindPose(int jointIndex) const { - return _relativeBindPoses[jointIndex]; -} - const AnimPose& AnimSkeleton::getRelativeDefaultPose(int jointIndex) const { return _relativeDefaultPoses[jointIndex]; } @@ -164,8 +156,6 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) _joints = joints; _jointsSize = (int)joints.size(); // build a cache of bind poses - _absoluteBindPoses.reserve(_jointsSize); - _relativeBindPoses.reserve(_jointsSize); // build a chache of default poses _absoluteDefaultPoses.reserve(_jointsSize); @@ -192,28 +182,6 @@ void AnimSkeleton::buildSkeletonFromJoints(const std::vector& joints) } else { _absoluteDefaultPoses.push_back(relDefaultPose); } - - // build relative and absolute bind poses - if (_joints[i].bindTransformFoundInCluster) { - // Use the FBXJoint::bindTransform, which is absolute model coordinates - // i.e. not relative to it's parent. - AnimPose absoluteBindPose(_joints[i].bindTransform); - _absoluteBindPoses.push_back(absoluteBindPose); - if (parentIndex >= 0) { - AnimPose inverseParentAbsoluteBindPose = _absoluteBindPoses[parentIndex].inverse(); - _relativeBindPoses.push_back(inverseParentAbsoluteBindPose * absoluteBindPose); - } else { - _relativeBindPoses.push_back(absoluteBindPose); - } - } else { - // use default transform instead - _relativeBindPoses.push_back(relDefaultPose); - if (parentIndex >= 0) { - _absoluteBindPoses.push_back(_absoluteBindPoses[parentIndex] * relDefaultPose); - } else { - _absoluteBindPoses.push_back(relDefaultPose); - } - } } for (int i = 0; i < _jointsSize; i++) { @@ -251,8 +219,6 @@ void AnimSkeleton::dump(bool verbose) const { qCDebug(animation) << " {"; qCDebug(animation) << " index =" << i; qCDebug(animation) << " name =" << getJointName(i); - qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); - qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); if (verbose) { @@ -287,8 +253,6 @@ void AnimSkeleton::dump(const AnimPoseVec& poses) const { qCDebug(animation) << " {"; qCDebug(animation) << " index =" << i; qCDebug(animation) << " name =" << getJointName(i); - qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); - qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); qCDebug(animation) << " absDefaultPose =" << getAbsoluteDefaultPose(i); qCDebug(animation) << " relDefaultPose =" << getRelativeDefaultPose(i); qCDebug(animation) << " pose =" << poses[i]; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index 99c9a148f7..664358f414 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -30,13 +30,6 @@ public: int getNumJoints() const; int getChainDepth(int jointIndex) const; - // absolute pose, not relative to parent - const AnimPose& getAbsoluteBindPose(int jointIndex) const; - - // relative to parent pose - const AnimPose& getRelativeBindPose(int jointIndex) const; - const AnimPoseVec& getRelativeBindPoses() const { return _relativeBindPoses; } - // the default poses are the orientations of the joints on frame 0. const AnimPose& getRelativeDefaultPose(int jointIndex) const; const AnimPoseVec& getRelativeDefaultPoses() const { return _relativeDefaultPoses; } @@ -72,8 +65,6 @@ protected: std::vector _joints; int _jointsSize { 0 }; - AnimPoseVec _absoluteBindPoses; - AnimPoseVec _relativeBindPoses; AnimPoseVec _relativeDefaultPoses; AnimPoseVec _absoluteDefaultPoses; AnimPoseVec _relativePreRotationPoses; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index daa1c2618f..1c9d9a3447 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -179,7 +179,7 @@ void Rig::restoreRoleAnimation(const QString& role) { } else { qCWarning(animation) << "Rig::restoreRoleAnimation could not find role " << role; } - + auto statesIter = _roleAnimStates.find(role); if (statesIter != _roleAnimStates.end()) { _roleAnimStates.erase(statesIter); @@ -1050,52 +1050,6 @@ void Rig::updateAnimations(float deltaTime, const glm::mat4& rootTransform, cons } } -void Rig::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, - const QVector& freeLineage, glm::mat4 rootTransform) { - ASSERT(false); -} - -bool Rig::restoreJointPosition(int jointIndex, float fraction, float priority, const QVector& freeLineage) { - ASSERT(false); - return false; -} - -float Rig::getLimbLength(int jointIndex, const QVector& freeLineage, - const glm::vec3 scale, const QVector& fbxJoints) const { - ASSERT(false); - return 1.0f; -} - -glm::quat Rig::setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority) { - ASSERT(false); - return glm::quat(); -} - -glm::vec3 Rig::getJointDefaultTranslationInConstrainedFrame(int jointIndex) { - ASSERT(false); - return glm::vec3(); -} - -glm::quat Rig::setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, float priority, float mix) { - ASSERT(false); - return glm::quat(); -} - -bool Rig::getJointRotationInConstrainedFrame(int jointIndex, glm::quat& quatOut) const { - ASSERT(false); - return false; -} - -void Rig::clearJointStatePriorities() { - ASSERT(false); -} - -glm::quat Rig::getJointDefaultRotationInParentFrame(int jointIndex) { - ASSERT(false); - return glm::quat(); -} - - void Rig::updateFromEyeParameters(const EyeParameters& params) { updateEyeJoint(params.leftEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); updateEyeJoint(params.rightEyeJointIndex, params.modelTranslation, params.modelRotation, params.eyeLookAt, params.eyeSaccade); diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 6968d7c3af..87277af754 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -172,36 +172,6 @@ public: // Regardless of who started the animations or how many, update the joints. void updateAnimations(float deltaTime, const glm::mat4& rootTransform, const glm::mat4& rigToWorldTransform); - // legacy - void inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority, - const QVector& freeLineage, glm::mat4 rootTransform); - - // legacy - bool restoreJointPosition(int jointIndex, float fraction, float priority, const QVector& freeLineage); - - // legacy - float getLimbLength(int jointIndex, const QVector& freeLineage, - const glm::vec3 scale, const QVector& fbxJoints) const; - - // legacy - glm::quat setJointRotationInBindFrame(int jointIndex, const glm::quat& rotation, float priority); - - // legacy - glm::vec3 getJointDefaultTranslationInConstrainedFrame(int jointIndex); - - // legacy - glm::quat setJointRotationInConstrainedFrame(int jointIndex, glm::quat targetRotation, - float priority, float mix = 1.0f); - - // legacy - bool getJointRotationInConstrainedFrame(int jointIndex, glm::quat& rotOut) const; - - // legacy - glm::quat getJointDefaultRotationInParentFrame(int jointIndex); - - // legacy - void clearJointStatePriorities(); - void updateFromControllerParameters(const ControllerParameters& params, float dt); void updateFromEyeParameters(const EyeParameters& params); @@ -345,7 +315,7 @@ protected: float firstFrame; float lastFrame; }; - + struct RoleAnimState { RoleAnimState() {} RoleAnimState(const QString& roleId, const QString& urlIn, float fpsIn, bool loopIn, float firstFrameIn, float lastFrameIn) : diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp index a13ef07cd4..1112ccde60 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.cpp @@ -237,30 +237,14 @@ bool SkeletonModel::getRightHandPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getRightHandJointIndex(), position); } -bool SkeletonModel::restoreLeftHandPosition(float fraction, float priority) { - return restoreJointPosition(getLeftHandJointIndex(), fraction, priority); -} - bool SkeletonModel::getLeftShoulderPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getLastFreeJointIndex(getLeftHandJointIndex()), position); } -float SkeletonModel::getLeftArmLength() const { - return getLimbLength(getLeftHandJointIndex()); -} - -bool SkeletonModel::restoreRightHandPosition(float fraction, float priority) { - return restoreJointPosition(getRightHandJointIndex(), fraction, priority); -} - bool SkeletonModel::getRightShoulderPosition(glm::vec3& position) const { return getJointPositionInWorldFrame(getLastFreeJointIndex(getRightHandJointIndex()), position); } -float SkeletonModel::getRightArmLength() const { - return getLimbLength(getRightHandJointIndex()); -} - bool SkeletonModel::getHeadPosition(glm::vec3& headPosition) const { return isActive() && getJointPositionInWorldFrame(getFBXGeometry().headJointIndex, headPosition); } diff --git a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h index f911ad0c5a..d82fce7412 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h +++ b/libraries/avatars-renderer/src/avatars-renderer/SkeletonModel.h @@ -57,11 +57,6 @@ public: /// \return true whether or not the position was found bool getRightHandPosition(glm::vec3& position) const; - /// Restores some fraction of the default position of the left hand. - /// \param fraction the fraction of the default position to restore - /// \return whether or not the left hand joint was found - bool restoreLeftHandPosition(float fraction = 1.0f, float priority = 1.0f); - /// Gets the position of the left shoulder. /// \return whether or not the left shoulder joint was found bool getLeftShoulderPosition(glm::vec3& position) const; @@ -69,18 +64,10 @@ public: /// Returns the extended length from the left hand to its last free ancestor. float getLeftArmLength() const; - /// Restores some fraction of the default position of the right hand. - /// \param fraction the fraction of the default position to restore - /// \return whether or not the right hand joint was found - bool restoreRightHandPosition(float fraction = 1.0f, float priority = 1.0f); - /// Gets the position of the right shoulder. /// \return whether or not the right shoulder joint was found bool getRightShoulderPosition(glm::vec3& position) const; - /// Returns the extended length from the right hand to its first free ancestor. - float getRightArmLength() const; - /// Returns the position of the head joint. /// \return whether or not the head was found bool getHeadPosition(glm::vec3& headPosition) const; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index d7dd2837cb..00cc760658 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -358,7 +358,7 @@ class AvatarData : public QObject, public SpatiallyNestable { 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) + Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName WRITE setSessionDisplayName NOTIFY sessionDisplayNameChanged) Q_PROPERTY(bool lookAtSnappingEnabled MEMBER _lookAtSnappingEnabled NOTIFY lookAtSnappingChanged) Q_PROPERTY(QString skeletonModelURL READ getSkeletonModelURLFromScript WRITE setSkeletonModelURLFromScript) Q_PROPERTY(QVector attachmentData READ getAttachmentData WRITE setAttachmentData) @@ -685,6 +685,7 @@ public: signals: void displayNameChanged(); + void sessionDisplayNameChanged(); void lookAtSnappingChanged(bool enabled); void sessionUUIDChanged(); diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index 64cd534c8b..1fd001e536 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -15,6 +15,7 @@ ScriptAvatarData::ScriptAvatarData(AvatarSharedPointer avatarData) : _avatarData(avatarData) { QObject::connect(avatarData.get(), &AvatarData::displayNameChanged, this, &ScriptAvatarData::displayNameChanged); + QObject::connect(avatarData.get(), &AvatarData::sessionDisplayNameChanged, this, &ScriptAvatarData::sessionDisplayNameChanged); QObject::connect(avatarData.get(), &AvatarData::lookAtSnappingChanged, this, &ScriptAvatarData::lookAtSnappingChanged); } diff --git a/libraries/avatars/src/ScriptAvatarData.h b/libraries/avatars/src/ScriptAvatarData.h index 46dfb5325f..68074b838d 100644 --- a/libraries/avatars/src/ScriptAvatarData.h +++ b/libraries/avatars/src/ScriptAvatarData.h @@ -44,7 +44,7 @@ class ScriptAvatarData : public QObject { // Q_PROPERTY(QUuid sessionUUID READ getSessionUUID) Q_PROPERTY(QString displayName READ getDisplayName NOTIFY displayNameChanged) - Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName) + Q_PROPERTY(QString sessionDisplayName READ getSessionDisplayName NOTIFY sessionDisplayNameChanged) Q_PROPERTY(bool isReplicated READ getIsReplicated) Q_PROPERTY(bool lookAtSnappingEnabled READ getLookAtSnappingEnabled NOTIFY lookAtSnappingChanged) @@ -131,6 +131,7 @@ public: signals: void displayNameChanged(); + void sessionDisplayNameChanged(); void lookAtSnappingChanged(bool enabled); public slots: diff --git a/libraries/fbx/src/FBXReader_Mesh.cpp b/libraries/fbx/src/FBXReader_Mesh.cpp index 4337eca605..b0e2faa600 100644 --- a/libraries/fbx/src/FBXReader_Mesh.cpp +++ b/libraries/fbx/src/FBXReader_Mesh.cpp @@ -603,7 +603,7 @@ void FBXReader::buildModelMesh(FBXMesh& extractedMesh, const QString& url) { if (!blendShape.normals.empty() && blendShape.tangents.empty()) { // Fill with a dummy value to force tangents to be present if there are normals blendShape.tangents.reserve(blendShape.normals.size()); - std::fill_n(std::back_inserter(fbxMesh.tangents), blendShape.normals.size(), Vectors::UNIT_X); + std::fill_n(std::back_inserter(blendShape.tangents), blendShape.normals.size(), Vectors::UNIT_X); } } diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp index b0a16bce11..72ebe2eb58 100644 --- a/libraries/networking/src/AddressManager.cpp +++ b/libraries/networking/src/AddressManager.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -757,11 +758,21 @@ void AddressManager::refreshPreviousLookup() { } void AddressManager::copyAddress() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyAddress"); + return; + } + // assume that the address is being copied because the user wants a shareable address QApplication::clipboard()->setText(currentShareableAddress().toString()); } void AddressManager::copyPath() { + if (QThread::currentThread() != qApp->thread()) { + QMetaObject::invokeMethod(this, "copyPath"); + return; + } + QApplication::clipboard()->setText(currentPath()); } diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 1b9ed07d6a..45c77a606d 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1267,25 +1267,6 @@ void Model::updateClusterMatrices() { } } -void Model::inverseKinematics(int endIndex, glm::vec3 targetPosition, const glm::quat& targetRotation, float priority) { - const FBXGeometry& geometry = getFBXGeometry(); - const QVector& freeLineage = geometry.joints.at(endIndex).freeLineage; - glm::mat4 parentTransform = glm::scale(_scale) * glm::translate(_offset); - _rig.inverseKinematics(endIndex, targetPosition, targetRotation, priority, freeLineage, parentTransform); -} - -bool Model::restoreJointPosition(int jointIndex, float fraction, float priority) { - const FBXGeometry& geometry = getFBXGeometry(); - const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - return _rig.restoreJointPosition(jointIndex, fraction, priority, freeLineage); -} - -float Model::getLimbLength(int jointIndex) const { - const FBXGeometry& geometry = getFBXGeometry(); - const QVector& freeLineage = geometry.joints.at(jointIndex).freeLineage; - return _rig.getLimbLength(jointIndex, freeLineage, _scale, geometry.joints); -} - bool Model::maybeStartBlender() { if (isLoaded()) { const FBXGeometry& fbxGeometry = getFBXGeometry(); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 3252fa4257..560aa82f0c 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -199,8 +199,6 @@ public: /// Returns the index of the parent of the indexed joint, or -1 if not found. int getParentJointIndex(int jointIndex) const; - void inverseKinematics(int jointIndex, glm::vec3 position, const glm::quat& rotation, float priority); - /// Returns the extents of the model in its bind pose. Extents getBindExtents() const; @@ -376,16 +374,6 @@ protected: void computeMeshPartLocalBounds(); virtual void updateRig(float deltaTime, glm::mat4 parentTransform); - /// Restores the indexed joint to its default position. - /// \param fraction the fraction of the default position to apply (i.e., 0.25f to slerp one fourth of the way to - /// the original position - /// \return true if the joint was found - bool restoreJointPosition(int jointIndex, float fraction = 1.0f, float priority = 0.0f); - - /// Computes and returns the extended length of the limb terminating at the specified joint and starting at the joint's - /// first free ancestor. - float getLimbLength(int jointIndex) const; - /// Allow sub classes to force invalidating the bboxes void invalidCalculatedMeshBoxes() { _triangleSetsValid = false; diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 905a224ef4..221f5013bf 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -666,7 +666,6 @@ void OffscreenUi::createDesktop(const QUrl& url) { Q_UNUSED(context) _desktop = static_cast(newObject); getSurfaceContext()->setContextProperty("desktop", _desktop); - _toolWindow = _desktop->findChild("ToolWindow"); _vrMenu = new VrMenu(this); for (const auto& menuInitializer : _queuedMenuInitializers) { @@ -686,10 +685,6 @@ QObject* OffscreenUi::getRootMenu() { return getRootItem()->findChild("rootMenu"); } -QQuickItem* OffscreenUi::getToolWindow() { - return _toolWindow; -} - void OffscreenUi::unfocusWindows() { bool invokeResult = QMetaObject::invokeMethod(_desktop, "unfocusWindows"); Q_ASSERT(invokeResult); diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 76e4261cd3..ab3a209820 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -80,7 +80,6 @@ public: QObject* getFlags(); Q_INVOKABLE bool isPointOnDesktopWindow(QVariant point); QQuickItem* getDesktop(); - QQuickItem* getToolWindow(); QObject* getRootMenu(); enum Icon { ICON_NONE = 0, @@ -261,7 +260,6 @@ private: ModalDialogListener* assetDialogAsync(const QVariantMap& properties); QQuickItem* _desktop { nullptr }; - QQuickItem* _toolWindow { nullptr }; QList _modalDialogListeners; std::unordered_map _pressedKeys; VrMenu* _vrMenu { nullptr }; diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 90b91c5ec2..1209d39dcf 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -32,7 +32,6 @@ static const char* const EVENT_BRIDGE_PROPERTY = "eventBridge"; static const char* const WIDTH_PROPERTY = "width"; static const char* const HEIGHT_PROPERTY = "height"; static const char* const VISIBILE_PROPERTY = "visible"; -static const char* const TOOLWINDOW_PROPERTY = "toolWindow"; static const uvec2 MAX_QML_WINDOW_SIZE { 1280, 720 }; static const uvec2 MIN_QML_WINDOW_SIZE { 120, 80 }; @@ -53,9 +52,6 @@ QVariantMap QmlWindowClass::parseArguments(QScriptContext* context) { if (context->argument(3).isNumber()) { properties[HEIGHT_PROPERTY] = context->argument(3).toInt32(); } - if (context->argument(4).isBool()) { - properties[TOOLWINDOW_PROPERTY] = context->argument(4).toBool(); - } } else { properties = context->argument(0).toVariant().toMap(); } @@ -96,52 +92,37 @@ void QmlWindowClass::initQml(QVariantMap properties) { auto offscreenUi = DependencyManager::get(); _source = properties[SOURCE_PROPERTY].toString(); -#if QML_TOOL_WINDOW - _toolWindow = properties.contains(TOOLWINDOW_PROPERTY) && properties[TOOLWINDOW_PROPERTY].toBool(); - if (_toolWindow) { - // Build the event bridge and wrapper on the main thread - _qmlWindow = offscreenUi->getToolWindow(); - properties[EVENT_BRIDGE_PROPERTY] = QVariant::fromValue(this); - QVariant newTabVar; - bool invokeResult = QMetaObject::invokeMethod(_qmlWindow, "addWebTab", Qt::DirectConnection, - Q_RETURN_ARG(QVariant, newTabVar), - Q_ARG(QVariant, QVariant::fromValue(properties))); - Q_ASSERT(invokeResult); - } else { -#endif - // Build the event bridge and wrapper on the main thread - offscreenUi->loadInNewContext(qmlSource(), [&](QQmlContext* context, QObject* object) { - _qmlWindow = object; - context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); - context->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); - context->engine()->setObjectOwnership(object, QQmlEngine::CppOwnership); - if (properties.contains(TITLE_PROPERTY)) { - object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); - } - if (properties.contains(HEIGHT_PROPERTY) && properties.contains(WIDTH_PROPERTY)) { - uvec2 requestedSize { properties[WIDTH_PROPERTY].toUInt(), properties[HEIGHT_PROPERTY].toUInt() }; - requestedSize = glm::clamp(requestedSize, MIN_QML_WINDOW_SIZE, MAX_QML_WINDOW_SIZE); - asQuickItem()->setSize(QSize(requestedSize.x, requestedSize.y)); - } + // Build the event bridge and wrapper on the main thread + offscreenUi->loadInNewContext(qmlSource(), [&](QQmlContext* context, QObject* object) { + _qmlWindow = object; + context->setContextProperty(EVENT_BRIDGE_PROPERTY, this); + context->engine()->setObjectOwnership(this, QQmlEngine::CppOwnership); + context->engine()->setObjectOwnership(object, QQmlEngine::CppOwnership); + if (properties.contains(TITLE_PROPERTY)) { + object->setProperty(TITLE_PROPERTY, properties[TITLE_PROPERTY].toString()); + } + if (properties.contains(HEIGHT_PROPERTY) && properties.contains(WIDTH_PROPERTY)) { + uvec2 requestedSize { properties[WIDTH_PROPERTY].toUInt(), properties[HEIGHT_PROPERTY].toUInt() }; + requestedSize = glm::clamp(requestedSize, MIN_QML_WINDOW_SIZE, MAX_QML_WINDOW_SIZE); + asQuickItem()->setSize(QSize(requestedSize.x, requestedSize.y)); + } - bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool(); - object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); - object->setProperty(SOURCE_PROPERTY, _source); + bool visible = !properties.contains(VISIBILE_PROPERTY) || properties[VISIBILE_PROPERTY].toBool(); + object->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); + object->setProperty(SOURCE_PROPERTY, _source); - const QMetaObject *metaObject = _qmlWindow->metaObject(); - // Forward messages received from QML on to the script - connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); - connect(_qmlWindow, SIGNAL(visibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); + const QMetaObject *metaObject = _qmlWindow->metaObject(); + // Forward messages received from QML on to the script + connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); + connect(_qmlWindow, SIGNAL(visibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); + + if (metaObject->indexOfSignal("resized") >= 0) + connect(_qmlWindow, SIGNAL(resized(QSizeF)), this, SIGNAL(resized(QSizeF)), Qt::QueuedConnection); + if (metaObject->indexOfSignal("moved") >= 0) + connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); + connect(_qmlWindow, SIGNAL(windowClosed()), this, SLOT(hasClosed()), Qt::QueuedConnection); + }); - if (metaObject->indexOfSignal("resized") >= 0) - connect(_qmlWindow, SIGNAL(resized(QSizeF)), this, SIGNAL(resized(QSizeF)), Qt::QueuedConnection); - if (metaObject->indexOfSignal("moved") >= 0) - connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); - connect(_qmlWindow, SIGNAL(windowClosed()), this, SLOT(hasClosed()), Qt::QueuedConnection); - }); -#if QML_TOOL_WINDOW - } -#endif Q_ASSERT(_qmlWindow); Q_ASSERT(dynamic_cast(_qmlWindow.data())); } @@ -215,11 +196,6 @@ QmlWindowClass::~QmlWindowClass() { } QQuickItem* QmlWindowClass::asQuickItem() const { -#if QML_TOOL_WINDOW - if (_toolWindow) { - return DependencyManager::get()->getToolWindow(); - } -#endif return _qmlWindow.isNull() ? nullptr : dynamic_cast(_qmlWindow.data()); } @@ -230,14 +206,6 @@ void QmlWindowClass::setVisible(bool visible) { } QQuickItem* targetWindow = asQuickItem(); -#if QML_TOOL_WINDOW - if (_toolWindow) { - // For tool window tabs we special case visibility as a function call on the tab parent - // The tool window itself has special logic based on whether any tabs are visible - QMetaObject::invokeMethod(targetWindow, "showTabForUrl", Qt::QueuedConnection, Q_ARG(QVariant, _source), Q_ARG(QVariant, visible)); - return; - } -#endif targetWindow->setProperty(OFFSCREEN_VISIBILITY_PROPERTY, visible); } @@ -253,12 +221,6 @@ bool QmlWindowClass::isVisible() { return false; } -#if QML_TOOL_WINDOW - if (_toolWindow) { - return dynamic_cast(_qmlWindow.data())->isEnabled(); - } -#endif - return asQuickItem()->isVisible(); } @@ -343,17 +305,6 @@ void QmlWindowClass::close() { return; } -#if QML_TOOL_WINDOW - if (_toolWindow) { - auto offscreenUi = DependencyManager::get(); - auto toolWindow = offscreenUi->getToolWindow(); - auto invokeResult = QMetaObject::invokeMethod(toolWindow, "removeTabForUrl", Qt::DirectConnection, - Q_ARG(QVariant, _source)); - Q_ASSERT(invokeResult); - return; - } -#endif - if (_qmlWindow) { _qmlWindow->deleteLater(); } diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index e01bc8f14b..f274501d35 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -19,8 +19,6 @@ class QScriptEngine; class QScriptContext; -#define QML_TOOL_WINDOW 0 - // FIXME refactor this class to be a QQuickItem derived type and eliminate the needless wrapping class QmlWindowClass : public QObject { Q_OBJECT @@ -87,12 +85,6 @@ protected: virtual QString qmlSource() const { return "QmlWindow.qml"; } -#if QML_TOOL_WINDOW - // FIXME needs to be initialized in the ctor once we have support - // for tool window panes in QML - bool _toolWindow { false }; -#endif - QPointer _qmlWindow; QString _source; diff --git a/scripts/developer/tests/playaPerformanceTest.js b/scripts/developer/tests/playaPerformanceTest.js index 4c8a728a15..6250fe1175 100644 --- a/scripts/developer/tests/playaPerformanceTest.js +++ b/scripts/developer/tests/playaPerformanceTest.js @@ -4,7 +4,6 @@ qmlWindow = new OverlayWindow({ source: qml, height: 320, width: 640, - toolWindow: false, visible: true }); diff --git a/scripts/developer/tests/qmlTest.js b/scripts/developer/tests/qmlTest.js index 0eaabac6d1..7d248a9cc7 100644 --- a/scripts/developer/tests/qmlTest.js +++ b/scripts/developer/tests/qmlTest.js @@ -4,7 +4,6 @@ qmlWindow = new OverlayWindow({ source: "qrc:///qml/OverlayWindowTest.qml", height: 240, width: 320, - toolWindow: false, visible: true }); diff --git a/scripts/developer/tests/textureStress.js b/scripts/developer/tests/textureStress.js index 1e3cf9a367..98af4b19b7 100644 --- a/scripts/developer/tests/textureStress.js +++ b/scripts/developer/tests/textureStress.js @@ -15,7 +15,6 @@ qmlWindow = new OverlayWindow({ source: qml, height: 240, width: 320, - toolWindow: false, visible: true }); diff --git a/scripts/developer/tests/toolWindowStressTest.js b/scripts/developer/tests/toolWindowStressTest.js deleted file mode 100644 index 44b059ebda..0000000000 --- a/scripts/developer/tests/toolWindowStressTest.js +++ /dev/null @@ -1,31 +0,0 @@ -var TOGGLE_RATE = 1000; -var RUNTIME = 60 * 1000; - -var webView; -var running = true; - -function toggleWindow() { - if (!running) { - return; - } - - if (webView) { - webView.close(); - webView = null; - } else { - webView = new OverlayWebWindow({ - title: 'Entity Properties', - source: "http://www.google.com", - toolWindow: true - }); - webView.setVisible(true); - } - Script.setTimeout(toggleWindow, TOGGLE_RATE) -} - -toggleWindow(); -print("Creating window?") - -Script.setTimeout(function(){ - print("Shutting down") -}, RUNTIME) diff --git a/scripts/system/assets/images/run.svg b/scripts/system/assets/images/run.svg new file mode 100644 index 0000000000..0957166346 --- /dev/null +++ b/scripts/system/assets/images/run.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index b3e3134380..0826325a57 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -15,6 +15,7 @@ (function () { // BEGIN LOCAL_SCOPE Script.include("/~/system/libraries/accountUtils.js"); + var request = Script.require('request').request; var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace"; @@ -51,6 +52,481 @@ tablet.sendToQml(message); } + //*********************************************** + // + // BEGIN Connection logic + // + //*********************************************** + // Function Names: + // - requestJSON + // - getAvailableConnections + // - getInfoAboutUser + // - getConnectionData + // + // Description: + // - Update all the usernames that I am entitled to see, using my login but not dependent on canKick. + var METAVERSE_BASE = Account.metaverseServerURL; + 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 getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successful. (Logs otherwise) + 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) { + callback(connectionsData.users); + }); + } + function getInfoAboutUser(specificUsername, callback) { + url = METAVERSE_BASE + '/api/v1/users?filter=connections' + requestJSON(url, function (connectionsData) { + for (user in connectionsData.users) { + if (connectionsData.users[user].username === specificUsername) { + callback(connectionsData.users[user]); + return; + } + } + callback(false); + }); + } + function getConnectionData(specificUsername, domain) { + 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.images.thumbnail, + placeName: (user.location.root || user.location.domain || {}).name || '' + }; + } + if (specificUsername) { + getInfoAboutUser(specificUsername, function (user) { + if (user) { + updateUser(frob(user)); + } else { + print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!'); + } + }); + } else { + getAvailableConnections(domain, function (users) { + if (domain) { + users.forEach(function (user) { + updateUser(frob(user)); + }); + } else { + sendToQml({ method: 'updateConnections', connections: users.map(frob) }); + } + }); + } + } + //*********************************************** + // + // END Connection logic + // + //*********************************************** + + //*********************************************** + // + // BEGIN Avatar Selector logic + // + //*********************************************** + var UNSELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-idle.png") + }; + var SELECTED_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-selected.png") + }; + var HOVER_TEXTURES = { + "idle-D": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png"), + "idle-E": Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx/Avatar-Overlay-v1.fbm/avatar-overlay-hover.png") + }; + + var UNSELECTED_COLOR = { red: 0x1F, green: 0xC6, blue: 0xA6 }; + var SELECTED_COLOR = { red: 0xF3, green: 0x91, blue: 0x29 }; + var HOVER_COLOR = { red: 0xD0, green: 0xD0, blue: 0xD0 }; + var conserveResources = true; + + var overlays = {}; // Keeps track of all our extended overlay data objects, keyed by target identifier. + + function ExtendedOverlay(key, type, properties, selected, hasModel) { // A wrapper around overlays to store the key it is associated with. + overlays[key] = this; + if (hasModel) { + var modelKey = key + "-m"; + this.model = new ExtendedOverlay(modelKey, "model", { + url: Script.resolvePath("./assets/models/Avatar-Overlay-v1.fbx"), + textures: textures(selected), + ignoreRayIntersection: true + }, false, false); + } else { + this.model = undefined; + } + this.key = key; + this.selected = selected || false; // not undefined + this.hovering = false; + this.activeOverlay = Overlays.addOverlay(type, properties); // We could use different overlays for (un)selected... + } + // Instance methods: + ExtendedOverlay.prototype.deleteOverlay = function () { // remove display and data of this overlay + Overlays.deleteOverlay(this.activeOverlay); + delete overlays[this.key]; + }; + + ExtendedOverlay.prototype.editOverlay = function (properties) { // change display of this overlay + Overlays.editOverlay(this.activeOverlay, properties); + }; + + function color(selected, hovering) { + var base = hovering ? HOVER_COLOR : selected ? SELECTED_COLOR : UNSELECTED_COLOR; + function scale(component) { + var delta = 0xFF - component; + return component; + } + return { red: scale(base.red), green: scale(base.green), blue: scale(base.blue) }; + } + + function textures(selected, hovering) { + return hovering ? HOVER_TEXTURES : selected ? SELECTED_TEXTURES : UNSELECTED_TEXTURES; + } + // so we don't have to traverse the overlays to get the last one + var lastHoveringId = 0; + ExtendedOverlay.prototype.hover = function (hovering) { + this.hovering = hovering; + if (this.key === lastHoveringId) { + if (hovering) { + return; + } + lastHoveringId = 0; + } + this.editOverlay({ color: color(this.selected, hovering) }); + if (this.model) { + this.model.editOverlay({ textures: textures(this.selected, hovering) }); + } + if (hovering) { + // un-hover the last hovering overlay + if (lastHoveringId && lastHoveringId !== this.key) { + ExtendedOverlay.get(lastHoveringId).hover(false); + } + lastHoveringId = this.key; + } + }; + ExtendedOverlay.prototype.select = function (selected) { + if (this.selected === selected) { + return; + } + + this.editOverlay({ color: color(selected, this.hovering) }); + if (this.model) { + this.model.editOverlay({ textures: textures(selected) }); + } + this.selected = selected; + }; + // Class methods: + var selectedIds = []; + ExtendedOverlay.isSelected = function (id) { + return -1 !== selectedIds.indexOf(id); + }; + ExtendedOverlay.get = function (key) { // answer the extended overlay data object associated with the given avatar identifier + return overlays[key]; + }; + ExtendedOverlay.some = function (iterator) { // Bails early as soon as iterator returns truthy. + var key; + for (key in overlays) { + if (iterator(ExtendedOverlay.get(key))) { + return; + } + } + }; + ExtendedOverlay.unHover = function () { // calls hover(false) on lastHoveringId (if any) + if (lastHoveringId) { + ExtendedOverlay.get(lastHoveringId).hover(false); + } + }; + + // hit(overlay) on the one overlay intersected by pickRay, if any. + // noHit() if no ExtendedOverlay was intersected (helps with hover) + ExtendedOverlay.applyPickRay = function (pickRay, hit, noHit) { + var pickedOverlay = Overlays.findRayIntersection(pickRay); // Depends on nearer coverOverlays to extend closer to us than farther ones. + if (!pickedOverlay.intersects) { + if (noHit) { + return noHit(); + } + return; + } + ExtendedOverlay.some(function (overlay) { // See if pickedOverlay is one of ours. + if ((overlay.activeOverlay) === pickedOverlay.overlayID) { + hit(overlay); + return true; + } + }); + }; + + function HighlightedEntity(id, entityProperties) { + this.id = id; + this.overlay = Overlays.addOverlay('cube', { + position: entityProperties.position, + rotation: entityProperties.rotation, + dimensions: entityProperties.dimensions, + solid: false, + color: { + red: 0xF3, + green: 0x91, + blue: 0x29 + }, + ignoreRayIntersection: true, + drawInFront: false // Arguable. For now, let's not distract with mysterious wires around the scene. + }); + HighlightedEntity.overlays.push(this); + } + HighlightedEntity.overlays = []; + HighlightedEntity.clearOverlays = function clearHighlightedEntities() { + HighlightedEntity.overlays.forEach(function (highlighted) { + Overlays.deleteOverlay(highlighted.overlay); + }); + HighlightedEntity.overlays = []; + }; + HighlightedEntity.updateOverlays = function updateHighlightedEntities() { + HighlightedEntity.overlays.forEach(function (highlighted) { + var properties = Entities.getEntityProperties(highlighted.id, ['position', 'rotation', 'dimensions']); + Overlays.editOverlay(highlighted.overlay, { + position: properties.position, + rotation: properties.rotation, + dimensions: properties.dimensions + }); + }); + }; + + + function addAvatarNode(id) { + var selected = ExtendedOverlay.isSelected(id); + return new ExtendedOverlay(id, "sphere", { + drawInFront: true, + solid: true, + alpha: 0.8, + color: color(selected, false), + ignoreRayIntersection: false + }, selected, !conserveResources); + } + + var pingPong = true; + function updateOverlays() { + var eye = Camera.position; + AvatarList.getAvatarIdentifiers().forEach(function (id) { + if (!id) { + return; // don't update ourself, or avatars we're not interested in + } + var avatar = AvatarList.getAvatar(id); + if (!avatar) { + return; // will be deleted below if there had been an overlay. + } + var overlay = ExtendedOverlay.get(id); + if (!overlay) { // For now, we're treating this as a temporary loss, as from the personal space bubble. Add it back. + overlay = addAvatarNode(id); + } + var target = avatar.position; + var distance = Vec3.distance(target, eye); + var offset = 0.2; + var diff = Vec3.subtract(target, eye); // get diff between target and eye (a vector pointing to the eye from avatar position) + 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; + } + + // move a bit in front, towards the camera + target = Vec3.subtract(target, Vec3.multiply(Vec3.normalize(diff), offset)); + + // now bump it up a bit + target.y = target.y + offset; + + overlay.ping = pingPong; + overlay.editOverlay({ + color: color(ExtendedOverlay.isSelected(id), overlay.hovering), + position: target, + dimensions: 0.032 * distance + }); + if (overlay.model) { + overlay.model.ping = pingPong; + overlay.model.editOverlay({ + position: target, + scale: 0.2 * distance, // constant apparent size + rotation: Camera.orientation + }); + } + }); + pingPong = !pingPong; + ExtendedOverlay.some(function (overlay) { // Remove any that weren't updated. (User is gone.) + if (overlay.ping === pingPong) { + overlay.deleteOverlay(); + } + }); + // We could re-populateNearbyUserList if anything added or removed, but not for now. + HighlightedEntity.updateOverlays(); + } + function removeOverlays() { + selectedIds = []; + lastHoveringId = 0; + HighlightedEntity.clearOverlays(); + ExtendedOverlay.some(function (overlay) { + overlay.deleteOverlay(); + }); + } + + // + // Clicks. + // + function usernameFromIDReply(id, username, machineFingerprint, isAdmin) { + if (selectedIds[0] === id) { + var message = { + method: 'updateSelectedRecipientUsername', + userName: username === "" ? "unknown username" : username + }; + sendToQml(message); + } + } + function handleClick(pickRay) { + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + var nextSelectedStatus = !overlay.selected; + var avatarId = overlay.key; + selectedIds = nextSelectedStatus ? [avatarId] : []; + if (nextSelectedStatus) { + Users.requestUsernameFromID(avatarId); + } + var message = { + method: 'selectRecipient', + id: [avatarId], + isSelected: nextSelectedStatus, + displayName: '"' + AvatarList.getAvatar(avatarId).sessionDisplayName + '"', + userName: '' + }; + sendToQml(message); + + ExtendedOverlay.some(function (overlay) { + var id = overlay.key; + var selected = ExtendedOverlay.isSelected(id); + overlay.select(selected); + }); + + HighlightedEntity.clearOverlays(); + if (selectedIds.length) { + Entities.findEntitiesInFrustum(Camera.frustum).forEach(function (id) { + // Because lastEditedBy is per session, the vast majority of entities won't match, + // so it would probably be worth reducing marshalling costs by asking for just we need. + // However, providing property name(s) is advisory and some additional properties are + // included anyway. As it turns out, asking for 'lastEditedBy' gives 'position', 'rotation', + // and 'dimensions', too, so we might as well make use of them instead of making a second + // getEntityProperties call. + // It would be nice if we could harden this against future changes by specifying all + // and only these four in an array, but see + // https://highfidelity.fogbugz.com/f/cases/2728/Entities-getEntityProperties-id-lastEditedBy-name-lastEditedBy-doesn-t-work + var properties = Entities.getEntityProperties(id, 'lastEditedBy'); + if (ExtendedOverlay.isSelected(properties.lastEditedBy)) { + new HighlightedEntity(id, properties); + } + }); + } + return true; + }); + } + function handleMouseEvent(mousePressEvent) { // handleClick if we get one. + if (!mousePressEvent.isLeftButton) { + return; + } + handleClick(Camera.computePickRay(mousePressEvent.x, mousePressEvent.y)); + } + function handleMouseMove(pickRay) { // given the pickRay, just do the hover logic + ExtendedOverlay.applyPickRay(pickRay, function (overlay) { + overlay.hover(true); + }, function () { + ExtendedOverlay.unHover(); + }); + } + + // handy global to keep track of which hand is the mouse (if any) + var currentHandPressed = 0; + var TRIGGER_CLICK_THRESHOLD = 0.85; + var TRIGGER_PRESS_THRESHOLD = 0.05; + + function handleMouseMoveEvent(event) { // find out which overlay (if any) is over the mouse position + var pickRay; + if (HMD.active) { + if (currentHandPressed !== 0) { + pickRay = controllerComputePickRay(currentHandPressed); + } else { + // nothing should hover, so + ExtendedOverlay.unHover(); + return; + } + } else { + pickRay = Camera.computePickRay(event.x, event.y); + } + handleMouseMove(pickRay); + } + function handleTriggerPressed(hand, value) { + // The idea is if you press one trigger, it is the one + // we will consider the mouse. Even if the other is pressed, + // we ignore it until this one is no longer pressed. + var isPressed = value > TRIGGER_PRESS_THRESHOLD; + if (currentHandPressed === 0) { + currentHandPressed = isPressed ? hand : 0; + return; + } + if (currentHandPressed === hand) { + currentHandPressed = isPressed ? hand : 0; + return; + } + // otherwise, the other hand is still triggered + // so do nothing. + } + + // We get mouseMoveEvents from the handControllers, via handControllerPointer. + // But we don't get mousePressEvents. + var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); + var triggerPressMapping = Controller.newMapping(Script.resolvePath('') + '-press'); + function controllerComputePickRay(hand) { + var controllerPose = getControllerWorldLocation(hand, true); + if (controllerPose.valid) { + return { origin: controllerPose.position, direction: Quat.getUp(controllerPose.orientation) }; + } + } + function makeClickHandler(hand) { + return function (clicked) { + if (clicked > TRIGGER_CLICK_THRESHOLD) { + var pickRay = controllerComputePickRay(hand); + handleClick(pickRay); + } + }; + } + function makePressHandler(hand) { + return function (value) { + handleTriggerPressed(hand, value); + }; + } + triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); + triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); + triggerPressMapping.from(Controller.Standard.RT).peek().to(makePressHandler(Controller.Standard.RightHand)); + triggerPressMapping.from(Controller.Standard.LT).peek().to(makePressHandler(Controller.Standard.LeftHand)); + //*********************************************** + // + // END Avatar Selector logic + // + //*********************************************** + // Function Name: fromQml() // // Description: @@ -110,6 +586,17 @@ case 'goToMarketplaceItemPage': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); break; + case 'refreshConnections': + print('Refreshing Connections...'); + getConnectionData(false); + break; + case 'enable_ChooseRecipientNearbyMode': + Script.update.connect(updateOverlays); + break; + case 'disable_ChooseRecipientNearbyMode': + Script.update.disconnect(updateOverlays); + removeOverlays(); + break; default: print('Unrecognized message from QML:', JSON.stringify(message)); } @@ -154,6 +641,17 @@ if (button) { button.editProperties({ isActive: onWalletScreen }); } + + if (onWalletScreen) { + isWired = true; + Users.usernameFromIDReply.connect(usernameFromIDReply); + Controller.mousePressEvent.connect(handleMouseEvent); + Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + triggerMapping.enable(); + triggerPressMapping.enable(); + } else { + off(); + } } // @@ -176,6 +674,19 @@ tablet.screenChanged.connect(onTabletScreenChanged); } } + var isWired = false; + function off() { + if (isWired) { // It is not ok to disconnect these twice, hence guard. + Users.usernameFromIDReply.disconnect(usernameFromIDReply); + Script.update.disconnect(updateOverlays); + Controller.mousePressEvent.disconnect(handleMouseEvent); + Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); + isWired = false; + } + triggerMapping.disable(); // It's ok if we disable twice. + triggerPressMapping.disable(); // see above + removeOverlays(); + } function shutdown() { button.clicked.disconnect(onButtonClicked); tablet.removeButton(button); @@ -185,6 +696,7 @@ tablet.gotoHomeScreen(); } } + off(); } // diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 8819960354..ee3a9ce7ec 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -575,6 +575,11 @@ var selectionDisplay = null; // for gridTool.js to ignore method: 'purchases_showMyItems' }); break; + case 'refreshConnections': + case 'enable_ChooseRecipientNearbyMode': + case 'disable_ChooseRecipientNearbyMode': + // NOP + break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message)); } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index ed7059f9f3..1b93bdde32 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -685,7 +685,6 @@ function startup() { }); button.clicked.connect(onTabletButtonClicked); tablet.screenChanged.connect(onTabletScreenChanged); - Users.usernameFromIDReply.connect(usernameFromIDReply); Window.domainChanged.connect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); @@ -708,6 +707,7 @@ function off() { Controller.mousePressEvent.disconnect(handleMouseEvent); Controller.mouseMoveEvent.disconnect(handleMouseMoveEvent); tablet.tabletShownChanged.disconnect(tabletVisibilityChanged); + Users.usernameFromIDReply.disconnect(usernameFromIDReply); isWired = false; ContextOverlay.enabled = true } @@ -744,6 +744,7 @@ function onTabletButtonClicked() { Script.update.connect(updateOverlays); Controller.mousePressEvent.connect(handleMouseEvent); Controller.mouseMoveEvent.connect(handleMouseMoveEvent); + Users.usernameFromIDReply.connect(usernameFromIDReply); triggerMapping.enable(); triggerPressMapping.enable(); audioTimer = createAudioInterval(conserveResources ? AUDIO_LEVEL_CONSERVED_UPDATE_INTERVAL_MS : AUDIO_LEVEL_UPDATE_INTERVAL_MS); @@ -890,7 +891,6 @@ function shutdown() { button.clicked.disconnect(onTabletButtonClicked); tablet.removeButton(button); tablet.screenChanged.disconnect(onTabletScreenChanged); - Users.usernameFromIDReply.disconnect(usernameFromIDReply); Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL); Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL); Messages.subscribe(CHANNEL); diff --git a/scripts/system/run.js b/scripts/system/run.js new file mode 100644 index 0000000000..054cca6d9c --- /dev/null +++ b/scripts/system/run.js @@ -0,0 +1,39 @@ +"use strict"; + +/* global Script, Tablet, MyAvatar */ + +(function() { // BEGIN LOCAL_SCOPE + var WALK_SPEED = 2.6; + var RUN_SPEED = 4.5; + var MIDDLE_SPEED = (WALK_SPEED + RUN_SPEED) / 2.0; + + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + var button = tablet.addButton({ + icon: Script.resolvePath("assets/images/run.svg"), + text: "RUN", + sortOrder: 15 + }); + + function onClicked() { + if (MyAvatar.walkSpeed < MIDDLE_SPEED) { + button.editProperties({isActive: true}); + MyAvatar.walkSpeed = RUN_SPEED; + } else { + button.editProperties({isActive: false}); + MyAvatar.walkSpeed = WALK_SPEED; + } + } + + function cleanup() { + button.clicked.disconnect(onClicked); + tablet.removeButton(button); + } + + button.clicked.connect(onClicked); + if (MyAvatar.walkSpeed < MIDDLE_SPEED) { + button.editProperties({isActive: false}); + } else { + button.editProperties({isActive: true}); + } + +}());