QML Marketplace support

Support QML UI for the Marketplace as some devices do not handle
web on 3d surfaces.
Checkpoint code
This commit is contained in:
Roxanne Skelly 2019-01-21 13:39:16 -08:00
parent 0ab13f1e48
commit 8010d86210
6 changed files with 565 additions and 104 deletions

View file

@ -0,0 +1,336 @@
//
// Marketplace.qml
// qml/hifi/commerce/marketplace
//
// Marketplace
//
// Created by Roxanne Skelly on 2019-01-18
// Copyright 2019 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
import Hifi 1.0 as Hifi
import QtQuick 2.9
import QtQuick.Controls 2.2
import QtGraphicalEffects 1.0
import stylesUit 1.0
import controlsUit 1.0 as HifiControlsUit
import "../../../controls" as HifiControls
import "../common" as HifiCommerceCommon
import "qrc:////qml//hifi//models" as HifiModels // Absolute path so the same code works everywhere.
import "../common/sendAsset"
import "../.." as HifiCommon
Rectangle {
HifiConstants { id: hifi; }
id: root;
property string activeView: "initialize";
property bool keyboardRaised: false;
property int category_index: -1;
property alias categoryChoices: categoriesModel;
anchors.fill: (typeof parent === undefined) ? undefined : parent;
Component.onDestruction: {
KeyboardScriptingInterface.raised = false;
}
Connections {
target: Marketplace;
onGetMarketplaceCategoriesResult: {
if (result.status !== 'success') {
console.log("Failed to get Marketplace Categories", result.data.message);
} else {
}
}
}
HifiCommerceCommon.CommerceLightbox {
id: lightboxPopup;
visible: false;
anchors.fill: parent;
}
//
// HEADER BAR START
//
Item {
id: header;
visible: true;
width: parent.width;
anchors.left: parent.left;
anchors.top: parent.top;
anchors.right: parent.right;
Item {
id: titleBarContainer;
visible: true;
// Size
width: parent.width;
height: 50;
// Anchors
anchors.left: parent.left;
anchors.top: parent.top;
// Wallet icon
Image {
id: walletIcon;
source: "../../../../images/hifi-logo-blackish.svg";
height: 20
width: walletIcon.height;
anchors.left: parent.left;
anchors.leftMargin: 8;
anchors.verticalCenter: parent.verticalCenter;
visible: true;
}
// Title Bar text
RalewaySemiBold {
id: titleBarText;
text: "Marketplace";
// Text size
size: hifi.fontSizes.overlayTitle;
// Anchors
anchors.top: parent.top;
anchors.left: walletIcon.right;
anchors.leftMargin: 6;
anchors.bottom: parent.bottom;
width: paintedWidth;
// Style
color: hifi.colors.black;
// Alignment
verticalAlignment: Text.AlignVCenter;
}
}
Item {
id: searchBarContainer;
visible: true;
// Size
width: parent.width;
anchors.top: titleBarContainer.bottom;
height: 50;
Rectangle {
id: categoriesButton;
anchors.left: parent.left;
anchors.leftMargin: 10;
anchors.verticalCenter: parent.verticalCenter;
height: 34;
width: categoriesText.width + 25;
color: "white";
radius: 4;
border.width: 1;
border.color: hifi.colors.lightGray;
// Categories Text
RalewayRegular {
id: categoriesText;
text: "Categories";
// Text size
size: 18;
// Style
color: hifi.colors.baseGray;
elide: Text.ElideRight;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
width: Math.min(textMetrics.width + 25, 110);
// Anchors
anchors.centerIn: parent;
rightPadding: 10;
}
HiFiGlyphs {
id: categoriesDropdownIcon;
text: hifi.glyphs.caratDn;
// Size
size: 34;
// Anchors
anchors.right: parent.right;
anchors.rightMargin: -8;
anchors.verticalCenter: parent.verticalCenter;
horizontalAlignment: Text.AlignHCenter;
// Style
color: hifi.colors.baseGray;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: enabled;
onClicked: {
categoriesDropdown.visible = !categoriesDropdown.visible;
}
onEntered: categoriesText.color = hifi.colors.baseGrayShadow;
onExited: categoriesText.color = hifi.colors.baseGray;
}
Component.onCompleted: {
console.log("Getting Marketplace Categories");
console.log(JSON.stringify(Marketplace));
Marketplace.getMarketplaceItems();
}
}
Rectangle {
id: categoriesContainer;
visible: true;
height: 50 * categoriesModel.count;
width: parent.width;
anchors.top: categoriesButton.bottom;
anchors.left: categoriesButton.left;
color: hifi.colors.white;
ListModel {
id: categoriesModel;
}
ListView {
id: dropdownListView;
interactive: false;
anchors.fill: parent;
model: categoriesModel;
delegate: Item {
width: parent.width;
height: 50;
Rectangle {
id: dropDownButton;
color: hifi.colors.white;
width: parent.width;
height: 50;
visible: true;
RalewaySemiBold {
id: dropDownButtonText;
text: model.displayName;
anchors.fill: parent;
anchors.topMargin: 2;
anchors.leftMargin: 12;
color: hifi.colors.baseGray;
horizontalAlignment: Text.AlignLeft;
verticalAlignment: Text.AlignVCenter;
size: 18;
}
MouseArea {
anchors.fill: parent;
hoverEnabled: true;
propagateComposedEvents: false;
onEntered: {
dropDownButton.color = hifi.colors.blueHighlight;
}
onExited: {
dropDownButton.color = hifi.colors.white;
}
onClicked: {
root.category_index = index;
dropdownContainer.visible = false;
}
}
}
Rectangle {
height: 2;
width: parent.width;
color: hifi.colors.lightGray;
visible: model.separator
}
}
}
}
// or
RalewayRegular {
id: orText;
text: "or";
// Text size
size: 18;
// Style
color: hifi.colors.baseGray;
elide: Text.ElideRight;
horizontalAlignment: Text.AlignHCenter;
verticalAlignment: Text.AlignVCenter;
width: Math.min(textMetrics.width + 25, 110);
// Anchors
anchors.left: categoriesButton.right;
rightPadding: 10;
leftPadding: 10;
anchors.verticalCenter: parent.verticalCenter;
}
HifiControlsUit.TextField {
id: searchField;
anchors.verticalCenter: parent.verticalCenter;
anchors.right: parent.right;
anchors.left: orText.right;
anchors.rightMargin: 10;
height: 34;
isSearchField: true;
colorScheme: hifi.colorSchemes.faintGray;
font.family: "Fira Sans"
font.pixelSize: hifi.fontSizes.textFieldInput;
placeholderText: "Search Marketplace";
TextMetrics {
id: primaryFilterTextMetrics;
font.family: "FiraSans Regular";
font.pixelSize: hifi.fontSizes.textFieldInput;
font.capitalization: Font.AllUppercase;
text: root.primaryFilter_displayName;
}
// workaround for https://bugreports.qt.io/browse/QTBUG-49297
Keys.onPressed: {
switch (event.key) {
case Qt.Key_Return:
case Qt.Key_Enter:
event.accepted = true;
// emit accepted signal manually
if (acceptableInput) {
root.accepted();
root.forceActiveFocus();
}
break;
case Qt.Key_Backspace:
if (textField.text === "") {
primaryFilter_index = -1;
}
break;
}
}
onAccepted: {
root.forceActiveFocus();
}
onActiveFocusChanged: {
if (!activeFocus) {
dropdownContainer.visible = false;
}
}
}
}
}
//
// HEADER BAR END
//
DropShadow {
anchors.fill: header;
horizontalOffset: 0;
verticalOffset: 4;
radius: 4.0;
samples: 9
color: Qt.rgba(0, 0, 0, 0.25);
source: header;
visible: header.visible;
}
}

