diff --git a/interface/resources/qml/Stats.qml b/interface/resources/qml/Stats.qml index 96e267f67f..8737e422cb 100644 --- a/interface/resources/qml/Stats.qml +++ b/interface/resources/qml/Stats.qml @@ -55,7 +55,11 @@ Item { text: "Avatars: " + root.avatarCount } StatText { - text: "Frame Rate: " + root.framerate.toFixed(2); + text: "Game Rate: " + root.gameLoopRate + } + StatText { + visible: root.expanded + text: root.gameUpdateStats } StatText { text: "Render Rate: " + root.renderrate.toFixed(2); @@ -64,21 +68,17 @@ Item { text: "Present Rate: " + root.presentrate.toFixed(2); } StatText { - text: "Present New Rate: " + root.presentnewrate.toFixed(2); + visible: root.expanded + text: " Present New Rate: " + root.presentnewrate.toFixed(2); } StatText { - text: "Present Drop Rate: " + root.presentdroprate.toFixed(2); + visible: root.expanded + text: " Present Drop Rate: " + root.presentdroprate.toFixed(2); } StatText { text: "Stutter Rate: " + root.stutterrate.toFixed(3); visible: root.stutterrate != -1; } - StatText { - text: "Simrate: " + root.simrate - } - StatText { - text: "Avatar Simrate: " + root.avatarSimrate - } StatText { text: "Missed Frame Count: " + root.appdropped; visible: root.appdropped > 0; @@ -261,9 +261,6 @@ Item { StatText { text: "GPU: " + root.gpuFrameTime.toFixed(1) + " ms" } - StatText { - text: "Avatar: " + root.avatarSimulationTime.toFixed(1) + " ms" - } StatText { text: "Triangles: " + root.triangles + " / Material Switches: " + root.materialSwitches diff --git a/interface/resources/qml/desktop/Desktop.qml b/interface/resources/qml/desktop/Desktop.qml index 579b4e7fd6..000e310a66 100644 --- a/interface/resources/qml/desktop/Desktop.qml +++ b/interface/resources/qml/desktop/Desktop.qml @@ -298,6 +298,23 @@ FocusScope { pinned = !pinned } + function isPointOnWindow(point) { + for (var i = 0; i < desktop.visibleChildren.length; i++) { + var child = desktop.visibleChildren[i]; + if (child.visible) { + if (child.hasOwnProperty("modality")) { + var mappedPoint = child.mapFromGlobal(point.x, point.y); + var outLine = child.frame.children[2]; + var framePoint = outLine.mapFromGlobal(point.x, point.y); + if (child.contains(mappedPoint) || outLine.contains(framePoint)) { + return true; + } + } + } + } + return false; + } + function setPinned(newPinned) { pinned = newPinned } diff --git a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml index 182a8df055..77180f7bab 100644 --- a/interface/resources/qml/hifi/commerce/checkout/Checkout.qml +++ b/interface/resources/qml/hifi/commerce/checkout/Checkout.qml @@ -26,72 +26,57 @@ Rectangle { HifiConstants { id: hifi; } id: root; + objectName: "checkout" property string activeView: "initialize"; property bool purchasesReceived: false; property bool balanceReceived: false; - property bool securityImageResultReceived: false; + property string itemName; property string itemId; - property string itemPreviewImageUrl; property string itemHref; property double balanceAfterPurchase; property bool alreadyOwned: false; - property int itemPrice: 0; + property int itemPrice; property bool itemIsJson: true; property bool shouldBuyWithControlledFailure: false; property bool debugCheckoutSuccess: false; - property bool canRezCertifiedItems: false; + property bool canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified; // Style color: hifi.colors.white; Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + notSetUpTimer.start(); + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + authSuccessStep(); + } else { + console.log("ERROR in Checkout.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; - } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "notSetUp") { - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); - } - } - - onSecurityImageResult: { - securityImageResultReceived = true; - if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); - } - } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - authSuccessStep(); + commerce.getWalletStatus(); } } onBuyResult: { if (result.status !== 'success') { - failureErrorText.text = "Here's some more info about the error:

" + (result.message); + failureErrorText.text = result.message; root.activeView = "checkoutFailure"; } else { root.activeView = "checkoutSuccess"; @@ -123,6 +108,19 @@ Rectangle { } } + onItemIdChanged: { + commerce.inventory(); + itemPreviewImage.source = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; + } + + onItemHrefChanged: { + itemIsJson = root.itemHref.indexOf('.json') !== -1; + } + + onItemPriceChanged: { + commerce.balance(); + } + Timer { id: notSetUpTimer; interval: 200; @@ -176,6 +174,13 @@ Rectangle { } } } + MouseArea { + enabled: titleBarContainer.usernameDropdownVisible; + anchors.fill: parent; + onClicked: { + titleBarContainer.usernameDropdownVisible = false; + } + } // // TITLE BAR END // @@ -190,10 +195,9 @@ Rectangle { color: hifi.colors.white; Component.onCompleted: { - securityImageResultReceived = false; purchasesReceived = false; balanceReceived = false; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } @@ -281,7 +285,6 @@ Rectangle { Image { id: itemPreviewImage; - source: root.itemPreviewImageUrl; anchors.left: parent.left; anchors.top: parent.top; anchors.bottom: parent.bottom; @@ -291,6 +294,7 @@ Rectangle { RalewaySemiBold { id: itemNameText; + text: root.itemName; // Text size size: 26; // Anchors @@ -315,19 +319,19 @@ Rectangle { anchors.top: parent.top; anchors.right: parent.right; height: 30; - width: childrenRect.width; + width: itemPriceTextLabel.width + itemPriceText.width + 20; // "HFC" balance label HiFiGlyphs { id: itemPriceTextLabel; text: hifi.glyphs.hfc; // Size - size: 36; + size: 30; // Anchors anchors.right: itemPriceText.left; anchors.rightMargin: 4; anchors.top: parent.top; - anchors.topMargin: -4; + anchors.topMargin: 0; width: paintedWidth; height: paintedHeight; // Style @@ -335,7 +339,7 @@ Rectangle { } FiraSansSemiBold { id: itemPriceText; - text: "--"; + text: root.itemPrice; // Text size size: 26; // Anchors @@ -405,7 +409,7 @@ Rectangle { verticalAlignment: Text.AlignTop; } - RalewaySemiBold { + RalewayRegular { id: buyText; // Text size size: 18; @@ -424,7 +428,7 @@ Rectangle { verticalAlignment: Text.AlignVCenter; onLinkActivated: { - sendToScript({method: 'checkout_goToPurchases', filterText: itemNameText.text}); + sendToScript({method: 'checkout_goToPurchases', filterText: root.itemName}); } } } @@ -508,7 +512,7 @@ Rectangle { RalewaySemiBold { id: completeText2; - text: "The item " + '' + itemNameText.text + '' + + text: "The item " + '' + root.itemName + '' + " has been added to your Purchases and a receipt will appear in your Wallet's transaction history."; // Text size size: 20; @@ -709,7 +713,9 @@ Rectangle { anchors.top: titleBarContainer.bottom; anchors.bottom: root.bottom; anchors.left: parent.left; + anchors.leftMargin: 16; anchors.right: parent.right; + anchors.rightMargin: 16; RalewayRegular { id: failureHeaderText; @@ -718,57 +724,65 @@ Rectangle { size: 24; // Anchors anchors.top: parent.top; - anchors.topMargin: 80; + anchors.topMargin: 40; height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; // Style color: hifi.colors.black; wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; } - RalewayRegular { - id: failureErrorText; - // Text size - size: 16; - // Anchors + Rectangle { + id: failureErrorTextContainer; anchors.top: failureHeaderText.bottom; anchors.topMargin: 35; - height: paintedHeight; anchors.left: parent.left; anchors.right: parent.right; - // Style - color: hifi.colors.black; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + height: failureErrorText.height + 30; + radius: 4; + border.width: 2; + border.color: "#F3808F"; + color: "#FFC3CD"; + + AnonymousProRegular { + id: failureErrorText; + // Text size + size: 16; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: parent.left; + anchors.leftMargin: 8; + anchors.right: parent.right; + anchors.rightMargin: 8; + height: paintedHeight; + // Style + color: hifi.colors.black; + wrapMode: Text.Wrap; + verticalAlignment: Text.AlignVCenter; + } } Item { id: backToMarketplaceButtonContainer; // Size width: root.width; - height: 130; + height: 50; // Anchors anchors.left: parent.left; anchors.bottom: parent.bottom; - anchors.bottomMargin: 8; + anchors.bottomMargin: 16; // "Back to Marketplace" button HifiControlsUit.Button { id: backToMarketplaceButton; - color: hifi.buttons.black; + color: hifi.buttons.noneBorderlessGray; colorScheme: hifi.colorSchemes.light; anchors.top: parent.top; - anchors.topMargin: 3; anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; + anchors.left: parent.left; + anchors.leftMargin: 16; + width: parent.width/2 - anchors.leftMargin*2; text: "Back to Marketplace"; onClicked: { sendToScript({method: 'checkout_continueShopping', itemId: itemId}); @@ -814,15 +828,9 @@ Rectangle { switch (message.method) { case 'updateCheckoutQML': itemId = message.params.itemId; - itemNameText.text = message.params.itemName; + itemName = message.params.itemName; root.itemPrice = message.params.itemPrice; - itemPriceText.text = root.itemPrice === 0 ? "Free" : root.itemPrice; itemHref = message.params.itemHref; - itemPreviewImageUrl = "https://hifi-metaverse.s3-us-west-1.amazonaws.com/marketplace/previews/" + itemId + "/thumbnail/hifi-mp-" + itemId + ".jpg"; - if (itemHref.indexOf('.json') === -1) { - root.itemIsJson = false; - } - root.canRezCertifiedItems = message.canRezCertifiedItems; setBuyText(); break; default: @@ -845,10 +853,10 @@ Rectangle { if (root.purchasesReceived && root.balanceReceived) { if (root.balanceAfterPurchase < 0) { if (root.alreadyOwned) { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
" + - 'View the copy you own in My Purchases'; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item again.
" + + 'View the copy you own in My Purchases
'; } else { - buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; + buyText.text = "Your Wallet does not have sufficient funds to purchase this item."; } buyTextContainer.color = "#FFC3CD"; buyTextContainer.border.color = "#F3808F"; @@ -856,8 +864,8 @@ Rectangle { buyGlyph.size = 54; } else { if (root.alreadyOwned) { - buyText.text = 'You already own this item.
Purchasing it will buy another copy.
View this item in My Purchases'; + buyText.text = 'You already own this item.
Purchasing it will buy another copy.
View this item in My Purchases
'; buyTextContainer.color = "#FFD6AD"; buyTextContainer.border.color = "#FAC07D"; buyGlyph.text = hifi.glyphs.alert; @@ -870,7 +878,7 @@ Rectangle { buyText.text = ""; } } else { - buyText.text = "This Marketplace item isn't an entity. It will not be added to your Purchases."; + buyText.text = "This free item will not be added to your Purchases. Non-entities can't yet be purchased for HFC."; buyTextContainer.color = "#FFD6AD"; buyTextContainer.border.color = "#FAC07D"; buyGlyph.text = hifi.glyphs.alert; @@ -884,12 +892,6 @@ Rectangle { } else { root.activeView = "checkoutSuccess"; } - if (!balanceReceived) { - commerce.balance(); - } - if (!purchasesReceived) { - commerce.inventory(); - } } // diff --git a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml index 0b236d4566..145b78bdc4 100644 --- a/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml +++ b/interface/resources/qml/hifi/commerce/common/CommerceLightbox.qml @@ -82,6 +82,7 @@ Rectangle { height: 140; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; } RalewayRegular { diff --git a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml index 5786350721..420b51ba15 100644 --- a/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml +++ b/interface/resources/qml/hifi/commerce/common/EmulatedMarketplaceHeader.qml @@ -27,23 +27,28 @@ Item { id: root; property string referrerURL: "https://metaverse.highfidelity.com/marketplace?"; readonly property int additionalDropdownHeight: usernameDropdown.height - myUsernameButton.anchors.bottomMargin; + property alias usernameDropdownVisible: usernameDropdown.visible; height: mainContainer.height + additionalDropdownHeight; Hifi.QmlCommerce { id: commerce; - onLoginStatusResult: { - if (!isLoggedIn) { + onWalletStatusResult: { + if (walletStatus === 0) { sendToParent({method: "needsLogIn"}); + } else if (walletStatus === 3) { + commerce.getSecurityImage(); + } else { + console.log("ERROR in EmulatedMarketplaceHeader.qml: Unknown wallet status: " + walletStatus); } } - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); + onLoginStatusResult: { + if (!isLoggedIn) { + sendToParent({method: "needsLogIn"}); } else { - // unsure how to handle a failure here. We definitely cannot proceed. + commerce.getWalletStatus(); } } @@ -56,8 +61,7 @@ Item { } Component.onCompleted: { - commerce.getLoginStatus(); - commerce.getSecurityImage(); + commerce.getWalletStatus(); } Connections { @@ -210,6 +214,7 @@ Item { anchors.bottomMargin: 16; width: height; mipmap: true; + cache: false; MouseArea { enabled: securityImage.visible; diff --git a/interface/resources/qml/hifi/commerce/common/SortableListModel.qml b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml new file mode 100644 index 0000000000..2d82e42ddb --- /dev/null +++ b/interface/resources/qml/hifi/commerce/common/SortableListModel.qml @@ -0,0 +1,68 @@ +// +// SortableListModel.qml +// qml/hifi/commerce/common +// +// SortableListModel +// +// Created by Zach Fox on 2017-09-28 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 + +ListModel { + id: root; + property string sortColumnName: ""; + property bool isSortingDescending: true; + + function swap(a, b) { + if (a < b) { + move(a, b, 1); + move (b - 1, a, 1); + } else if (a > b) { + move(b, a, 1); + move(a - 1, b, 1); + } + } + + function partition(begin, end, pivot) { + var piv = get(pivot)[sortColumnName]; + swap(pivot, end - 1); + var store = begin; + + for (var i = begin; i < end - 1; ++i) { + if (isSortingDescending) { + if (get(i)[sortColumnName] < piv) { + swap(store, i); + ++store; + } + } else { + if (get(i)[sortColumnName] > piv) { + swap(store, i); + ++store; + } + } + } + swap(end - 1, store); + + return store; + } + + function qsort(begin, end) { + if (end - 1 > begin) { + var pivot = begin + Math.floor(Math.random() * (end - begin)); + + pivot = partition(begin, end, pivot); + + qsort(begin, pivot); + qsort(pivot + 1, end); + } + } + + function quickSort() { + qsort(0, count) + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml index 65bfcfc4b3..19728daa82 100644 --- a/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml +++ b/interface/resources/qml/hifi/commerce/inspectionCertificate/InspectionCertificate.qml @@ -30,12 +30,20 @@ Rectangle { property string itemOwner: "--"; property string itemEdition: "--"; property string dateOfPurchase: ""; - property bool closeGoesToPurchases: false; + property bool isLightbox: false; // Style color: hifi.colors.faintGray; Hifi.QmlCommerce { id: commerce; - } + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } Image { anchors.fill: parent; @@ -262,7 +270,11 @@ Rectangle { height: 50; text: "close"; onClicked: { - sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + if (root.isLightbox) { + root.visible = false; + } else { + sendToScript({method: 'inspectionCertificate_closeClicked', closeGoesToPurchases: root.closeGoesToPurchases}); + } } } @@ -303,7 +315,6 @@ Rectangle { switch (message.method) { case 'inspectionCertificate_setMarketplaceId': root.marketplaceId = message.marketplaceId; - root.closeGoesToPurchases = message.closeGoesToPurchases; break; case 'inspectionCertificate_setItemInfo': root.itemName = message.itemName; diff --git a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml index b5d7efe818..2e8ad6db65 100644 --- a/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml +++ b/interface/resources/qml/hifi/commerce/purchases/FirstUseTutorial.qml @@ -13,6 +13,7 @@ import Hifi 1.0 as Hifi import QtQuick 2.5 +import QtGraphicalEffects 1.0 import QtQuick.Controls 1.4 import "../../../styles-uit" import "../../../controls-uit" as HifiControlsUit @@ -24,40 +25,133 @@ Rectangle { HifiConstants { id: hifi; } id: root; - property string activeView: "step_1"; - // Style - color: hifi.colors.baseGray; + property int activeView: 1; + + Image { + anchors.fill: parent; + source: "images/Purchase-First-Run-" + root.activeView + ".jpg"; + } + + // This object is always used in a popup. + // This MouseArea is used to prevent a user from being + // able to click on a button/mouseArea underneath the popup. + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + Item { + id: header; + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + height: childrenRect.height; + + Image { + id: marketplaceHeaderImage; + source: "../common/images/marketplaceHeaderImage.png"; + anchors.top: parent.top; + anchors.topMargin: 2; + anchors.left: parent.left; + anchors.leftMargin: 8; + width: 140; + height: 58; + fillMode: Image.PreserveAspectFit; + visible: false; + } + ColorOverlay { + anchors.fill: marketplaceHeaderImage; + source: marketplaceHeaderImage; + color: "#FFFFFF" + } + RalewayRegular { + id: introText1; + text: "INTRODUCTION TO"; + // Text size + size: 15; + // Anchors + anchors.top: marketplaceHeaderImage.bottom; + anchors.topMargin: 8; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + } + RalewayRegular { + id: introText2; + text: "My Purchases"; + // Text size + size: 28; + // Anchors + anchors.top: introText1.bottom; + anchors.left: parent.left; + anchors.leftMargin: 12; + anchors.right: parent.right; + height: paintedHeight; + // Style + color: hifi.colors.white; + } + } // // "STEP 1" START // Item { id: step_1; - visible: root.activeView === "step_1"; - anchors.top: parent.top; + visible: root.activeView === 1; + anchors.top: header.bottom; + anchors.topMargin: 100; anchors.left: parent.left; + anchors.leftMargin: 30; anchors.right: parent.right; - anchors.bottom: tutorialActionButtonsContainer.top; + anchors.bottom: parent.bottom; RalewayRegular { id: step1text; - text: "This is the first-time Purchases tutorial.

Here is some bold text " + - "inside Step 1."; + text: "The 'REZ IT' button makes your purchase appear in front of you."; // Text size - size: 24; + size: 20; // Anchors anchors.top: parent.top; - anchors.bottom: parent.bottom; anchors.left: parent.left; - anchors.leftMargin: 16; + width: 180; + height: paintedHeight; + // Style + color: hifi.colors.white; + wrapMode: Text.WordWrap; + } + + // "Next" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: step1text.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + width: 150; + height: 40; + text: "Next"; + onClicked: { + root.activeView++; + } + } + + // "SKIP" button + HifiControlsUit.Button { + color: hifi.buttons.noneBorderlessGray; + colorScheme: hifi.colorSchemes.dark; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 32; anchors.right: parent.right; anchors.rightMargin: 16; - // Style - color: hifi.colors.faintGray; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + width: 150; + height: 40; + text: "SKIP"; + onClicked: { + sendSignalToParent({method: 'tutorial_finished'}); + } } } // @@ -69,127 +163,52 @@ Rectangle { // Item { id: step_2; - visible: root.activeView === "step_2"; - anchors.top: parent.top; + visible: root.activeView === 2; + anchors.top: header.bottom; + anchors.topMargin: 45; anchors.left: parent.left; + anchors.leftMargin: 30; anchors.right: parent.right; - anchors.bottom: tutorialActionButtonsContainer.top; + anchors.bottom: parent.bottom; RalewayRegular { id: step2text; - text: "STEP TWOOO!!!"; + text: "If you rez an item twice, the first one will disappear."; // Text size - size: 24; + size: 20; // Anchors anchors.top: parent.top; - anchors.bottom: parent.bottom; anchors.left: parent.left; - anchors.leftMargin: 16; - anchors.right: parent.right; - anchors.rightMargin: 16; + width: 180; + height: paintedHeight; // Style - color: hifi.colors.faintGray; + color: hifi.colors.white; wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignHCenter; - verticalAlignment: Text.AlignVCenter; + } + + // "GOT IT" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: step2text.bottom; + anchors.topMargin: 16; + anchors.left: parent.left; + width: 150; + height: 40; + text: "GOT IT"; + onClicked: { + sendSignalToParent({method: 'tutorial_finished'}); + } } } // // "STEP 2" END // - Item { - id: tutorialActionButtonsContainer; - // Size - width: root.width; - height: 70; - // Anchors - anchors.left: parent.left; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 24; - - // "Skip" or "Back" button - HifiControlsUit.Button { - id: skipOrBackButton; - color: hifi.buttons.black; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.left: parent.left; - anchors.leftMargin: 20; - width: parent.width/2 - anchors.leftMargin*2; - text: root.activeView === "step_1" ? "Skip" : "Back"; - onClicked: { - if (root.activeView === "step_1") { - sendSignalToParent({method: 'tutorial_skipClicked'}); - } else { - root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) - 1); - } - } - } - - // "Next" or "Finish" button - HifiControlsUit.Button { - id: nextButton; - color: hifi.buttons.blue; - colorScheme: hifi.colorSchemes.dark; - anchors.top: parent.top; - anchors.topMargin: 3; - anchors.bottom: parent.bottom; - anchors.bottomMargin: 3; - anchors.right: parent.right; - anchors.rightMargin: 20; - width: parent.width/2 - anchors.rightMargin*2; - text: root.activeView === "step_2" ? "Finish" : "Next"; - onClicked: { - // If this is the final step... - if (root.activeView === "step_2") { - sendSignalToParent({method: 'tutorial_finished'}); - } else { - root.activeView = "step_" + (parseInt(root.activeView.split("_")[1]) + 1); - } - } - } - } - // // FUNCTION DEFINITIONS START // - // - // Function Name: fromScript() - // - // Relevant Variables: - // None - // - // Arguments: - // message: The message sent from the JavaScript, in this case the Marketplaces 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 'updatePurchases': - referrerURL = message.referrerURL; - break; - case 'purchases_getIsFirstUseResult': - if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { - root.activeView = "firstUseTutorial"; - } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - commerce.inventory(); - } - break; - default: - console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); - } - } signal sendSignalToParent(var message); - // // FUNCTION DEFINITIONS END // diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 1186687a82..a026a818c0 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -34,14 +34,19 @@ Item { property string itemId; property string itemPreviewImageUrl; property string itemHref; - property int ownedItemCount; + property int displayedItemCount; property int itemEdition; + property string originalStatusText; + property string originalStatusColor; + height: 110; width: parent.width; onPurchaseStatusChangedChanged: { if (root.purchaseStatusChanged === true && root.purchaseStatus === "confirmed") { + root.originalStatusText = statusText.text; + root.originalStatusColor = statusText.color; statusText.text = "CONFIRMED!"; statusText.color = hifi.colors.blueAccent; confirmedTimer.start(); @@ -53,7 +58,8 @@ Item { id: confirmedTimer; interval: 3000; onTriggered: { - root.purchaseStatus = ""; + statusText.text = root.originalStatusText; + statusText.color = root.originalStatusColor; } } @@ -174,9 +180,30 @@ Item { } Item { - id: statusContainer; + id: editionContainer; + visible: root.displayedItemCount > 1 && !statusContainer.visible; + anchors.left: itemName.left; + anchors.top: certificateContainer.bottom; + anchors.topMargin: 8; + anchors.bottom: parent.bottom; + anchors.right: buttonContainer.left; + anchors.rightMargin: 2; - visible: root.purchaseStatus || root.ownedItemCount > 1; + FiraSansRegular { + anchors.left: parent.left; + anchors.top: parent.top; + anchors.bottom: parent.bottom; + width: paintedWidth; + text: "#" + root.itemEdition; + size: 15; + color: "#cc6a6a6a"; + verticalAlignment: Text.AlignTop; + } + } + + Item { + id: statusContainer; + visible: root.purchaseStatus === "pending" || root.purchaseStatus === "invalidated"; anchors.left: itemName.left; anchors.top: certificateContainer.bottom; anchors.topMargin: 8; @@ -195,8 +222,6 @@ Item { "PENDING..." } else if (root.purchaseStatus === "invalidated") { "INVALIDATED" - } else if (root.ownedItemCount > 1) { - "(#" + root.itemEdition + ") You own " + root.ownedItemCount + " others" } else { "" } @@ -207,8 +232,6 @@ Item { hifi.colors.blueAccent } else if (root.purchaseStatus === "invalidated") { hifi.colors.redAccent - } else if (root.ownedItemCount > 1) { - hifi.colors.blueAccent } else { hifi.colors.baseGray } @@ -240,8 +263,6 @@ Item { hifi.colors.blueAccent } else if (root.purchaseStatus === "invalidated") { hifi.colors.redAccent - } else if (root.ownedItemCount > 1) { - hifi.colors.blueAccent } else { hifi.colors.baseGray } @@ -257,8 +278,6 @@ Item { sendToPurchases({method: 'showPendingLightbox'}); } else if (root.purchaseStatus === "invalidated") { sendToPurchases({method: 'showInvalidatedLightbox'}); - } else if (root.ownedItemCount > 1) { - sendToPurchases({method: 'setFilterText', filterText: root.itemName}); } } onEntered: { @@ -268,9 +287,6 @@ Item { } else if (root.purchaseStatus === "invalidated") { statusText.color = hifi.colors.redAccent; statusIcon.color = hifi.colors.redAccent; - } else if (root.ownedItemCount > 1) { - statusText.color = hifi.colors.blueHighlight; - statusIcon.color = hifi.colors.blueHighlight; } } onExited: { @@ -280,9 +296,6 @@ Item { } else if (root.purchaseStatus === "invalidated") { statusText.color = hifi.colors.redHighlight; statusIcon.color = hifi.colors.redHighlight; - } else if (root.ownedItemCount > 1) { - statusText.color = hifi.colors.blueAccent; - statusIcon.color = hifi.colors.blueAccent; } } } diff --git a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml index 1002fb881b..0bb1515b69 100644 --- a/interface/resources/qml/hifi/commerce/purchases/Purchases.qml +++ b/interface/resources/qml/hifi/commerce/purchases/Purchases.qml @@ -19,6 +19,7 @@ import "../../../controls-uit" as HifiControlsUit import "../../../controls" as HifiControls import "../wallet" as HifiWallet import "../common" as HifiCommerceCommon +import "../inspectionCertificate" as HifiInspectionCertificate // references XXX from root context @@ -31,54 +32,46 @@ Rectangle { property bool securityImageResultReceived: false; property bool purchasesReceived: false; property bool punctuationMode: false; - property bool canRezCertifiedItems: false; + property bool canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified; property bool pendingInventoryReply: true; + property bool isShowingMyItems: false; + property bool isDebuggingFirstUseTutorial: false; // Style color: hifi.colors.white; Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "notSetUp") { + root.activeView = "notSetUp"; + notSetUpTimer.start(); + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + if ((Settings.getValue("isFirstUseOfPurchases", true) || root.isDebuggingFirstUseTutorial) && root.activeView !== "firstUseTutorial") { + root.activeView = "firstUseTutorial"; + } else if (!Settings.getValue("isFirstUseOfPurchases", true) && root.activeView === "initialize") { + root.activeView = "purchasesMain"; + commerce.inventory(); + } + } else { + console.log("ERROR in Purchases.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; - } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "notSetUp") { - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); - } - } - - onSecurityImageResult: { - securityImageResultReceived = true; - if (!exists && root.activeView !== "notSetUp") { // "If security image is not set up" - root.activeView = "notSetUp"; - notSetUpTimer.start(); - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); - } - } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - sendToScript({method: 'purchases_getIsFirstUse'}); + commerce.getWalletStatus(); } } @@ -121,6 +114,19 @@ Rectangle { } } + HifiInspectionCertificate.InspectionCertificate { + id: inspectionCertificate; + z: 999; + visible: false; + anchors.fill: parent; + + Connections { + onSendToScript: { + sendToScript(message); + } + } + } + HifiCommerceCommon.CommerceLightbox { id: lightboxPopup; visible: false; @@ -165,6 +171,13 @@ Rectangle { } } } + MouseArea { + enabled: titleBarContainer.usernameDropdownVisible; + anchors.fill: parent; + onClicked: { + titleBarContainer.usernameDropdownVisible = false; + } + } // // TITLE BAR END // @@ -182,7 +195,7 @@ Rectangle { Component.onCompleted: { securityImageResultReceived = false; purchasesReceived = false; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } @@ -218,7 +231,7 @@ Rectangle { onSendSignalToParent: { if (msg.method === "authSuccess") { root.activeView = "initialize"; - sendToScript({method: 'purchases_getIsFirstUse'}); + commerce.getWalletStatus(); } else { sendToScript(msg); } @@ -228,19 +241,16 @@ Rectangle { FirstUseTutorial { id: firstUseTutorial; + z: 999; visible: root.activeView === "firstUseTutorial"; - anchors.top: titleBarContainer.bottom; - anchors.topMargin: -titleBarContainer.additionalDropdownHeight; - anchors.bottom: parent.bottom; - anchors.left: parent.left; - anchors.right: parent.right; + anchors.fill: parent; Connections { onSendSignalToParent: { switch (message.method) { case 'tutorial_skipClicked': case 'tutorial_finished': - sendToScript({method: 'purchases_setIsFirstUse'}); + Settings.setValue("isFirstUseOfPurchases", false); root.activeView = "purchasesMain"; commerce.inventory(); break; @@ -278,7 +288,7 @@ Rectangle { anchors.topMargin: 4; RalewayRegular { - id: myPurchasesText; + id: myText; anchors.top: parent.top; anchors.topMargin: 10; anchors.bottom: parent.bottom; @@ -286,7 +296,7 @@ Rectangle { anchors.left: parent.left; anchors.leftMargin: 4; width: paintedWidth; - text: "My Purchases"; + text: isShowingMyItems ? "My Items" : "My Purchases"; color: hifi.colors.baseGray; size: 28; } @@ -296,7 +306,7 @@ Rectangle { colorScheme: hifi.colorSchemes.faintGray; hasClearButton: true; hasRoundedBorder: true; - anchors.left: myPurchasesText.right; + anchors.left: myText.right; anchors.leftMargin: 16; anchors.top: parent.top; anchors.bottom: parent.bottom; @@ -331,7 +341,7 @@ Rectangle { ListModel { id: previousPurchasesModel; } - ListModel { + HifiCommerceCommon.SortableListModel { id: filteredPurchasesModel; } @@ -400,7 +410,7 @@ Rectangle { ListView { id: purchasesContentsList; - visible: purchasesModel.count !== 0; + visible: (root.isShowingMyItems && filteredPurchasesModel.count !== 0) || (!root.isShowingMyItems && filteredPurchasesModel.count !== 0); clip: true; model: filteredPurchasesModel; // Anchors @@ -417,6 +427,8 @@ Rectangle { itemHref: root_file_url; purchaseStatus: status; purchaseStatusChanged: statusChanged; + itemEdition: model.edition_number; + displayedItemCount: model.displayedItemCount; anchors.topMargin: 12; anchors.bottomMargin: 12; @@ -425,6 +437,8 @@ Rectangle { if (msg.method === 'purchases_itemInfoClicked') { sendToScript({method: 'purchases_itemInfoClicked', itemId: itemId}); } else if (msg.method === 'purchases_itemCertificateClicked') { + inspectionCertificate.visible = true; + inspectionCertificate.isLightbox = true; sendToScript(msg); } else if (msg.method === "showInvalidatedLightbox") { lightboxPopup.titleText = "Item Invalidated"; @@ -448,9 +462,55 @@ Rectangle { } } + Item { + id: noItemsAlertContainer; + visible: !purchasesContentsList.visible && root.purchasesReceived && root.isShowingMyItems && filterBar.text === ""; + anchors.top: filterBarContainer.bottom; + anchors.topMargin: 12; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + width: parent.width; + + // Explanitory text + RalewayRegular { + id: noItemsYet; + text: "You haven't submitted anything to the Marketplace yet!

Submit an item to the Marketplace to add it to My Items."; + // Text size + size: 22; + // Anchors + anchors.top: parent.top; + anchors.topMargin: 150; + anchors.left: parent.left; + anchors.leftMargin: 24; + anchors.right: parent.right; + anchors.rightMargin: 24; + height: paintedHeight; + // Style + color: hifi.colors.baseGray; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignHCenter; + } + + // "Go To Marketplace" button + HifiControlsUit.Button { + color: hifi.buttons.blue; + colorScheme: hifi.colorSchemes.dark; + anchors.top: noItemsYet.bottom; + anchors.topMargin: 20; + anchors.horizontalCenter: parent.horizontalCenter; + width: parent.width * 2 / 3; + height: 50; + text: "Visit Marketplace"; + onClicked: { + sendToScript({method: 'purchases_goToMarketplaceClicked'}); + } + } + } + Item { id: noPurchasesAlertContainer; - visible: !purchasesContentsList.visible && root.purchasesReceived; + visible: !purchasesContentsList.visible && root.purchasesReceived && !root.isShowingMyItems && filterBar.text === ""; anchors.top: filterBarContainer.bottom; anchors.topMargin: 12; anchors.left: parent.left; @@ -478,7 +538,7 @@ Rectangle { horizontalAlignment: Text.AlignHCenter; } - // "Set Up" button + // "Go To Marketplace" button HifiControlsUit.Button { color: hifi.buttons.blue; colorScheme: hifi.colorSchemes.dark; @@ -530,17 +590,43 @@ Rectangle { // FUNCTION DEFINITIONS START // + function populateDisplayedItemCounts() { + var itemCountDictionary = {}; + var currentItemId; + for (var i = 0; i < filteredPurchasesModel.count; i++) { + currentItemId = filteredPurchasesModel.get(i).id; + if (itemCountDictionary[currentItemId] === undefined) { + itemCountDictionary[currentItemId] = 1; + } else { + itemCountDictionary[currentItemId]++; + } + } + + for (var i = 0; i < filteredPurchasesModel.count; i++) { + filteredPurchasesModel.setProperty(i, "displayedItemCount", itemCountDictionary[filteredPurchasesModel.get(i).id]); + } + } + + function sortByDate() { + filteredPurchasesModel.sortColumnName = "purchase_date"; + filteredPurchasesModel.isSortingDescending = false; + filteredPurchasesModel.quickSort(); + } + function buildFilteredPurchasesModel() { filteredPurchasesModel.clear(); for (var i = 0; i < purchasesModel.count; i++) { if (purchasesModel.get(i).title.toLowerCase().indexOf(filterBar.text.toLowerCase()) !== -1) { - if (purchasesModel.get(i).status !== "confirmed") { + if (purchasesModel.get(i).status !== "confirmed" && !root.isShowingMyItems) { filteredPurchasesModel.insert(0, purchasesModel.get(i)); - } else { + } else if ((root.isShowingMyItems && purchasesModel.get(i).edition_number === -1) || !root.isShowingMyItems) { filteredPurchasesModel.append(purchasesModel.get(i)); } } } + + populateDisplayedItemCounts(); + sortByDate(); } function checkIfAnyItemStatusChanged() { @@ -581,16 +667,14 @@ Rectangle { case 'updatePurchases': referrerURL = message.referrerURL; titleBarContainer.referrerURL = message.referrerURL; - root.canRezCertifiedItems = message.canRezCertifiedItems; filterBar.text = message.filterText ? message.filterText : ""; break; - case 'purchases_getIsFirstUseResult': - if (message.isFirstUseOfPurchases && root.activeView !== "firstUseTutorial") { - root.activeView = "firstUseTutorial"; - } else if (!message.isFirstUseOfPurchases && root.activeView === "initialize") { - root.activeView = "purchasesMain"; - commerce.inventory(); - } + case 'inspectionCertificate_setMarketplaceId': + case 'inspectionCertificate_setItemInfo': + inspectionCertificate.fromScript(message); + break; + case 'purchases_showMyItems': + root.isShowingMyItems = true; break; default: console.log('Unrecognized message from marketplaces.js:', JSON.stringify(message)); diff --git a/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg new file mode 100644 index 0000000000..ce0d87363f Binary files /dev/null and b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-1.jpg differ diff --git a/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg new file mode 100644 index 0000000000..40bed974af Binary files /dev/null and b/interface/resources/qml/hifi/commerce/purchases/images/Purchase-First-Run-2.jpg differ diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml index e560384807..a75d511793 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseChange.qml @@ -90,7 +90,7 @@ Item { } else { // Error submitting new passphrase resetSubmitButton(); - passphraseSelection.setErrorText("Backend error"); + passphraseSelection.setErrorText("Current passphrase incorrect - try again"); } } else { sendSignalToWallet(msg); @@ -137,9 +137,10 @@ Item { width: 150; text: "Submit"; onClicked: { - if (passphraseSelection.validateAndSubmitPassphrase()) { - passphraseSubmitButton.text = "Submitting..."; - passphraseSubmitButton.enabled = false; + passphraseSubmitButton.text = "Submitting..."; + passphraseSubmitButton.enabled = false; + if (!passphraseSelection.validateAndSubmitPassphrase()) { + resetSubmitButton(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml index 8ab0c3af60..5bd88ba790 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseModal.qml @@ -129,6 +129,7 @@ Item { width: height; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; diff --git a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml index f0338bf1f1..7c0cecd98d 100644 --- a/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/PassphraseSelection.qml @@ -26,6 +26,7 @@ Item { id: root; property bool isChangingPassphrase: false; property bool isShowingTip: false; + property bool shouldImmediatelyFocus: true; // This object is always used in a popup. // This MouseArea is used to prevent a user from being @@ -42,8 +43,8 @@ Item { passphrasePageSecurityImage.source = "image://security/securityImage"; } - onWalletAuthenticatedStatusResult: { - sendMessageToLightbox({method: 'statusResult', status: isAuthenticated}); + onChangePassphraseStatusResult: { + sendMessageToLightbox({method: 'statusResult', status: changeSuccess}); } } @@ -53,10 +54,8 @@ Item { // TODO: Fix this unlikely bug onVisibleChanged: { if (visible) { - if (root.isChangingPassphrase) { - currentPassphraseField.focus = true; - } else { - passphraseField.focus = true; + if (root.shouldImmediatelyFocus) { + focusFirstTextField(); } sendMessageToLightbox({method: 'disableHmdPreview'}); } else { @@ -311,7 +310,7 @@ Item { passphraseFieldAgain.error = false; currentPassphraseField.error = false; setErrorText(""); - commerce.setPassphrase(passphraseField.text); + commerce.changePassphrase(currentPassphraseField.text, passphraseField.text); return true; } } @@ -327,5 +326,13 @@ Item { setErrorText(""); } + function focusFirstTextField() { + if (root.isChangingPassphrase) { + currentPassphraseField.focus = true; + } else { + passphraseField.focus = true; + } + } + signal sendMessageToLightbox(var msg); } diff --git a/interface/resources/qml/hifi/commerce/wallet/Security.qml b/interface/resources/qml/hifi/commerce/wallet/Security.qml index fcce798646..9b70bb1f71 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Security.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Security.qml @@ -280,6 +280,34 @@ Item { verticalAlignment: Text.AlignVCenter; } + Rectangle { + id: removeHmdContainer; + z: 998; + visible: false; + color: hifi.colors.blueHighlight; + anchors.fill: backupInstructionsButton; + radius: 5; + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + } + + RalewayBold { + anchors.fill: parent; + text: "INSTRUCTIONS OPEN ON DESKTOP"; + size: 15; + color: hifi.colors.white; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; + } + + Timer { + id: removeHmdContainerTimer; + interval: 5000; + onTriggered: removeHmdContainer.visible = false + } + } + HifiControlsUit.Button { id: backupInstructionsButton; text: "View Backup Instructions"; @@ -293,6 +321,9 @@ Item { onClicked: { Qt.openUrlExternally("https://www.highfidelity.com/"); + Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); + removeHmdContainer.visible = true; + removeHmdContainerTimer.start(); } } } diff --git a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml index 706684ed39..e12332cd0c 100644 --- a/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml +++ b/interface/resources/qml/hifi/commerce/wallet/SecurityImageSelection.qml @@ -65,6 +65,7 @@ Item { anchors.verticalCenter: parent.verticalCenter; fillMode: Image.PreserveAspectFit; mipmap: true; + cache: false; } } MouseArea { diff --git a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml index 4cc1f2f8ec..9056d5bed3 100644 --- a/interface/resources/qml/hifi/commerce/wallet/Wallet.qml +++ b/interface/resources/qml/hifi/commerce/wallet/Wallet.qml @@ -38,48 +38,41 @@ Rectangle { Hifi.QmlCommerce { id: commerce; + onWalletStatusResult: { + if (walletStatus === 0) { + if (root.activeView !== "needsLogIn") { + root.activeView = "needsLogIn"; + } + } else if (walletStatus === 1) { + if (root.activeView !== "walletSetup") { + root.activeView = "walletSetup"; + } + } else if (walletStatus === 2) { + if (root.activeView !== "passphraseModal") { + root.activeView = "passphraseModal"; + } + } else if (walletStatus === 3) { + root.activeView = "walletHome"; + commerce.getSecurityImage(); + } else { + console.log("ERROR in Wallet.qml: Unknown wallet status: " + walletStatus); + } + } + onLoginStatusResult: { if (!isLoggedIn && root.activeView !== "needsLogIn") { root.activeView = "needsLogIn"; } else if (isLoggedIn) { - root.activeView = "initialize"; - commerce.account(); - } - } - - onAccountResult: { - if (result.status === "success") { - commerce.getKeyFilePathIfExists(); - } else { - // unsure how to handle a failure here. We definitely cannot proceed. - } - } - - onKeyFilePathIfExistsResult: { - if (path === "" && root.activeView !== "walletSetup") { - root.activeView = "walletSetup"; - } else if (path !== "" && root.activeView === "initialize") { - commerce.getSecurityImage(); + commerce.getWalletStatus(); } } onSecurityImageResult: { - if (!exists && root.activeView !== "walletSetup") { // "If security image is not set up" - root.activeView = "walletSetup"; - } else if (exists && root.activeView === "initialize") { - commerce.getWalletAuthenticatedStatus(); + if (exists) { titleBarSecurityImage.source = ""; titleBarSecurityImage.source = "image://security/securityImage"; } } - - onWalletAuthenticatedStatusResult: { - if (!isAuthenticated && passphraseModal && root.activeView !== "passphraseModal") { - root.activeView = "passphraseModal"; - } else if (isAuthenticated) { - root.activeView = "walletHome"; - } - } } SecurityImageModel { @@ -149,6 +142,7 @@ Rectangle { anchors.bottomMargin: 6; width: height; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; @@ -179,7 +173,7 @@ Rectangle { if (msg.method === 'walletSetup_finished') { if (msg.referrer === '') { root.activeView = "initialize"; - commerce.getLoginStatus(); + commerce.getWalletStatus(); } else if (msg.referrer === 'purchases') { sendToScript({method: 'goToPurchases'}); } @@ -254,7 +248,7 @@ Rectangle { color: hifi.colors.baseGray; Component.onCompleted: { - commerce.getLoginStatus(); + commerce.getWalletStatus(); } } diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml index 7083592c1d..b94616bd7a 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletHome.qml @@ -26,6 +26,7 @@ Item { id: root; property bool historyReceived: false; + property int pendingCount: 0; Hifi.QmlCommerce { id: commerce; @@ -39,6 +40,8 @@ Item { if (result.status === 'success') { transactionHistoryModel.clear(); transactionHistoryModel.append(result.data.history); + + calculatePendingAndInvalidated(); } } } @@ -200,55 +203,74 @@ Item { model: transactionHistoryModel; delegate: Item { width: parent.width; - height: transactionText.height + 30; + height: (model.transaction_type === "pendingCount" && root.pendingCount !== 0) ? 40 : ((model.status === "confirmed" || model.status === "invalidated") ? transactionText.height + 30 : 0); - HifiControlsUit.Separator { - visible: index === 0; - colorScheme: 1; - anchors.left: parent.left; - anchors.right: parent.right; - anchors.top: parent.top; - } - - AnonymousProRegular { - id: dateText; - text: getFormattedDate(model.created_at * 1000); - // Style - size: 18; + Item { + visible: model.transaction_type === "pendingCount" && root.pendingCount !== 0; + anchors.top: parent.top; anchors.left: parent.left; - anchors.top: parent.top; - anchors.topMargin: 15; - width: 118; - height: paintedHeight; - color: hifi.colors.blueAccent; - wrapMode: Text.WordWrap; - // Alignment - horizontalAlignment: Text.AlignRight; - } + width: parent.width; + height: visible ? parent.height : 0; - AnonymousProRegular { - id: transactionText; - text: model.text; - size: 18; - anchors.top: parent.top; - anchors.topMargin: 15; - anchors.left: dateText.right; - anchors.leftMargin: 20; - anchors.right: parent.right; - height: paintedHeight; - color: hifi.colors.baseGrayHighlight; - wrapMode: Text.WordWrap; - - onLinkActivated: { - sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + AnonymousProRegular { + id: pendingCountText; + anchors.fill: parent; + text: root.pendingCount + ' Transaction' + (root.pendingCount > 1 ? 's' : '') + ' Pending'; + size: 18; + color: hifi.colors.blueAccent; + verticalAlignment: Text.AlignVCenter; + horizontalAlignment: Text.AlignHCenter; } } - HifiControlsUit.Separator { - colorScheme: 1; + Item { + visible: model.transaction_type !== "pendingCount" && (model.status === "confirmed" || model.status === "invalidated"); + anchors.top: parent.top; anchors.left: parent.left; - anchors.right: parent.right; - anchors.bottom: parent.bottom; + width: parent.width; + height: visible ? parent.height : 0; + + AnonymousProRegular { + id: dateText; + text: model.created_at ? getFormattedDate(model.created_at * 1000) : ""; + // Style + size: 18; + anchors.left: parent.left; + anchors.top: parent.top; + anchors.topMargin: 15; + width: 118; + height: paintedHeight; + color: hifi.colors.blueAccent; + wrapMode: Text.WordWrap; + // Alignment + horizontalAlignment: Text.AlignRight; + } + + AnonymousProRegular { + id: transactionText; + text: model.text ? (model.status === "invalidated" ? ("INVALIDATED: " + model.text) : model.text) : ""; + size: 18; + anchors.top: parent.top; + anchors.topMargin: 15; + anchors.left: dateText.right; + anchors.leftMargin: 20; + anchors.right: parent.right; + height: paintedHeight; + color: model.status === "invalidated" ? hifi.colors.redAccent : hifi.colors.baseGrayHighlight; + wrapMode: Text.WordWrap; + font.strikeout: model.status === "invalidated"; + + onLinkActivated: { + sendSignalToWallet({method: 'transactionHistory_linkClicked', marketplaceLink: link}); + } + } + + HifiControlsUit.Separator { + colorScheme: 1; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + } } } onAtYEndChanged: { @@ -299,6 +321,19 @@ Item { return year + '-' + month + '-' + day + '
' + drawnHour + ':' + min + amOrPm; } + + function calculatePendingAndInvalidated(startingPendingCount) { + var pendingCount = startingPendingCount ? startingPendingCount : 0; + for (var i = 0; i < transactionHistoryModel.count; i++) { + if (transactionHistoryModel.get(i).status === "pending") { + pendingCount++; + } + } + + root.pendingCount = pendingCount; + transactionHistoryModel.insert(0, {"transaction_type": "pendingCount"}); + } + // // Function Name: fromScript() // diff --git a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml index c2b20a37ef..898cdf0ef2 100644 --- a/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml +++ b/interface/resources/qml/hifi/commerce/wallet/WalletSetup.qml @@ -43,7 +43,7 @@ Item { if (!exists && root.lastPage === "step_2") { // ERROR! Invalid security image. root.activeView = "step_2"; - } else { + } else if (exists) { titleBarSecurityImage.source = ""; titleBarSecurityImage.source = "image://security/securityImage"; } @@ -116,7 +116,7 @@ Item { Image { id: titleBarSecurityImage; source: ""; - visible: !securityImageTip.visible && titleBarSecurityImage.source !== ""; + visible: !securityImageTip.visible && titleBarSecurityImage.source !== "" && root.activeView !== "step_1" && root.activeView !== "step_2"; anchors.right: parent.right; anchors.rightMargin: 6; anchors.top: parent.top; @@ -125,6 +125,7 @@ Item { anchors.bottomMargin: 6; width: height; mipmap: true; + cache: false; MouseArea { enabled: titleBarSecurityImage.visible; @@ -422,6 +423,7 @@ Item { onClicked: { root.hasShownSecurityImageTip = true; securityImageTip.visible = false; + passphraseSelection.focusFirstTextField(); } } } @@ -466,6 +468,7 @@ Item { PassphraseSelection { id: passphraseSelection; + shouldImmediatelyFocus: root.hasShownSecurityImageTip; isShowingTip: securityImageTip.visible; anchors.top: passphraseTitleHelper.bottom; anchors.topMargin: 30; @@ -680,6 +683,7 @@ Item { instructions02Container.visible = true; keysReadyPageFinishButton.visible = true; Qt.openUrlExternally("https://www.highfidelity.com/"); + Qt.openUrlExternally("file:///" + root.keyFilePath.substring(0, root.keyFilePath.lastIndexOf('/'))); } } } diff --git a/interface/resources/qml/windows/Frame.qml b/interface/resources/qml/windows/Frame.qml index 030af974f6..c3b8399e01 100644 --- a/interface/resources/qml/windows/Frame.qml +++ b/interface/resources/qml/windows/Frame.qml @@ -26,6 +26,7 @@ Item { readonly property int frameMarginRight: frame.decoration ? frame.decoration.frameMarginRight : 0 readonly property int frameMarginTop: frame.decoration ? frame.decoration.frameMarginTop : 0 readonly property int frameMarginBottom: frame.decoration ? frame.decoration.frameMarginBottom : 0 + readonly property int offsetCorrection: 20 // Frames always fill their parents, but their decorations may extend // beyond the window via negative margin sizes @@ -73,7 +74,7 @@ Item { Rectangle { id: sizeOutline x: -frameMarginLeft - y: -frameMarginTop + y: -frameMarginTop - offsetCorrection width: window ? window.width + frameMarginLeft + frameMarginRight + 2 : 0 height: window ? window.height + frameMarginTop + frameMarginBottom + 2 : 0 color: hifi.colors.baseGrayHighlight15 diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 5f5896a978..49d8b505d5 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -167,6 +167,7 @@ #include "scripting/ControllerScriptingInterface.h" #include "scripting/RatesScriptingInterface.h" #include "scripting/SelectionScriptingInterface.h" +#include "scripting/WalletScriptingInterface.h" #if defined(Q_OS_MAC) || defined(Q_OS_WIN) #include "SpeechRecognizer.h" #endif @@ -686,6 +687,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); @@ -1544,15 +1546,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo auto displayPlugin = qApp->getActiveDisplayPlugin(); - properties["fps"] = _frameCounter.rate(); - properties["target_frame_rate"] = getTargetFrameRate(); - properties["render_rate"] = displayPlugin->renderRate(); + properties["render_rate"] = _renderLoopCounter.rate(); + properties["target_render_rate"] = getTargetRenderFrameRate(); properties["present_rate"] = displayPlugin->presentRate(); properties["new_frame_present_rate"] = displayPlugin->newFramePresentRate(); properties["dropped_frame_rate"] = displayPlugin->droppedFrameRate(); properties["stutter_rate"] = displayPlugin->stutterRate(); - properties["sim_rate"] = getAverageSimsPerSecond(); - properties["avatar_sim_rate"] = getAvatarSimrate(); + properties["game_rate"] = getGameLoopRate(); properties["has_async_reprojection"] = displayPlugin->hasAsyncReprojection(); properties["hardware_stats"] = displayPlugin->getHardwareStats(); @@ -2331,6 +2331,7 @@ void Application::initializeUi() { surfaceContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance()); surfaceContext->setContextProperty("Selection", DependencyManager::get().data()); surfaceContext->setContextProperty("ContextOverlay", DependencyManager::get().data()); + surfaceContext->setContextProperty("Wallet", DependencyManager::get().data()); if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) { surfaceContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get())); @@ -2475,7 +2476,7 @@ void Application::updateCamera(RenderArgs& renderArgs) { } // Update camera position if (!isHMDMode()) { - _myCamera.update(1.0f / _frameCounter.rate()); + _myCamera.update(); } renderArgs._cameraMode = (int8_t)_myCamera.getMode(); @@ -3843,7 +3844,7 @@ void Application::idle() { if (displayPlugin) { PROFILE_COUNTER_IF_CHANGED(app, "present", float, displayPlugin->presentRate()); } - PROFILE_COUNTER_IF_CHANGED(app, "fps", float, _frameCounter.rate()); + PROFILE_COUNTER_IF_CHANGED(app, "renderLoopRate", float, _renderLoopCounter.rate()); PROFILE_COUNTER_IF_CHANGED(app, "currentDownloads", int, ResourceCache::getLoadingRequests().length()); PROFILE_COUNTER_IF_CHANGED(app, "pendingDownloads", int, ResourceCache::getPendingRequestCount()); PROFILE_COUNTER_IF_CHANGED(app, "currentProcessing", int, DependencyManager::get()->getStat("Processing").toInt()); @@ -3879,8 +3880,6 @@ void Application::idle() { Stats::getInstance()->updateStats(); - _simCounter.increment(); - // Normally we check PipelineWarnings, but since idle will often take more than 10ms we only show these idle timing // details if we're in ExtraDebugging mode. However, the ::update() and its subcomponents will show their timing // details normally. @@ -3960,6 +3959,7 @@ void Application::idle() { Menu::getInstance()->setIsOptionChecked(MenuOption::ThirdPerson, !(myAvatar->getBoomLength() <= MyAvatar::ZOOM_MIN)); cameraMenuChanged(); } + _gameLoopCounter.increment(); } ivec2 Application::getMouse() const { @@ -4646,12 +4646,7 @@ static bool domainLoadingInProgress = false; void Application::update(float deltaTime) { - PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_frameCount + 1); - - bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); - PerformanceWarning warn(showWarnings, "Application::update()"); - - updateLOD(deltaTime); + PROFILE_RANGE_EX(app, __FUNCTION__, 0xffff0000, (uint64_t)_renderFrameCount + 1); if (!_physicsEnabled) { if (!domainLoadingInProgress) { @@ -4682,6 +4677,7 @@ void Application::update(float deltaTime) { PROFILE_ASYNC_END(app, "Scene Loading", ""); } + auto myAvatar = getMyAvatar(); { PerformanceTimer perfTimer("devices"); @@ -4715,129 +4711,127 @@ void Application::update(float deltaTime) { _lastFaceTrackerUpdate = 0; } - } + auto userInputMapper = DependencyManager::get(); - auto myAvatar = getMyAvatar(); - auto userInputMapper = DependencyManager::get(); + controller::InputCalibrationData calibrationData = { + myAvatar->getSensorToWorldMatrix(), + createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), + myAvatar->getHMDSensorMatrix(), + myAvatar->getCenterEyeCalibrationMat(), + myAvatar->getHeadCalibrationMat(), + myAvatar->getSpine2CalibrationMat(), + myAvatar->getHipsCalibrationMat(), + myAvatar->getLeftFootCalibrationMat(), + myAvatar->getRightFootCalibrationMat(), + myAvatar->getRightArmCalibrationMat(), + myAvatar->getLeftArmCalibrationMat(), + myAvatar->getRightHandCalibrationMat(), + myAvatar->getLeftHandCalibrationMat() + }; - controller::InputCalibrationData calibrationData = { - myAvatar->getSensorToWorldMatrix(), - createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()), - myAvatar->getHMDSensorMatrix(), - myAvatar->getCenterEyeCalibrationMat(), - myAvatar->getHeadCalibrationMat(), - myAvatar->getSpine2CalibrationMat(), - myAvatar->getHipsCalibrationMat(), - myAvatar->getLeftFootCalibrationMat(), - myAvatar->getRightFootCalibrationMat(), - myAvatar->getRightArmCalibrationMat(), - myAvatar->getLeftArmCalibrationMat(), - myAvatar->getRightHandCalibrationMat(), - myAvatar->getLeftHandCalibrationMat() - }; - - InputPluginPointer keyboardMousePlugin; - for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { - if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { - keyboardMousePlugin = inputPlugin; - } else if (inputPlugin->isActive()) { - inputPlugin->pluginUpdate(deltaTime, calibrationData); - } - } - - userInputMapper->setInputCalibrationData(calibrationData); - userInputMapper->update(deltaTime); - - if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { - keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); - } - - // Transfer the user inputs to the driveKeys - // FIXME can we drop drive keys and just have the avatar read the action states directly? - myAvatar->clearDriveKeys(); - if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { - if (!_controllerScriptingInterface->areActionsCaptured()) { - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); - myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); - if (deltaTime > FLT_EPSILON) { - myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); - myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); - myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + InputPluginPointer keyboardMousePlugin; + for (auto inputPlugin : PluginManager::getInstance()->getInputPlugins()) { + if (inputPlugin->getName() == KeyboardMouseDevice::NAME) { + keyboardMousePlugin = inputPlugin; + } else if (inputPlugin->isActive()) { + inputPlugin->pluginUpdate(deltaTime, calibrationData); } } - myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); - } - static const std::vector avatarControllerActions = { - controller::Action::LEFT_HAND, - controller::Action::RIGHT_HAND, - controller::Action::LEFT_FOOT, - controller::Action::RIGHT_FOOT, - controller::Action::HIPS, - controller::Action::SPINE2, - controller::Action::HEAD, - controller::Action::LEFT_HAND_THUMB1, - controller::Action::LEFT_HAND_THUMB2, - controller::Action::LEFT_HAND_THUMB3, - controller::Action::LEFT_HAND_THUMB4, - controller::Action::LEFT_HAND_INDEX1, - controller::Action::LEFT_HAND_INDEX2, - controller::Action::LEFT_HAND_INDEX3, - controller::Action::LEFT_HAND_INDEX4, - controller::Action::LEFT_HAND_MIDDLE1, - controller::Action::LEFT_HAND_MIDDLE2, - controller::Action::LEFT_HAND_MIDDLE3, - controller::Action::LEFT_HAND_MIDDLE4, - controller::Action::LEFT_HAND_RING1, - controller::Action::LEFT_HAND_RING2, - controller::Action::LEFT_HAND_RING3, - controller::Action::LEFT_HAND_RING4, - controller::Action::LEFT_HAND_PINKY1, - controller::Action::LEFT_HAND_PINKY2, - controller::Action::LEFT_HAND_PINKY3, - controller::Action::LEFT_HAND_PINKY4, - controller::Action::RIGHT_HAND_THUMB1, - controller::Action::RIGHT_HAND_THUMB2, - controller::Action::RIGHT_HAND_THUMB3, - controller::Action::RIGHT_HAND_THUMB4, - controller::Action::RIGHT_HAND_INDEX1, - controller::Action::RIGHT_HAND_INDEX2, - controller::Action::RIGHT_HAND_INDEX3, - controller::Action::RIGHT_HAND_INDEX4, - controller::Action::RIGHT_HAND_MIDDLE1, - controller::Action::RIGHT_HAND_MIDDLE2, - controller::Action::RIGHT_HAND_MIDDLE3, - controller::Action::RIGHT_HAND_MIDDLE4, - controller::Action::RIGHT_HAND_RING1, - controller::Action::RIGHT_HAND_RING2, - controller::Action::RIGHT_HAND_RING3, - controller::Action::RIGHT_HAND_RING4, - controller::Action::RIGHT_HAND_PINKY1, - controller::Action::RIGHT_HAND_PINKY2, - controller::Action::RIGHT_HAND_PINKY3, - controller::Action::RIGHT_HAND_PINKY4, - controller::Action::LEFT_ARM, - controller::Action::RIGHT_ARM, - controller::Action::LEFT_SHOULDER, - controller::Action::RIGHT_SHOULDER, - controller::Action::LEFT_FORE_ARM, - controller::Action::RIGHT_FORE_ARM, - controller::Action::LEFT_LEG, - controller::Action::RIGHT_LEG, - controller::Action::LEFT_UP_LEG, - controller::Action::RIGHT_UP_LEG, - controller::Action::LEFT_TOE_BASE, - controller::Action::RIGHT_TOE_BASE - }; + userInputMapper->setInputCalibrationData(calibrationData); + userInputMapper->update(deltaTime); - // copy controller poses from userInputMapper to myAvatar. - glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()); - glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); - glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; - for (auto& action : avatarControllerActions) { - controller::Pose pose = userInputMapper->getPoseState(action); - myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + if (keyboardMousePlugin && keyboardMousePlugin->isActive()) { + keyboardMousePlugin->pluginUpdate(deltaTime, calibrationData); + } + + // Transfer the user inputs to the driveKeys + // FIXME can we drop drive keys and just have the avatar read the action states directly? + myAvatar->clearDriveKeys(); + if (_myCamera.getMode() != CAMERA_MODE_INDEPENDENT) { + if (!_controllerScriptingInterface->areActionsCaptured()) { + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Z, -1.0f * userInputMapper->getActionState(controller::Action::TRANSLATE_Z)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_Y, userInputMapper->getActionState(controller::Action::TRANSLATE_Y)); + myAvatar->setDriveKey(MyAvatar::TRANSLATE_X, userInputMapper->getActionState(controller::Action::TRANSLATE_X)); + if (deltaTime > FLT_EPSILON) { + myAvatar->setDriveKey(MyAvatar::PITCH, -1.0f * userInputMapper->getActionState(controller::Action::PITCH)); + myAvatar->setDriveKey(MyAvatar::YAW, -1.0f * userInputMapper->getActionState(controller::Action::YAW)); + myAvatar->setDriveKey(MyAvatar::STEP_YAW, -1.0f * userInputMapper->getActionState(controller::Action::STEP_YAW)); + } + } + myAvatar->setDriveKey(MyAvatar::ZOOM, userInputMapper->getActionState(controller::Action::TRANSLATE_CAMERA_Z)); + } + + static const std::vector avatarControllerActions = { + controller::Action::LEFT_HAND, + controller::Action::RIGHT_HAND, + controller::Action::LEFT_FOOT, + controller::Action::RIGHT_FOOT, + controller::Action::HIPS, + controller::Action::SPINE2, + controller::Action::HEAD, + controller::Action::LEFT_HAND_THUMB1, + controller::Action::LEFT_HAND_THUMB2, + controller::Action::LEFT_HAND_THUMB3, + controller::Action::LEFT_HAND_THUMB4, + controller::Action::LEFT_HAND_INDEX1, + controller::Action::LEFT_HAND_INDEX2, + controller::Action::LEFT_HAND_INDEX3, + controller::Action::LEFT_HAND_INDEX4, + controller::Action::LEFT_HAND_MIDDLE1, + controller::Action::LEFT_HAND_MIDDLE2, + controller::Action::LEFT_HAND_MIDDLE3, + controller::Action::LEFT_HAND_MIDDLE4, + controller::Action::LEFT_HAND_RING1, + controller::Action::LEFT_HAND_RING2, + controller::Action::LEFT_HAND_RING3, + controller::Action::LEFT_HAND_RING4, + controller::Action::LEFT_HAND_PINKY1, + controller::Action::LEFT_HAND_PINKY2, + controller::Action::LEFT_HAND_PINKY3, + controller::Action::LEFT_HAND_PINKY4, + controller::Action::RIGHT_HAND_THUMB1, + controller::Action::RIGHT_HAND_THUMB2, + controller::Action::RIGHT_HAND_THUMB3, + controller::Action::RIGHT_HAND_THUMB4, + controller::Action::RIGHT_HAND_INDEX1, + controller::Action::RIGHT_HAND_INDEX2, + controller::Action::RIGHT_HAND_INDEX3, + controller::Action::RIGHT_HAND_INDEX4, + controller::Action::RIGHT_HAND_MIDDLE1, + controller::Action::RIGHT_HAND_MIDDLE2, + controller::Action::RIGHT_HAND_MIDDLE3, + controller::Action::RIGHT_HAND_MIDDLE4, + controller::Action::RIGHT_HAND_RING1, + controller::Action::RIGHT_HAND_RING2, + controller::Action::RIGHT_HAND_RING3, + controller::Action::RIGHT_HAND_RING4, + controller::Action::RIGHT_HAND_PINKY1, + controller::Action::RIGHT_HAND_PINKY2, + controller::Action::RIGHT_HAND_PINKY3, + controller::Action::RIGHT_HAND_PINKY4, + controller::Action::LEFT_ARM, + controller::Action::RIGHT_ARM, + controller::Action::LEFT_SHOULDER, + controller::Action::RIGHT_SHOULDER, + controller::Action::LEFT_FORE_ARM, + controller::Action::RIGHT_FORE_ARM, + controller::Action::LEFT_LEG, + controller::Action::RIGHT_LEG, + controller::Action::LEFT_UP_LEG, + controller::Action::RIGHT_UP_LEG, + controller::Action::LEFT_TOE_BASE, + controller::Action::RIGHT_TOE_BASE + }; + + // copy controller poses from userInputMapper to myAvatar. + glm::mat4 myAvatarMatrix = createMatFromQuatAndPos(myAvatar->getOrientation(), myAvatar->getPosition()); + glm::mat4 worldToSensorMatrix = glm::inverse(myAvatar->getSensorToWorldMatrix()); + glm::mat4 avatarToSensorMatrix = worldToSensorMatrix * myAvatarMatrix; + for (auto& action : avatarControllerActions) { + controller::Pose pose = userInputMapper->getPoseState(action); + myAvatar->setControllerPoseInSensorFrame(action, pose.transform(avatarToSensorMatrix)); + } } updateThreads(deltaTime); // If running non-threaded, then give the threads some time to process... @@ -4845,117 +4839,123 @@ void Application::update(float deltaTime) { QSharedPointer avatarManager = DependencyManager::get(); - if (_physicsEnabled) { + { PROFILE_RANGE_EX(simulation_physics, "Physics", 0xffff0000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("physics"); + if (_physicsEnabled) { + { + PROFILE_RANGE_EX(simulation_physics, "UpdateStates", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); - { - PROFILE_RANGE_EX(simulation_physics, "UpdateStates", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("updateStates)"); + static VectorOfMotionStates motionStates; + _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); + _physicsEngine->removeObjects(motionStates); + _entitySimulation->deleteObjectsRemovedFromPhysics(); - PerformanceTimer perfTimer("updateStates)"); - static VectorOfMotionStates motionStates; - _entitySimulation->getObjectsToRemoveFromPhysics(motionStates); - _physicsEngine->removeObjects(motionStates); - _entitySimulation->deleteObjectsRemovedFromPhysics(); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToAddToPhysics(motionStates); + _physicsEngine->addObjects(motionStates); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToAddToPhysics(motionStates); - _physicsEngine->addObjects(motionStates); - - }); - getEntities()->getTree()->withReadLock([&] { - _entitySimulation->getObjectsToChange(motionStates); - VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); - _entitySimulation->setObjectsToChange(stillNeedChange); - }); - - _entitySimulation->applyDynamicChanges(); - - avatarManager->getObjectsToRemoveFromPhysics(motionStates); - _physicsEngine->removeObjects(motionStates); - avatarManager->getObjectsToAddToPhysics(motionStates); - _physicsEngine->addObjects(motionStates); - avatarManager->getObjectsToChange(motionStates); - _physicsEngine->changeObjects(motionStates); - - myAvatar->prepareForPhysicsSimulation(); - _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { - dynamic->prepareForPhysicsSimulation(); - }); - } - { - PROFILE_RANGE_EX(simulation_physics, "StepSimulation", 0xffff8000, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("stepSimulation"); - getEntities()->getTree()->withWriteLock([&] { - _physicsEngine->stepSimulation(); - }); - } - { - PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); - PerformanceTimer perfTimer("harvestChanges"); - if (_physicsEngine->hasOutgoingChanges()) { - // grab the collision events BEFORE handleOutgoingChanges() because at this point - // we have a better idea of which objects we own or should own. - auto& collisionEvents = _physicsEngine->getCollisionEvents(); - - getEntities()->getTree()->withWriteLock([&] { - PerformanceTimer perfTimer("handleOutgoingChanges"); - - const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); - _entitySimulation->handleChangedMotionStates(outgoingChanges); - avatarManager->handleChangedMotionStates(outgoingChanges); - - const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); - _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + getEntities()->getTree()->withReadLock([&] { + _entitySimulation->getObjectsToChange(motionStates); + VectorOfMotionStates stillNeedChange = _physicsEngine->changeObjects(motionStates); + _entitySimulation->setObjectsToChange(stillNeedChange); }); - if (!_aboutToQuit) { - // handleCollisionEvents() AFTER handleOutgoinChanges() - PerformanceTimer perfTimer("entities"); - avatarManager->handleCollisionEvents(collisionEvents); - // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk - // deadlock.) - _entitySimulation->handleCollisionEvents(collisionEvents); + _entitySimulation->applyDynamicChanges(); - // NOTE: the getEntities()->update() call below will wait for lock - // and will simulate entity motion (the EntityTree has been given an EntitySimulation). - getEntities()->update(true); // update the models... - } + avatarManager->getObjectsToRemoveFromPhysics(motionStates); + _physicsEngine->removeObjects(motionStates); + avatarManager->getObjectsToAddToPhysics(motionStates); + _physicsEngine->addObjects(motionStates); + avatarManager->getObjectsToChange(motionStates); + _physicsEngine->changeObjects(motionStates); - myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); - - if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { - _physicsEngine->harvestPerformanceStats(); - } - // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework - _physicsEngine->dumpStatsIfNecessary(); + myAvatar->prepareForPhysicsSimulation(); + _physicsEngine->forEachDynamic([&](EntityDynamicPointer dynamic) { + dynamic->prepareForPhysicsSimulation(); + }); } + { + PROFILE_RANGE_EX(simulation_physics, "StepSimulation", 0xffff8000, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("stepSimulation"); + getEntities()->getTree()->withWriteLock([&] { + _physicsEngine->stepSimulation(); + }); + } + { + PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("harvestChanges"); + if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + + getEntities()->getTree()->withWriteLock([&] { + PerformanceTimer perfTimer("handleOutgoingChanges"); + + const VectorOfMotionStates& outgoingChanges = _physicsEngine->getChangedMotionStates(); + _entitySimulation->handleChangedMotionStates(outgoingChanges); + avatarManager->handleChangedMotionStates(outgoingChanges); + + const VectorOfMotionStates& deactivations = _physicsEngine->getDeactivatedMotionStates(); + _entitySimulation->handleDeactivatedMotionStates(deactivations); + }); + + if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() + PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); + // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk + // deadlock.) + _entitySimulation->handleCollisionEvents(collisionEvents); + + // NOTE: the getEntities()->update() call below will wait for lock + // and will simulate entity motion (the EntityTree has been given an EntitySimulation). + getEntities()->update(true); // update the models... + } + + myAvatar->harvestResultsFromPhysicsSimulation(deltaTime); + + if (Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && + Menu::getInstance()->isOptionChecked(MenuOption::ExpandPhysicsSimulationTiming)) { + _physicsEngine->harvestPerformanceStats(); + } + // NOTE: the PhysicsEngine stats are written to stdout NOT to Qt log framework + _physicsEngine->dumpStatsIfNecessary(); + } + } + } else { + // update the rendering without any simulation + getEntities()->update(false); } - } else { - // update the rendering without any simulation - getEntities()->update(false); } // AvatarManager update { - PerformanceTimer perfTimer("AvatarManager"); - _avatarSimCounter.increment(); - { + PerformanceTimer perfTimer("otherAvatars"); PROFILE_RANGE_EX(simulation, "OtherAvatars", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); avatarManager->updateOtherAvatars(deltaTime); } - qApp->updateMyAvatarLookAtPosition(); - { PROFILE_RANGE_EX(simulation, "MyAvatar", 0xffff00ff, (uint64_t)getActiveDisplayPlugin()->presentCount()); + PerformanceTimer perfTimer("MyAvatar"); + qApp->updateMyAvatarLookAtPosition(); avatarManager->updateMyAvatar(deltaTime); } } + PerformanceTimer perfTimer("misc"); + + bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); + PerformanceWarning warn(showWarnings, "Application::update()"); + + updateLOD(deltaTime); + + // TODO: break these out into distinct perfTimers when they prove interesting { PROFILE_RANGE(app, "RayPickManager"); _rayPickManager.update(); @@ -5387,7 +5387,7 @@ bool Application::isHMDMode() const { return getActiveDisplayPlugin()->isHmd(); } -float Application::getTargetFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } +float Application::getTargetRenderFrameRate() const { return getActiveDisplayPlugin()->getTargetFrameRate(); } QRect Application::getDesirableApplicationGeometry() const { QRect applicationGeometry = getWindow()->geometry(); @@ -5867,6 +5867,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEnginePointe scriptEngine->registerGlobalObject("AvatarInputs", AvatarInputs::getInstance()); scriptEngine->registerGlobalObject("Selection", DependencyManager::get().data()); scriptEngine->registerGlobalObject("ContextOverlay", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Wallet", DependencyManager::get().data()); qScriptRegisterMetaType(scriptEngine.data(), OverlayIDtoScriptValue, OverlayIDfromScriptValue); diff --git a/interface/src/Application.h b/interface/src/Application.h index 54a8fadd28..478642e2da 100644 --- a/interface/src/Application.h +++ b/interface/src/Application.h @@ -194,10 +194,9 @@ public: Overlays& getOverlays() { return _overlays; } - - size_t getFrameCount() const { return _frameCount; } - float getFps() const { return _frameCounter.rate(); } - float getTargetFrameRate() const; // frames/second + size_t getRenderFrameCount() const { return _renderFrameCount; } + float getRenderLoopRate() const { return _renderLoopCounter.rate(); } + float getTargetRenderFrameRate() const; // frames/second float getFieldOfView() { return _fieldOfView.get(); } void setFieldOfView(float fov); @@ -268,8 +267,7 @@ public: void updateMyAvatarLookAtPosition(); - float getAvatarSimrate() const { return _avatarSimCounter.rate(); } - float getAverageSimsPerSecond() const { return _simCounter.rate(); } + float getGameLoopRate() const { return _gameLoopCounter.rate(); } void takeSnapshot(bool notify, bool includeAnimated = false, float aspectRatio = 0.0f); void takeSecondaryCameraSnapshot(); @@ -531,12 +529,11 @@ private: QUndoStack _undoStack; UndoStackScriptingInterface _undoStackScriptingInterface; - uint32_t _frameCount { 0 }; + uint32_t _renderFrameCount { 0 }; // Frame Rate Measurement - RateCounter<> _frameCounter; - RateCounter<> _avatarSimCounter; - RateCounter<> _simCounter; + RateCounter<500> _renderLoopCounter; + RateCounter<500> _gameLoopCounter; FrameTimingsScriptingInterface _frameTimingsScriptingInterface; diff --git a/interface/src/Application_render.cpp b/interface/src/Application_render.cpp index ac9aecf66c..bbcd8266d7 100644 --- a/interface/src/Application_render.cpp +++ b/interface/src/Application_render.cpp @@ -33,11 +33,11 @@ void Application::paintGL() { return; } - _frameCount++; + _renderFrameCount++; _lastTimeRendered.start(); auto lastPaintBegin = usecTimestampNow(); - PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_frameCount); + PROFILE_RANGE_EX(render, __FUNCTION__, 0xff0000ff, (uint64_t)_renderFrameCount); PerformanceTimer perfTimer("paintGL"); if (nullptr == _displayPlugin) { @@ -54,7 +54,7 @@ void Application::paintGL() { PROFILE_RANGE(render, "/pluginBeginFrameRender"); // If a display plugin loses it's underlying support, it // needs to be able to signal us to not use it - if (!displayPlugin->beginFrameRender(_frameCount)) { + if (!displayPlugin->beginFrameRender(_renderFrameCount)) { updateDisplayMode(); return; } @@ -105,7 +105,7 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/updateCompositor"); - getApplicationCompositor().setFrameInfo(_frameCount, eyeToWorld, sensorToWorld); + getApplicationCompositor().setFrameInfo(_renderFrameCount, eyeToWorld, sensorToWorld); } gpu::FramebufferPointer finalFramebuffer; @@ -141,7 +141,7 @@ void Application::paintGL() { } auto frame = _gpuContext->endFrame(); - frame->frameIndex = _frameCount; + frame->frameIndex = _renderFrameCount; frame->framebuffer = finalFramebuffer; frame->framebufferRecycler = [](const gpu::FramebufferPointer& framebuffer) { DependencyManager::get()->releaseFramebuffer(framebuffer); @@ -152,7 +152,7 @@ void Application::paintGL() { { PROFILE_RANGE(render, "/pluginOutput"); PerformanceTimer perfTimer("pluginOutput"); - _frameCounter.increment(); + _renderLoopCounter.increment(); displayPlugin->submitFrame(frame); } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index a6d77c8d03..5d8393ba7a 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -252,11 +252,12 @@ void AvatarManager::updateOtherAvatars(float deltaTime) { qApp->getMain3DScene()->enqueueTransaction(transaction); } - _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; _numAvatarsUpdated = numAvatarsUpdated; _numAvatarsNotUpdated = numAVatarsNotUpdated; simulateAvatarFades(deltaTime); + + _avatarSimulationTime = (float)(usecTimestampNow() - startTime) / (float)USECS_PER_MSEC; } void AvatarManager::postUpdate(float deltaTime, const render::ScenePointer& scene) { diff --git a/interface/src/commerce/Ledger.cpp b/interface/src/commerce/Ledger.cpp index d8860192ad..a68a6fe929 100644 --- a/interface/src/commerce/Ledger.cpp +++ b/interface/src/commerce/Ledger.cpp @@ -164,11 +164,7 @@ void Ledger::historySuccess(QNetworkReply& reply) { // 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 -#ifdef Q_OS_MAC - QDateTime createdAt = QDateTime::fromTime_t(valueObject["created_at"].toInt(), Qt::UTC); -#else QDateTime createdAt = QDateTime::fromSecsSinceEpoch(valueObject["created_at"].toInt(), Qt::UTC); -#endif QDateTime localCreatedAt = createdAt.toTimeZone(QTimeZone::systemTimeZone()); valueObject["text"] = QString("%1 sent %2 %3 with message \"%4\""). arg(from, to, coloredQuantityAndAssetTitle, valueObject["message"].toString()); diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 93de2ef566..dbd84594bc 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -15,6 +15,7 @@ #include "Ledger.h" #include "Wallet.h" #include +#include "scripting/WalletScriptingInterface.h" HIFI_QML_DEF(QmlCommerce) @@ -28,6 +29,37 @@ QmlCommerce::QmlCommerce(QQuickItem* parent) : OffscreenQmlDialog(parent) { connect(ledger.data(), &Ledger::historyResult, this, &QmlCommerce::historyResult); connect(wallet.data(), &Wallet::keyFilePathIfExistsResult, this, &QmlCommerce::keyFilePathIfExistsResult); connect(ledger.data(), &Ledger::accountResult, this, &QmlCommerce::accountResult); + connect(ledger.data(), &Ledger::accountResult, this, [&]() { + auto wallet = DependencyManager::get(); + auto walletScriptingInterface = DependencyManager::get(); + uint status; + + if (wallet->getKeyFilePath() == "" || !wallet->getSecurityImage()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_SET_UP; + } else if (!wallet->walletIsAuthenticatedWithPassphrase()) { + status = (uint)WalletStatus::WALLET_STATUS_NOT_AUTHENTICATED; + } else { + status = (uint)WalletStatus::WALLET_STATUS_READY; + } + + walletScriptingInterface->setWalletStatus(status); + emit walletStatusResult(status); + }); +} + +void QmlCommerce::getWalletStatus() { + auto walletScriptingInterface = DependencyManager::get(); + uint status; + + if (DependencyManager::get()->isLoggedIn()) { + // This will set account info for the wallet, allowing us to decrypt and display the security image. + account(); + } else { + status = (uint)WalletStatus::WALLET_STATUS_NOT_LOGGED_IN; + emit walletStatusResult(status); + walletScriptingInterface->setWalletStatus(status); + return; + } } void QmlCommerce::getLoginStatus() { @@ -36,7 +68,7 @@ void QmlCommerce::getLoginStatus() { void QmlCommerce::getKeyFilePathIfExists() { auto wallet = DependencyManager::get(); - wallet->sendKeyFilePathIfExists(); + emit keyFilePathIfExistsResult(wallet->getKeyFilePath()); } void QmlCommerce::getWalletAuthenticatedStatus() { @@ -85,13 +117,18 @@ void QmlCommerce::history() { ledger->history(wallet->listPublicKeys()); } +void QmlCommerce::changePassphrase(const QString& oldPassphrase, const QString& newPassphrase) { + auto wallet = DependencyManager::get(); + if ((wallet->getPassphrase()->isEmpty() || wallet->getPassphrase() == oldPassphrase) && !newPassphrase.isEmpty()) { + emit changePassphraseStatusResult(wallet->changePassphrase(newPassphrase)); + } else { + emit changePassphraseStatusResult(false); + } +} + void QmlCommerce::setPassphrase(const QString& passphrase) { auto wallet = DependencyManager::get(); - if(wallet->getPassphrase() && !wallet->getPassphrase()->isEmpty() && !passphrase.isEmpty()) { - wallet->changePassphrase(passphrase); - } else { - wallet->setPassphrase(passphrase); - } + wallet->setPassphrase(passphrase); getWalletAuthenticatedStatus(); } diff --git a/interface/src/commerce/QmlCommerce.h b/interface/src/commerce/QmlCommerce.h index 42f44a3a85..8e6af6da65 100644 --- a/interface/src/commerce/QmlCommerce.h +++ b/interface/src/commerce/QmlCommerce.h @@ -27,11 +27,21 @@ class QmlCommerce : public OffscreenQmlDialog { public: QmlCommerce(QQuickItem* parent = nullptr); + enum WalletStatus { + WALLET_STATUS_NOT_LOGGED_IN = 0, + WALLET_STATUS_NOT_SET_UP, + WALLET_STATUS_NOT_AUTHENTICATED, + WALLET_STATUS_READY + }; + signals: + void walletStatusResult(uint walletStatus); + void loginStatusResult(bool isLoggedIn); void keyFilePathIfExistsResult(const QString& path); void securityImageResult(bool exists); void walletAuthenticatedStatusResult(bool isAuthenticated); + void changePassphraseStatusResult(bool changeSuccess); void buyResult(QJsonObject result); // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and @@ -42,6 +52,8 @@ signals: void accountResult(QJsonObject result); protected: + Q_INVOKABLE void getWalletStatus(); + Q_INVOKABLE void getLoginStatus(); Q_INVOKABLE void getKeyFilePathIfExists(); Q_INVOKABLE void getSecurityImage(); @@ -49,6 +61,7 @@ protected: Q_INVOKABLE void chooseSecurityImage(const QString& imageFile); Q_INVOKABLE void setPassphrase(const QString& passphrase); + Q_INVOKABLE void changePassphrase(const QString& oldPassphrase, const QString& newPassphrase); Q_INVOKABLE void buy(const QString& assetId, int cost, const bool controlledFailure = false); Q_INVOKABLE void balance(); diff --git a/interface/src/commerce/Wallet.cpp b/interface/src/commerce/Wallet.cpp index e1e2849a04..cc2039da48 100644 --- a/interface/src/commerce/Wallet.cpp +++ b/interface/src/commerce/Wallet.cpp @@ -284,7 +284,7 @@ Wallet::Wallet() { auto nodeList = DependencyManager::get(); auto& packetReceiver = nodeList->getPacketReceiver(); - packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "verifyOwnerChallenge"); + packetReceiver.registerListener(PacketType::ChallengeOwnership, this, "handleChallengeOwnershipPacket"); } Wallet::~Wallet() { @@ -468,7 +468,7 @@ bool Wallet::generateKeyPair() { // TODO: redo this soon -- need error checking and so on writeSecurityImage(_securityImage, keyFilePath()); - sendKeyFilePathIfExists(); + emit keyFilePathIfExistsResult(getKeyFilePath()); QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last(); QString key = keyPair.first->toBase64(); _publicKeys.push_back(key); @@ -559,14 +559,14 @@ void Wallet::chooseSecurityImage(const QString& filename) { emit securityImageResult(success); } -void Wallet::getSecurityImage() { +bool Wallet::getSecurityImage() { unsigned char* data; int dataLen; // if already decrypted, don't do it again if (_securityImage) { emit securityImageResult(true); - return; + return true; } bool success = false; @@ -585,14 +585,15 @@ void Wallet::getSecurityImage() { success = true; } emit securityImageResult(success); + return success; } -void Wallet::sendKeyFilePathIfExists() { +QString Wallet::getKeyFilePath() { QString filePath(keyFilePath()); QFileInfo fileInfo(filePath); if (fileInfo.exists()) { - emit keyFilePathIfExistsResult(filePath); + return filePath; } else { - emit keyFilePathIfExistsResult(""); + return ""; } } diff --git a/interface/src/commerce/Wallet.h b/interface/src/commerce/Wallet.h index 59812d5222..807080e6ea 100644 --- a/interface/src/commerce/Wallet.h +++ b/interface/src/commerce/Wallet.h @@ -32,8 +32,8 @@ public: QStringList listPublicKeys(); QString signWithKey(const QByteArray& text, const QString& key); void chooseSecurityImage(const QString& imageFile); - void getSecurityImage(); - void sendKeyFilePathIfExists(); + bool getSecurityImage(); + QString getKeyFilePath(); void setSalt(const QByteArray& salt) { _salt = salt; } QByteArray getSalt() { return _salt; } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index cb90160cfe..5c07bebc23 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -49,6 +49,10 @@ int main(int argc, const char* argv[]) { CrashReporter crashReporter { BUG_SPLAT_DATABASE, BUG_SPLAT_APPLICATION_NAME, BuildInfo::VERSION }; #endif +#ifdef Q_OS_LINUX + QApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); +#endif + disableQtBearerPoll(); // Fixes wifi ping spikes QElapsedTimer startupTime; diff --git a/interface/src/scripting/RatesScriptingInterface.h b/interface/src/scripting/RatesScriptingInterface.h index 7bcab0ea70..5658ed99a0 100644 --- a/interface/src/scripting/RatesScriptingInterface.h +++ b/interface/src/scripting/RatesScriptingInterface.h @@ -22,16 +22,14 @@ class RatesScriptingInterface : public QObject { Q_PROPERTY(float newFrame READ getNewFrameRate) Q_PROPERTY(float dropped READ getDropRate) Q_PROPERTY(float simulation READ getSimulationRate) - Q_PROPERTY(float avatar READ getAvatarRate) public: RatesScriptingInterface(QObject* parent) : QObject(parent) {} - float getRenderRate() { return qApp->getFps(); } + float getRenderRate() { return qApp->getRenderLoopRate(); } float getPresentRate() { return qApp->getActiveDisplayPlugin()->presentRate(); } float getNewFrameRate() { return qApp->getActiveDisplayPlugin()->newFramePresentRate(); } float getDropRate() { return qApp->getActiveDisplayPlugin()->droppedFrameRate(); } - float getSimulationRate() { return qApp->getAverageSimsPerSecond(); } - float getAvatarRate() { return qApp->getAvatarSimrate(); } + float getSimulationRate() { return qApp->getGameLoopRate(); } }; #endif // HIFI_INTERFACE_RATES_SCRIPTING_INTERFACE_H diff --git a/interface/src/scripting/WalletScriptingInterface.cpp b/interface/src/scripting/WalletScriptingInterface.cpp new file mode 100644 index 0000000000..555e9477b0 --- /dev/null +++ b/interface/src/scripting/WalletScriptingInterface.cpp @@ -0,0 +1,47 @@ +// +// WalletScriptingInterface.cpp +// interface/src/scripting +// +// Created by Zach Fox on 2017-09-29. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "WalletScriptingInterface.h" + +CheckoutProxy::CheckoutProxy(QObject* qmlObject, QObject* parent) : QmlWrapper(qmlObject, parent) { + Q_ASSERT(QThread::currentThread() == qApp->thread()); +} + +WalletScriptingInterface::WalletScriptingInterface() { +} + +static const QString CHECKOUT_QML_PATH = qApp->applicationDirPath() + "../../../qml/hifi/commerce/checkout/Checkout.qml"; +void WalletScriptingInterface::buy(const QString& name, const QString& id, const int& price, const QString& href) { + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "buy", Q_ARG(const QString&, name), Q_ARG(const QString&, id), Q_ARG(const int&, price), Q_ARG(const QString&, href)); + return; + } + + auto tabletScriptingInterface = DependencyManager::get(); + auto tablet = dynamic_cast(tabletScriptingInterface->getTablet("com.highfidelity.interface.tablet.system")); + + tablet->loadQMLSource(CHECKOUT_QML_PATH); + DependencyManager::get()->openTablet(); + + QQuickItem* root = nullptr; + if (tablet->getToolbarMode() || (!tablet->getTabletRoot() && !qApp->isHMDMode())) { + root = DependencyManager::get()->getRootItem(); + } else { + root = tablet->getTabletRoot(); + } + CheckoutProxy* checkout = new CheckoutProxy(root->findChild("checkout")); + + // Example: Wallet.buy("Test Flaregun", "0d90d21c-ce7a-4990-ad18-e9d2cf991027", 17, "http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json"); + checkout->writeProperty("itemName", name); + checkout->writeProperty("itemId", id); + checkout->writeProperty("itemPrice", price); + checkout->writeProperty("itemHref", href); +} \ No newline at end of file diff --git a/interface/src/scripting/WalletScriptingInterface.h b/interface/src/scripting/WalletScriptingInterface.h new file mode 100644 index 0000000000..31b42094cf --- /dev/null +++ b/interface/src/scripting/WalletScriptingInterface.h @@ -0,0 +1,51 @@ + +// WalletScriptingInterface.h +// interface/src/scripting +// +// Created by Zach Fox on 2017-09-29. +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_WalletScriptingInterface_h +#define hifi_WalletScriptingInterface_h + +#include +#include + +#include "scripting/HMDScriptingInterface.h" +#include +#include +#include +#include "Application.h" + +class CheckoutProxy : public QmlWrapper { + Q_OBJECT +public: + CheckoutProxy(QObject* qmlObject, QObject* parent = nullptr); +}; + + +class WalletScriptingInterface : public QObject, public Dependency { + Q_OBJECT + + Q_PROPERTY(uint walletStatus READ getWalletStatus WRITE setWalletStatus NOTIFY walletStatusChanged) + +public: + WalletScriptingInterface(); + + Q_INVOKABLE uint getWalletStatus() { return _walletStatus; } + void setWalletStatus(const uint& status) { _walletStatus = status; } + + Q_INVOKABLE void buy(const QString& name, const QString& id, const int& price, const QString& href); + +signals: + void walletStatusChanged(); + +private: + uint _walletStatus; +}; + +#endif // hifi_WalletScriptingInterface_h diff --git a/interface/src/scripting/WindowScriptingInterface.cpp b/interface/src/scripting/WindowScriptingInterface.cpp index 4b981207f1..c99e190d12 100644 --- a/interface/src/scripting/WindowScriptingInterface.cpp +++ b/interface/src/scripting/WindowScriptingInterface.cpp @@ -171,6 +171,11 @@ void WindowScriptingInterface::setPreviousBrowseAssetLocation(const QString& loc Setting::Handle(LAST_BROWSE_ASSETS_LOCATION_SETTING).set(location); } +bool WindowScriptingInterface::isPointOnDesktopWindow(QVariant point) { + auto offscreenUi = DependencyManager::get(); + return offscreenUi->isPointOnDesktopWindow(point); +} + /// Makes sure that the reticle is visible, use this in blocking forms that require a reticle and /// might be in same thread as a script that sets the reticle to invisible void WindowScriptingInterface::ensureReticleVisible() const { diff --git a/interface/src/scripting/WindowScriptingInterface.h b/interface/src/scripting/WindowScriptingInterface.h index 0d58e6162d..61aaec7bea 100644 --- a/interface/src/scripting/WindowScriptingInterface.h +++ b/interface/src/scripting/WindowScriptingInterface.h @@ -72,6 +72,7 @@ public slots: void shareSnapshot(const QString& path, const QUrl& href = QUrl("")); bool isPhysicsEnabled(); bool setDisplayTexture(const QString& name); + bool isPointOnDesktopWindow(QVariant point); int openMessageBox(QString title, QString text, int buttons, int defaultButton); void updateMessageBox(int id, QString title, QString text, int buttons, int defaultButton); diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index 767e499503..8140dfb8ae 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -8,6 +8,7 @@ #include "Stats.h" +#include #include #include @@ -116,12 +117,6 @@ void Stats::updateStats(bool force) { } } - bool shouldDisplayTimingDetail = Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails) && - Menu::getInstance()->isOptionChecked(MenuOption::Stats) && isExpanded(); - if (shouldDisplayTimingDetail != PerformanceTimer::isActive()) { - PerformanceTimer::setActive(shouldDisplayTimingDetail); - } - auto nodeList = DependencyManager::get(); auto avatarManager = DependencyManager::get(); // we need to take one avatar out so we don't include ourselves @@ -129,7 +124,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(updatedAvatarCount, avatarManager->getNumAvatarsUpdated()); STAT_UPDATE(notUpdatedAvatarCount, avatarManager->getNumAvatarsNotUpdated()); STAT_UPDATE(serverCount, (int)nodeList->size()); - STAT_UPDATE_FLOAT(framerate, qApp->getFps(), 0.1f); + STAT_UPDATE_FLOAT(renderrate, qApp->getRenderLoopRate(), 0.1f); if (qApp->getActiveDisplayPlugin()) { auto displayPlugin = qApp->getActiveDisplayPlugin(); auto stats = displayPlugin->getHardwareStats(); @@ -137,7 +132,6 @@ void Stats::updateStats(bool force) { STAT_UPDATE(longrenders, stats["long_render_count"].toInt()); STAT_UPDATE(longsubmits, stats["long_submit_count"].toInt()); STAT_UPDATE(longframes, stats["long_frame_count"].toInt()); - STAT_UPDATE_FLOAT(renderrate, displayPlugin->renderRate(), 0.1f); STAT_UPDATE_FLOAT(presentrate, displayPlugin->presentRate(), 0.1f); STAT_UPDATE_FLOAT(presentnewrate, displayPlugin->newFramePresentRate(), 0.1f); STAT_UPDATE_FLOAT(presentdroprate, displayPlugin->droppedFrameRate(), 0.1f); @@ -150,8 +144,7 @@ void Stats::updateStats(bool force) { STAT_UPDATE(presentnewrate, -1); STAT_UPDATE(presentdroprate, -1); } - STAT_UPDATE(simrate, (int)qApp->getAverageSimsPerSecond()); - STAT_UPDATE(avatarSimrate, (int)qApp->getAvatarSimrate()); + STAT_UPDATE(gameLoopRate, (int)qApp->getGameLoopRate()); auto bandwidthRecorder = DependencyManager::get(); STAT_UPDATE(packetInCount, (int)bandwidthRecorder->getCachedTotalAverageInputPacketsPerSecond()); @@ -406,14 +399,21 @@ void Stats::updateStats(bool force) { STAT_UPDATE(lodStatus, "You can see " + DependencyManager::get()->getLODFeedbackText()); } - bool performanceTimerIsActive = PerformanceTimer::isActive(); - bool displayPerf = _expanded && Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails); - if (displayPerf && performanceTimerIsActive) { - if (!_timingExpanded) { - _timingExpanded = true; + + bool performanceTimerShouldBeActive = Menu::getInstance()->isOptionChecked(MenuOption::Stats) && _expanded; + if (performanceTimerShouldBeActive != PerformanceTimer::isActive()) { + PerformanceTimer::setActive(performanceTimerShouldBeActive); + } + if (performanceTimerShouldBeActive) { + PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up + } + + if (performanceTimerShouldBeActive && + Menu::getInstance()->isOptionChecked(MenuOption::DisplayDebugTimingDetails)) { + if (!_showTimingDetails) { + _showTimingDetails = true; emit timingExpandedChanged(); } - PerformanceTimer::tallyAllTimerRecords(); // do this even if we're not displaying them, so they don't stack up // we will also include room for 1 line per timing record and a header of 4 lines // Timing details... @@ -453,10 +453,55 @@ void Stats::updateStats(bool force) { } _timingStats = perfLines; emit timingStatsChanged(); - } else if (_timingExpanded) { - _timingExpanded = false; + } else if (_showTimingDetails) { + _showTimingDetails = false; emit timingExpandedChanged(); } + + if (_expanded && performanceTimerShouldBeActive) { + if (!_showGameUpdateStats) { + _showGameUpdateStats = true; + } + class SortableStat { + public: + SortableStat(QString a, float p) : message(a), priority(p) {} + QString message; + float priority; + bool operator<(const SortableStat& other) const { return priority < other.priority; } + }; + + const QMap& allRecords = PerformanceTimer::getAllTimerRecords(); + std::priority_queue idleUpdateStats; + auto itr = allRecords.find("/idle/update"); + if (itr != allRecords.end()) { + float dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + _gameUpdateStats = QString("/idle/update = %1 ms").arg(dt); + + QVector categories = { "devices", "physics", "otherAvatars", "MyAvatar", "misc" }; + for (int32_t j = 0; j < categories.size(); ++j) { + QString recordKey = "/idle/update/" + categories[j]; + itr = allRecords.find(recordKey); + if (itr != allRecords.end()) { + float dt = (float)itr.value().getMovingAverage() / (float)USECS_PER_MSEC; + QString message = QString("\n %1 = %2").arg(categories[j]).arg(dt); + idleUpdateStats.push(SortableStat(message, dt)); + } + } + while (!idleUpdateStats.empty()) { + SortableStat stat = idleUpdateStats.top(); + _gameUpdateStats += stat.message; + idleUpdateStats.pop(); + } + emit gameUpdateStatsChanged(); + } else if (_gameUpdateStats != "") { + _gameUpdateStats = ""; + emit gameUpdateStatsChanged(); + } + } else if (_showGameUpdateStats) { + _showGameUpdateStats = false; + _gameUpdateStats = ""; + emit gameUpdateStatsChanged(); + } } void Stats::setRenderDetails(const render::RenderDetails& details) { diff --git a/interface/src/ui/Stats.h b/interface/src/ui/Stats.h index b3c920d4ef..af3189f20b 100644 --- a/interface/src/ui/Stats.h +++ b/interface/src/ui/Stats.h @@ -32,8 +32,6 @@ class Stats : public QQuickItem { STATS_PROPERTY(int, serverCount, 0) // How often the app is creating new gpu::Frames - STATS_PROPERTY(float, framerate, 0) - // How often the display plugin is executing a given frame STATS_PROPERTY(float, renderrate, 0) // How often the display plugin is presenting to the device STATS_PROPERTY(float, presentrate, 0) @@ -47,8 +45,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(float, presentnewrate, 0) STATS_PROPERTY(float, presentdroprate, 0) - STATS_PROPERTY(int, simrate, 0) - STATS_PROPERTY(int, avatarSimrate, 0) + STATS_PROPERTY(int, gameLoopRate, 0) STATS_PROPERTY(int, avatarCount, 0) STATS_PROPERTY(int, updatedAvatarCount, 0) STATS_PROPERTY(int, notUpdatedAvatarCount, 0) @@ -108,6 +105,7 @@ class Stats : public QQuickItem { STATS_PROPERTY(QString, packetStats, QString()) STATS_PROPERTY(QString, lodStatus, QString()) STATS_PROPERTY(QString, timingStats, QString()) + STATS_PROPERTY(QString, gameUpdateStats, QString()) STATS_PROPERTY(int, serverElements, 0) STATS_PROPERTY(int, serverInternal, 0) STATS_PROPERTY(int, serverLeaves, 0) @@ -148,7 +146,7 @@ public: void updateStats(bool force = false); bool isExpanded() { return _expanded; } - bool isTimingExpanded() { return _timingExpanded; } + bool isTimingExpanded() { return _showTimingDetails; } void setExpanded(bool expanded) { if (_expanded != expanded) { @@ -167,7 +165,6 @@ signals: void longrendersChanged(); void longframesChanged(); void appdroppedChanged(); - void framerateChanged(); void expandedChanged(); void timingExpandedChanged(); void serverCountChanged(); @@ -176,8 +173,7 @@ signals: void presentnewrateChanged(); void presentdroprateChanged(); void stutterrateChanged(); - void simrateChanged(); - void avatarSimrateChanged(); + void gameLoopRateChanged(); void avatarCountChanged(); void updatedAvatarCountChanged(); void notUpdatedAvatarCountChanged(); @@ -242,6 +238,7 @@ signals: void localInternalChanged(); void localLeavesChanged(); void timingStatsChanged(); + void gameUpdateStatsChanged(); void glContextSwapchainMemoryChanged(); void qmlTextureMemoryChanged(); void texturePendingTransfersChanged(); @@ -267,7 +264,8 @@ private: int _recentMaxPackets{ 0 } ; // recent max incoming voxel packets to process bool _resetRecentMaxPacketsSoon{ true }; bool _expanded{ false }; - bool _timingExpanded{ false }; + bool _showTimingDetails{ false }; + bool _showGameUpdateStats{ false }; QString _monospaceFont; const AudioIOStats* _audioStats; QStringList _downloadUrls = QStringList(); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.cpp b/interface/src/ui/overlays/ContextOverlayInterface.cpp index 8e4254a786..39fd4f9377 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.cpp +++ b/interface/src/ui/overlays/ContextOverlayInterface.cpp @@ -201,7 +201,8 @@ bool ContextOverlayInterface::destroyContextOverlay(const EntityItemID& entityIt void ContextOverlayInterface::contextOverlays_mousePressOnOverlay(const OverlayID& overlayID, const PointerEvent& event) { if (overlayID == _contextOverlayID && event.getButton() == PointerEvent::PrimaryButton) { qCDebug(context_overlay) << "Clicked Context Overlay. Entity ID:" << _currentEntityWithContextOverlay << "Overlay ID:" << overlayID; - if (_commerceSettingSwitch.get()) { + Setting::Handle _settingSwitch{ "commerce", false }; + if (_settingSwitch.get()) { openInspectionCertificate(); } else { openMarketplace(); diff --git a/interface/src/ui/overlays/ContextOverlayInterface.h b/interface/src/ui/overlays/ContextOverlayInterface.h index ec5913444f..b4d3ddc0c2 100644 --- a/interface/src/ui/overlays/ContextOverlayInterface.h +++ b/interface/src/ui/overlays/ContextOverlayInterface.h @@ -79,8 +79,6 @@ private: bool _isInMarketplaceInspectionMode { false }; - Setting::Handle _commerceSettingSwitch{ "commerce", false }; - void openInspectionCertificate(); void openMarketplace(); void enableEntityHighlight(const EntityItemID& entityItemID); diff --git a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h index 2080fa5ea6..51f4a33bbd 100644 --- a/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h +++ b/libraries/display-plugins/src/display-plugins/OpenGLDisplayPlugin.h @@ -129,10 +129,10 @@ protected: bool _vsyncEnabled { true }; QThread* _presentThread{ nullptr }; std::queue _newFrameQueue; - RateCounter<100> _droppedFrameRate; - RateCounter<100> _newFrameRate; - RateCounter<100> _presentRate; - RateCounter<100> _renderRate; + RateCounter<200> _droppedFrameRate; + RateCounter<200> _newFrameRate; + RateCounter<200> _presentRate; + RateCounter<200> _renderRate; gpu::FramePointer _currentFrame; gpu::Frame* _lastFrame { nullptr }; diff --git a/libraries/entities/CMakeLists.txt b/libraries/entities/CMakeLists.txt index 19341ec3e2..322d69da16 100644 --- a/libraries/entities/CMakeLists.txt +++ b/libraries/entities/CMakeLists.txt @@ -1,3 +1,4 @@ set(TARGET_NAME entities) setup_hifi_library(Network Script) +include_directories(SYSTEM "${OPENSSL_INCLUDE_DIR}") link_hifi_libraries(shared networking octree avatars) diff --git a/libraries/entities/src/EntityItem.cpp b/libraries/entities/src/EntityItem.cpp index 6e2e52b380..a1b7ff54de 100644 --- a/libraries/entities/src/EntityItem.cpp +++ b/libraries/entities/src/EntityItem.cpp @@ -13,6 +13,10 @@ #include #include +#include +#include // see comments for DEBUG_CERT +#include +#include #include @@ -34,6 +38,7 @@ Q_DECLARE_METATYPE(EntityItemPointer); int entityItemPointernMetaTypeId = qRegisterMetaType(); + int EntityItem::_maxActionsDataSize = 800; quint64 EntityItem::_rememberDeletedActionTime = 20 * USECS_PER_SECOND; @@ -95,7 +100,19 @@ EntityPropertyFlags EntityItem::getEntityProperties(EncodeBitstreamParams& param requestedProperties += PROP_DYNAMIC; requestedProperties += PROP_LOCKED; requestedProperties += PROP_USER_DATA; + + // Certifiable properties + requestedProperties += PROP_ITEM_NAME; + requestedProperties += PROP_ITEM_DESCRIPTION; + requestedProperties += PROP_ITEM_CATEGORIES; + requestedProperties += PROP_ITEM_ARTIST; + requestedProperties += PROP_ITEM_LICENSE; + requestedProperties += PROP_LIMITED_RUN; requestedProperties += PROP_MARKETPLACE_ID; + requestedProperties += PROP_EDITION_NUMBER; + requestedProperties += PROP_ENTITY_INSTANCE_NUMBER; + requestedProperties += PROP_CERTIFICATE_ID; + requestedProperties += PROP_NAME; requestedProperties += PROP_HREF; requestedProperties += PROP_DESCRIPTION; @@ -240,7 +257,19 @@ OctreeElement::AppendState EntityItem::appendEntityData(OctreePacketData* packet APPEND_ENTITY_PROPERTY(PROP_DYNAMIC, getDynamic()); APPEND_ENTITY_PROPERTY(PROP_LOCKED, getLocked()); APPEND_ENTITY_PROPERTY(PROP_USER_DATA, getUserData()); + + // Certifiable Properties APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, getItemName()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, getItemDescription()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, getItemCategories()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_ARTIST, getItemArtist()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_LICENSE, getItemLicense()); + APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, getLimitedRun()); + APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, getEntityInstanceNumber()); + APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, getCertificateID()); + APPEND_ENTITY_PROPERTY(PROP_NAME, getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_HREF, getHref()); @@ -791,6 +820,17 @@ int EntityItem::readEntityDataFromBuffer(const unsigned char* data, int bytesLef if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_MARKETPLACE_ID) { READ_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, QString, setMarketplaceID); } + if (args.bitstreamVersion >= VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES) { + READ_ENTITY_PROPERTY(PROP_ITEM_NAME, QString, setItemName); + READ_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, QString, setItemDescription); + READ_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, QString, setItemCategories); + READ_ENTITY_PROPERTY(PROP_ITEM_ARTIST, QString, setItemArtist); + READ_ENTITY_PROPERTY(PROP_ITEM_LICENSE, QString, setItemLicense); + READ_ENTITY_PROPERTY(PROP_LIMITED_RUN, quint32, setLimitedRun); + READ_ENTITY_PROPERTY(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); + READ_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, QString, setCertificateID); + } READ_ENTITY_PROPERTY(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); @@ -1208,7 +1248,19 @@ EntityItemProperties EntityItem::getProperties(EntityPropertyFlags desiredProper COPY_ENTITY_PROPERTY_TO_PROPERTIES(dynamic, getDynamic); COPY_ENTITY_PROPERTY_TO_PROPERTIES(locked, getLocked); COPY_ENTITY_PROPERTY_TO_PROPERTIES(userData, getUserData); + + // Certifiable Properties + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemName, getItemName); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemDescription, getItemDescription); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemCategories, getItemCategories); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemArtist, getItemArtist); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(itemLicense, getItemLicense); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(limitedRun, getLimitedRun); COPY_ENTITY_PROPERTY_TO_PROPERTIES(marketplaceID, getMarketplaceID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(editionNumber, getEditionNumber); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(entityInstanceNumber, getEntityInstanceNumber); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(certificateID, getCertificateID); + COPY_ENTITY_PROPERTY_TO_PROPERTIES(name, getName); COPY_ENTITY_PROPERTY_TO_PROPERTIES(href, getHref); COPY_ENTITY_PROPERTY_TO_PROPERTIES(description, getDescription); @@ -1303,7 +1355,19 @@ bool EntityItem::setProperties(const EntityItemProperties& properties) { SET_ENTITY_PROPERTY_FROM_PROPERTIES(localRenderAlpha, setLocalRenderAlpha); SET_ENTITY_PROPERTY_FROM_PROPERTIES(visible, setVisible); SET_ENTITY_PROPERTY_FROM_PROPERTIES(userData, setUserData); + + // Certifiable Properties + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemName, setItemName); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemDescription, setItemDescription); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemCategories, setItemCategories); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemArtist, setItemArtist); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(itemLicense, setItemLicense); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(limitedRun, setLimitedRun); SET_ENTITY_PROPERTY_FROM_PROPERTIES(marketplaceID, setMarketplaceID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(editionNumber, setEditionNumber); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(entityInstanceNumber, setEntityInstanceNumber); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(certificateID, setCertificateID); + SET_ENTITY_PROPERTY_FROM_PROPERTIES(name, setName); SET_ENTITY_PROPERTY_FROM_PROPERTIES(href, setHref); SET_ENTITY_PROPERTY_FROM_PROPERTIES(description, setDescription); @@ -1510,6 +1574,107 @@ float EntityItem::getRadius() const { return 0.5f * glm::length(getDimensions()); } +// Checking Certifiable Properties +#define ADD_STRING_PROPERTY(n, N) if (!propertySet.get##N().isEmpty()) json[#n] = propertySet.get##N() +#define ADD_ENUM_PROPERTY(n, N) json[#n] = propertySet.get##N##AsString() +#define ADD_INT_PROPERTY(n, N) if (propertySet.get##N() != 0) json[#n] = (propertySet.get##N() == (quint32) -1) ? -1.0 : ((double) propertySet.get##N()) +QByteArray EntityItem::getStaticCertificateJSON() const { + // Produce a compact json of every non-default static certificate property, with the property names in alphabetical order. + // The static certificate properties include all an only those properties that cannot be changed without altering the identity + // of the entity as reviewed during the certification submission. + + QJsonObject json; + EntityItemProperties propertySet = getProperties(); // Note: neither EntityItem nor EntityitemProperties "properties" are QObject "properties"! + // It is important that this be reproducible in the same order each time. Since we also generate these on the server, we do it alphabetically + // to help maintainence in two different code bases. + if (!propertySet.getAnimation().getURL().isEmpty()) { + json["animation.url"] = propertySet.getAnimation().getURL(); + } + ADD_STRING_PROPERTY(collisionSoundURL, CollisionSoundURL); + ADD_STRING_PROPERTY(compoundShapeURL, CompoundShapeURL); + ADD_INT_PROPERTY(editionNumber, EditionNumber); + ADD_INT_PROPERTY(entityInstanceNumber, EntityInstanceNumber); + ADD_STRING_PROPERTY(itemArtist, ItemArtist); + ADD_STRING_PROPERTY(itemCategories, ItemCategories); + ADD_STRING_PROPERTY(itemDescription, ItemDescription); + ADD_STRING_PROPERTY(itemLicense, ItemLicense); + ADD_STRING_PROPERTY(itemName, ItemName); + ADD_INT_PROPERTY(limitedRun, LimitedRun); + ADD_STRING_PROPERTY(marketplaceID, MarketplaceID); + ADD_STRING_PROPERTY(modelURL, ModelURL); + ADD_STRING_PROPERTY(script, Script); + ADD_ENUM_PROPERTY(shapeType, ShapeType); + json["type"] = EntityTypes::getEntityTypeName(propertySet.getType()); + + return QJsonDocument(json).toJson(QJsonDocument::Compact); +} +QByteArray EntityItem::getStaticCertificateHash() const { + return QCryptographicHash::hash(getStaticCertificateJSON(), QCryptographicHash::Sha256); +} + +#ifdef DEBUG_CERT +QString EntityItem::computeCertificateID() { + // Until the marketplace generates it, compute and answer the certificateID here. + // Does not set it, as that will have to be done from script engine in order to update server, etc. + const auto hash = getStaticCertificateHash(); + const auto text = reinterpret_cast(hash.constData()); + const unsigned int textLength = hash.length(); + + const char privateKey[] = "-----BEGIN RSA PRIVATE KEY-----\n\ +MIIBOQIBAAJBALCoBiDAZOClO26tC5pd7JikBL61WIgpAqbcNnrV/TcG6LPI7Zbi\n\ +MjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQJABvOlwhYwIhL+gr12jm2R\n\ +yPPzZ9nVEQ6kFxLlZfIT09119fd6OU1X5d4sHWfMfSIEgjwQIDS3ZU1kY3XKo87X\n\ +zQIhAOPHlYa1OC7BLhaTouy68qIU2vCKLP8mt4S31/TT0UOnAiEAxor6gU6yupTQ\n\ +yuyV3yHvr5LkZKBGqhjmOTmDfgtX7ncCIChGbgX3nQuHVOLhD/nTxHssPNozVGl5\n\ +KxHof+LmYSYZAiB4U+yEh9SsXdq40W/3fpLMPuNq1PRezJ5jGidGMcvF+wIgUNec\n\ +3Kg2U+CVZr8/bDT/vXRrsKj1zfobYuvbfVH02QY=\n\ +-----END RSA PRIVATE KEY-----"; + BIO* bio = BIO_new_mem_buf((void*)privateKey, sizeof(privateKey)); + RSA* rsa = PEM_read_bio_RSAPrivateKey(bio, NULL, NULL, NULL); + + QByteArray signature(RSA_size(rsa), 0); + unsigned int signatureLength = 0; + const int signOK = RSA_sign(NID_sha256, text, textLength, reinterpret_cast(signature.data()), &signatureLength, rsa); + BIO_free(bio); + RSA_free(rsa); + if (!signOK) { + qCWarning(entities) << "Unable to compute signature for" << getName() << getEntityItemID(); + return ""; + } + return signature.toBase64(); +#endif +} + +bool EntityItem::verifyStaticCertificateProperties() { + // True IIF a non-empty certificateID matches the static certificate json. + // I.e., if we can verify that the certificateID was produced by High Fidelity signing the static certificate hash. + + if (getCertificateID().isEmpty()) { + return false; + } + const auto signatureBytes = QByteArray::fromBase64(getCertificateID().toLatin1()); + const auto signature = reinterpret_cast(signatureBytes.constData()); + const unsigned int signatureLength = signatureBytes.length(); + + const auto hash = getStaticCertificateHash(); + const auto text = reinterpret_cast(hash.constData()); + const unsigned int textLength = hash.length(); + + // After DEBUG_CERT ends, we will get/cache this once from the marketplace when needed, and it likely won't be RSA. + const char publicKey[] = "-----BEGIN PUBLIC KEY-----\n\ +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALCoBiDAZOClO26tC5pd7JikBL61WIgp\n\ +AqbcNnrV/TcG6LPI7ZbiMjdUixmTNvYMRZH3Wlqtl2IKG1W68y3stKECAwEAAQ==\n\ +-----END PUBLIC KEY-----"; + BIO *bio = BIO_new_mem_buf((void*)publicKey, sizeof(publicKey)); + EVP_PKEY* evp_key = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); + RSA* rsa = EVP_PKEY_get1_RSA(evp_key); + bool answer = RSA_verify(NID_sha256, text, textLength, signature, signatureLength, rsa); + BIO_free(bio); + RSA_free(rsa); + EVP_PKEY_free(evp_key); + return answer; +} + void EntityItem::adjustShapeInfoByRegistration(ShapeInfo& info) const { if (_registrationPoint != ENTITY_ITEM_DEFAULT_REGISTRATION_POINT) { glm::mat4 scale = glm::scale(getDimensions()); @@ -2762,19 +2927,33 @@ void EntityItem::setUserData(const QString& value) { }); } -QString EntityItem::getMarketplaceID() const { - QString result; - withReadLock([&] { - result = _marketplaceID; - }); - return result; +// Certifiable Properties +#define DEFINE_PROPERTY_GETTER(type, accessor, var) \ +type EntityItem::get##accessor() const { \ + type result; \ + withReadLock([&] { \ + result = _##var; \ + }); \ + return result; \ } -void EntityItem::setMarketplaceID(const QString& value) { - withWriteLock([&] { - _marketplaceID = value; - }); +#define DEFINE_PROPERTY_SETTER(type, accessor, var) \ +void EntityItem::set##accessor(const type & value) { \ + withWriteLock([&] { \ + _##var = value; \ + }); \ } +#define DEFINE_PROPERTY_ACCESSOR(type, accessor, var) DEFINE_PROPERTY_GETTER(type, accessor, var) DEFINE_PROPERTY_SETTER(type, accessor, var) +DEFINE_PROPERTY_ACCESSOR(QString, ItemName, itemName) +DEFINE_PROPERTY_ACCESSOR(QString, ItemDescription, itemDescription) +DEFINE_PROPERTY_ACCESSOR(QString, ItemCategories, itemCategories) +DEFINE_PROPERTY_ACCESSOR(QString, ItemArtist, itemArtist) +DEFINE_PROPERTY_ACCESSOR(QString, ItemLicense, itemLicense) +DEFINE_PROPERTY_ACCESSOR(quint32, LimitedRun, limitedRun) +DEFINE_PROPERTY_ACCESSOR(QString, MarketplaceID, marketplaceID) +DEFINE_PROPERTY_ACCESSOR(quint32, EditionNumber, editionNumber) +DEFINE_PROPERTY_ACCESSOR(quint32, EntityInstanceNumber, entityInstanceNumber) +DEFINE_PROPERTY_ACCESSOR(QString, CertificateID, certificateID) uint32_t EntityItem::getDirtyFlags() const { uint32_t result; diff --git a/libraries/entities/src/EntityItem.h b/libraries/entities/src/EntityItem.h index 88750da463..c26f1694a9 100644 --- a/libraries/entities/src/EntityItem.h +++ b/libraries/entities/src/EntityItem.h @@ -36,6 +36,9 @@ #include "SimulationFlags.h" #include "EntityDynamicInterface.h" +// FIXME: The server-side marketplace will soon create the certificateID. At that point, all of the DEBUG_CERT stuff will go away. +#define DEBUG_CERT 1 + class EntitySimulation; class EntityTreeElement; class EntityTreeElementExtraEncodeData; @@ -304,8 +307,33 @@ public: uint8_t getPendingOwnershipPriority() const { return _simulationOwner.getPendingPriority(); } void rememberHasSimulationOwnershipBid() const; + // Certifiable Properties + QString getItemName() const; + void setItemName(const QString& value); + QString getItemDescription() const; + void setItemDescription(const QString& value); + QString getItemCategories() const; + void setItemCategories(const QString& value); + QString getItemArtist() const; + void setItemArtist(const QString& value); + QString getItemLicense() const; + void setItemLicense(const QString& value); + quint32 getLimitedRun() const; + void setLimitedRun(const quint32&); QString getMarketplaceID() const; void setMarketplaceID(const QString& value); + quint32 getEditionNumber() const; + void setEditionNumber(const quint32&); + quint32 getEntityInstanceNumber() const; + void setEntityInstanceNumber(const quint32&); + QString getCertificateID() const; + void setCertificateID(const QString& value); + QByteArray getStaticCertificateJSON() const; + QByteArray getStaticCertificateHash() const; + bool verifyStaticCertificateProperties(); +#ifdef DEBUG_CERT + QString computeCertificateID(); +#endif // TODO: get rid of users of getRadius()... float getRadius() const; @@ -524,12 +552,24 @@ protected: bool _locked { ENTITY_ITEM_DEFAULT_LOCKED }; QString _userData { ENTITY_ITEM_DEFAULT_USER_DATA }; SimulationOwner _simulationOwner; - QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; bool _shouldHighlight { false }; QString _name { ENTITY_ITEM_DEFAULT_NAME }; QString _href; //Hyperlink href QString _description; //Hyperlink description + // Certifiable Properties + QString _itemName { ENTITY_ITEM_DEFAULT_ITEM_NAME }; + QString _itemDescription { ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION }; + QString _itemCategories { ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES }; + QString _itemArtist { ENTITY_ITEM_DEFAULT_ITEM_ARTIST }; + QString _itemLicense { ENTITY_ITEM_DEFAULT_ITEM_LICENSE }; + quint32 _limitedRun { ENTITY_ITEM_DEFAULT_LIMITED_RUN }; + QString _certificateID { ENTITY_ITEM_DEFAULT_CERTIFICATE_ID }; + quint32 _editionNumber { ENTITY_ITEM_DEFAULT_EDITION_NUMBER }; + quint32 _entityInstanceNumber { ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER }; + QString _marketplaceID { ENTITY_ITEM_DEFAULT_MARKETPLACE_ID }; + + // NOTE: Damping is applied like this: v *= pow(1 - damping, dt) // // Hence the damping coefficient must range from 0 (no damping) to 1 (immediate stop). diff --git a/libraries/entities/src/EntityItemProperties.cpp b/libraries/entities/src/EntityItemProperties.cpp index d6de4ec614..007c5dcc6c 100644 --- a/libraries/entities/src/EntityItemProperties.cpp +++ b/libraries/entities/src/EntityItemProperties.cpp @@ -288,7 +288,19 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const { CHECK_PROPERTY_CHANGE(PROP_RADIUS_SPREAD, radiusSpread); CHECK_PROPERTY_CHANGE(PROP_RADIUS_START, radiusStart); CHECK_PROPERTY_CHANGE(PROP_RADIUS_FINISH, radiusFinish); + + // Certifiable Properties + CHECK_PROPERTY_CHANGE(PROP_ITEM_NAME, itemName); + CHECK_PROPERTY_CHANGE(PROP_ITEM_DESCRIPTION, itemDescription); + CHECK_PROPERTY_CHANGE(PROP_ITEM_CATEGORIES, itemCategories); + CHECK_PROPERTY_CHANGE(PROP_ITEM_ARTIST, itemArtist); + CHECK_PROPERTY_CHANGE(PROP_ITEM_LICENSE, itemLicense); + CHECK_PROPERTY_CHANGE(PROP_LIMITED_RUN, limitedRun); CHECK_PROPERTY_CHANGE(PROP_MARKETPLACE_ID, marketplaceID); + CHECK_PROPERTY_CHANGE(PROP_EDITION_NUMBER, editionNumber); + CHECK_PROPERTY_CHANGE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); + CHECK_PROPERTY_CHANGE(PROP_CERTIFICATE_ID, certificateID); + CHECK_PROPERTY_CHANGE(PROP_NAME, name); CHECK_PROPERTY_CHANGE(PROP_BACKGROUND_MODE, backgroundMode); CHECK_PROPERTY_CHANGE(PROP_SOURCE_URL, sourceUrl); @@ -405,7 +417,19 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ACTION_DATA, actionData); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LOCKED, locked); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_USER_DATA, userData); + + // Certifiable Properties + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_NAME, itemName); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_DESCRIPTION, itemDescription); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_CATEGORIES, itemCategories); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_ARTIST, itemArtist); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ITEM_LICENSE, itemLicense); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_LIMITED_RUN, limitedRun); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_MARKETPLACE_ID, marketplaceID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_EDITION_NUMBER, editionNumber); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_ENTITY_INSTANCE_NUMBER, entityInstanceNumber); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_CERTIFICATE_ID, certificateID); + COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_NAME, name); COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_COLLISION_SOUND_URL, collisionSoundURL); @@ -671,7 +695,19 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusSpread, float, setRadiusSpread); COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusStart, float, setRadiusStart); COPY_PROPERTY_FROM_QSCRIPTVALUE(radiusFinish, float, setRadiusFinish); + + // Certifiable Properties + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemName, QString, setItemName); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemDescription, QString, setItemDescription); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemCategories, QString, setItemCategories); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemArtist, QString, setItemArtist); + COPY_PROPERTY_FROM_QSCRIPTVALUE(itemLicense, QString, setItemLicense); + COPY_PROPERTY_FROM_QSCRIPTVALUE(limitedRun, quint32, setLimitedRun); COPY_PROPERTY_FROM_QSCRIPTVALUE(marketplaceID, QString, setMarketplaceID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(editionNumber, quint32, setEditionNumber); + COPY_PROPERTY_FROM_QSCRIPTVALUE(entityInstanceNumber, quint32, setEntityInstanceNumber); + COPY_PROPERTY_FROM_QSCRIPTVALUE(certificateID, QString, setCertificateID); + COPY_PROPERTY_FROM_QSCRIPTVALUE(name, QString, setName); COPY_PROPERTY_FROM_QSCRIPTVALUE(collisionSoundURL, QString, setCollisionSoundURL); @@ -809,7 +845,19 @@ void EntityItemProperties::merge(const EntityItemProperties& other) { COPY_PROPERTY_IF_CHANGED(radiusSpread); COPY_PROPERTY_IF_CHANGED(radiusStart); COPY_PROPERTY_IF_CHANGED(radiusFinish); + + // Certifiable Properties + COPY_PROPERTY_IF_CHANGED(itemName); + COPY_PROPERTY_IF_CHANGED(itemDescription); + COPY_PROPERTY_IF_CHANGED(itemCategories); + COPY_PROPERTY_IF_CHANGED(itemArtist); + COPY_PROPERTY_IF_CHANGED(itemLicense); + COPY_PROPERTY_IF_CHANGED(limitedRun); COPY_PROPERTY_IF_CHANGED(marketplaceID); + COPY_PROPERTY_IF_CHANGED(editionNumber); + COPY_PROPERTY_IF_CHANGED(entityInstanceNumber); + COPY_PROPERTY_IF_CHANGED(certificateID); + COPY_PROPERTY_IF_CHANGED(name); COPY_PROPERTY_IF_CHANGED(collisionSoundURL); @@ -981,7 +1029,19 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue ADD_PROPERTY_TO_MAP(PROP_RADIUS_SPREAD, RadiusSpread, radiusSpread, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_START, RadiusStart, radiusStart, float); ADD_PROPERTY_TO_MAP(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float); + + // Certifiable Properties + ADD_PROPERTY_TO_MAP(PROP_ITEM_NAME, ItemName, itemName, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_CATEGORIES, ItemCategories, itemCategories, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_ARTIST, ItemArtist, itemArtist, QString); + ADD_PROPERTY_TO_MAP(PROP_ITEM_LICENSE, ItemLicense, itemLicense, QString); + ADD_PROPERTY_TO_MAP(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32); ADD_PROPERTY_TO_MAP(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString); + ADD_PROPERTY_TO_MAP(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32); + ADD_PROPERTY_TO_MAP(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32); + ADD_PROPERTY_TO_MAP(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString); + ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_COLOR, KeyLightColor, keyLightColor, xColor); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_INTENSITY, KeyLightIntensity, keyLightIntensity, float); ADD_PROPERTY_TO_MAP(PROP_KEYLIGHT_AMBIENT_INTENSITY, KeyLightAmbientIntensity, keyLightAmbientIntensity, float); @@ -1334,11 +1394,22 @@ bool EntityItemProperties::encodeEntityEditPacket(PacketType command, EntityItem properties.getType() == EntityTypes::Sphere) { APPEND_ENTITY_PROPERTY(PROP_SHAPE, properties.getShape()); } - APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); APPEND_ENTITY_PROPERTY(PROP_NAME, properties.getName()); APPEND_ENTITY_PROPERTY(PROP_COLLISION_SOUND_URL, properties.getCollisionSoundURL()); APPEND_ENTITY_PROPERTY(PROP_ACTION_DATA, properties.getActionData()); APPEND_ENTITY_PROPERTY(PROP_ALPHA, properties.getAlpha()); + + // Certifiable Properties + APPEND_ENTITY_PROPERTY(PROP_ITEM_NAME, properties.getItemName()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_DESCRIPTION, properties.getItemDescription()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_CATEGORIES, properties.getItemCategories()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_ARTIST, properties.getItemArtist()); + APPEND_ENTITY_PROPERTY(PROP_ITEM_LICENSE, properties.getItemLicense()); + APPEND_ENTITY_PROPERTY(PROP_LIMITED_RUN, properties.getLimitedRun()); + APPEND_ENTITY_PROPERTY(PROP_MARKETPLACE_ID, properties.getMarketplaceID()); + APPEND_ENTITY_PROPERTY(PROP_EDITION_NUMBER, properties.getEditionNumber()); + APPEND_ENTITY_PROPERTY(PROP_ENTITY_INSTANCE_NUMBER, properties.getEntityInstanceNumber()); + APPEND_ENTITY_PROPERTY(PROP_CERTIFICATE_ID, properties.getCertificateID()); } if (propertyCount > 0) { @@ -1632,12 +1703,23 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SHAPE, QString, setShape); } - READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_NAME, QString, setName); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_COLLISION_SOUND_URL, QString, setCollisionSoundURL); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ACTION_DATA, QByteArray, setActionData); READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ALPHA, float, setAlpha); + // Certifiable Properties + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_NAME, QString, setItemName); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_DESCRIPTION, QString, setItemDescription); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_CATEGORIES, QString, setItemCategories); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_ARTIST, QString, setItemArtist); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ITEM_LICENSE, QString, setItemLicense); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_LIMITED_RUN, quint32, setLimitedRun); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_MARKETPLACE_ID, QString, setMarketplaceID); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_EDITION_NUMBER, quint32, setEditionNumber); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_ENTITY_INSTANCE_NUMBER, quint32, setEntityInstanceNumber); + READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_CERTIFICATE_ID, QString, setCertificateID); + return valid; } @@ -1746,7 +1828,17 @@ void EntityItemProperties::markAllChanged() { //_alphaStartChanged = true; //_alphaFinishChanged = true; + // Certifiable Properties + _itemNameChanged = true; + _itemDescriptionChanged = true; + _itemCategoriesChanged = true; + _itemArtistChanged = true; + _itemLicenseChanged = true; + _limitedRunChanged = true; _marketplaceIDChanged = true; + _editionNumberChanged = true; + _entityInstanceNumberChanged = true; + _certificateIDChanged = true; _keyLight.markAllChanged(); @@ -2053,9 +2145,39 @@ QList EntityItemProperties::listChangedProperties() { if (radiusFinishChanged()) { out += "radiusFinish"; } + + // Certifiable Properties + if (itemNameChanged()) { + out += "itemName"; + } + if (itemDescriptionChanged()) { + out += "itemDescription"; + } + if (itemCategoriesChanged()) { + out += "itemCategories"; + } + if (itemArtistChanged()) { + out += "itemArtist"; + } + if (itemLicenseChanged()) { + out += "itemLicense"; + } + if (limitedRunChanged()) { + out += "limitedRun"; + } if (marketplaceIDChanged()) { out += "marketplaceID"; } + if (editionNumberChanged()) { + out += "editionNumber"; + } + if (entityInstanceNumberChanged()) { + out += "entityInstanceNumber"; + } + if (certificateIDChanged()) { + out += "certificateID"; + } + if (backgroundModeChanged()) { out += "backgroundMode"; } diff --git a/libraries/entities/src/EntityItemProperties.h b/libraries/entities/src/EntityItemProperties.h index dd8ce952d3..1f5b4a4660 100644 --- a/libraries/entities/src/EntityItemProperties.h +++ b/libraries/entities/src/EntityItemProperties.h @@ -170,7 +170,6 @@ public: DEFINE_PROPERTY(PROP_RADIUS_START, RadiusStart, radiusStart, float, particle::DEFAULT_RADIUS_START); DEFINE_PROPERTY(PROP_RADIUS_FINISH, RadiusFinish, radiusFinish, float, particle::DEFAULT_RADIUS_FINISH); DEFINE_PROPERTY(PROP_EMITTER_SHOULD_TRAIL, EmitterShouldTrail, emitterShouldTrail, bool, particle::DEFAULT_EMITTER_SHOULD_TRAIL); - DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); DEFINE_PROPERTY_GROUP(KeyLight, keyLight, KeyLightPropertyGroup); DEFINE_PROPERTY_REF(PROP_VOXEL_VOLUME_SIZE, VoxelVolumeSize, voxelVolumeSize, glm::vec3, PolyVoxEntityItem::DEFAULT_VOXEL_VOLUME_SIZE); DEFINE_PROPERTY_REF(PROP_VOXEL_DATA, VoxelData, voxelData, QByteArray, PolyVoxEntityItem::DEFAULT_VOXEL_DATA); @@ -203,6 +202,18 @@ public: DEFINE_PROPERTY_REF(PROP_QUERY_AA_CUBE, QueryAACube, queryAACube, AACube, AACube()); DEFINE_PROPERTY_REF(PROP_SHAPE, Shape, shape, QString, "Sphere"); + // Certifiable Properties - related to Proof of Purchase certificates + DEFINE_PROPERTY_REF(PROP_ITEM_NAME, ItemName, itemName, QString, ENTITY_ITEM_DEFAULT_ITEM_NAME); + DEFINE_PROPERTY_REF(PROP_ITEM_DESCRIPTION, ItemDescription, itemDescription, QString, ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION); + DEFINE_PROPERTY_REF(PROP_ITEM_CATEGORIES, ItemCategories, itemCategories, QString, ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES); + DEFINE_PROPERTY_REF(PROP_ITEM_ARTIST, ItemArtist, itemArtist, QString, ENTITY_ITEM_DEFAULT_ITEM_ARTIST); + DEFINE_PROPERTY_REF(PROP_ITEM_LICENSE, ItemLicense, itemLicense, QString, ENTITY_ITEM_DEFAULT_ITEM_LICENSE); + DEFINE_PROPERTY_REF(PROP_LIMITED_RUN, LimitedRun, limitedRun, quint32, ENTITY_ITEM_DEFAULT_LIMITED_RUN); + DEFINE_PROPERTY_REF(PROP_MARKETPLACE_ID, MarketplaceID, marketplaceID, QString, ENTITY_ITEM_DEFAULT_MARKETPLACE_ID); + DEFINE_PROPERTY_REF(PROP_EDITION_NUMBER, EditionNumber, editionNumber, quint32, ENTITY_ITEM_DEFAULT_EDITION_NUMBER); + DEFINE_PROPERTY_REF(PROP_ENTITY_INSTANCE_NUMBER, EntityInstanceNumber, entityInstanceNumber, quint32, ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER); + DEFINE_PROPERTY_REF(PROP_CERTIFICATE_ID, CertificateID, certificateID, QString, ENTITY_ITEM_DEFAULT_CERTIFICATE_ID); + // these are used when bouncing location data into and out of scripts DEFINE_PROPERTY_REF(PROP_LOCAL_POSITION, LocalPosition, localPosition, glmVec3, ENTITY_ITEM_ZERO_VEC3); DEFINE_PROPERTY_REF(PROP_LOCAL_ROTATION, LocalRotation, localRotation, glmQuat, ENTITY_ITEM_DEFAULT_ROTATION); @@ -426,7 +437,19 @@ inline QDebug operator<<(QDebug debug, const EntityItemProperties& properties) { DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusSpread, radiusSpread, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusStart, radiusStart, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, RadiusFinish, radiusFinish, ""); + + // Certifiable Properties + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemName, itemName, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemDescription, itemDescription, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemCategories, itemCategories, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemArtist, itemArtist, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, ItemLicense, itemLicense, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, LimitedRun, limitedRun, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, MarketplaceID, marketplaceID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, EditionNumber, editionNumber, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, EntityInstanceNumber, entityInstanceNumber, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, CertificateID, certificateID, ""); + DEBUG_PROPERTY_IF_CHANGED(debug, properties, BackgroundMode, backgroundMode, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelVolumeSize, voxelVolumeSize, ""); DEBUG_PROPERTY_IF_CHANGED(debug, properties, VoxelData, voxelData, ""); diff --git a/libraries/entities/src/EntityItemPropertiesDefaults.h b/libraries/entities/src/EntityItemPropertiesDefaults.h index d52c5d9aab..ab5d1d8094 100644 --- a/libraries/entities/src/EntityItemPropertiesDefaults.h +++ b/libraries/entities/src/EntityItemPropertiesDefaults.h @@ -26,9 +26,20 @@ const glm::vec3 ENTITY_ITEM_HALF_VEC3 = glm::vec3(0.5f); const bool ENTITY_ITEM_DEFAULT_LOCKED = false; const QString ENTITY_ITEM_DEFAULT_USER_DATA = QString(""); -const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); const QUuid ENTITY_ITEM_DEFAULT_SIMULATOR_ID = QUuid(); +// Certifiable Properties +const QString ENTITY_ITEM_DEFAULT_ITEM_NAME = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_DESCRIPTION = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_CATEGORIES = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_ARTIST = QString(""); +const QString ENTITY_ITEM_DEFAULT_ITEM_LICENSE = QString(""); +const quint32 ENTITY_ITEM_DEFAULT_LIMITED_RUN = -1; +const QString ENTITY_ITEM_DEFAULT_MARKETPLACE_ID = QString(""); +const quint32 ENTITY_ITEM_DEFAULT_EDITION_NUMBER = 0; +const quint32 ENTITY_ITEM_DEFAULT_ENTITY_INSTANCE_NUMBER = 0; +const QString ENTITY_ITEM_DEFAULT_CERTIFICATE_ID = QString(""); + const float ENTITY_ITEM_DEFAULT_ALPHA = 1.0f; const float ENTITY_ITEM_DEFAULT_LOCAL_RENDER_ALPHA = 1.0f; const bool ENTITY_ITEM_DEFAULT_VISIBLE = true; diff --git a/libraries/entities/src/EntityPropertyFlags.h b/libraries/entities/src/EntityPropertyFlags.h index d97be6348f..c03630f8bf 100644 --- a/libraries/entities/src/EntityPropertyFlags.h +++ b/libraries/entities/src/EntityPropertyFlags.h @@ -187,7 +187,19 @@ enum EntityPropertyList { PROP_SERVER_SCRIPTS, PROP_FILTER_URL, - + + // Certificable Properties + PROP_ITEM_NAME, + PROP_ITEM_DESCRIPTION, + PROP_ITEM_CATEGORIES, + PROP_ITEM_ARTIST, + PROP_ITEM_LICENSE, + PROP_LIMITED_RUN, + // PROP_MARKETPLACE_ID is above + PROP_EDITION_NUMBER, + PROP_ENTITY_INSTANCE_NUMBER, + PROP_CERTIFICATE_ID, + //////////////////////////////////////////////////////////////////////////////////////////////////// // ATTENTION: add new properties to end of list just ABOVE this line PROP_AFTER_LAST_ITEM, diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index b8396e7357..f5117dddc0 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1765,3 +1765,31 @@ glm::mat4 EntityScriptingInterface::getEntityLocalTransform(const QUuid& entityI } return result; } + +bool EntityScriptingInterface::verifyStaticCertificateProperties(const QUuid& entityID) { + bool result = false; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + result = entity->verifyStaticCertificateProperties(); + } + }); + } + return result; +} + +#ifdef DEBUG_CERT +QString EntityScriptingInterface::computeCertificateID(const QUuid& entityID) { + QString result { "" }; + if (_entityTree) { + _entityTree->withReadLock([&] { + EntityItemPointer entity = _entityTree->findEntityByEntityItemID(EntityItemID(entityID)); + if (entity) { + result = entity->computeCertificateID(); + } + }); + } + return result; +} +#endif \ No newline at end of file diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h index e594f555d1..989d3dd89d 100644 --- a/libraries/entities/src/EntityScriptingInterface.h +++ b/libraries/entities/src/EntityScriptingInterface.h @@ -386,6 +386,11 @@ public slots: */ Q_INVOKABLE glm::mat4 getEntityLocalTransform(const QUuid& entityID); + Q_INVOKABLE bool verifyStaticCertificateProperties(const QUuid& entityID); +#ifdef DEBUG_CERT + Q_INVOKABLE QString computeCertificateID(const QUuid& entityID); +#endif + signals: void collisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, const Collision& collision); diff --git a/libraries/entities/src/ModelEntityItem.cpp b/libraries/entities/src/ModelEntityItem.cpp index b02cf04651..14813a68fe 100644 --- a/libraries/entities/src/ModelEntityItem.cpp +++ b/libraries/entities/src/ModelEntityItem.cpp @@ -215,16 +215,18 @@ void ModelEntityItem::debugDump() const { } void ModelEntityItem::setShapeType(ShapeType type) { - if (type != _shapeType) { - if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { - // dynamic and STATIC_MESH are incompatible - // since the shape is being set here we clear the dynamic bit - _dynamic = false; - _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + withWriteLock([&] { + if (type != _shapeType) { + if (type == SHAPE_TYPE_STATIC_MESH && _dynamic) { + // dynamic and STATIC_MESH are incompatible + // since the shape is being set here we clear the dynamic bit + _dynamic = false; + _dirtyFlags |= Simulation::DIRTY_MOTION_TYPE; + } + _shapeType = type; + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; } - _shapeType = type; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + }); } ShapeType ModelEntityItem::getShapeType() const { @@ -257,13 +259,15 @@ void ModelEntityItem::setModelURL(const QString& url) { } void ModelEntityItem::setCompoundShapeURL(const QString& url) { - if (_compoundShapeURL != url) { - ShapeType oldType = computeTrueShapeType(); - _compoundShapeURL = url; - if (oldType != computeTrueShapeType()) { - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + withWriteLock([&] { + if (_compoundShapeURL.get() != url) { + ShapeType oldType = computeTrueShapeType(); + _compoundShapeURL.set(url); + if (oldType != computeTrueShapeType()) { + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } } - } + }); } void ModelEntityItem::setAnimationURL(const QString& url) { @@ -492,10 +496,8 @@ bool ModelEntityItem::hasModel() const { return !_modelURL.isEmpty(); }); } -bool ModelEntityItem::hasCompoundShapeURL() const { - return resultWithReadLock([&] { - return !_compoundShapeURL.isEmpty(); - }); +bool ModelEntityItem::hasCompoundShapeURL() const { + return _compoundShapeURL.get().isEmpty(); } QString ModelEntityItem::getModelURL() const { @@ -505,9 +507,7 @@ QString ModelEntityItem::getModelURL() const { } QString ModelEntityItem::getCompoundShapeURL() const { - return resultWithReadLock([&] { - return _compoundShapeURL; - }); + return _compoundShapeURL.get(); } void ModelEntityItem::setColor(const rgbColor& value) { diff --git a/libraries/entities/src/ModelEntityItem.h b/libraries/entities/src/ModelEntityItem.h index c4b3e82f23..7efb493735 100644 --- a/libraries/entities/src/ModelEntityItem.h +++ b/libraries/entities/src/ModelEntityItem.h @@ -14,6 +14,7 @@ #include "EntityItem.h" #include +#include #include "AnimationPropertyGroup.h" class ModelEntityItem : public EntityItem { @@ -153,7 +154,8 @@ protected: rgbColor _color; QString _modelURL; - QString _compoundShapeURL; + + ThreadSafeValueCache _compoundShapeURL; AnimationPropertyGroup _animationProperties; diff --git a/libraries/entities/src/ParticleEffectEntityItem.cpp b/libraries/entities/src/ParticleEffectEntityItem.cpp index 9bbf4323da..c6616f8cd3 100644 --- a/libraries/entities/src/ParticleEffectEntityItem.cpp +++ b/libraries/entities/src/ParticleEffectEntityItem.cpp @@ -633,10 +633,12 @@ void ParticleEffectEntityItem::debugDump() const { } void ParticleEffectEntityItem::setShapeType(ShapeType type) { - if (type != _shapeType) { - _shapeType = type; - _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; - } + withWriteLock([&] { + if (type != _shapeType) { + _shapeType = type; + _dirtyFlags |= Simulation::DIRTY_SHAPE | Simulation::DIRTY_MASS; + } + }); } void ParticleEffectEntityItem::setMaxParticles(quint32 maxParticles) { diff --git a/libraries/entities/src/ZoneEntityItem.h b/libraries/entities/src/ZoneEntityItem.h index 14e7cd2f40..c3be53599c 100644 --- a/libraries/entities/src/ZoneEntityItem.h +++ b/libraries/entities/src/ZoneEntityItem.h @@ -56,7 +56,7 @@ public: static void setDrawZoneBoundaries(bool value) { _drawZoneBoundaries = value; } virtual bool isReadyToComputeShape() const override { return false; } - void setShapeType(ShapeType type) override { _shapeType = type; } + void setShapeType(ShapeType type) override { withWriteLock([&] { _shapeType = type; }); } virtual ShapeType getShapeType() const override; virtual bool hasCompoundShapeURL() const; diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index b95f2ff6a0..2f3f65c90b 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -30,7 +30,7 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::EntityEdit: case PacketType::EntityData: case PacketType::EntityPhysics: - return VERSION_ENTITIES_ANIMATION_ALLOW_TRANSLATION_PROPERTIES; + return VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES; case PacketType::EntityQuery: return static_cast(EntityQueryPacketVersion::JSONFilterWithFamilyTree); case PacketType::AvatarIdentity: diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index f492e42460..122be60870 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -267,6 +267,7 @@ const PacketVersion VERSION_ENTITIES_BULLET_DYNAMICS = 70; const PacketVersion VERSION_ENTITIES_HAS_SHOULD_HIGHLIGHT = 71; const PacketVersion VERSION_ENTITIES_HAS_HIGHLIGHT_SCRIPTING_INTERFACE = 72; const PacketVersion VERSION_ENTITIES_ANIMATION_ALLOW_TRANSLATION_PROPERTIES = 73; +const PacketVersion VERSION_ENTITIES_HAS_CERTIFICATE_PROPERTIES = 74; enum class EntityQueryPacketVersion: PacketVersion { JSONFilter = 18, diff --git a/libraries/render/src/render/DrawSceneOctree.cpp b/libraries/render/src/render/DrawSceneOctree.cpp index de5ff1f74c..36663a454a 100644 --- a/libraries/render/src/render/DrawSceneOctree.cpp +++ b/libraries/render/src/render/DrawSceneOctree.cpp @@ -151,7 +151,7 @@ void DrawSceneOctree::run(const RenderContextPointer& renderContext, const ItemS float angle = glm::degrees(getAccuracyAngle(args->_sizeScale, args->_boundaryLevelAdjust)); Transform crosshairModel; crosshairModel.setTranslation(glm::vec3(0.0, 0.0, -1000.0)); - crosshairModel.setScale(1000.0 * tan(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO + crosshairModel.setScale(1000.0f * tanf(glm::radians(angle))); // Scaling at the actual tan of the lod angle => Multiplied by TWO batch.resetViewTransform(); batch.setModelTransform(crosshairModel); batch.setPipeline(getDrawLODReticlePipeline()); diff --git a/libraries/shared/src/shared/Camera.cpp b/libraries/shared/src/shared/Camera.cpp index 48fea9e835..ab841c4717 100644 --- a/libraries/shared/src/shared/Camera.cpp +++ b/libraries/shared/src/shared/Camera.cpp @@ -45,7 +45,7 @@ Camera::Camera() : { } -void Camera::update(float deltaTime) { +void Camera::update() { if (_isKeepLookingAt) { lookAt(_lookingAt); } diff --git a/libraries/shared/src/shared/Camera.h b/libraries/shared/src/shared/Camera.h index c7b943f0dd..3ad08bd719 100644 --- a/libraries/shared/src/shared/Camera.h +++ b/libraries/shared/src/shared/Camera.h @@ -53,7 +53,7 @@ public: void initialize(); // instantly put the camera at the ideal position and orientation. - void update( float deltaTime ); + void update(); CameraMode getMode() const { return _mode; } void setMode(CameraMode m); diff --git a/libraries/shared/src/shared/RateCounter.h b/libraries/shared/src/shared/RateCounter.h index 3cf509b6bf..208446e9b1 100644 --- a/libraries/shared/src/shared/RateCounter.h +++ b/libraries/shared/src/shared/RateCounter.h @@ -35,19 +35,19 @@ public: uint32_t interval() const { return INTERVAL; } private: - mutable uint64_t _start { usecTimestampNow() }; + mutable uint64_t _expiry { usecTimestampNow() + INTERVAL * USECS_PER_MSEC}; mutable size_t _count { 0 }; const float _scale { powf(10, PRECISION) }; mutable std::atomic _rate; void checkRate() const { auto now = usecTimestampNow(); - float currentIntervalMs = (now - _start) / (float)USECS_PER_MSEC; - if (currentIntervalMs > (float)INTERVAL) { - float currentCount = _count; - float intervalSeconds = currentIntervalMs / (float)MSECS_PER_SECOND; - _rate = roundf(currentCount / intervalSeconds * _scale) / _scale; - _start = now; + if (now > _expiry) { + float MSECS_PER_USEC = 0.001f; + float SECS_PER_MSEC = 0.001f; + float intervalSeconds = ((float)INTERVAL + (float)(now - _expiry) * MSECS_PER_USEC) * SECS_PER_MSEC; + _rate = roundf((float)_count / intervalSeconds * _scale) / _scale; + _expiry = now + INTERVAL * USECS_PER_MSEC; _count = 0; }; } diff --git a/libraries/ui/src/OffscreenUi.cpp b/libraries/ui/src/OffscreenUi.cpp index 9dfe831081..297ed9ca50 100644 --- a/libraries/ui/src/OffscreenUi.cpp +++ b/libraries/ui/src/OffscreenUi.cpp @@ -136,6 +136,14 @@ void OffscreenUi::toggle(const QUrl& url, const QString& name, std::functionfindChild(name); if (item) { diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 391d7da6c7..93c55d06f7 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -78,6 +78,7 @@ public: bool eventFilter(QObject* originalDestination, QEvent* event) override; void addMenuInitializer(std::function f); QObject* getFlags(); + Q_INVOKABLE bool isPointOnDesktopWindow(QVariant point); QQuickItem* getDesktop(); QQuickItem* getToolWindow(); QObject* getRootMenu(); diff --git a/libraries/ui/src/ui/types/RequestFilters.cpp b/libraries/ui/src/ui/types/RequestFilters.cpp index 233a9458fe..f9686d2311 100644 --- a/libraries/ui/src/ui/types/RequestFilters.cpp +++ b/libraries/ui/src/ui/types/RequestFilters.cpp @@ -61,7 +61,7 @@ void RequestFilters::interceptHFWebEngineRequest(QWebEngineUrlRequestInfo& info) // During the period in which we have HFC commerce in the system, but not applied everywhere: const QString tokenStringCommerce{ "Chrome/48.0 (HighFidelityInterface WithHFC)" }; - static Setting::Handle _settingSwitch{ "commerce", false }; + Setting::Handle _settingSwitch{ "commerce", false }; bool isMoney = _settingSwitch.get(); const QString tokenString = !isAuthable ? tokenStringMobile : (isMoney ? tokenStringCommerce : tokenStringMetaverse); diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 0e58d1ecd3..7553ca4eeb 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -78,6 +78,7 @@ onButtonClicked(); break; case 'walletReset': + Settings.setValue("isFirstUseOfPurchases", true); onButtonClicked(); onButtonClicked(); break; diff --git a/scripts/system/controllers/controllerModules/farActionGrabEntity.js b/scripts/system/controllers/controllerModules/farActionGrabEntity.js index c5b82f75f0..0ef0e67471 100644 --- a/scripts/system/controllers/controllerModules/farActionGrabEntity.js +++ b/scripts/system/controllers/controllerModules/farActionGrabEntity.js @@ -108,13 +108,17 @@ Script.include("/~/system/libraries/controllers.js"); "userData" ]; - + var MARGIN = 25; function FarActionGrabEntity(hand) { this.hand = hand; this.grabbedThingID = null; this.actionID = null; // action this script created... this.entityWithContextOverlay = false; this.contextOverlayTimer = false; + this.reticleMinX = MARGIN; + this.reticleMaxX; + this.reticleMinY = MARGIN; + this.reticleMaxY; var ACTION_TTL = 15; // seconds @@ -344,12 +348,28 @@ Script.include("/~/system/libraries/controllers.js"); this.grabbedThingID = null; }; + this.updateRecommendedArea = function() { + var dims = Controller.getViewportDimensions(); + this.reticleMaxX = dims.x - MARGIN; + this.reticleMaxY = dims.y - MARGIN; + }; + + this.calculateNewReticlePosition = function(intersection) { + this.updateRecommendedArea(); + var point2d = HMD.overlayFromWorldPoint(intersection); + point2d.x = Math.max(this.reticleMinX, Math.min(point2d.x, this.reticleMaxX)); + point2d.y = Math.max(this.reticleMinY, Math.min(point2d.y, this.reticleMaxY)); + return point2d; + }; + this.notPointingAtEntity = function(controllerData) { var intersection = controllerData.rayPicks[this.hand]; var entityProperty = Entities.getEntityProperties(intersection.objectID); var entityType = entityProperty.type; + var hudRayPick = controllerData.hudRayPicks[this.hand]; + var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); if ((intersection.type === RayPick.INTERSECTED_ENTITY && entityType === "Web") || - intersection.type === RayPick.INTERSECTED_OVERLAY) { + intersection.type === RayPick.INTERSECTED_OVERLAY || Window.isPointOnDesktopWindow(point2d)) { return true; } return false; diff --git a/scripts/system/controllers/controllerModules/hudOverlayPointer.js b/scripts/system/controllers/controllerModules/hudOverlayPointer.js index 487e491201..912eeccadb 100644 --- a/scripts/system/controllers/controllerModules/hudOverlayPointer.js +++ b/scripts/system/controllers/controllerModules/hudOverlayPointer.js @@ -178,11 +178,11 @@ } var hudRayPick = controllerData.hudRayPicks[this.hand]; var point2d = this.calculateNewReticlePosition(hudRayPick.intersection); - this.setReticlePosition(point2d); - if (!Reticle.isPointingAtSystemOverlay(point2d)) { + if (!Window.isPointOnDesktopWindow(point2d) && !this.triggerClicked) { this.exitModule(); return false; } + this.setReticlePosition(point2d); Reticle.visible = false; this.movedAway = false; this.triggerClicked = controllerData.triggerClicks[this.hand]; @@ -239,7 +239,7 @@ function cleanup() { ControllerDispatcherUtils.disableDispatcherModule("LeftHudOverlayPointer"); - ControllerDispatcherUtils.disbaleDispatcherModule("RightHudOverlayPointer"); + ControllerDispatcherUtils.disableDispatcherModule("RightHudOverlayPointer"); } Script.scriptEnding.connect(cleanup); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 0c82ab4343..ded4542c51 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -28,6 +28,7 @@ var confirmAllPurchases = false; // Set this to "true" to cause Checkout.qml to popup for all items, even if free var userIsLoggedIn = false; + var walletNeedsSetup = false; function injectCommonCode(isDirectoryPage) { @@ -91,6 +92,48 @@ }); } + emitWalletSetupEvent = function() { + EventBridge.emitWebEvent(JSON.stringify({ + type: "WALLET_SETUP" + })); + } + + function maybeAddSetupWalletButton() { + if (userIsLoggedIn && walletNeedsSetup) { + var resultsElement = document.getElementById('results'); + var setupWalletElement = document.createElement('div'); + setupWalletElement.classList.add("row"); + setupWalletElement.id = "setupWalletDiv"; + setupWalletElement.style = "height:60px;margin:20px 10px 10px 10px;padding:12px 5px;" + + "background-color:#D6F4D8;border-color:#aee9b2;border-width:2px;border-style:solid;border-radius:5px;"; + + var span = document.createElement('span'); + span.style = "margin:10px 5px;color:#1b6420;font-size:15px;"; + span.innerHTML = "Setup your Wallet to get money and shop in Marketplace."; + + var xButton = document.createElement('a'); + xButton.id = "xButton"; + xButton.setAttribute('href', "#"); + xButton.style = "width:50px;height:100%;margin:0;color:#ccc;font-size:20px;"; + xButton.innerHTML = "X"; + xButton.onclick = function () { + setupWalletElement.remove(); + dummyRow.remove(); + }; + + setupWalletElement.appendChild(span); + setupWalletElement.appendChild(xButton); + + resultsElement.insertBefore(setupWalletElement, resultsElement.firstChild); + + // Dummy row for padding + var dummyRow = document.createElement('div'); + dummyRow.classList.add("row"); + dummyRow.style = "height:15px;"; + resultsElement.insertBefore(dummyRow, resultsElement.firstChild); + } + } + function maybeAddLogInButton() { if (!userIsLoggedIn) { var resultsElement = document.getElementById('results'); @@ -149,7 +192,7 @@ var dropDownElement = document.getElementById('user-dropdown'); purchasesElement.id = "purchasesButton"; purchasesElement.setAttribute('href', "#"); - purchasesElement.innerHTML = "MY PURCHASES"; + purchasesElement.innerHTML = "My Purchases"; // FRONTEND WEBDEV RANT: The username dropdown should REALLY not be programmed to be on the same // line as the search bar, overlaid on top of the search bar, floated right, and then relatively bumped up using "top:-50px". purchasesElement.style = "height:100%;margin-top:18px;font-weight:bold;float:right;margin-right:" + (dropDownElement.offsetWidth + 30) + @@ -164,12 +207,34 @@ } } + function changeDropdownMenu() { + var logInOrOutButton = document.createElement('a'); + logInOrOutButton.id = "logInOrOutButton"; + logInOrOutButton.setAttribute('href', "#"); + logInOrOutButton.innerHTML = userIsLoggedIn ? "Log Out" : "Log In"; + logInOrOutButton.onclick = function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "LOGIN" + })); + }; + + $($('.dropdown-menu').find('li')[0]).append(logInOrOutButton); + + $('a[href="/marketplace?view=mine"]').each(function () { + $(this).attr('href', '#'); + $(this).on('click', function () { + EventBridge.emitWebEvent(JSON.stringify({ + type: "MY_ITEMS" + })); + }); + }); + } + function buyButtonClicked(id, name, author, price, href) { EventBridge.emitWebEvent(JSON.stringify({ type: "CHECKOUT", itemId: id, itemName: name, - itemAuthor: author, itemPrice: price ? parseInt(price, 10) : 0, itemHref: href })); @@ -235,9 +300,13 @@ } function injectHiFiCode() { - if (confirmAllPurchases) { + if (!$('body').hasClass("code-injected") && confirmAllPurchases) { + + $('body').addClass("code-injected"); maybeAddLogInButton(); + maybeAddSetupWalletButton(); + changeDropdownMenu(); var target = document.getElementById('templated-items'); // MutationObserver is necessary because the DOM is populated after the page is loaded. @@ -260,9 +329,13 @@ } function injectHiFiItemPageCode() { - if (confirmAllPurchases) { + if (!$('body').hasClass("code-injected") && confirmAllPurchases) { + + $('body').addClass("code-injected"); maybeAddLogInButton(); + maybeAddSetupWalletButton(); + changeDropdownMenu(); var purchaseButton = $('#side-info').find('.btn').first(); @@ -551,7 +624,8 @@ if (parsedJsonMessage.type === "marketplaces") { if (parsedJsonMessage.action === "commerceSetting") { confirmAllPurchases = !!parsedJsonMessage.data.commerceMode; - userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn + userIsLoggedIn = !!parsedJsonMessage.data.userIsLoggedIn; + walletNeedsSetup = !!parsedJsonMessage.data.walletNeedsSetup; injectCode(); } } @@ -567,4 +641,5 @@ // Load / unload. window.addEventListener("load", onLoad); // More robust to Web site issues than using $(document).ready(). + window.addEventListener("page:change", onLoad); // Triggered after Marketplace HTML is changed }()); diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 5a00b20441..e94b227a4a 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -19,7 +19,8 @@ var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + "?"; // Append "?" to signal injected script that it's the initial page. var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); - var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/checkout/Checkout.qml"; + var MARKETPLACE_CHECKOUT_QML_PATH_BASE = "qml/hifi/commerce/checkout/Checkout.qml"; + var MARKETPLACE_CHECKOUT_QML_PATH = Script.resourcesPath() + MARKETPLACE_CHECKOUT_QML_PATH_BASE; var MARKETPLACE_PURCHASES_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = Script.resourcesPath() + "qml/hifi/commerce/wallet/Wallet.qml"; var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "commerce/inspectionCertificate/InspectionCertificate.qml"; @@ -60,6 +61,7 @@ var onCommerceScreen = false; var debugCheckout = false; + var debugError = false; function showMarketplace() { if (!debugCheckout) { UserActivityLogger.openedMarketplace(); @@ -70,8 +72,7 @@ method: 'updateCheckoutQML', params: { itemId: '0d90d21c-ce7a-4990-ad18-e9d2cf991027', itemName: 'Test Flaregun', - itemAuthor: 'hifiDave', - itemPrice: 17, + itemPrice: (debugError ? 10 : 17), itemHref: 'http://mpassets.highfidelity.com/0d90d21c-ce7a-4990-ad18-e9d2cf991027-v1/flaregun.json', }, canRezCertifiedItems: Entities.canRezCertified || Entities.canRezTmpCertified @@ -106,8 +107,8 @@ var referrerURL; // Used for updating Purchases QML var filterText; // Used for updating Purchases QML function onScreenChanged(type, url) { - onMarketplaceScreen = type === "Web" && url === MARKETPLACE_URL_INITIAL; - onCommerceScreen = type === "QML" && (url === MARKETPLACE_CHECKOUT_QML_PATH || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; + onCommerceScreen = type === "QML" && (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH_BASE) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); wireEventBridge(onCommerceScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { @@ -128,12 +129,11 @@ } } - function setCertificateInfo(currentEntityWithContextOverlay, itemMarketplaceId, closeGoesToPurchases) { + function setCertificateInfo(currentEntityWithContextOverlay, itemMarketplaceId) { wireEventBridge(true); tablet.sendToQml({ method: 'inspectionCertificate_setMarketplaceId', - marketplaceId: itemMarketplaceId || Entities.getEntityProperties(currentEntityWithContextOverlay, ['marketplaceID']).marketplaceID, - closeGoesToPurchases: closeGoesToPurchases + marketplaceId: itemMarketplaceId || Entities.getEntityProperties(currentEntityWithContextOverlay, ['marketplaceID']).marketplaceID }); // ZRF FIXME! Make a call to the endpoint to get item info instead of this silliness Script.setTimeout(function () { @@ -203,7 +203,8 @@ action: "commerceSetting", data: { commerceMode: Settings.getValue("commerce", false), - userIsLoggedIn: Account.loggedIn + userIsLoggedIn: Account.loggedIn, + walletNeedsSetup: Wallet.walletStatus === 1 } })); } else if (parsedJsonMessage.type === "PURCHASES") { @@ -212,6 +213,16 @@ tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); } else if (parsedJsonMessage.type === "LOGIN") { openLoginWindow(); + } else if (parsedJsonMessage.type === "WALLET_SETUP") { + tablet.pushOntoStack(MARKETPLACE_WALLET_QML_PATH); + } else if (parsedJsonMessage.type === "MY_ITEMS") { + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + wireEventBridge(true); + tablet.sendToQml({ + method: 'purchases_showMyItems' + }); } } } @@ -325,36 +336,26 @@ case 'maybeEnableHmdPreview': Menu.setIsOptionChecked("Disable Preview", isHmdPreviewDisabled); break; - case 'purchases_getIsFirstUse': - tablet.sendToQml({ - method: 'purchases_getIsFirstUseResult', - isFirstUseOfPurchases: Settings.getValue("isFirstUseOfPurchases", true) - }); - break; - case 'purchases_setIsFirstUse': - Settings.setValue("isFirstUseOfPurchases", false); - break; case 'purchases_openGoTo': tablet.loadQMLSource("TabletAddressDialog.qml"); break; case 'purchases_itemCertificateClicked': - tablet.loadQMLSource("../commerce/inspectionCertificate/InspectionCertificate.qml"); - setCertificateInfo("", message.itemMarketplaceId, true); + setCertificateInfo("", message.itemMarketplaceId); break; case 'inspectionCertificate_closeClicked': - if (message.closeGoesToPurchases) { - referrerURL = MARKETPLACE_URL_INITIAL; - filterText = ""; - tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); - } else { - tablet.gotoHomeScreen(); - } + tablet.gotoHomeScreen(); break; case 'inspectionCertificate_showInMarketplaceClicked': tablet.gotoWebScreen(MARKETPLACE_URL + '/items/' + message.itemId, MARKETPLACES_INJECT_SCRIPT_URL); break; case 'header_myItemsClicked': - tablet.gotoWebScreen(MARKETPLACE_URL + '?view=mine', MARKETPLACES_INJECT_SCRIPT_URL); + referrerURL = MARKETPLACE_URL_INITIAL; + filterText = ""; + tablet.pushOntoStack(MARKETPLACE_PURCHASES_QML_PATH); + wireEventBridge(true); + tablet.sendToQml({ + method: 'purchases_showMyItems' + }); break; default: print('Unrecognized message from Checkout.qml or Purchases.qml: ' + JSON.stringify(message));