diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml new file mode 100644 index 0000000000..8f391f24c0 --- /dev/null +++ b/interface/resources/qml/hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml @@ -0,0 +1,290 @@ +// +// marketplaceItemTester +// qml/hifi/commerce/marketplaceItemTester +// +// Load items not in the marketplace for testing purposes +// +// Created by Zach Fox on 2018-09-05 +// Copyright 2018 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import QtQuick.Controls.Styles 1.4 +import QtQuick.Dialogs 1.0 +import QtQuick.Layouts 1.1 +import Hifi 1.0 as Hifi +import "../../../styles-uit" as HifiStylesUit +import "../../../controls-uit" as HifiControlsUit + + + +Rectangle { + id: root + + property string installedApps + property var nextResourceObjectId: 0 + signal sendToScript(var message) + + HifiStylesUit.HifiConstants { id: hifi } + ListModel { id: resourceListModel } + + color: hifi.colors.white + + AnimatedImage { + id: spinner; + source: "spinner.gif" + width: 74; + height: width; + anchors.verticalCenter: parent.verticalCenter; + anchors.horizontalCenter: parent.horizontalCenter; + } + + function fromScript(message) { + switch (message.method) { + case "newResourceObjectInTest": + var resourceObject = message.resourceObject; + resourceListModel.append(resourceObject); + spinner.visible = false; + break; + case "nextObjectIdInTest": + nextResourceObjectId = message.id; + spinner.visible = false; + break; + } + } + + function buildResourceObj(resource) { + resource = resource.trim(); + var assetType = (resource.match(/\.app\.json$/) ? "application" : + resource.match(/\.fst$/) ? "avatar" : + resource.match(/\.json\.gz$/) ? "content set" : + resource.match(/\.json$/) ? "entity or wearable" : + "unknown"); + return { "id": nextResourceObjectId++, + "resource": resource, + "assetType": assetType }; + } + + function installResourceObj(resourceObj) { + if ("application" === resourceObj.assetType) { + Commerce.installApp(resourceObj.resource); + } + } + + function addAllInstalledAppsToList() { + var i, apps = Commerce.getInstalledApps().split(","), len = apps.length; + for(i = 0; i < len - 1; ++i) { + if (i in apps) { + resourceListModel.append(buildResourceObj(apps[i])); + } + } + } + + function toUrl(resource) { + var httpPattern = /^http/i; + return httpPattern.test(resource) ? resource : "file:///" + resource; + } + + function rezEntity(resource, entityType) { + sendToScript({ + method: 'tester_rezClicked', + itemHref: toUrl(resource), + itemType: entityType}); + } + + ListView { + anchors.fill: parent + anchors.leftMargin: 12 + anchors.bottomMargin: 40 + anchors.rightMargin: 12 + model: resourceListModel + spacing: 5 + interactive: false + + delegate: RowLayout { + anchors.left: parent.left + width: parent.width + spacing: 5 + + property var actions: { + "forward": function(resource, assetType){ + switch(assetType) { + case "application": + Commerce.openApp(resource); + break; + case "avatar": + MyAvatar.useFullAvatarURL(resource); + break; + case "content set": + urlHandler.handleUrl("hifi://localhost/0,0,0"); + Commerce.replaceContentSet(toUrl(resource), ""); + break; + case "entity": + case "wearable": + rezEntity(resource, assetType); + break; + default: + print("Marketplace item tester unsupported assetType " + assetType); + } + }, + "trash": function(){ + if ("application" === assetType) { + Commerce.uninstallApp(resource); + } + sendToScript({ + method: "tester_deleteResourceObject", + objectId: resourceListModel.get(index).id}); + resourceListModel.remove(index); + } + } + + Column { + Layout.preferredWidth: root.width * .6 + spacing: 5 + Text { + text: { + var match = resource.match(/\/([^/]*)$/); + return match ? match[1] : resource; + } + font.pointSize: 12 + horizontalAlignment: Text.AlignBottom + } + Text { + text: resource + font.pointSize: 8 + width: root.width * .6 + horizontalAlignment: Text.AlignBottom + wrapMode: Text.WrapAnywhere + } + } + + ComboBox { + id: comboBox + + Layout.preferredWidth: root.width * .2 + + model: [ + "application", + "avatar", + "content set", + "entity", + "wearable", + "unknown" + ] + + currentIndex: (("entity or wearable" === assetType) ? + model.indexOf("unknown") : model.indexOf(assetType)) + + Component.onCompleted: { + onCurrentIndexChanged.connect(function() { + assetType = model[currentIndex]; + sendToScript({ + method: "tester_updateResourceObjectAssetType", + objectId: resourceListModel.get(index)["id"], + assetType: assetType }); + }); + } + } + + Repeater { + model: [ "forward", "trash" ] + + HifiStylesUit.HiFiGlyphs { + property var glyphs: { + "application": hifi.glyphs.install, + "avatar": hifi.glyphs.avatar, + "content set": hifi.glyphs.globe, + "entity": hifi.glyphs.wand, + "trash": hifi.glyphs.trash, + "unknown": hifi.glyphs.circleSlash, + "wearable": hifi.glyphs.hat, + } + text: (("trash" === modelData) ? + glyphs.trash : + glyphs[comboBox.model[comboBox.currentIndex]]) + size: ("trash" === modelData) ? 22 : 30 + color: hifi.colors.black + horizontalAlignment: Text.AlignHCenter + MouseArea { + anchors.fill: parent + onClicked: { + actions[modelData](resource, comboBox.currentText); + } + } + } + } + } + + headerPositioning: ListView.OverlayHeader + header: HifiStylesUit.RalewayRegular { + id: rootHeader + text: "Marketplace Item Tester" + height: 80 + width: paintedWidth + size: 22 + color: hifi.colors.black + anchors.left: parent.left + anchors.leftMargin: 12 + } + + footerPositioning: ListView.OverlayFooter + footer: Row { + id: rootActions + spacing: 20 + anchors.horizontalCenter: parent.horizontalCenter + + property string currentAction + property var actions: { + "Load File": function(){ + rootActions.currentAction = "load file"; + Window.browseChanged.connect(onResourceSelected); + Window.browseAsync("Please select a file (*.app.json *.json *.fst *.json.gz)", "", "Assets (*.app.json *.json *.fst *.json.gz)"); + }, + "Load URL": function(){ + rootActions.currentAction = "load url"; + Window.promptTextChanged.connect(onResourceSelected); + Window.promptAsync("Please enter a URL", ""); + } + } + + function onResourceSelected(resource) { + // It is possible that we received the present signal + // from something other than our browserAsync window. + // Alas, there is nothing we can do about that so charge + // ahead as though we are sure the present signal is one + // we expect. + switch(currentAction) { + case "load file": + Window.browseChanged.disconnect(onResourceSelected); + break + case "load url": + Window.promptTextChanged.disconnect(onResourceSelected); + break; + } + if (resource) { + var resourceObj = buildResourceObj(resource); + installResourceObj(resourceObj); + sendToScript({ + method: 'tester_newResourceObject', + resourceObject: resourceObj }); + } + } + + Repeater { + model: [ "Load File", "Load URL" ] + HifiControlsUit.Button { + color: hifi.buttons.blue + fontSize: 20 + text: modelData + width: root.width / 3 + height: 40 + onClicked: actions[text]() + } + } + } + } +} diff --git a/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif new file mode 100644 index 0000000000..00f75ae62f Binary files /dev/null and b/interface/resources/qml/hifi/commerce/marketplaceItemTester/spinner.gif differ diff --git a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml index 032d9b0199..eeb9ac3c54 100644 --- a/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml +++ b/interface/resources/qml/hifi/commerce/purchases/PurchasedItem.qml @@ -59,7 +59,7 @@ Item { Connections { target: Commerce; - + onContentSetChanged: { if (contentSetHref === root.itemHref) { showConfirmation = true; @@ -135,7 +135,7 @@ Item { anchors.topMargin: 8; width: 30; height: width; - + HiFiGlyphs { id: closeContextMenuGlyph; text: hifi.glyphs.close; @@ -376,7 +376,7 @@ Item { } } } - + transform: Rotation { id: rotation; origin.x: flipable.width/2; @@ -509,7 +509,7 @@ Item { } verticalAlignment: Text.AlignTop; } - + HiFiGlyphs { id: statusIcon; text: { @@ -588,7 +588,7 @@ Item { border.width: 1; border.color: "#E2334D"; } - + HiFiGlyphs { id: contextMenuGlyph; text: hifi.glyphs.verticalEllipsis; @@ -615,7 +615,7 @@ Item { } } } - + Rectangle { id: rezzedNotifContainer; z: 998; @@ -663,13 +663,13 @@ Item { Tablet.playSound(TabletEnums.ButtonHover); } } - + onFocusChanged: { if (focus) { Tablet.playSound(TabletEnums.ButtonHover); } } - + onClicked: { Tablet.playSound(TabletEnums.ButtonClick); if (root.itemType === "contentSet") { @@ -775,7 +775,7 @@ Item { // Style color: hifi.colors.redAccent; horizontalAlignment: Text.AlignRight; - + MouseArea { anchors.fill: parent; hoverEnabled: true; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index ce51f4b5ce..3a34c87ef1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -1691,21 +1691,21 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo return DependencyManager::get()->navigationFocused() ? 1 : 0; }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_WINDOWS, []() -> float { -#if defined(Q_OS_WIN) +#if defined(Q_OS_WIN) return 1; #else return 0; #endif }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_MAC, []() -> float { -#if defined(Q_OS_MAC) +#if defined(Q_OS_MAC) return 1; #else return 0; #endif }); _applicationStateDevice->setInputVariant(STATE_PLATFORM_ANDROID, []() -> float { -#if defined(Q_OS_ANDROID) +#if defined(Q_OS_ANDROID) return 1; #else return 0; @@ -2883,9 +2883,10 @@ void Application::initializeUi() { QUrl{ "hifi/commerce/common/CommerceLightbox.qml" }, QUrl{ "hifi/commerce/common/EmulatedMarketplaceHeader.qml" }, QUrl{ "hifi/commerce/common/FirstUseTutorial.qml" }, - QUrl{ "hifi/commerce/common/SortableListModel.qml" }, QUrl{ "hifi/commerce/common/sendAsset/SendAsset.qml" }, + QUrl{ "hifi/commerce/common/SortableListModel.qml" }, QUrl{ "hifi/commerce/inspectionCertificate/InspectionCertificate.qml" }, + QUrl{ "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"}, QUrl{ "hifi/commerce/purchases/PurchasedItem.qml" }, QUrl{ "hifi/commerce/purchases/Purchases.qml" }, QUrl{ "hifi/commerce/wallet/Help.qml" }, diff --git a/interface/src/commerce/QmlCommerce.cpp b/interface/src/commerce/QmlCommerce.cpp index 06da18148f..7c5df0f3e3 100644 --- a/interface/src/commerce/QmlCommerce.cpp +++ b/interface/src/commerce/QmlCommerce.cpp @@ -199,12 +199,18 @@ void QmlCommerce::transferAssetToUsername(const QString& username, } void QmlCommerce::replaceContentSet(const QString& itemHref, const QString& certificateID) { - auto ledger = DependencyManager::get(); - ledger->updateLocation(certificateID, DependencyManager::get()->getPlaceName(), true); + if (!certificateID.isEmpty()) { + auto ledger = DependencyManager::get(); + ledger->updateLocation( + certificateID, + DependencyManager::get()->getPlaceName(), + true); + } qApp->replaceDomainContent(itemHref); - QJsonObject messageProperties = { { "status", "SuccessfulRequestToReplaceContent" }, { "content_set_url", itemHref } }; + QJsonObject messageProperties = { + { "status", "SuccessfulRequestToReplaceContent" }, + { "content_set_url", itemHref } }; UserActivityLogger::getInstance().logAction("replace_domain_content", messageProperties); - emit contentSetChanged(itemHref); } @@ -228,6 +234,7 @@ QString QmlCommerce::getInstalledApps(const QString& justInstalledAppID) { // Thus, we protect against deleting the .app.json from the user's disk (below) // by skipping that check for the app we just installed. if ((justInstalledAppID != "") && ((justInstalledAppID + ".app.json") == appFileName)) { + installedAppsFromMarketplace += appFileName + ","; continue; } diff --git a/scripts/system/commerce/wallet.js b/scripts/system/commerce/wallet.js index 993ea30c2e..b12191b00c 100644 --- a/scripts/system/commerce/wallet.js +++ b/scripts/system/commerce/wallet.js @@ -500,6 +500,35 @@ function walletClosed() { // // Manage the connection between the button and the window. // +var DEVELOPER_MENU = "Developer"; +var MARKETPLACE_ITEM_TESTER_LABEL = "Marketplace Item Tester"; +var MARKETPLACE_ITEM_TESTER_QML_SOURCE = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; +function installMarketplaceItemTester() { + if (!Menu.menuExists(DEVELOPER_MENU)) { + Menu.addMenu(DEVELOPER_MENU); + } + if (!Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL)) { + Menu.addMenuItem({ menuName: DEVELOPER_MENU, + menuItemName: MARKETPLACE_ITEM_TESTER_LABEL, + isCheckable: false }) + } + + Menu.menuItemEvent.connect(function (menuItem) { + if (menuItem === MARKETPLACE_ITEM_TESTER_LABEL) { + var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system"); + tablet.loadQMLSource(MARKETPLACE_ITEM_TESTER_QML_SOURCE); + } + }); +} + +function uninstallMarketplaceItemTester() { + if (Menu.menuExists(DEVELOPER_MENU) && + Menu.menuItemExists(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL) + ) { + Menu.removeMenuItem(DEVELOPER_MENU, MARKETPLACE_ITEM_TESTER_LABEL); + } +} + var BUTTON_NAME = "WALLET"; var WALLET_QML_SOURCE = "hifi/commerce/wallet/Wallet.qml"; var ui; @@ -513,7 +542,9 @@ function startup() { onMessage: fromQml }); GlobalServices.myUsernameChanged.connect(onUsernameChanged); + installMarketplaceItemTester(); } + var isUpdateOverlaysWired = false; function off() { Users.usernameFromIDReply.disconnect(usernameFromIDReply); @@ -528,9 +559,11 @@ function off() { } removeOverlays(); } + function shutdown() { GlobalServices.myUsernameChanged.disconnect(onUsernameChanged); deleteSendMoneyParticleEffect(); + uninstallMarketplaceItemTester(); off(); } diff --git a/scripts/system/marketplaces/marketplaces.js b/scripts/system/marketplaces/marketplaces.js index 13ad1f6b69..cc5ff99673 100644 --- a/scripts/system/marketplaces/marketplaces.js +++ b/scripts/system/marketplaces/marketplaces.js @@ -20,13 +20,14 @@ var AppUi = Script.require('appUi'); Script.include("/~/system/libraries/gridTool.js"); Script.include("/~/system/libraries/connectionUtils.js"); -var METAVERSE_SERVER_URL = Account.metaverseServerURL; -var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); -var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); var MARKETPLACE_CHECKOUT_QML_PATH = "hifi/commerce/checkout/Checkout.qml"; +var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var MARKETPLACE_ITEM_TESTER_QML_PATH = "hifi/commerce/marketplaceItemTester/MarketplaceItemTester.qml"; var MARKETPLACE_PURCHASES_QML_PATH = "hifi/commerce/purchases/Purchases.qml"; var MARKETPLACE_WALLET_QML_PATH = "hifi/commerce/wallet/Wallet.qml"; -var MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH = "hifi/commerce/inspectionCertificate/InspectionCertificate.qml"; +var MARKETPLACES_INJECT_SCRIPT_URL = Script.resolvePath("../html/js/marketplacesInject.js"); +var MARKETPLACES_URL = Script.resolvePath("../html/marketplaces.html"); +var METAVERSE_SERVER_URL = Account.metaverseServerURL; var REZZING_SOUND = SoundCache.getSound(Script.resolvePath("../assets/sounds/rezzing.wav")); // Event bridge messages. @@ -756,7 +757,7 @@ function deleteSendAssetParticleEffect() { } sendAssetRecipient = null; } - + var savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); var UI_FADE_TIMEOUT_MS = 150; function maybeEnableHMDPreview() { @@ -768,6 +769,13 @@ function maybeEnableHMDPreview() { }, UI_FADE_TIMEOUT_MS); } +var resourceObjectsInTest = []; +function signalNewResourceObjectInTest(resourceObject) { + ui.tablet.sendToQml({ + method: "newResourceObjectInTest", + resourceObject: resourceObject }); +} + var onQmlMessageReceived = function onQmlMessageReceived(message) { if (message.messageSrc === "HTML") { return; @@ -817,8 +825,20 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { break; case 'checkout_rezClicked': case 'purchases_rezClicked': + case 'tester_rezClicked': rezEntity(message.itemHref, message.itemType); break; + case 'tester_newResourceObject': + var resourceObject = message.resourceObject; + resourceObjectsInTest[resourceObject.id] = resourceObject; + signalNewResourceObjectInTest(resourceObject); + break; + case 'tester_updateResourceObjectAssetType': + resourceObjectsInTest[message.objectId].assetType = message.assetType; + break; + case 'tester_deleteResourceObject': + delete resourceObjectsInTest[message.objectId]; + break; case 'header_marketplaceImageClicked': case 'purchases_backClicked': ui.open(message.referrerURL, MARKETPLACES_INJECT_SCRIPT_URL); @@ -841,10 +861,6 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { openLoginWindow(); break; case 'disableHmdPreview': - if (!savedDisablePreviewOption) { - savedDisablePreviewOption = Menu.isOptionChecked("Disable Preview"); - } - if (!savedDisablePreviewOption) { DesktopPreviewProvider.setPreviewDisabledReason("SECURE_SCREEN"); Menu.setIsOptionChecked("Disable Preview", true); @@ -962,34 +978,65 @@ var onQmlMessageReceived = function onQmlMessageReceived(message) { } }; +function pushResourceObjectsInTest() { + var maxObjectId = -1; + for (var objectId in resourceObjectsInTest) { + signalNewResourceObjectInTest(resourceObjectsInTest[objectId]); + maxObjectId = (maxObjectId < objectId) ? parseInt(objectId) : maxObjectId; + } + // N.B. Thinking about removing the following sendToQml? Be sure + // that the marketplace item tester QML has heard from us, at least + // so that it can indicate to the user that all of the resoruce + // objects in test have been transmitted to it. + ui.tablet.sendToQml({ method: "nextObjectIdInTest", id: maxObjectId + 1 }); +} + // Function Name: onTabletScreenChanged() // // Description: // -Called when the TabletScriptingInterface::screenChanged() signal is emitted. The "type" argument can be either the string // value of "Home", "Web", "Menu", "QML", or "Closed". The "url" argument is only valid for Web and QML. -var onMarketplaceScreen = false; -var onWalletScreen = false; + var onCommerceScreen = false; var onInspectionCertificateScreen = false; +var onMarketplaceItemTesterScreen = false; +var onMarketplaceScreen = false; +var onWalletScreen = false; var onTabletScreenChanged = function onTabletScreenChanged(type, url) { ui.setCurrentVisibleScreenMetadata(type, url); onMarketplaceScreen = type === "Web" && url.indexOf(MARKETPLACE_URL) !== -1; onInspectionCertificateScreen = type === "QML" && url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1; var onWalletScreenNow = url.indexOf(MARKETPLACE_WALLET_QML_PATH) !== -1; - var onCommerceScreenNow = type === "QML" && - (url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || url === MARKETPLACE_PURCHASES_QML_PATH || - onInspectionCertificateScreen); + var onCommerceScreenNow = type === "QML" && ( + url.indexOf(MARKETPLACE_CHECKOUT_QML_PATH) !== -1 || + url === MARKETPLACE_PURCHASES_QML_PATH || + url.indexOf(MARKETPLACE_INSPECTIONCERTIFICATE_QML_PATH) !== -1); + var onMarketplaceItemTesterScreenNow = ( + url.indexOf(MARKETPLACE_ITEM_TESTER_QML_PATH) !== -1 || + url === MARKETPLACE_ITEM_TESTER_QML_PATH); - // exiting wallet or commerce screen - if ((!onWalletScreenNow && onWalletScreen) || (!onCommerceScreenNow && onCommerceScreen)) { + if ((!onWalletScreenNow && onWalletScreen) || + (!onCommerceScreenNow && onCommerceScreen) || + (!onMarketplaceItemTesterScreenNow && onMarketplaceScreen) + ) { + // exiting wallet, commerce, or marketplace item tester screen maybeEnableHMDPreview(); } onCommerceScreen = onCommerceScreenNow; onWalletScreen = onWalletScreenNow; - wireQmlEventBridge(onMarketplaceScreen || onCommerceScreen || onWalletScreen); + onMarketplaceItemTesterScreen = onMarketplaceItemTesterScreenNow; + + wireQmlEventBridge( + onMarketplaceScreen || + onCommerceScreen || + onWalletScreen || + onMarketplaceItemTesterScreen); if (url === MARKETPLACE_PURCHASES_QML_PATH) { + // FIXME? Is there a race condition here in which the event + // bridge may not be up yet? If so, Script.setTimeout(..., 750) + // may help avoid the condition. ui.tablet.sendToQml({ method: 'updatePurchases', referrerURL: referrerURL, @@ -1026,6 +1073,14 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) { }); off(); } + + if (onMarketplaceItemTesterScreen) { + // Why setTimeout? The QML event bridge, wired above, requires a + // variable amount of time to come up, in practice less than + // 750ms. + Script.setTimeout(pushResourceObjectsInTest, 750); + } + console.debug(ui.buttonName + " app reports: Tablet screen changed.\nNew screen type: " + type + "\nNew screen URL: " + url + "\nCurrent app open status: " + ui.isOpen + "\n"); };