View file

@ -232,6 +232,7 @@
#include "commerce/Ledger.h"
#include "commerce/Wallet.h"
#include "commerce/QmlCommerce.h"
#include "commerce/QmlMarketplace.h"
#include "ResourceRequestObserver.h"
#include "webbrowser/WebBrowserSuggestionsEngine.h"
@ -2913,6 +2914,14 @@ void Application::initializeUi() {
QUrl{ "hifi/dialogs/security/SecurityImageModel.qml" },
QUrl{ "hifi/dialogs/security/SecurityImageSelection.qml" },
}, commerceCallback);
QmlContextCallback marketplaceCallback = [](QQmlContext* context) {
context->setContextProperty("Marketplace", new QmlMarketplace());
};
OffscreenQmlSurface::addWhitelistContextHandler({
QUrl{ "hifi/commerce/marketplace/Marketplace.qml" },
}, marketplaceCallback);
QmlContextCallback ttsCallback = [](QQmlContext* context) {
context->setContextProperty("TextToSpeech", DependencyManager::get<TTSScriptingInterface>().data());
};

View file

@ -0,0 +1,131 @@
//
// QmlMarketplace.cpp
// interface/src/commerce
//
// Guard for safe use of Marketplace by authorized QML.
//
// Created by Roxanne Skelly on 1/18/19.
// Copyright 2019 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 "QmlMarketplace.h"
#include "CommerceLogging.h"
#include "Application.h"
#include "DependencyManager.h"
#include <Application.h>
#include <UserActivityLogger.h>
#include <ScriptEngines.h>
#include <ui/TabletScriptingInterface.h>
#include "scripting/HMDScriptingInterface.h"
#define ApiHandler(NAME) void QmlMarketplace::NAME##Success(QNetworkReply* reply) { emit NAME##Result(apiResponse(#NAME, reply)); }
#define FailHandler(NAME) void QmlMarketplace::NAME##Failure(QNetworkReply* reply) { emit NAME##Result(failResponse(#NAME, reply)); }
#define Handler(NAME) ApiHandler(NAME) FailHandler(NAME)
Handler(getMarketplaceItems)
Handler(getMarketplaceItem)
Handler(marketplaceItemLike)
Handler(getMarketplaceCategories)
QmlMarketplace::QmlMarketplace() {
}
void QmlMarketplace::openMarketplace(const QString& marketplaceItemId) {
auto tablet = dynamic_cast<TabletProxy*>(
DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system"));
tablet->loadQMLSource("hifi/commerce/marketplace/Marketplace.qml");
DependencyManager::get<HMDScriptingInterface>()->openTablet();
if (!marketplaceItemId.isEmpty()) {
tablet->sendToQml(QVariantMap({ { "method", "marketplace_openItem" }, { "itemId", marketplaceItemId } }));
}
}
void QmlMarketplace::getMarketplaceItems(
const QString& q,
const QString& view,
const QString& category,
const QString& adminFilter,
const QString& adminFilterCost,
const QString& sort,
const bool isFree,
const int& page,
const int& perPage) {
QString endpoint = "items";
QJsonObject request;
request["q"] = q;
request["view"] = view;
request["category"] = category;
request["adminFilter"] = adminFilter;
request["adminFilterCost"] = adminFilterCost;
request["sort"] = sort;
request["isFree"] = isFree;
request["page"] = page;
request["perPage"] = perPage;
send(endpoint, "getMarketplaceItemsSuccess", "getMarketplaceItemsFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request);
}
void QmlMarketplace::getMarketplaceItem(const QString& marketplaceItemId) {
QString endpoint = QString("items/") + marketplaceItemId;
QJsonObject request;
send(endpoint, "getMarketplaceItemSuccess", "getMarketplaceItemFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::Optional, request);
}
void QmlMarketplace::marketplaceItemLike(const QString& marketplaceItemId, const bool like) {
QString endpoint = QString("items/") + marketplaceItemId + "/like";
QJsonObject request;
send(endpoint, "marketplaceItemLikeSuccess", "marketplaceItemLikeFailure", like ? QNetworkAccessManager::PutOperation : QNetworkAccessManager::DeleteOperation, AccountManagerAuth::Required, request);
}
void QmlMarketplace::getMarketplaceCategories() {
QString endpoint = "categories";
QJsonObject request;
send(endpoint, "getMarketplaceCategoriesSuccess", "getMarketplaceCategoriesFailure", QNetworkAccessManager::GetOperation, AccountManagerAuth::None, request);
}
void QmlMarketplace::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request) {
auto accountManager = DependencyManager::get<AccountManager>();
const QString URL = "/api/v1/marketplace/";
JSONCallbackParameters callbackParams(this, success, fail);
#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy.
qCInfo(commerce) << "Sending" << QJsonDocument(request).toJson(QJsonDocument::Compact);
#endif
accountManager->sendRequest(URL + endpoint,
authType,
method,
callbackParams,
QJsonDocument(request).toJson());
}
QJsonObject QmlMarketplace::apiResponse(const QString& label, QNetworkReply* reply) {
QByteArray response = reply->readAll();
QJsonObject data = QJsonDocument::fromJson(response).object();
#if defined(DEV_BUILD) // Don't expose user's personal data in the wild. But during development this can be handy.
qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact);
#endif
return data;
}
// Non-200 responses are not json:
QJsonObject QmlMarketplace::failResponse(const QString& label, QNetworkReply* reply) {
QString response = reply->readAll();
qWarning(commerce) << "FAILED" << label << response;
// tempResult will be NULL if the response isn't valid JSON.
QJsonDocument tempResult = QJsonDocument::fromJson(response.toLocal8Bit());
if (tempResult.isNull()) {
QJsonObject result
{
{ "status", "fail" },
{ "message", response }
};
return result;
}
else {
return tempResult.object();
}
}

View file

@ -0,0 +1,68 @@
//
// QmlMarketplace.h
// interface/src/commerce
//
// Guard for safe use of Marketplace by authorized QML.
//
// Created by Roxanne Skelly on 1/18/19.
// Copyright 2019 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
//
#pragma once
#ifndef hifi_QmlMarketplace_h
#define hifi_QmlMarketplace_h
#include <QJsonObject>
#include <QPixmap>
#include <QtNetwork/QNetworkReply>
#include "AccountManager.h"
class QmlMarketplace : public QObject {
Q_OBJECT
public:
QmlMarketplace();
public slots:
void getMarketplaceItemsSuccess(QNetworkReply* reply);
void getMarketplaceItemsFailure(QNetworkReply* reply);
void getMarketplaceItemSuccess(QNetworkReply* reply);
void getMarketplaceItemFailure(QNetworkReply* reply);
void getMarketplaceCategoriesSuccess(QNetworkReply* reply);
void getMarketplaceCategoriesFailure(QNetworkReply* reply);
void marketplaceItemLikeSuccess(QNetworkReply* reply);
void marketplaceItemLikeFailure(QNetworkReply* reply);
protected:
Q_INVOKABLE void openMarketplace(const QString& marketplaceItemId = QString());
Q_INVOKABLE void getMarketplaceItems(
const QString& q = QString(),
const QString& view = QString(),
const QString& category = QString(),
const QString& adminFilter = QString("published"),
const QString& adminFilterCost = QString(),
const QString& sort = QString(),
const bool isFree = false,
const int& page = 1,
const int& perPage = 20);
Q_INVOKABLE void getMarketplaceItem(const QString& marketplaceItemId);
Q_INVOKABLE void marketplaceItemLike(const QString& marketplaceItemId, const bool like = true);
Q_INVOKABLE void getMarketplaceCategories();
signals:
void getMarketplaceItemsResult(QJsonObject result);
void getMarketplaceItemResult(QJsonObject result);
void getMarketplaceCategoriesResult(QJsonObject result);
void marketplaceItemLikeResult(QJsonObject result);
private:
void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, AccountManagerAuth::Type authType, QJsonObject request);
QJsonObject apiResponse(const QString& label, QNetworkReply* reply);
QJsonObject failResponse(const QString& label, QNetworkReply* reply);
};
#endif // hifi_QmlMarketplace_h

View file

@ -1,8 +1,8 @@
//
// marketplace.js
//
// Created by Eric Levin on 8 Jan 2016
// Copyright 2016 High Fidelity, Inc.
// Created by Roxanne Skelly on 1/18/2019
// Copyright 2019 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
@ -10,108 +10,27 @@
(function() { // BEGIN LOCAL_SCOPE
/* global WebTablet */
Script.include("../libraries/WebTablet.js");
var AppUi = Script.require('appUi');
var toolIconUrl = Script.resolvePath("../assets/images/tools/");
var BUTTON_NAME = "MARKET";
var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml";
var ui;
function startup() {
var MARKETPLACE_URL = Account.metaverseServerURL + "/marketplace";
var marketplaceWindow = new OverlayWebWindow({
title: "Marketplace",
source: "about:blank",
width: 900,
height: 700,
visible: false
});
var toolHeight = 50;
var toolWidth = 50;
var TOOLBAR_MARGIN_Y = 0;
var marketplaceVisible = false;
var marketplaceWebTablet;
// We persist avatarEntity data in the .ini file, and reconsistitute it on restart.
// To keep things consistent, we pickle the tablet data in Settings, and kill any existing such on restart and domain change.
var persistenceKey = "io.highfidelity.lastDomainTablet";
function shouldShowWebTablet() {
var rightPose = Controller.getPoseValue(Controller.Standard.RightHand);
var leftPose = Controller.getPoseValue(Controller.Standard.LeftHand);
var hasHydra = !!Controller.Hardware.Hydra;
return HMD.active && (leftPose.valid || rightPose.valid || hasHydra);
ui = new AppUi({
buttonName: BUTTON_NAME,
sortOrder: 10,
home: MARKETPLACE_QML_SOURCE
});
}
function showMarketplace(marketplaceID) {
var url = MARKETPLACE_URL;
if (marketplaceID) {
url = url + "/items/" + marketplaceID;
}
tablet.gotoWebScreen(url);
marketplaceVisible = true;
UserActivityLogger.openedMarketplace();
function shutdown() {
}
function hideTablet(tablet) {
if (!tablet) {
return;
}
updateButtonState(false);
tablet.unregister();
tablet.destroy();
marketplaceWebTablet = null;
Settings.setValue(persistenceKey, "");
}
function clearOldTablet() { // If there was a tablet from previous domain or session, kill it and let it be recreated
}
function hideMarketplace() {
if (marketplaceWindow.visible) {
marketplaceWindow.setVisible(false);
marketplaceWindow.setURL("about:blank");
} else if (marketplaceWebTablet) {
}
marketplaceVisible = false;
}
function toggleMarketplace() {
if (marketplaceVisible) {
hideMarketplace();
} else {
showMarketplace();
}
}
var tablet = Tablet.getTablet("com.highfidelity.interface.tablet.system");
var browseExamplesButton = tablet.addButton({
icon: "icons/tablet-icons/market-i.svg",
text: "MARKET"
});
function updateButtonState(visible) {
}
function onMarketplaceWindowVisibilityChanged() {
updateButtonState(marketplaceWindow.visible);
marketplaceVisible = marketplaceWindow.visible;
}
function onClick() {
toggleMarketplace();
}
browseExamplesButton.clicked.connect(onClick);
marketplaceWindow.visibleChanged.connect(onMarketplaceWindowVisibilityChanged);
clearOldTablet(); // Run once at startup, in case there's anything laying around from a crash.
// We could also optionally do something like Window.domainChanged.connect(function () {Script.setTimeout(clearOldTablet, 2000)}),
// but the HUD version stays around, so lets do the same.
Script.scriptEnding.connect(function () {
browseExamplesButton.clicked.disconnect(onClick);
tablet.removeButton(browseExamplesButton);
marketplaceWindow.visibleChanged.disconnect(onMarketplaceWindowVisibilityChanged);
});
//
// Run the functions.
//
startup();
Script.scriptEnding.connect(shutdown);
}()); // END LOCAL_SCOPE

View file

@ -769,16 +769,14 @@ var onTabletScreenChanged = function onTabletScreenChanged(type, url) {
var BUTTON_NAME = "MARKET";
var MARKETPLACE_URL = METAVERSE_SERVER_URL + "/marketplace";
// Append "?" if necessary to signal injected script that it's the initial page.
var MARKETPLACE_URL_INITIAL = MARKETPLACE_URL + (MARKETPLACE_URL.indexOf("?") > -1 ? "" : "?");
var MARKETPLACE_QML_SOURCE = "hifi/commerce/marketplace/Marketplace.qml";
var ui;
function startup() {
ui = new AppUi({
buttonName: BUTTON_NAME,
sortOrder: 9,
inject: MARKETPLACES_INJECT_SCRIPT_URL,
home: MARKETPLACE_URL_INITIAL,
home: MARKETPLACE_QML_SOURCE,
onScreenChanged: onTabletScreenChanged,
onMessage: onQmlMessageReceived
});