From 3943fe2861511ff26014f66fa0f15c918b4ea844 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Mon, 10 Dec 2018 21:29:03 +0100 Subject: [PATCH 01/43] avatar packager initial --- .../resources/qml/hifi/AvatarPackager.qml | 54 +++++++++++++++++++ interface/src/Application.cpp | 3 ++ interface/src/Menu.cpp | 7 +++ interface/src/Menu.h | 1 + interface/src/avatar/AvatarPackager.cpp | 44 +++++++++++++++ interface/src/avatar/AvatarPackager.h | 35 ++++++++++++ interface/src/avatar/AvatarProject.cpp | 27 ++++++++++ interface/src/avatar/AvatarProject.h | 45 ++++++++++++++++ 8 files changed, 216 insertions(+) create mode 100644 interface/resources/qml/hifi/AvatarPackager.qml create mode 100644 interface/src/avatar/AvatarPackager.cpp create mode 100644 interface/src/avatar/AvatarPackager.h create mode 100644 interface/src/avatar/AvatarProject.cpp create mode 100644 interface/src/avatar/AvatarProject.h diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml new file mode 100644 index 0000000000..787c8d6b69 --- /dev/null +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -0,0 +1,54 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controlsUit" 1.0 as HifiControls +import "../stylesUit" 1.0 +import "../windows" as Windows +import "../dialogs" + +Windows.ScrollingWindow { + id: root + objectName: "AvatarPackager" + width: 480 + height: 706 + title: "Avatar Packager" + resizable: true + opacity: parent.opacity + destroyOnHidden: true + implicitWidth: 384; implicitHeight: 640 + minSize: Qt.vector2d(200, 300) + Rectangle { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + RalewaySemiBold { + id: displayNameLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + text: 'Avatar Projects' + } + + HifiControls.Button { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: displayNameLabel.bottom + text: qsTr("Open Avatar Project") + // color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + let avatarProject = AvatarPackagerCore.openAvatarProject(); + if (avatarProject) { + console.log("LOAD COMPLETE"); + } else { + console.log("LOAD FAILED"); + } + } + } + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 0b53e24a8e..7c4975e7a9 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -158,6 +158,7 @@ #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" #include "avatar/MyHead.h" +#include "avatar/AvatarPackager.h" #include "CrashRecoveryHandler.h" #include "CrashHandler.h" #include "devices/DdeFaceTracker.h" @@ -912,6 +913,7 @@ bool setupEssentials(int& argc, char** argv, bool runningMarkerExisted) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); return previousSessionCrashed; } @@ -2639,6 +2641,7 @@ void Application::cleanupBeforeQuit() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); qCDebug(interfaceapp) << "Application::cleanupBeforeQuit() complete"; } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index e9a44b1e87..4056458dcc 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -35,6 +35,7 @@ #include "assets/ATPAssetMigrator.h" #include "audio/AudioScope.h" #include "avatar/AvatarManager.h" +#include "avatar/AvatarPackager.h" #include "AvatarBookmarks.h" #include "devices/DdeFaceTracker.h" #include "MainWindow.h" @@ -145,6 +146,12 @@ Menu::Menu() { addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel())); + // Edit > Avatar Packager + action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager); + connect(action, &QAction::triggered, [] { + DependencyManager::get()->open(); + }); + // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 7168b7294e..3611faaf8f 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -46,6 +46,7 @@ namespace MenuOption { const QString AutoMuteAudio = "Auto Mute Microphone"; const QString AvatarReceiveStats = "Show Receive Stats"; const QString AvatarBookmarks = "Avatar Bookmarks"; + const QString AvatarPackager = "Avatar Packager"; const QString Back = "Back"; const QString BinaryEyelidControl = "Binary Eyelid Control"; const QString BookmarkAvatar = "Bookmark Avatar"; diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp new file mode 100644 index 0000000000..ef127d4459 --- /dev/null +++ b/interface/src/avatar/AvatarPackager.cpp @@ -0,0 +1,44 @@ +// +// AvatarPackager.cpp +// +// +// Created by Thijs Wenker on 12/6/2018 +// 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 +// + +#include "AvatarPackager.h" + +#include +#include +#include + +#include +#include "ModelSelector.h" + +bool AvatarPackager::open() { + static const QUrl url{ "hifi/AvatarPackager.qml" }; + + const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { + context->setContextProperty("AvatarPackagerCore", this); + }; + DependencyManager::get()->show(url, "AvatarPackager", packageModelDialogCreated); + return true; +} + +QObject* AvatarPackager::openAvatarProject() { + // TODO: use QML file browser here, could handle this on QML side instead + static Setting::Handle lastModelBrowseLocation("LastModelBrowseLocation", + QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); + const QString filename = QFileDialog::getOpenFileName(nullptr, "Select your model file ...", + lastModelBrowseLocation.get(), + "Model files (*.fst *.fbx)"); + QFileInfo fileInfo(filename); + + if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { + return AvatarProject::openAvatarProject(fileInfo.absoluteFilePath()); + } + return nullptr; +} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h new file mode 100644 index 0000000000..8f29f98c0b --- /dev/null +++ b/interface/src/avatar/AvatarPackager.h @@ -0,0 +1,35 @@ +// +// AvatarPackager.h +// +// +// Created by Thijs Wenker on 12/6/2018 +// 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 +// + +#pragma once +#ifndef hifi_AvatarPackager_h +#define hifi_AvatarPackager_h + +#include +#include + +#include "avatar/AvatarProject.h" + +class AvatarPackager : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public: + bool open(); + + Q_INVOKABLE void openAvatarProjectWithoutReturnType() { + openAvatarProject(); + } + + Q_INVOKABLE QObject* openAvatarProject(); +}; + +#endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp new file mode 100644 index 0000000000..876bd1725b --- /dev/null +++ b/interface/src/avatar/AvatarProject.cpp @@ -0,0 +1,27 @@ +// +// AvatarProject.h +// +// +// Created by Thijs Wenker on 12/7/2018 +// 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 +// + +#include "AvatarProject.h" + +AvatarProject* AvatarProject::openAvatarProject(QString path) { + const auto pathToLower = path.toLower(); + if (pathToLower.endsWith(".fst")) { + // TODO: do we open FSTs from any path? + return new AvatarProject(path); + } + + if (pathToLower.endsWith(".fbx")) { + // TODO: Create FST here: + + } + + return nullptr; +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h new file mode 100644 index 0000000000..cc347c3d4b --- /dev/null +++ b/interface/src/avatar/AvatarProject.h @@ -0,0 +1,45 @@ +// +// AvatarProject.h +// +// +// Created by Thijs Wenker on 12/7/2018 +// 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 +// + +#pragma once +#ifndef hifi_AvatarProject_h +#define hifi_AvatarProject_h + +#include + +class AvatarProject : public QObject { +public: + Q_INVOKABLE bool write() { + // Write FST here + return false; + } + + Q_INVOKABLE QObject* upload() { + // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML + return nullptr; + } + + /** + * returns the AvatarProject or a nullptr on failure. + */ + static AvatarProject* openAvatarProject(QString path); + +private: + AvatarProject(QString fstPath) { + + } + + ~AvatarProject() { + // TODO: cleanup FST / AvatarProjectUploader etc. + } +}; + +#endif // hifi_AvatarProject_h From c0bd9121fbe1935cb537feccf9d28f470e94becf Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 12 Dec 2018 19:55:14 +0100 Subject: [PATCH 02/43] open avatar projects --- .../resources/qml/hifi/AvatarPackager.qml | 66 +++++++++++++++---- .../qml/hifi/avatarPackager/AvatarProject.qml | 59 +++++++++++++++++ interface/src/avatar/AvatarPackager.cpp | 19 ++---- interface/src/avatar/AvatarPackager.h | 9 +-- interface/src/avatar/AvatarProject.cpp | 2 +- interface/src/avatar/AvatarProject.h | 26 +++++++- 6 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProject.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 787c8d6b69..d93a3900a8 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -7,6 +7,7 @@ import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" +import "avatarPackager" Windows.ScrollingWindow { id: root @@ -19,35 +20,78 @@ Windows.ScrollingWindow { destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) + + //HifiConstants { id: hifi } + + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + } + Rectangle { + id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom RalewaySemiBold { - id: displayNameLabel + id: avatarPackagerLabel size: 24; anchors.left: parent.left anchors.top: parent.top anchors.topMargin: 25 - text: 'Avatar Projects' + anchors.bottomMargin: 25 + text: 'Avatar Packager' } HifiControls.Button { + id: createProjectButton anchors.left: parent.left anchors.right: parent.right - anchors.top: displayNameLabel.bottom - text: qsTr("Open Avatar Project") - // color: hifi.buttons.blue + anchors.top: avatarPackagerLabel.bottom + text: qsTr("Create Project") colorScheme: root.colorScheme height: 30 onClicked: function() { - let avatarProject = AvatarPackagerCore.openAvatarProject(); - if (avatarProject) { - console.log("LOAD COMPLETE"); - } else { - console.log("LOAD FAILED"); - } + + } + } + HifiControls.Button { + id: openProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: createProjectButton.bottom + text: qsTr("Open Avatar Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + console.log("path = " + avatarProjectsPath); + + // TODO: make the dialog modal + + var browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(avatarProjectsPath), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)" + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + console.log("FOUND PATH " + fileUrl); + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let avatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (avatarProject) { + console.log("LOAD COMPLETE"); + console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); + avatarProject.visible = true; + avatarPackagerMain.visible = false; + } + }); } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml new file mode 100644 index 0000000000..a34c709567 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -0,0 +1,59 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarProject + + HifiConstants { id: hifi } + + + z: 3 + property int colorScheme; + + visible: false + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + RalewaySemiBold { + id: avatarProjectLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + anchors.bottomMargin: 25 + text: 'Avatar Project' + } + HifiControls.Button { + id: openFolderButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: avatarProjectLabel.bottom + text: qsTr("Open Project Folder") + colorScheme: avatarProject.colorScheme + height: 30 + onClicked: function() { + fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); + } + } + HifiControls.Button { + id: uploadButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: openFolderButton.bottom + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: avatarProject.colorScheme + height: 30 + onClicked: function() { + + } + } +} diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index ef127d4459..c6c1318eab 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -12,11 +12,9 @@ #include "AvatarPackager.h" #include -#include #include #include -#include "ModelSelector.h" bool AvatarPackager::open() { static const QUrl url{ "hifi/AvatarPackager.qml" }; @@ -28,17 +26,10 @@ bool AvatarPackager::open() { return true; } -QObject* AvatarPackager::openAvatarProject() { - // TODO: use QML file browser here, could handle this on QML side instead - static Setting::Handle lastModelBrowseLocation("LastModelBrowseLocation", - QStandardPaths::writableLocation(QStandardPaths::DownloadLocation)); - const QString filename = QFileDialog::getOpenFileName(nullptr, "Select your model file ...", - lastModelBrowseLocation.get(), - "Model files (*.fst *.fbx)"); - QFileInfo fileInfo(filename); - - if (fileInfo.isFile() && fileInfo.completeSuffix().contains(QRegExp("fst|fbx|FST|FBX"))) { - return AvatarProject::openAvatarProject(fileInfo.absoluteFilePath()); +QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { + if (_currentAvatarProject) { + _currentAvatarProject->deleteLater(); } - return nullptr; + _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 8f29f98c0b..a8ef6c6421 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -21,15 +21,16 @@ class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject) public: bool open(); - Q_INVOKABLE void openAvatarProjectWithoutReturnType() { - openAvatarProject(); - } + Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); - Q_INVOKABLE QObject* openAvatarProject(); +private: + Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; + AvatarProject* _currentAvatarProject { nullptr }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 876bd1725b..a3b22ab2c2 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -1,5 +1,5 @@ // -// AvatarProject.h +// AvatarProject.cpp // // // Created by Thijs Wenker on 12/7/2018 diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index cc347c3d4b..2114b147dd 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -14,8 +14,15 @@ #define hifi_AvatarProject_h #include +#include +#include class AvatarProject : public QObject { + Q_OBJECT + Q_PROPERTY(QString projectFolderPath READ getProjectPath) + Q_PROPERTY(QString projectFSTPath READ getFSTPath) + Q_PROPERTY(QString projectFBXPath READ getFBXPath) + public: Q_INVOKABLE bool write() { // Write FST here @@ -33,13 +40,28 @@ public: static AvatarProject* openAvatarProject(QString path); private: - AvatarProject(QString fstPath) { - + AvatarProject(QString fstPath) : + _fstPath(fstPath) { + auto fileInfo = QFileInfo(_fstPath); + _projectPath = fileInfo.absoluteDir().absolutePath(); + + _fstPath = _projectPath + "TemporaryFBX.fbx"; + } ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. } + + Q_INVOKABLE QString getProjectPath() const { return _projectPath; } + Q_INVOKABLE QString getFSTPath() const { return _fstPath; } + Q_INVOKABLE QString getFBXPath() const { return _fbxPath; } + + QString _projectPath; + QString _fstPath; + // TODO: handle this in the FST Class + QString _fbxPath; + }; #endif // hifi_AvatarProject_h From 96673bb5b9087194ebf610c0a65a416f3ee27a0e Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 12 Dec 2018 21:15:44 +0100 Subject: [PATCH 03/43] show project page --- .../resources/qml/hifi/AvatarPackager.qml | 121 +++++++++--------- .../qml/hifi/avatarPackager/AvatarProject.qml | 10 +- interface/src/avatar/AvatarPackager.cpp | 3 +- 3 files changed, 70 insertions(+), 64 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index d93a3900a8..2044529427 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -22,76 +22,83 @@ Windows.ScrollingWindow { minSize: Qt.vector2d(200, 300) //HifiConstants { id: hifi } - - AvatarProject { - id: avatarProject - colorScheme: root.colorScheme - } - Rectangle { - id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.bottom: parent.bottom - RalewaySemiBold { - id: avatarPackagerLabel - size: 24; + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + visible: false + } + + Rectangle { + id: avatarPackagerMain anchors.left: parent.left + anchors.right: parent.right anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: 'Avatar Packager' - } - - HifiControls.Button { - id: createProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: avatarPackagerLabel.bottom - text: qsTr("Create Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - + anchors.bottom: parent.bottom + RalewaySemiBold { + id: avatarPackagerLabel + size: 24; + anchors.left: parent.left + anchors.top: parent.top + anchors.topMargin: 25 + anchors.bottomMargin: 25 + text: 'Avatar Packager' } - } - HifiControls.Button { - id: openProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: createProjectButton.bottom - text: qsTr("Open Avatar Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - console.log("path = " + avatarProjectsPath); - // TODO: make the dialog modal + HifiControls.Button { + id: createProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: avatarPackagerLabel.bottom + text: qsTr("Create Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + + } + } + HifiControls.Button { + id: openProjectButton + anchors.left: parent.left + anchors.right: parent.right + anchors.top: createProjectButton.bottom + text: qsTr("Open Avatar Project") + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + console.log("path = " + avatarProjectsPath); - var browser = desktop.fileDialog({ - selectDirectory: false, - dir: fileDialogHelper.pathToUrl(avatarProjectsPath), - filter: "Avatar Project FST Files (*.fst)", - title: "Open Project (.fst)" - }); + // TODO: make the dialog modal - browser.canceled.connect(function() { + var browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(avatarProjectsPath), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)" + }); + + browser.canceled.connect(function() { - }); + }); - browser.selectedFile.connect(function(fileUrl) { - console.log("FOUND PATH " + fileUrl); - let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let avatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); - if (avatarProject) { - console.log("LOAD COMPLETE"); - console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); - avatarProject.visible = true; - avatarPackagerMain.visible = false; - } - }); + browser.selectedFile.connect(function(fileUrl) { + console.log("FOUND PATH " + fileUrl); + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (currentAvatarProject) { + console.log("LOAD COMPLETE"); + console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); + + avatarPackagerMain.visible = false; + avatarProject.visible = true; + } + }); + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a34c709567..0a6ed6c459 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -7,15 +7,13 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Rectangle { - id: avatarProject + id: root HifiConstants { id: hifi } - - z: 3 property int colorScheme; - visible: false + visible: true anchors.left: parent.left anchors.right: parent.right @@ -37,7 +35,7 @@ Rectangle { anchors.right: parent.right anchors.top: avatarProjectLabel.bottom text: qsTr("Open Project Folder") - colorScheme: avatarProject.colorScheme + colorScheme: root.colorScheme height: 30 onClicked: function() { fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); @@ -50,7 +48,7 @@ Rectangle { anchors.top: openFolderButton.bottom text: qsTr("Upload") color: hifi.buttons.blue - colorScheme: avatarProject.colorScheme + colorScheme: root.colorScheme height: 30 onClicked: function() { diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index c6c1318eab..1088c862d4 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -28,7 +28,8 @@ bool AvatarPackager::open() { QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { if (_currentAvatarProject) { - _currentAvatarProject->deleteLater(); + //_currentAvatarProject->deleteLater(); + //_currentAvatarProject = nullptr; } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); return _currentAvatarProject; From 78c4c2599e007cfec2765b3fff4d1c8a99c73f93 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 13 Dec 2018 13:22:52 -0800 Subject: [PATCH 04/43] Add start of marketplace uploading and project file list --- .../resources/qml/hifi/AvatarPackager.qml | 7 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 38 ++++++--- interface/src/avatar/AvatarPackager.cpp | 18 +++++ interface/src/avatar/AvatarPackager.h | 13 +++- interface/src/avatar/AvatarProject.cpp | 47 ++++++++++- interface/src/avatar/AvatarProject.h | 36 +++++---- .../src/avatar/MarketplaceItemUploader.cpp | 57 ++++++++++++++ .../src/avatar/MarketplaceItemUploader.h | 55 +++++++++++++ libraries/fbx/src/FST.cpp | 45 +++++++++++ libraries/fbx/src/FST.h | 49 ++++++++++++ libraries/networking/src/AccountManager.cpp | 78 ++++++++++--------- libraries/networking/src/AccountManager.h | 34 ++++---- 12 files changed, 388 insertions(+), 89 deletions(-) create mode 100644 interface/src/avatar/MarketplaceItemUploader.cpp create mode 100644 interface/src/avatar/MarketplaceItemUploader.h create mode 100644 libraries/fbx/src/FST.cpp create mode 100644 libraries/fbx/src/FST.h diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 2044529427..434cd4128f 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -23,14 +23,13 @@ Windows.ScrollingWindow { //HifiConstants { id: hifi } Rectangle { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom + anchors.fill: parent + AvatarProject { id: avatarProject colorScheme: root.colorScheme visible: false + anchors.fill: parent } Rectangle { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 0a6ed6c459..c365d7436e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -13,26 +13,23 @@ Rectangle { property int colorScheme; + color: "blue" + visible: true - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom + + anchors.fill: parent RalewaySemiBold { id: avatarProjectLabel size: 24; - anchors.left: parent.left - anchors.top: parent.top + width: parent.width anchors.topMargin: 25 anchors.bottomMargin: 25 text: 'Avatar Project' } HifiControls.Button { id: openFolderButton - anchors.left: parent.left - anchors.right: parent.right + width: parent.width anchors.top: avatarProjectLabel.bottom text: qsTr("Open Project Folder") colorScheme: root.colorScheme @@ -43,15 +40,32 @@ Rectangle { } HifiControls.Button { id: uploadButton - anchors.left: parent.left - anchors.right: parent.right + width: parent.width anchors.top: openFolderButton.bottom text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme height: 30 onClicked: function() { - + } + } + Text { + id: modelText + anchors.top: uploadButton.bottom + height: 30 + text: parent.height + } + Rectangle { + color: 'white' + visible: AvatarPackagerCore.currentAvatarProject !== null + width: parent.width + anchors.top: modelText.bottom + height: 1000 + + ListView { + anchors.fill: parent + model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles + delegate: Text { text: 'File: ' + modelData } } } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 1088c862d4..3fdf193087 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -15,6 +15,18 @@ #include #include +#include "ModelSelector.h" +#include + +#include +#include + +std::once_flag setupQMLTypesFlag; +AvatarPackager::AvatarPackager() { + std::call_once(setupQMLTypesFlag, []() { + qmlRegisterType(); + }); +} bool AvatarPackager::open() { static const QUrl url{ "hifi/AvatarPackager.qml" }; @@ -32,5 +44,11 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { //_currentAvatarProject = nullptr; } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + emit avatarProjectChanged(); return _currentAvatarProject; } + +QObject* AvatarPackager::uploadItem() { + std::vector filePaths; + return new MarketplaceItemUploader(QUuid(), filePaths); +} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index a8ef6c6421..f002631f17 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -21,16 +21,23 @@ class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject) + Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) public: + AvatarPackager(); bool open(); Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); +signals: + void avatarProjectChanged(); + private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; - AvatarProject* _currentAvatarProject { nullptr }; + //Q_INVOKABLE QObject* openAvatarProject(); + Q_INVOKABLE QObject* uploadItem(); + + AvatarProject* _currentAvatarProject{ nullptr }; }; -#endif // hifi_AvatarPackager_h +#endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index a3b22ab2c2..32fe4febcd 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -11,17 +11,56 @@ #include "AvatarProject.h" -AvatarProject* AvatarProject::openAvatarProject(QString path) { +#include + +#include +#include +#include +#include + +AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); if (pathToLower.endsWith(".fst")) { - // TODO: do we open FSTs from any path? - return new AvatarProject(path); + QFile file{ path }; + if (!file.open(QIODevice::ReadOnly)) { + return nullptr; + } + return new AvatarProject(path, file.readAll()); } if (pathToLower.endsWith(".fbx")) { // TODO: Create FST here: - } return nullptr; } + +AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : + _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + + _directory = QFileInfo(_fstPath).absoluteDir(); + + //_projectFiles = _directory.entryList(); + refreshProjectFiles(); + + auto fileInfo = QFileInfo(_fstPath); + _projectPath = fileInfo.absoluteDir().absolutePath(); +} + +void AvatarProject::appendDirectory(QString prefix, QDir dir) { + qDebug() << "Inside of " << prefix << dir.absolutePath(); + auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + for (auto& entry : dir.entryInfoList({}, flags)) { + if (entry.isFile()) { + _projectFiles.append(prefix + "/" + entry.fileName()); + } else if (entry.isDir()) { + qDebug() << "Found dir " << entry.absoluteFilePath() << " in " << dir.absolutePath(); + appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); + } + } +} + +void AvatarProject::refreshProjectFiles() { + _projectFiles.clear(); + appendDirectory("", _directory); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 2114b147dd..6dc64cda6f 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -13,12 +13,21 @@ #ifndef hifi_AvatarProject_h #define hifi_AvatarProject_h +#include "FST.h" + +#include #include #include #include +#include +#include class AvatarProject : public QObject { Q_OBJECT + Q_PROPERTY(FST* fst READ getFST) + + Q_PROPERTY(QStringList projectFiles MEMBER _projectFiles) + Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) @@ -37,17 +46,10 @@ public: /** * returns the AvatarProject or a nullptr on failure. */ - static AvatarProject* openAvatarProject(QString path); + static AvatarProject* openAvatarProject(const QString& path); private: - AvatarProject(QString fstPath) : - _fstPath(fstPath) { - auto fileInfo = QFileInfo(_fstPath); - _projectPath = fileInfo.absoluteDir().absolutePath(); - - _fstPath = _projectPath + "TemporaryFBX.fbx"; - - } + AvatarProject(const QString& fstPath, const QByteArray& data); ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. @@ -55,13 +57,19 @@ private: Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getFSTPath() const { return _fstPath; } - Q_INVOKABLE QString getFBXPath() const { return _fbxPath; } + Q_INVOKABLE QString getFBXPath() const { return _fst.getModelPath(); } + FST* getFST() { return &_fst; } + + void refreshProjectFiles(); + void appendDirectory(QString prefix, QDir dir); + + FST _fst; + + QDir _directory; + QStringList _projectFiles{}; QString _projectPath; QString _fstPath; - // TODO: handle this in the FST Class - QString _fbxPath; - }; -#endif // hifi_AvatarProject_h +#endif // hifi_AvatarProject_h diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp new file mode 100644 index 0000000000..c2671aadec --- /dev/null +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -0,0 +1,57 @@ +// +// MarketplaceItemUploader.cpp +// +// +// Created by Ryan Huffman on 12/10/2018 +// 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 +// + +#include "MarketplaceItemUploader.h" + +#include +#include + +#include + +MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector filePaths) + : _filePaths(filePaths), _marketplaceID(marketplaceID) { + +} + +void MarketplaceItemUploader::send() { + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest("/marketplace/item", AccountManagerAuth::Required); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QByteArray data; + + /* + auto reply = networkAccessManager.post(request, data); + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + } else { + } + emit complete(); + }); + */ + + QTimer* timer = new QTimer(); + timer->setInterval(1000); + connect(timer, &QTimer::timeout, this, [this, timer]() { + if (progress <= 1.0f) { + progress += 0.1; + emit uploadProgress(progress * 100.0f, 100.0f); + } else { + emit complete(); + timer->stop(); + } + }); + timer->start(); +} diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h new file mode 100644 index 0000000000..2a3071244e --- /dev/null +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -0,0 +1,55 @@ +// +// MarketplaceItemUploader.h +// +// +// Created by Ryan Huffman on 12/10/2018 +// 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 +// + +#pragma once +#ifndef hifi_MarketplaceItemUploader_h +#define hifi_MarketplaceItemUploader_h + +#include +#include + +class QNetworkReply; + +class MarketplaceItemUploader : public QObject { + Q_OBJECT +public: + enum class Error + { + None, + ItemNotUpdateable, + ItemDoesNotExist, + RequestTimedOut, + Unknown + }; + enum class State + { + Ready, + Sent + }; + + MarketplaceItemUploader(QUuid markertplaceID, std::vector filePaths); + + float progress{ 0.0f }; + + Q_INVOKABLE void send(); + +signals: + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + void complete(); + +private: + + QNetworkReply* _reply; + QUuid _marketplaceID; + std::vector _filePaths; +}; + +#endif // hifi_MarketplaceItemUploader_h diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp new file mode 100644 index 0000000000..6574b66e51 --- /dev/null +++ b/libraries/fbx/src/FST.cpp @@ -0,0 +1,45 @@ +// +// FST.cpp +// +// Created by Ryan Huffman on 12/11/15. +// 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 +// + + +#include "FST.h" + +#include +#include + +FST::FST(QString fstPath, QVariantHash data) : _fstPath(fstPath) { + if (data.contains("name")) { + _name = data["name"].toString(); + data.remove("name"); + } + + if (data.contains("filename")) { + _modelPath = data["filename"].toString(); + data.remove("filename"); + } + + _other = data; +} + +QString FST::absoluteModelPath() const { + QFileInfo fileInfo{ _fstPath }; + QDir dir{ fileInfo.absoluteDir() }; + return dir.absoluteFilePath(_modelPath); +} + +void FST::setName(const QString& name) { + _name = name; + emit nameChanged(name); +} + +void FST::setModelPath(const QString& modelPath) { + _modelPath = modelPath; + emit modelPathChanged(modelPath); +} \ No newline at end of file diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h new file mode 100644 index 0000000000..e8c67c6c6b --- /dev/null +++ b/libraries/fbx/src/FST.h @@ -0,0 +1,49 @@ +// +// FST.h +// +// Created by Ryan Huffman on 12/11/15. +// 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 +// + +#ifndef hifi_FST_h +#define hifi_FST_h + +#include +#include + +class FST : public QObject { + Q_OBJECT + Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged) + Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) + Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) +public: + FST(QString fstPath, QVariantHash data); + + QString absoluteModelPath() const; + + QString getName() const { return _name; } + void setName(const QString& name); + + QString getModelPath() const { return _modelPath; } + void setModelPath(const QString& modelPath); + + QUuid getMarketplaceID() const { return _marketplaceID; } + +signals: + void nameChanged(const QString& name); + void modelPathChanged(const QString& modelPath); + +private: + QString _fstPath; + + QString _name{}; + QString _modelPath{}; + QUuid _marketplaceID{}; + + QVariantHash _other{}; +}; + +#endif // hifi_FST_h diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index 5721ac9334..989661cb81 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -208,6 +208,44 @@ void AccountManager::setSessionID(const QUuid& sessionID) { } } +QNetworkRequest AccountManager::createRequest(QString path, AccountManagerAuth::Type authType) { + QNetworkRequest networkRequest; + networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); + + networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, + uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); + + QUrl requestURL = _authURL; + + if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL. + requestURL = getMetaverseServerURL(); + } + + if (path.startsWith("/")) { + requestURL.setPath(path); + } else { + requestURL.setPath("/" + path); + } + + if (authType != AccountManagerAuth::None ) { + if (hasValidAccessToken()) { + networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, + _accountInfo.getAccessToken().authorizationHeaderValue()); + } else { + if (authType == AccountManagerAuth::Required) { + qCDebug(networking) << "No valid access token present. Bailing on invoked request to" + << path << "that requires authentication"; + return QNetworkRequest(); + } + } + } + + networkRequest.setUrl(requestURL); + + return networkRequest; +} + void AccountManager::sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation, @@ -231,46 +269,10 @@ void AccountManager::sendRequest(const QString& path, QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkRequest networkRequest; - networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); - networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); - - networkRequest.setRawHeader(METAVERSE_SESSION_ID_HEADER, - uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); - - QUrl requestURL = _authURL; - - if (requestURL.isEmpty()) { // Assignment client doesn't set _authURL. - requestURL = getMetaverseServerURL(); - } - - if (path.startsWith("/")) { - requestURL.setPath(path); - } else { - requestURL.setPath("/" + path); - } - - if (!query.isEmpty()) { - requestURL.setQuery(query); - } - - if (authType != AccountManagerAuth::None ) { - if (hasValidAccessToken()) { - networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, - _accountInfo.getAccessToken().authorizationHeaderValue()); - } else { - if (authType == AccountManagerAuth::Required) { - qCDebug(networking) << "No valid access token present. Bailing on invoked request to" - << path << "that requires authentication"; - return; - } - } - } - - networkRequest.setUrl(requestURL); + QNetworkRequest networkRequest = createRequest(path, authType); if (VERBOSE_HTTP_REQUEST_DEBUGGING) { - qCDebug(networking) << "Making a request to" << qPrintable(requestURL.toString()); + qCDebug(networking) << "Making a request to" << qPrintable(networkRequest.url().toString()); if (!dataByteArray.isEmpty()) { qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index d5406707e7..77f20472fa 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -28,7 +28,8 @@ class JSONCallbackParameters { public: - JSONCallbackParameters(QObject* callbackReceiver = nullptr, const QString& jsonCallbackMethod = QString(), + JSONCallbackParameters(QObject* callbackReceiver = nullptr, + const QString& jsonCallbackMethod = QString(), const QString& errorCallbackMethod = QString()); bool isEmpty() const { return !callbackReceiver; } @@ -39,11 +40,12 @@ public: }; namespace AccountManagerAuth { - enum Type { - None, - Required, - Optional - }; +enum Type +{ + None, + Required, + Optional +}; } Q_DECLARE_METATYPE(AccountManagerAuth::Type); @@ -60,6 +62,7 @@ class AccountManager : public QObject, public Dependency { public: AccountManager(UserAgentGetter userAgentGetter = DEFAULT_USER_AGENT_GETTER); + QNetworkRequest createRequest(QString path, AccountManagerAuth::Type authType); Q_INVOKABLE void sendRequest(const QString& path, AccountManagerAuth::Type authType, QNetworkAccessManager::Operation operation = QNetworkAccessManager::GetOperation, @@ -84,7 +87,7 @@ public: void requestProfile(); DataServerAccountInfo& getAccountInfo() { return _accountInfo; } - void setAccountInfo(const DataServerAccountInfo &newAccountInfo); + void setAccountInfo(const DataServerAccountInfo& newAccountInfo); static QJsonObject dataObjectFromResponse(QNetworkReply* requestReply); @@ -104,7 +107,10 @@ public: public slots: void requestAccessToken(const QString& login, const QString& password); void requestAccessTokenWithSteam(QByteArray authSessionTicket); - void requestAccessTokenWithAuthCode(const QString& authCode, const QString& clientId, const QString& clientSecret, const QString& redirectUri); + void requestAccessTokenWithAuthCode(const QString& authCode, + const QString& clientId, + const QString& clientSecret, + const QString& redirectUri); void refreshAccessToken(); void requestAccessTokenFinished(); @@ -148,15 +154,15 @@ private: QUrl _authURL; DataServerAccountInfo _accountInfo; - bool _isWaitingForTokenRefresh { false }; - bool _isAgent { false }; + bool _isWaitingForTokenRefresh{ false }; + bool _isAgent{ false }; - bool _isWaitingForKeypairResponse { false }; + bool _isWaitingForKeypairResponse{ false }; QByteArray _pendingPrivateKey; - QUuid _sessionID { QUuid::createUuid() }; + QUuid _sessionID{ QUuid::createUuid() }; - bool _limitedCommerce { false }; + bool _limitedCommerce{ false }; }; -#endif // hifi_AccountManager_h +#endif // hifi_AccountManager_h From 2269447741c405d150a38340afaab51bd6040df5 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 13 Dec 2018 14:52:36 -0800 Subject: [PATCH 05/43] Fix AvatarPackager QML size --- .../resources/qml/hifi/AvatarPackager.qml | 8 ++-- .../qml/hifi/avatarPackager/AvatarProject.qml | 43 +++++++++---------- interface/src/avatar/AvatarProject.cpp | 5 ++- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 434cd4128f..5a51a3c873 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -21,9 +21,11 @@ Windows.ScrollingWindow { implicitWidth: 384; implicitHeight: 640 minSize: Qt.vector2d(200, 300) + //HifiConstants { id: hifi } - Rectangle { - anchors.fill: parent + Item { + height: pane.height + width: pane.width AvatarProject { id: avatarProject @@ -32,7 +34,7 @@ Windows.ScrollingWindow { anchors.fill: parent } - Rectangle { + Item { id: avatarPackagerMain anchors.left: parent.left anchors.right: parent.right diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index c365d7436e..085f1acdce 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -6,18 +6,17 @@ import QtGraphicalEffects 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 -Rectangle { +Item { id: root HifiConstants { id: hifi } property int colorScheme; - color: "blue" - visible: true anchors.fill: parent + anchors.margins: 10 RalewaySemiBold { id: avatarProjectLabel @@ -26,11 +25,13 @@ Rectangle { anchors.topMargin: 25 anchors.bottomMargin: 25 text: 'Avatar Project' + color: "white" } HifiControls.Button { id: openFolderButton width: parent.width anchors.top: avatarProjectLabel.bottom + anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme height: 30 @@ -38,28 +39,15 @@ Rectangle { fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); } } - HifiControls.Button { - id: uploadButton - width: parent.width - anchors.top: openFolderButton.bottom - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - } - } - Text { - id: modelText - anchors.top: uploadButton.bottom - height: 30 - text: parent.height - } Rectangle { color: 'white' visible: AvatarPackagerCore.currentAvatarProject !== null - width: parent.width - anchors.top: modelText.bottom + anchors.top: openFolderButton.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: uploadButton.top + anchors.topMargin: 10 + anchors.bottomMargin: 10 height: 1000 ListView { @@ -68,4 +56,15 @@ Rectangle { delegate: Text { text: 'File: ' + modelData } } } + HifiControls.Button { + id: uploadButton + width: parent.width + anchors.bottom: parent.bottom + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + height: 30 + onClicked: function() { + } + } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 32fe4febcd..c7ea7e52ac 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -17,6 +17,7 @@ #include #include #include +#include AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); @@ -25,7 +26,9 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { if (!file.open(QIODevice::ReadOnly)) { return nullptr; } - return new AvatarProject(path, file.readAll()); + auto project = new AvatarProject(path, file.readAll()); + QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); + return project; } if (pathToLower.endsWith(".fbx")) { From ad471387f7212559fcbf34d4f37d1cbe0010ed85 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 14 Dec 2018 16:10:42 -0800 Subject: [PATCH 06/43] Integrate marketplace upload API --- .../resources/qml/controlsUit/Button.qml | 7 + .../resources/qml/hifi/AvatarPackager.qml | 3 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 110 ++++++++- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 4 +- interface/src/avatar/AvatarPackager.cpp | 6 +- interface/src/avatar/AvatarPackager.h | 1 - interface/src/avatar/AvatarProject.cpp | 12 +- interface/src/avatar/AvatarProject.h | 7 +- .../src/avatar/MarketplaceItemUploader.cpp | 225 ++++++++++++++++-- .../src/avatar/MarketplaceItemUploader.h | 52 +++- scripts/system/html/js/entityProperties.js | 9 + 12 files changed, 386 insertions(+), 52 deletions(-) diff --git a/interface/resources/qml/controlsUit/Button.qml b/interface/resources/qml/controlsUit/Button.qml index 6ea7ce4b4c..9d92ff5e9a 100644 --- a/interface/resources/qml/controlsUit/Button.qml +++ b/interface/resources/qml/controlsUit/Button.qml @@ -28,6 +28,10 @@ Original.Button { width: hifi.dimensions.buttonWidth height: hifi.dimensions.controlLineHeight + property size implicitPadding: Qt.size(20, 16) + property int implicitWidth: content.implicitWidth + implicitPadding.width + property int implicitHeight: content.implicitHeight + implicitPadding.height + HifiConstants { id: hifi } onHoveredChanged: { @@ -89,6 +93,9 @@ Original.Button { } contentItem: Item { + id: content + implicitWidth: (buttonGlyph.visible ? buttonGlyph.implicitWidth : 0) + buttonText.implicitWidth + implicitHeight: buttonText.implicitHeight HiFiGlyphs { id: buttonGlyph; visible: control.buttonGlyph !== ""; diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 5a51a3c873..d5e21d9653 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -47,7 +47,7 @@ Windows.ScrollingWindow { anchors.top: parent.top anchors.topMargin: 25 anchors.bottomMargin: 25 - text: 'Avatar Packager' + text: 'Avatar Packager ' + parent.width + " " + parent.height } HifiControls.Button { @@ -72,6 +72,7 @@ Windows.ScrollingWindow { height: 30 onClicked: function() { var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; + var avatarProjectsPath = "C:/Users/ryanh/Documents/High Fidelity Avatars"; console.log("path = " + avatarProjectsPath); // TODO: make the dialog modal diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 085f1acdce..4f42927676 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -3,6 +3,8 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 +import QtQuick.Controls 2.2 as Original + import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 @@ -12,6 +14,7 @@ Item { HifiConstants { id: hifi } property int colorScheme; + property var uploader: undefined; visible: true @@ -58,13 +61,118 @@ Item { } HifiControls.Button { id: uploadButton + width: parent.width + height: 30 anchors.bottom: parent.bottom + text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme - height: 30 onClicked: function() { + console.log("Uploading"); + parent.uploader = AvatarPackagerCore.currentAvatarProject.upload(); + console.log("uploader: "+ parent.uploader); + parent.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + parent.uploader.completed.connect(function() { + try { + var response = JSON.parse(parent.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + parent.uploader.reponseData); + } + }); + parent.uploader.send(); } } + + Rectangle { + id: uploadingScreen + + visible: !!root.uploader + anchors.fill: parent + + color: "black" + + Item { + visible: !!root.uploader && !root.uploader.complete + + anchors.fill: parent + + AnimatedImage { + id: uploadSpinner + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 + } + } + + Item { + visible: !!root.uploader && root.uploader.complete + + anchors.fill: parent + + HiFiGlyphs { + id: successIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "\ue01a" + color: "#1FC6A6" + } + + Text { + text: "Congratulations!" + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: successIcon.bottom + + color: "white" + } + + HifiControls.Button { + width: implicitWidth + height: implicitHeight + + anchors.bottom: parent.bottom + anchors.right: parent.right + + text: "View in Inventory" + + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: function() { + console.log("Opening in inventory"); + } + } + } + + Column { + Text { + id: uploadStatus + + text: "Uploading" + color: "white" + + } + Text { + text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") + color: "white" + } + } + + } } diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 7c4975e7a9..10f8d66855 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -460,7 +460,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - deadlockDetectionCrash(); + //deadlockDetectionCrash(); #endif } } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 7ca18ca258..1a6b510ea1 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -536,6 +536,7 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.) if (collision.idA.isNull() || collision.idB.isNull()) { auto myAvatar = getMyAvatar(); + myAvatar->collisionWithEntity(collision); auto collisionSound = myAvatar->getCollisionSound(); if (collisionSound) { const auto characterController = myAvatar->getCharacterController(); @@ -571,9 +572,8 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } - myAvatar->collisionWithEntity(collision); - return; } + return; } } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 3fdf193087..04ecc87067 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -25,6 +25,7 @@ std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { std::call_once(setupQMLTypesFlag, []() { qmlRegisterType(); + qmlRegisterType(); }); } @@ -47,8 +48,3 @@ QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { emit avatarProjectChanged(); return _currentAvatarProject; } - -QObject* AvatarPackager::uploadItem() { - std::vector filePaths; - return new MarketplaceItemUploader(QUuid(), filePaths); -} diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index f002631f17..def82b6311 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -35,7 +35,6 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; //Q_INVOKABLE QObject* openAvatarProject(); - Q_INVOKABLE QObject* uploadItem(); AvatarProject* _currentAvatarProject{ nullptr }; }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index c7ea7e52ac..cc23027562 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -41,6 +41,9 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + _fstFilename = QFileInfo(_fstPath).fileName(); + qDebug() << "Pointers: " << this << &_fst; + _directory = QFileInfo(_fstPath).absoluteDir(); //_projectFiles = _directory.entryList(); @@ -51,13 +54,12 @@ AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : } void AvatarProject::appendDirectory(QString prefix, QDir dir) { - qDebug() << "Inside of " << prefix << dir.absolutePath(); auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { - _projectFiles.append(prefix + "/" + entry.fileName()); + //_projectFiles.append(prefix + "/" + entry.fileName()); + _projectFiles.append(entry.absoluteFilePath()); } else if (entry.isDir()) { - qDebug() << "Found dir " << entry.absoluteFilePath() << " in " << dir.absolutePath(); appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); } } @@ -67,3 +69,7 @@ void AvatarProject::refreshProjectFiles() { _projectFiles.clear(); appendDirectory("", _directory); } + +Q_INVOKABLE MarketplaceItemUploader* AvatarProject::upload() { + return new MarketplaceItemUploader("test_avatar", "blank description", _fstFilename, QUuid(), _projectFiles); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6dc64cda6f..1a0ed5cc5e 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -13,6 +13,7 @@ #ifndef hifi_AvatarProject_h #define hifi_AvatarProject_h +#include "MarketplaceItemUploader.h" #include "FST.h" #include @@ -38,10 +39,7 @@ public: return false; } - Q_INVOKABLE QObject* upload() { - // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML - return nullptr; - } + Q_INVOKABLE MarketplaceItemUploader* upload(); /** * returns the AvatarProject or a nullptr on failure. @@ -70,6 +68,7 @@ private: QStringList _projectFiles{}; QString _projectPath; QString _fstPath; + QString _fstFilename; }; #endif // hifi_AvatarProject_h diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index c2671aadec..7a5abacce4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -14,44 +14,221 @@ #include #include -#include - -MarketplaceItemUploader::MarketplaceItemUploader(QUuid marketplaceID, std::vector filePaths) - : _filePaths(filePaths), _marketplaceID(marketplaceID) { +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +MarketplaceItemUploader::MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths) : + _title(title), + _description(description), _rootFilename(rootFilename), _filePaths(filePaths), _marketplaceID(marketplaceID) { + qWarning() << "File paths: " << _filePaths.join(", "); + //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); +} + +void MarketplaceItemUploader::setState(State newState) { + qDebug() << "Setting uploader state to: " << newState; + + _state = newState; + emit stateChanged(newState); + if (newState == State::Complete) { + emit completed(); + } } void MarketplaceItemUploader::send() { + doGetCategories(); +} + +void MarketplaceItemUploader::doGetCategories() { + setState(State::GettingCategories); + + static const QString path = "/api/v1/marketplace/categories"; + auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest("/marketplace/item", AccountManagerAuth::Required); + auto request = accountManager->createRequest(path, AccountManagerAuth::None); + + qWarning() << "Request url is: " << request.url(); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QByteArray data; - /* - auto reply = networkAccessManager.post(request, data); - - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + QNetworkReply* reply = networkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto doc = QJsonDocument::fromJson(reply->readAll()); + auto error = reply->error(); if (error == QNetworkReply::NoError) { - } else { - } - emit complete(); - }); - */ + auto extractCategoryID = [&doc]() -> std::pair { + auto items = doc.object()["data"].toObject()["items"]; + if (!items.isArray()) { + qWarning() << "Categories parse error: data.items is not an array"; + return { false, 0 }; + } - QTimer* timer = new QTimer(); - timer->setInterval(1000); - connect(timer, &QTimer::timeout, this, [this, timer]() { - if (progress <= 1.0f) { - progress += 0.1; - emit uploadProgress(progress * 100.0f, 100.0f); + auto itemsArray = items.toArray(); + for (const auto item : itemsArray) { + if (!item.isObject()) { + qWarning() << "Categories parse error: item is not an object"; + return { false, 0 }; + } + + auto itemObject = item.toObject(); + if (itemObject["name"].toString() == "Avatars") { + auto idValue = itemObject["id"]; + if (!idValue.isDouble()) { + qWarning() << "Categories parse error: id is not a number"; + return { false, 0 }; + } + return { true, (int)idValue.toDouble() }; + } + } + + qWarning() << "Categories parse error: could not find a category for 'Avatar'"; + return { false, 0 }; + }; + + bool success; + int id; + std::tie(success, id) = extractCategoryID(); + qDebug() << "Done " << success << id; + if (!success) { + qWarning() << "Failed to find marketplace category id"; + _error = Error::Unknown; + setState(State::Complete); + } else { + doUploadAvatar(); + } } else { - emit complete(); - timer->stop(); + _error = Error::Unknown; + setState(State::Complete); } }); - timer->start(); +} + +void MarketplaceItemUploader::doUploadAvatar() { + QBuffer buffer{ &_fileData }; + //buffer.open(QIODevice::WriteOnly); + QuaZip zip{ &buffer }; + if (!zip.open(QuaZip::Mode::mdAdd)) { + qWarning() << "Failed to open zip!!"; + } + + for (auto& filePath : _filePaths) { + qWarning() << "Zipping: " << filePath; + QFileInfo fileInfo{ filePath }; + + QuaZipFile zipFile{ &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + qWarning() << "Could not open zip file:" << zipFile.getZipError(); + _error = Error::Unknown; + setState(State::Complete); + return; + } + QFile file{ filePath }; + if (file.open(QIODevice::ReadOnly)) { + zipFile.write(file.readAll()); + } else { + qWarning() << "Failed to open: " << filePath; + } + file.close(); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qWarning() << "Could not close zip file: " << zipFile.getZipError(); + setState(State::Complete); + return; + } + } + + zip.close(); + + qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; + + QString path = "/api/v1/marketplace/items"; + bool creating = true; + if (!_marketplaceID.isNull()) { + creating = false; + auto idWithBraces = _marketplaceID.toString(); + auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2); + path += "/" + idWithoutBraces; + } + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + qWarning() << "Request url is: " << request.url(); + + QJsonObject root{ { "marketplace_item", + QJsonObject{ { "title", _title }, + { "description", _description }, + { "root_file_key", _rootFilename }, + { "category_ids", QJsonArray({ 5 }) }, + //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, + { "license", 0 }, + { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + QJsonDocument doc{ root }; + + qWarning() << "data: " << doc.toJson(); + + _fileData.toBase64(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply{ nullptr }; + if (creating) { + reply = networkAccessManager.post(request, doc.toJson()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + doWaitForInventory(); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); + + setState(State::UploadingAvatar); +} + +void MarketplaceItemUploader::doWaitForInventory() { + static const QString path = "/api/v1/commerce/inventory"; + + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + + qWarning() << "Request url is: " << request.url(); + + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply = networkAccessManager.post(request, ""); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); + qWarning() << "Finished inventory request " << data; + + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + } else { + _error = Error::Unknown; + } + setState(State::Complete); + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 2a3071244e..a0ec3f6991 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -20,36 +20,68 @@ class QNetworkReply; class MarketplaceItemUploader : public QObject { Q_OBJECT + + Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) + Q_PROPERTY(State state READ getState NOTIFY stateChanged) + Q_PROPERTY(Error error READ getError) + Q_PROPERTY(QString responseData READ getResponseData) public: enum class Error { None, - ItemNotUpdateable, - ItemDoesNotExist, - RequestTimedOut, Unknown }; + Q_ENUM(Error); + enum class State { - Ready, - Sent + Idle, + GettingCategories, + UploadingAvatar, + WaitingForInventory, + Complete }; + Q_ENUM(State); - MarketplaceItemUploader(QUuid markertplaceID, std::vector filePaths); - - float progress{ 0.0f }; + MarketplaceItemUploader(QString title, + QString description, + QString rootFilename, + QUuid marketplaceID, + QStringList filePaths); Q_INVOKABLE void send(); + QString getResponseData() const { return _responseData; } + void setState(State newState); + State getState() const { return _state; } + bool getComplete() const { return _state == State::Complete; } + + Error getError() const { return _error; } + signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); - void complete(); + void completed(); + void stateChanged(State newState); private: + void doGetCategories(); + void doUploadAvatar(); + void doWaitForInventory(); QNetworkReply* _reply; + + State _state{ State::Idle }; + Error _error{ Error::None }; + + QString _title; + QString _description; + QString _rootFilename; QUuid _marketplaceID; - std::vector _filePaths; + + QString _responseData; + + QStringList _filePaths; + QByteArray _fileData; }; #endif // hifi_MarketplaceItemUploader_h diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index dc304c6803..23c7346df8 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -112,6 +112,15 @@ const GROUPS = [ type: "color", propertyID: "color", }, + { + label: "Alpha", + type: "", + type: "number", + min: 0, + max: 1, + step: 0.001, + propertyID: "alpha", + }, ] }, { From cb74313de8210710e6666836b838e1456786f487 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Wed, 19 Dec 2018 19:23:24 +0100 Subject: [PATCH 07/43] create projects / style changes --- .../resources/qml/hifi/AvatarPackager.qml | 196 ++++++++++++------ .../avatarPackager/AvatarPackagerFooter.qml | 24 +++ .../avatarPackager/AvatarPackagerHeader.qml | 71 +++++++ .../qml/hifi/avatarPackager/AvatarProject.qml | 55 +++-- .../avatarPackager/CreateAvatarProject.qml | 108 ++++++++++ .../avatarPackager/ProjectInputControl.qml | 76 +++++++ interface/src/avatar/AvatarPackager.cpp | 21 +- interface/src/avatar/AvatarPackager.h | 13 +- interface/src/avatar/AvatarProject.cpp | 83 ++++++-- interface/src/avatar/AvatarProject.h | 25 ++- libraries/fbx/src/FST.cpp | 137 +++++++++++- libraries/fbx/src/FST.h | 15 +- 12 files changed, 693 insertions(+), 131 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 5a51a3c873..8eb765716e 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -15,92 +15,160 @@ Windows.ScrollingWindow { width: 480 height: 706 title: "Avatar Packager" - resizable: true + resizable: false opacity: parent.opacity destroyOnHidden: true implicitWidth: 384; implicitHeight: 640 - minSize: Qt.vector2d(200, 300) + minSize: Qt.vector2d(480, 706) + HifiConstants { id: hifi } - //HifiConstants { id: hifi } Item { + id: windowContent height: pane.height width: pane.width + anchors.fill: parent - AvatarProject { - id: avatarProject - colorScheme: root.colorScheme - visible: false + // FIXME: modal overlay does not show + Rectangle { + id: modalOverlay anchors.fill: parent + z: 20000 + color: "#aa031b33" + clip: true + visible: true } - Item { - id: avatarPackagerMain - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - anchors.bottom: parent.bottom - RalewaySemiBold { - id: avatarPackagerLabel - size: 24; - anchors.left: parent.left - anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: 'Avatar Packager' - } + Column { + id: avatarPackager + anchors.fill: parent + state: "main" + states: [ + State { + name: "main" + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } + PropertyChanges { target: avatarPackagerMain; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } + }, + State { + name: "createProject" + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } + PropertyChanges { target: createAvatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } + }, + State { + name: "project" + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + } + ] - HifiControls.Button { - id: createProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: avatarPackagerLabel.bottom - text: qsTr("Create Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - + AvatarPackagerHeader { + id: avatarPackagerHeader + onBackButtonClicked: { + avatarPackager.state = "main" } } - HifiControls.Button { - id: openProjectButton - anchors.left: parent.left - anchors.right: parent.right - anchors.top: createProjectButton.bottom - text: qsTr("Open Avatar Project") - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - console.log("path = " + avatarProjectsPath); - // TODO: make the dialog modal + Item { + height: pane.height - avatarPackagerHeader.height - avatarPackagerFooter.height + width: pane.width - var browser = desktop.fileDialog({ - selectDirectory: false, - dir: fileDialogHelper.pathToUrl(avatarProjectsPath), - filter: "Avatar Project FST Files (*.fst)", - title: "Open Project (.fst)" - }); + Rectangle { + anchors.fill: parent + color: "#404040" + } - browser.canceled.connect(function() { - - }); + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + anchors.fill: parent + } - browser.selectedFile.connect(function(fileUrl) { - console.log("FOUND PATH " + fileUrl); - let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); - if (currentAvatarProject) { - console.log("LOAD COMPLETE"); - console.log("file dir = " + AvatarPackagerCore.currentAvatarProject.projectFolderPath); - - avatarPackagerMain.visible = false; - avatarProject.visible = true; + CreateAvatarProject { + id: createAvatarProject + colorScheme: root.colorScheme + anchors.fill: parent + } + + Item { + id: avatarPackagerMain + visible: false + anchors.fill: parent + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: createProjectButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: openProjectButton.left + anchors.rightMargin: 22 + height: 40 + width: 134 + text: qsTr("New Project") + colorScheme: root.colorScheme + onClicked: { + avatarPackager.state = "createProject"; + } } - }); + + HifiControls.Button { + id: openProjectButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 40 + width: 133 + text: qsTr("Open Project") + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: { + // TODO: make the dialog modal + let browser = desktop.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)", + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + if (currentAvatarProject) { + avatarPackager.state = "project"; + } + }); + } + } + } + Flow { + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("Use a custom avatar to express your identity") + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("To learn more about using this tool, visit our docs") + } + } } } + AvatarPackagerFooter { + id: avatarPackagerFooter + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml new file mode 100644 index 0000000000..526a2047e3 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -0,0 +1,24 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarPackagerFooter + + color: "#404040" + height: 74 + width: parent.width + + property var content: Item { } + + children: [background, content] + + property var background: Rectangle { + anchors.fill: parent + color: "#404040" + // TODO Use a shadow instead / border is just here for element debug purposes + border.width: 2; + } + +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml new file mode 100644 index 0000000000..6dcb1267d4 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -0,0 +1,71 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Rectangle { + id: avatarPackagerHeader + + width: parent.width + height: 74 + color: "#252525" + + property alias title: title.text + property alias faqEnabled: faq.visible + property alias backButtonEnabled: back.visible + signal backButtonClicked + + RalewaySemiBold { + id: back + visible: true + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.leftMargin: 16 + anchors.verticalCenter: back.verticalCenter + text: "◀" + color: "white" + MouseArea { + anchors.fill: parent + onClicked: avatarPackagerHeader.backButtonClicked() + hoverEnabled: true + onEntered: { state = "hovering" } + onExited: { state = "" } + states: [ + State { + name: "hovering" + PropertyChanges { + target: back + color: "gray" + } + } + ] + } + } + + RalewaySemiBold { + id: title + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: back.visible ? back.right : parent.left + anchors.leftMargin: back.visible ? 11 : 21 + anchors.verticalCenter: title.verticalCenter + text: qsTr("Avatar Packager") + color: "white" + } + + RalewaySemiBold { + id: faq + visible: false + size: 28 + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.rightMargin: 16 + anchors.verticalCenter: faq.verticalCenter + text: qsTr("FAQ") + color: "white" + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 085f1acdce..1e1d256024 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -11,36 +11,56 @@ Item { HifiConstants { id: hifi } - property int colorScheme; - - visible: true + property int colorScheme + visible: false anchors.fill: parent anchors.margins: 10 + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: uploadButton + //width: parent.width + //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: { + } + } + } + RalewaySemiBold { - id: avatarProjectLabel - size: 24; - width: parent.width + id: avatarFBXNameLabel + size: 14 + anchors.left: parent.left + anchors.top: parent.top anchors.topMargin: 25 anchors.bottomMargin: 25 - text: 'Avatar Project' - color: "white" + text: qsTr("FBX file here") } + HifiControls.Button { id: openFolderButton width: parent.width - anchors.top: avatarProjectLabel.bottom + anchors.top: avatarFBXNameLabel.bottom anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme height: 30 - onClicked: function() { - fileDialogHelper.openDirectory(AvatarPackagerCore.currentAvatarProject.projectFolderPath); + onClicked: { + fileDialogHelper.openDirectory(fileDialogHelper.pathToUrl(AvatarPackagerCore.currentAvatarProject.projectFolderPath)); } } + Rectangle { - color: 'white' + color: "white" visible: AvatarPackagerCore.currentAvatarProject !== null anchors.top: openFolderButton.bottom anchors.left: parent.left @@ -56,15 +76,4 @@ Item { delegate: Text { text: 'File: ' + modelData } } } - HifiControls.Button { - id: uploadButton - width: parent.width - anchors.bottom: parent.bottom - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - height: 30 - onClicked: function() { - } - } } diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml new file mode 100644 index 0000000000..4d1f745fa5 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -0,0 +1,108 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: root + + HifiConstants { id: hifi } + + property int colorScheme + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: createButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 30 + width: 133 + text: qsTr("Create") + onClicked: { + if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { + Window.alert('Failed to create project') + return; + } + avatarPackager.state = "project"; + } + } + } + + visible: false + anchors.fill: parent + height: parent.height + width: parent.width + + Column { + id: createAvatarColumns + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 10 + + spacing: 17 + + ProjectInputControl { + id: name + label: "Name" + colorScheme: root.colorScheme + } + + ProjectInputControl { + id: projectLocation + label: "Specify Project Location" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: true + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseTitle: "Project Location" + text: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + onTextChanged: { + //TODO: valid folder? Does project with name exist here already? + } + } + + ProjectInputControl { + id: avatarModel + label: "Specify Avatar Model (.fbx)" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: false + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseFilter: "Avatar Model File (*.fbx)" + browseTitle: "Open Avatar Model (.fbx)" + onTextChanged: { + //TODO: try to get texture folder from fbx if none is set? + } + } + + ProjectInputControl { + id: textureFolder + label: "Specify Texture Folder" + colorScheme: root.colorScheme + browseEnabled: true + browseFolder: true + browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseTitle: "Texture Folder" + onTextChanged: { + //TODO: valid folder? + + } + } + } + RalewayRegular { + text: "A folder with that name already exists at that location. Please choose a different project name or location." + color: "#EA4C5F"; + wrapMode: Text.WordWrap + size: 20 + anchors { + top: createAvatarColumns.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml new file mode 100644 index 0000000000..472db47c2f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -0,0 +1,76 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Column { + id: control + + anchors.left: parent.left + anchors.leftMargin: 21 + anchors.right: parent.right + anchors.rightMargin: 16 + + height: 75 + + spacing: 4 + + property alias label: label.text + property alias browseEnabled: browseButton.visible + property bool browseFolder: false + property string browseFilter: "All Files (*.*)" + property string browseTitle: "Open file" + property string browseDir: "" + property alias text: input.text + + property int colorScheme + + Row { + RalewaySemiBold { + id: label + size: 20 + font.weight: Font.Medium + text: "" + color: "white" + } + } + Row { + width: control.width + spacing: 16 + height: 40 + HifiControls.TextField { + id: input + colorScheme: control.colorScheme + font.family: "Fira Sans" + font.pixelSize: 18 + height: parent.height + width: browseButton.visible ? parent.width - browseButton.width - parent.spacing : parent.width + } + + HifiControls.Button { + id: browseButton + visible: false + height: parent.height + width: 133 + text: qsTr("Browse") + colorScheme: root.colorScheme + onClicked: { + // TODO: make the dialog modal + let browser = desktop.fileDialog({ + selectDirectory: browseFolder, + dir: browseDir, + filter: browseFilter, + title: browseTitle, + }); + + browser.canceled.connect(function() { + + }); + + browser.selectedFile.connect(function(fileUrl) { + input.text = fileDialogHelper.urlToPath(fileUrl); + }); + } + } + } +} diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 3fdf193087..8075bc5bdc 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -12,6 +12,7 @@ #include "AvatarPackager.h" #include +#include #include #include @@ -25,6 +26,8 @@ std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { std::call_once(setupQMLTypesFlag, []() { qmlRegisterType(); + qRegisterMetaType(); + qRegisterMetaType(); }); } @@ -38,12 +41,24 @@ bool AvatarPackager::open() { return true; } -QObject* AvatarPackager::openAvatarProject(QString avatarProjectFSTPath) { +AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { if (_currentAvatarProject) { - //_currentAvatarProject->deleteLater(); - //_currentAvatarProject = nullptr; + _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); + emit avatarProjectChanged(); + return _currentAvatarProject; +} + +AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { + if (_currentAvatarProject) { + _currentAvatarProject->deleteLater(); + } + _currentAvatarProject = AvatarProject::createAvatarProject(avatarProjectName, avatarModelPath); + qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index f002631f17..e0268747c2 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -16,25 +16,28 @@ #include #include +#include "FileDialogHelper.h" + #include "avatar/AvatarProject.h" class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY - Q_PROPERTY(QObject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) - -public: + Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) + Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT) +public: AvatarPackager(); bool open(); - Q_INVOKABLE QObject* openAvatarProject(QString avatarProjectFSTPath); + Q_INVOKABLE AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); + Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); signals: void avatarProjectChanged(); private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; - //Q_INVOKABLE QObject* openAvatarProject(); + Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } Q_INVOKABLE QObject* uploadItem(); AvatarProject* _currentAvatarProject{ nullptr }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index c7ea7e52ac..4321d2ef40 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -15,44 +15,93 @@ #include #include -#include #include #include +#include "FBXSerializer.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { - const auto pathToLower = path.toLower(); - if (pathToLower.endsWith(".fst")) { - QFile file{ path }; - if (!file.open(QIODevice::ReadOnly)) { - return nullptr; - } - auto project = new AvatarProject(path, file.readAll()); - QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); - return project; + if (!path.toLower().endsWith(".fst")) { + return nullptr; + } + QFile file{ path }; + if (!file.open(QIODevice::ReadOnly)) { + return nullptr; + } + const auto project = new AvatarProject(path, file.readAll()); + QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); + return project; +} + +AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath) { + if (!isValidNewProjectName(avatarProjectName)) { + return nullptr; + } + QDir dir(getDefaultProjectsPath() + "/" + avatarProjectName); + if (!dir.mkpath(".")) { + return nullptr; + } + const auto fileName = QFileInfo(avatarModelPath).fileName(); + const auto newModelPath = dir.absoluteFilePath(fileName); + const auto newFSTPath = dir.absoluteFilePath("avatar.fst"); + QFile::copy(avatarModelPath, newModelPath); + + QFileInfo fbxInfo(newModelPath); + QFile fbx(fbxInfo.filePath()); + if (!fbxInfo.exists() || !fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { + // TODO: Can't open model FBX (throw error here) + return nullptr; } - if (pathToLower.endsWith(".fbx")) { - // TODO: Create FST here: + std::shared_ptr hfmModel; + + + try { + qDebug() << "Reading FBX file : " << fbxInfo.filePath(); + const QByteArray fbxContents = fbx.readAll(); + hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); + } + catch (const QString& error) { + qDebug() << "Error reading: " << error; + return nullptr; + } + //TODO: copy/fix textures here: + + + + FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); + + fst->setName(avatarProjectName); + + if (!fst->write()) { + return nullptr; } - return nullptr; + return new AvatarProject(fst); +} + +bool AvatarProject::isValidNewProjectName(const QString& projectName) { + QDir dir(getDefaultProjectsPath() + "/" + projectName); + return !dir.exists(); } AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : - _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { + AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { - _directory = QFileInfo(_fstPath).absoluteDir(); +} +AvatarProject::AvatarProject(FST* fst) { + _fst = fst; + auto fileInfo = QFileInfo(getFSTPath()); + _directory = fileInfo.absoluteDir(); //_projectFiles = _directory.entryList(); refreshProjectFiles(); - auto fileInfo = QFileInfo(_fstPath); _projectPath = fileInfo.absoluteDir().absolutePath(); } void AvatarProject::appendDirectory(QString prefix, QDir dir) { qDebug() << "Inside of " << prefix << dir.absolutePath(); - auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + const auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { _projectFiles.append(prefix + "/" + entry.fileName()); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6dc64cda6f..9708c9fa83 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -21,6 +21,7 @@ #include #include #include +#include class AvatarProject : public QObject { Q_OBJECT @@ -31,6 +32,7 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) + Q_PROPERTY(QString name READ getProjectName) public: Q_INVOKABLE bool write() { @@ -38,38 +40,41 @@ public: return false; } - Q_INVOKABLE QObject* upload() { - // TODO: create new AvatarProjectUploader here, launch it and return it for status tracking in QML - return nullptr; - } - /** * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); + static AvatarProject* createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath); + + static bool isValidNewProjectName(const QString& projectName); + + static QString getDefaultProjectsPath() { + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; + } private: AvatarProject(const QString& fstPath, const QByteArray& data); + AvatarProject(FST* fst); ~AvatarProject() { // TODO: cleanup FST / AvatarProjectUploader etc. } + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } - Q_INVOKABLE QString getFSTPath() const { return _fstPath; } - Q_INVOKABLE QString getFBXPath() const { return _fst.getModelPath(); } + Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } + Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } - FST* getFST() { return &_fst; } + FST* getFST() { return _fst; } void refreshProjectFiles(); void appendDirectory(QString prefix, QDir dir); - FST _fst; + FST* _fst; QDir _directory; QStringList _projectFiles{}; QString _projectPath; - QString _fstPath; }; #endif // hifi_AvatarProject_h diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 6574b66e51..2631fe951e 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -13,21 +13,125 @@ #include #include +#include -FST::FST(QString fstPath, QVariantHash data) : _fstPath(fstPath) { - if (data.contains("name")) { - _name = data["name"].toString(); - data.remove("name"); +FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { + if (data.contains(NAME_FIELD)) { + _name = data[NAME_FIELD].toString(); + data.remove(NAME_FIELD); } - if (data.contains("filename")) { - _modelPath = data["filename"].toString(); - data.remove("filename"); + if (data.contains(FILENAME_FIELD)) { + _modelPath = data[FILENAME_FIELD].toString(); + data.remove(FILENAME_FIELD); } _other = data; } +FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel) { + QVariantHash mapping; + + // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will + // be rewritten, so we detect the existence of several different blendshapes which indicate we're likely a mixamo file + bool likelyMixamoFile = hfmModel.applicationName == "mixamo.com" || + (hfmModel.blendshapeChannelNames.contains("BrowsDown_Right") && + hfmModel.blendshapeChannelNames.contains("MouthOpen") && + hfmModel.blendshapeChannelNames.contains("Blink_Left") && + hfmModel.blendshapeChannelNames.contains("Blink_Right") && + hfmModel.blendshapeChannelNames.contains("Squint_Right")); + + mapping.insert(NAME_FIELD, QFileInfo(fstPath).baseName()); + QDir root(modelFilePath); + mapping.insert(FILENAME_FIELD, root.relativeFilePath(fstPath)); + mapping.insert(TEXDIR_FIELD, "textures"); + mapping.insert(SCRIPT_FIELD, "scripts"); + + // mixamo/autodesk defaults + mapping.insert(SCALE_FIELD, 1.0); + QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); + joints.insert("jointEyeLeft", hfmModel.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : + (hfmModel.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); + + joints.insert("jointEyeRight", hfmModel.jointIndices.contains("jointEyeRight") ? "jointEyeRight" : + hfmModel.jointIndices.contains("EyeRight") ? "EyeRight" : "RightEye"); + + joints.insert("jointNeck", hfmModel.jointIndices.contains("jointNeck") ? "jointNeck" : "Neck"); + joints.insert("jointRoot", "Hips"); + joints.insert("jointLean", "Spine"); + joints.insert("jointLeftHand", "LeftHand"); + joints.insert("jointRightHand", "RightHand"); + + const char* topName = likelyMixamoFile ? "HeadTop_End" : "HeadEnd"; + joints.insert("jointHead", hfmModel.jointIndices.contains(topName) ? topName : "Head"); + + mapping.insert(JOINT_FIELD, joints); + + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); + mapping.insertMulti(FREE_JOINT_FIELD, "RightForeArm"); + + + // If there are no blendshape mappings, and we detect that this is likely a mixamo file, + // then we can add the default mixamo to "faceshift" mappings + if (likelyMixamoFile) { + QVariantHash blendshapes; + blendshapes.insertMulti("BrowsD_L", QVariantList() << "BrowsDown_Left" << 1.0); + blendshapes.insertMulti("BrowsD_R", QVariantList() << "BrowsDown_Right" << 1.0); + blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Left" << 1.0); + blendshapes.insertMulti("BrowsU_C", QVariantList() << "BrowsUp_Right" << 1.0); + blendshapes.insertMulti("BrowsU_L", QVariantList() << "BrowsUp_Left" << 1.0); + blendshapes.insertMulti("BrowsU_R", QVariantList() << "BrowsUp_Right" << 1.0); + blendshapes.insertMulti("ChinLowerRaise", QVariantList() << "Jaw_Up" << 1.0); + blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Left" << 0.5); + blendshapes.insertMulti("ChinUpperRaise", QVariantList() << "UpperLipUp_Right" << 0.5); + blendshapes.insertMulti("EyeBlink_L", QVariantList() << "Blink_Left" << 1.0); + blendshapes.insertMulti("EyeBlink_R", QVariantList() << "Blink_Right" << 1.0); + blendshapes.insertMulti("EyeOpen_L", QVariantList() << "EyesWide_Left" << 1.0); + blendshapes.insertMulti("EyeOpen_R", QVariantList() << "EyesWide_Right" << 1.0); + blendshapes.insertMulti("EyeSquint_L", QVariantList() << "Squint_Left" << 1.0); + blendshapes.insertMulti("EyeSquint_R", QVariantList() << "Squint_Right" << 1.0); + blendshapes.insertMulti("JawFwd", QVariantList() << "JawForeward" << 1.0); + blendshapes.insertMulti("JawLeft", QVariantList() << "JawRotateY_Left" << 0.5); + blendshapes.insertMulti("JawOpen", QVariantList() << "MouthOpen" << 0.7); + blendshapes.insertMulti("JawRight", QVariantList() << "Jaw_Right" << 1.0); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "JawForeward" << 0.39); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "Jaw_Down" << 0.36); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Left" << 1.0); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthNarrow_Right" << 1.0); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Left" << 0.5); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "MouthWhistle_NarrowAdjust_Right" << 0.5); + blendshapes.insertMulti("LipsFunnel", QVariantList() << "TongueUp" << 1.0); + blendshapes.insertMulti("LipsLowerClose", QVariantList() << "LowerLipIn" << 1.0); + blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Left" << 0.7); + blendshapes.insertMulti("LipsLowerDown", QVariantList() << "LowerLipDown_Right" << 0.7); + blendshapes.insertMulti("LipsLowerOpen", QVariantList() << "LowerLipOut" << 1.0); + blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Left" << 1.0); + blendshapes.insertMulti("LipsPucker", QVariantList() << "MouthNarrow_Right" << 1.0); + blendshapes.insertMulti("LipsUpperClose", QVariantList() << "UpperLipIn" << 1.0); + blendshapes.insertMulti("LipsUpperOpen", QVariantList() << "UpperLipOut" << 1.0); + blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Left" << 0.7); + blendshapes.insertMulti("LipsUpperUp", QVariantList() << "UpperLipUp_Right" << 0.7); + blendshapes.insertMulti("MouthDimple_L", QVariantList() << "Smile_Left" << 0.25); + blendshapes.insertMulti("MouthDimple_R", QVariantList() << "Smile_Right" << 0.25); + blendshapes.insertMulti("MouthFrown_L", QVariantList() << "Frown_Left" << 1.0); + blendshapes.insertMulti("MouthFrown_R", QVariantList() << "Frown_Right" << 1.0); + blendshapes.insertMulti("MouthLeft", QVariantList() << "Midmouth_Left" << 1.0); + blendshapes.insertMulti("MouthRight", QVariantList() << "Midmouth_Right" << 1.0); + blendshapes.insertMulti("MouthSmile_L", QVariantList() << "Smile_Left" << 1.0); + blendshapes.insertMulti("MouthSmile_R", QVariantList() << "Smile_Right" << 1.0); + blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Left" << 1.0); + blendshapes.insertMulti("Puff", QVariantList() << "CheekPuff_Right" << 1.0); + blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Left" << 0.75); + blendshapes.insertMulti("Sneer", QVariantList() << "NoseScrunch_Right" << 0.75); + blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Left" << 0.5); + blendshapes.insertMulti("Sneer", QVariantList() << "Squint_Right" << 0.5); + mapping.insert(BLENDSHAPE_FIELD, blendshapes); + } + return new FST(fstPath, mapping); +} + QString FST::absoluteModelPath() const { QFileInfo fileInfo{ _fstPath }; QDir dir{ fileInfo.absoluteDir() }; @@ -42,4 +146,21 @@ void FST::setName(const QString& name) { void FST::setModelPath(const QString& modelPath) { _modelPath = modelPath; emit modelPathChanged(modelPath); -} \ No newline at end of file +} + +QVariantHash FST::getMapping() { + QVariantHash mapping; + mapping.insertMulti(NAME_FIELD, _name); + mapping.insertMulti(FILENAME_FIELD, _modelPath); + mapping.unite(_other); + return mapping; +} + +bool FST::write() { + QFile fst(_fstPath); + if (!fst.open(QIODevice::WriteOnly)) { + return false; + } + fst.write(FSTReader::writeMapping(getMapping())); + return true; +} diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index e8c67c6c6b..524463b721 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -13,6 +13,11 @@ #include #include +#include "FSTReader.h" + +namespace hfm { + class Model; +}; class FST : public QObject { Q_OBJECT @@ -20,7 +25,9 @@ class FST : public QObject { Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) public: - FST(QString fstPath, QVariantHash data); + FST(const QString& fstPath, QVariantHash data); + + static FST* createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel); QString absoluteModelPath() const; @@ -32,6 +39,12 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } + QString getPath() { return _fstPath; } + + QVariantHash getMapping(); + + bool write(); + signals: void nameChanged(const QString& name); void modelPathChanged(const QString& modelPath); From c2aca64b11efe5ffc95c961e6f6421fdaa1437a2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Wed, 19 Dec 2018 11:30:21 -0800 Subject: [PATCH 08/43] Add start of opening uploaded avatar in inventory --- interface/resources/qml/hifi/AvatarPackager.qml | 1 - .../qml/hifi/avatarPackager/AvatarProject.qml | 2 ++ interface/src/avatar/AvatarProject.cpp | 16 ++++++++++++++-- interface/src/avatar/AvatarProject.h | 1 + 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index d5e21d9653..f0835c56bf 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -72,7 +72,6 @@ Windows.ScrollingWindow { height: 30 onClicked: function() { var avatarProjectsPath = fileDialogHelper.standardPath(/*fileDialogHelper.StandardLocation.DocumentsLocation*/ 1) + "/High Fidelity/Avatar Projects"; - var avatarProjectsPath = "C:/Users/ryanh/Documents/High Fidelity Avatars"; console.log("path = " + avatarProjectsPath); // TODO: make the dialog modal diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 4f42927676..1ba41b1de6 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -156,6 +156,8 @@ Item { colorScheme: root.colorScheme onClicked: function() { console.log("Opening in inventory"); + + AvatarPackagerCore.currentAvatarProject.openInInventory(); } } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index cc23027562..d08eafb43c 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -19,6 +19,9 @@ #include #include +#include +#include "scripting/HMDScriptingInterface.h" + AvatarProject* AvatarProject::openAvatarProject(const QString& path) { const auto pathToLower = path.toLower(); if (pathToLower.endsWith(".fst")) { @@ -40,7 +43,6 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : _fstPath(fstPath), _fst(fstPath, FSTReader::readMapping(data)) { - _fstFilename = QFileInfo(_fstPath).fileName(); qDebug() << "Pointers: " << this << &_fst; @@ -70,6 +72,16 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } -Q_INVOKABLE MarketplaceItemUploader* AvatarProject::upload() { +MarketplaceItemUploader* AvatarProject::upload() { return new MarketplaceItemUploader("test_avatar", "blank description", _fstFilename, QUuid(), _projectFiles); } + +void AvatarProject::openInInventory() { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); + DependencyManager::get()->openTablet(); + tablet->sendToQml(QVariantMap({ + { "method", "updatePurchases" }, + { "filterText", "filtertext" } })); +} diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 1a0ed5cc5e..de7751008f 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -40,6 +40,7 @@ public: } Q_INVOKABLE MarketplaceItemUploader* upload(); + Q_INVOKABLE void openInInventory(); /** * returns the AvatarProject or a nullptr on failure. From 556f516be669721be33e9537e930a93017ecb792 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 20 Dec 2018 16:30:08 -0800 Subject: [PATCH 09/43] Update uploader screen --- .../resources/qml/hifi/AvatarPackager.qml | 25 +- .../avatarPackager/AvatarPackagerFooter.qml | 6 +- .../qml/hifi/avatarPackager/AvatarProject.qml | 223 +++++++++------- interface/src/avatar/AvatarProject.cpp | 39 ++- interface/src/avatar/AvatarProject.h | 2 +- .../src/avatar/MarketplaceItemUploader.cpp | 240 +++++++++++------- .../src/avatar/MarketplaceItemUploader.h | 5 + libraries/fbx/src/FST.h | 2 + 8 files changed, 339 insertions(+), 203 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 8eb765716e..4d27e6fce4 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -8,6 +8,7 @@ import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" import "avatarPackager" +import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { id: root @@ -27,18 +28,14 @@ Windows.ScrollingWindow { id: windowContent height: pane.height width: pane.width - anchors.fill: parent - // FIXME: modal overlay does not show - Rectangle { - id: modalOverlay + AvatarApp.MessageBox { + id: popup anchors.fill: parent - z: 20000 - color: "#aa031b33" - clip: true - visible: true + visible: false } + // FIXME: modal overlay does not show Column { id: avatarPackager anchors.fill: parent @@ -61,6 +58,12 @@ Windows.ScrollingWindow { PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + }, + State { + name: "project-upload" + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarUploader; visible: true } + PropertyChanges { target: avatarPackagerFooter; color: "blue"; visible: false } } ] @@ -86,6 +89,12 @@ Windows.ScrollingWindow { anchors.fill: parent } + AvatarProjectUpload { + id: avatarUploader + anchors.fill: parent + root: avatarProject + } + CreateAvatarProject { id: createAvatarProject colorScheme: root.colorScheme diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index 526a2047e3..8498d20858 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -7,10 +7,10 @@ Rectangle { id: avatarPackagerFooter color: "#404040" - height: 74 + height: content === defaultContent ? 0 : 74 width: parent.width - property var content: Item { } + property var content: Item { id: defaultContent } children: [background, content] @@ -21,4 +21,4 @@ Rectangle { border.width: 2; } -} +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 0aca722352..286ebb23ba 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,6 +25,7 @@ Item { anchors.rightMargin: 17 HifiControls.Button { id: uploadButton + enabled: Account.loggedIn //width: parent.width //anchors.bottom: parent.bottom anchors.verticalCenter: parent.verticalCenter @@ -35,26 +36,82 @@ Item { width: 133 height: 40 onClicked: function() { - console.log("Uploading"); - root.uploader = AvatarPackagerCore.currentAvatarProject.upload(); - console.log("uploader: "+ root.uploader); - root.uploader.uploadProgress.connect(function(uploaded, total) { - console.log("Uploader progress: " + uploaded + " / " + total); - }); - root.uploader.completed.connect(function() { - try { - var response = JSON.parse(root.uploader.responseData); - console.log("Uploader complete! " + response); - uploadStatus.text = response.status; - } catch (e) { - console.log("Error parsing JSON: " + root.uploader.reponseData); - } - }); - root.uploader.send(); + if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } else { + uploadNew(); + } } } } + function uploadNew() { + console.log("Uploading new"); + upload(false); + } + function uploadUpdate() { + console.log("Uploading update"); + upload(true); + } + + function upload(updateExisting) { + root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting); + console.log("uploader: "+ root.uploader); + root.uploader.uploadProgress.connect(function(uploaded, total) { + console.log("Uploader progress: " + uploaded + " / " + total); + }); + root.uploader.completed.connect(function() { + try { + var response = JSON.parse(root.uploader.responseData); + console.log("Uploader complete! " + response); + uploadStatus.text = response.status; + } catch (e) { + console.log("Error parsing JSON: " + root.uploader.reponseData); + } + }); + root.uploader.send(); + avatarPackager.state = "project-upload"; + } + + function showConfirmUploadPopup() { + popup.titleText = 'Overwrite Avatar' + popup.bodyText = 'You have previously uploaded the avatar file from this project.' + + ' This will overwrite that avatar and you won’t be able to access the older version.' + + popup.button1text = 'CREATE NEW'; + popup.button2text = 'OVERWRITE'; + + popup.onButton2Clicked = function() { + popup.close(); + uploadUpdate(); + } + popup.onButton1Clicked = function() { + popup.close(); + showConfirmCreateNewPopup(); + }; + + popup.open(); + //popup.forceActiveFocus(); + } + + function showConfirmCreateNewPopup(confirmCallback) { + popup.titleText = 'Create New' + popup.bodyText = 'This will upload your current files with the same avatar name.' + + ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?' + + popup.button1text = 'CANCEL'; + popup.button2text = 'CONFIRM'; + + popup.onButton1Clicked = function() { popup.close() }; + popup.onButton2Clicked = function() { + popup.close(); + uploadNew(); + }; + + popup.open(); + //popup.forceActiveFocus(); + } + RalewaySemiBold { id: avatarFBXNameLabel size: 14 @@ -79,11 +136,15 @@ Item { } Rectangle { + id: fileList + + visible: false + color: "white" - visible: AvatarPackagerCore.currentAvatarProject !== null anchors.top: openFolderButton.bottom anchors.left: parent.left anchors.right: parent.right + anchors.bottom: showFilesText.top //anchors.bottom: uploadButton.top anchors.topMargin: 10 anchors.bottomMargin: 10 @@ -92,96 +153,82 @@ Item { ListView { anchors.fill: parent model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Text { text: 'File: ' + modelData } + delegate: Rectangle { + width: parent.width + height: fileText.implicitHeight + 10 + color: (index % 2 == 0) ? "white" : "grey" + Text { + id: fileText + anchors.top: parent.top + 5 + anchors.left: parent.left + 5 + text: modelData + } + } } } + Text { + id: showFilesText + + visible: AvatarPackagerCore.currentAvatarProject !== null + + anchors.bottom: loginRequiredMessage.top + anchors.bottomMargin: 10 + + font.pointSize: 12 + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. " + (fileList.visible ? "Hide" : "Show") + " list" + + onLinkActivated: fileList.visible = !fileList.visible + } + Rectangle { - id: uploadingScreen + id: loginRequiredMessage - visible: !!root.uploader - anchors.fill: parent + visible: !Account.loggedIn + height: loginRequiredTextRow.height + 20 - color: "black" - - Item { - visible: !!root.uploader && !root.uploader.complete - - anchors.fill: parent - - AnimatedImage { - id: uploadSpinner - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 - } + anchors { + bottom: parent.bottom + left: parent.left + right: parent.right } - Item { - visible: !!root.uploader && root.uploader.complete + color: "#FFD6AD" + border.color: "#F39622" + border.width: 2 + radius: 2 + + Item { + id: loginRequiredTextRow + + height: Math.max(loginWarningGlyph.implicitHeight, loginWarningText.implicitHeight) anchors.fill: parent + anchors.margins: 10 HiFiGlyphs { - id: successIcon + id: loginWarningGlyph - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left - size: 128 - text: "\ue01a" - color: "#1FC6A6" - } - - Text { - text: "Congratulations!" - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: successIcon.bottom - - color: "white" - } - - HifiControls.Button { width: implicitWidth - height: implicitHeight - anchors.bottom: parent.bottom + font.pointSize: 20 + text: "+" + color: "black" + } + Text { + id: loginWarningText + + anchors.verticalCenter: parent.verticalCenter + anchors.left: loginWarningGlyph.right anchors.right: parent.right - text: "View in Inventory" - - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: function() { - console.log("Opening in inventory"); - - AvatarPackagerCore.currentAvatarProject.openInInventory(); - } + text: "Please login to upload your avatar to High Fidelity hosting." + font.pointSize: 12 + wrapMode: Text.Wrap } } - - Column { - Text { - id: uploadStatus - - text: "Uploading" - color: "white" - - } - Text { - text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") - color: "white" - } - } - } } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 7ba9d395eb..4a14479cce 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -17,8 +17,9 @@ #include #include #include -#include "FBXSerializer.h" +#include +#include "FBXSerializer.h" #include #include "scripting/HMDScriptingInterface.h" @@ -57,20 +58,16 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa std::shared_ptr hfmModel; - try { qDebug() << "Reading FBX file : " << fbxInfo.filePath(); const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); - } - catch (const QString& error) { + } catch (const QString& error) { qDebug() << "Error reading: " << error; return nullptr; } //TODO: copy/fix textures here: - - FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); fst->setName(avatarProjectName); @@ -89,7 +86,6 @@ bool AvatarProject::isValidNewProjectName(const QString& projectName) { AvatarProject::AvatarProject(const QString& fstPath, const QByteArray& data) : AvatarProject::AvatarProject(new FST(fstPath, FSTReader::readMapping(data))) { - } AvatarProject::AvatarProject(FST* fst) { _fst = fst; @@ -119,8 +115,22 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } -MarketplaceItemUploader* AvatarProject::upload() { - return new MarketplaceItemUploader("test_avatar", "blank description", QFileInfo(getFSTPath()).fileName(), QUuid(), _projectFiles); +MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { + QUuid itemID; + if (updateExisting) { + itemID = _fst->getMarketplaceID(); + } + auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, + _projectFiles); + connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { + if (uploader->getError() == MarketplaceItemUploader::Error::None) { + _fst->setMarketplaceID(uploader->getMarketplaceID()); + // TODO(thoys) uncomment this + //_fst->write(); + } + }); + + return uploader; } void AvatarProject::openInInventory() { @@ -128,7 +138,12 @@ void AvatarProject::openInInventory() { DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get()->openTablet(); - tablet->sendToQml(QVariantMap({ - { "method", "updatePurchases" }, - { "filterText", "filtertext" } })); + auto name = getProjectName(); + + // I'm not a fan of this, but it's the only current option. + QTimer::singleShot(1000, [name]() { + auto tablet = dynamic_cast( + DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system")); + tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); + }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 023cd7b079..b164e67e0d 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -41,7 +41,7 @@ public: return false; } - Q_INVOKABLE MarketplaceItemUploader* upload(); + Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); /** diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 7a5abacce4..1559f359a7 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -67,10 +67,9 @@ void MarketplaceItemUploader::doGetCategories() { QNetworkReply* reply = networkAccessManager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto doc = QJsonDocument::fromJson(reply->readAll()); - auto error = reply->error(); if (error == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(reply->readAll()); auto extractCategoryID = [&doc]() -> std::pair { auto items = doc.object()["data"].toObject()["items"]; if (!items.isArray()) { @@ -119,116 +118,175 @@ void MarketplaceItemUploader::doGetCategories() { } void MarketplaceItemUploader::doUploadAvatar() { - QBuffer buffer{ &_fileData }; - //buffer.open(QIODevice::WriteOnly); - QuaZip zip{ &buffer }; - if (!zip.open(QuaZip::Mode::mdAdd)) { - qWarning() << "Failed to open zip!!"; + QBuffer buffer{ &_fileData }; + //buffer.open(QIODevice::WriteOnly); + QuaZip zip{ &buffer }; + if (!zip.open(QuaZip::Mode::mdAdd)) { + qWarning() << "Failed to open zip!!"; + } + + for (auto& filePath : _filePaths) { + qWarning() << "Zipping: " << filePath; + QFileInfo fileInfo{ filePath }; + + QuaZipFile zipFile{ &zip }; + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + qWarning() << "Could not open zip file:" << zipFile.getZipError(); + _error = Error::Unknown; + setState(State::Complete); + return; } - - for (auto& filePath : _filePaths) { - qWarning() << "Zipping: " << filePath; - QFileInfo fileInfo{ filePath }; - - QuaZipFile zipFile{ &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { - qWarning() << "Could not open zip file:" << zipFile.getZipError(); - _error = Error::Unknown; - setState(State::Complete); - return; - } - QFile file{ filePath }; - if (file.open(QIODevice::ReadOnly)) { - zipFile.write(file.readAll()); - } else { - qWarning() << "Failed to open: " << filePath; - } - file.close(); - zipFile.close(); - if (zipFile.getZipError() != UNZ_OK) { - qWarning() << "Could not close zip file: " << zipFile.getZipError(); - setState(State::Complete); - return; - } - } - - zip.close(); - - qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - - QString path = "/api/v1/marketplace/items"; - bool creating = true; - if (!_marketplaceID.isNull()) { - creating = false; - auto idWithBraces = _marketplaceID.toString(); - auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2); - path += "/" + idWithoutBraces; - } - auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); - - QJsonObject root{ { "marketplace_item", - QJsonObject{ { "title", _title }, - { "description", _description }, - { "root_file_key", _rootFilename }, - { "category_ids", QJsonArray({ 5 }) }, - //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, - { "license", 0 }, - { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; - request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - QJsonDocument doc{ root }; - - qWarning() << "data: " << doc.toJson(); - - _fileData.toBase64(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - - QNetworkReply* reply{ nullptr }; - if (creating) { - reply = networkAccessManager.post(request, doc.toJson()); + QFile file{ filePath }; + if (file.open(QIODevice::ReadOnly)) { + zipFile.write(file.readAll()); } else { - reply = networkAccessManager.put(request, doc.toJson()); + qWarning() << "Failed to open: " << filePath; } + file.close(); + zipFile.close(); + if (zipFile.getZipError() != UNZ_OK) { + qWarning() << "Could not close zip file: " << zipFile.getZipError(); + setState(State::Complete); + return; + } + } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + zip.close(); - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; + + QString path = "/api/v1/marketplace/items"; + bool creating = true; + if (!_marketplaceID.isNull()) { + creating = false; + auto idWithBraces = _marketplaceID.toString(); + auto idWithoutBraces = idWithBraces.mid(1, idWithBraces.length() - 2); + path += "/" + idWithoutBraces; + } + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + qWarning() << "Request url is: " << request.url(); + + QJsonObject root{ { "marketplace_item", + QJsonObject{ { "title", _title }, + { "description", _description }, + { "root_file_key", _rootFilename }, + { "category_ids", QJsonArray({ 5 }) }, + //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, + { "license", 0 }, + { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; + request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); + QJsonDocument doc{ root }; + + qWarning() << "data: " << doc.toJson(); + + _fileData.toBase64(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + + QNetworkReply* reply{ nullptr }; + if (creating) { + reply = networkAccessManager.post(request, doc.toJson()); + } else { + reply = networkAccessManager.put(request, doc.toJson()); + } + + connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + //_responseData = reply->readAll(); + auto error = reply->error(); + if (error == QNetworkReply::NoError) { _responseData = reply->readAll(); qWarning() << "Finished request " << _responseData; - auto error = reply->error(); - if (error == QNetworkReply::NoError) { + + auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); + auto status = doc.object()["status"].toString(); + if (status == "success") { + _marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString()); + _itemVersion = doc["data"].toObject()["version"].toDouble(); doWaitForInventory(); } else { _error = Error::Unknown; setState(State::Complete); } - }); + } else { + _error = Error::Unknown; + setState(State::Complete); + } + }); - setState(State::UploadingAvatar); + setState(State::UploadingAvatar); } void MarketplaceItemUploader::doWaitForInventory() { - static const QString path = "/api/v1/commerce/inventory"; + static const QString path = "/api/v1/commerce/inventory"; - auto accountManager = DependencyManager::get(); - auto request = accountManager->createRequest(path, AccountManagerAuth::Required); + auto accountManager = DependencyManager::get(); + auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); + QNetworkReply* reply = networkAccessManager.post(request, ""); - QNetworkReply* reply = networkAccessManager.post(request, ""); + _numRequestsForInventory++; - connect(reply, &QNetworkReply::finished, this, [this, reply]() { - auto data = reply->readAll(); - qWarning() << "Finished inventory request " << data; + connect(reply, &QNetworkReply::finished, this, [this, reply]() { + auto data = reply->readAll(); - auto error = reply->error(); - if (error == QNetworkReply::NoError) { - } else { - _error = Error::Unknown; - } + bool success = false; + + auto error = reply->error(); + if (error == QNetworkReply::NoError) { + // Parse response data + auto doc = QJsonDocument::fromJson(data); + auto isAssetAvailable = [this, &doc]() -> bool { + if (!doc.isObject()) { + return false; + } + auto root = doc.object(); + auto status = root["status"].toString(); + if (status != "success") { + return false; + } + auto data = root["data"]; + if (!data.isObject()) { + return false; + } + auto assets = data.toObject()["assets"]; + if (!assets.isArray()) { + return false; + } + for (auto asset : assets.toArray()) { + auto assetObject = asset.toObject(); + auto id = QUuid::fromString(assetObject["id"].toString()); + if (id.isNull()) { + continue; + } + if (id == _marketplaceID) { + auto version = assetObject["version"]; + if (version.isDouble()) { + int versionInt = version.toDouble(); + if (versionInt >= _itemVersion) { + return true; + } + } + } + } + return false; + }; + + success = isAssetAvailable(); + } + if (success) { setState(State::Complete); - }); + } else { + qDebug() << "Failed to find item in inventory"; + if (_numRequestsForInventory > 8) { + _error = Error::Unknown; + setState(State::Complete); + } else { + QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); + } + } + }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index a0ec3f6991..9cfd531aca 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -56,6 +56,8 @@ public: State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } + QUuid getMarketplaceID() const { return _marketplaceID; } + Error getError() const { return _error; } signals: @@ -77,9 +79,12 @@ private: QString _description; QString _rootFilename; QUuid _marketplaceID; + int _itemVersion; QString _responseData; + int _numRequestsForInventory{ 0 }; + QStringList _filePaths; QByteArray _fileData; }; diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 524463b721..83bb1e1933 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -37,7 +37,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); + Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } + void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } QString getPath() { return _fstPath; } From 7602da6f928925afa5eb42aaa76ae31c85f9b355 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 21 Dec 2018 08:16:18 -0800 Subject: [PATCH 10/43] Add AvatarProjectUpload.qml --- .../avatarPackager/AvatarProjectUpload.qml | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml new file mode 100644 index 0000000000..bbeca6ab3b --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -0,0 +1,180 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +import QtQuick.Controls 2.2 as Original + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + + +Item { + id: uploadingScreen + + property var root: undefined + //visible: !!root.uploader + visible: false + anchors.fill: parent + + Timer { + id: backToProjectTimer + interval: 2000 + running: false + repeat: false + onTriggered: avatarPackager.state = "project" + } + + onVisibleChanged: { + console.log("Visibility changed"); + if (visible) { + root.uploader.completed.connect(function() { + console.log("Did complete"); + backToProjectTimer.start(); + }); + } + } + + Item { + visible: !!root.uploader && !root.uploader.complete + + anchors.fill: parent + + AnimatedImage { + id: uploadSpinner + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 + } + } + + Item { + id: failureScreen + + visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 + + anchors.fill: parent + + HiFiGlyphs { + id: errorIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "w" + color: "red" + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: errorIcon.bottom + Text { + anchors.horizontalCenter: parent.horizontalCenter + + text: "Error" + font.pointSize: 24 + + color: "white" + } + Text { + text: "Your avatar has not been uploaded." + font.pointSize: 16 + + anchors.horizontalCenter: parent.horizontalCenter + + color: "white" + } + } + } + + Item { + id: successScreen + + visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + + anchors.fill: parent + + HiFiGlyphs { + id: successIcon + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + size: 128 + text: "\ue01a" + color: "#1FC6A6" + } + + Column { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: successIcon.bottom + + Text { + id: successText + + anchors.horizontalCenter: parent.horizontalCenter + + text: "Congratulations!" + font.pointSize: 24 + + color: "white" + } + Text { + text: "Your avatar has been uploaded." + font.pointSize: 16 + + anchors.horizontalCenter: parent.horizontalCenter + + color: "white" + } + } + + HifiControls.Button { + width: implicitWidth + height: implicitHeight + + anchors.bottom: parent.bottom + anchors.right: parent.right + + text: "View in Inventory" + + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: function() { + console.log("Opening in inventory"); + + AvatarPackagerCore.currentAvatarProject.openInInventory(); + } + } + } + + Column { + id: debugInfo + + visible: false + + Text { + id: uploadStatus + + text: "Uploading" + color: "white" + + } + Text { + text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") + color: "white" + } + } + +} \ No newline at end of file From ad2d9bc79a9c4a2691468fc7c37ceb8229125370 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Fri, 21 Dec 2018 19:34:54 +0100 Subject: [PATCH 11/43] - fst read/write should work - images are being copied into the correct directory - scripts are added to fst upon project load - modal overlay fix --- .../resources/qml/hifi/AvatarPackager.qml | 35 ++++++--- .../avatarPackager/AvatarPackagerFooter.qml | 1 + .../avatarPackager/AvatarPackagerHeader.qml | 2 +- .../avatarPackager/AvatarPackagerState.qml | 9 +++ .../avatarPackager/CreateAvatarProject.qml | 48 +++++++----- .../avatarPackager/ProjectInputControl.qml | 5 +- .../resources/qml/hifi/avatarPackager/qmldir | 2 + interface/src/avatar/AvatarPackager.cpp | 5 +- interface/src/avatar/AvatarProject.cpp | 76 ++++++++++++++++--- interface/src/avatar/AvatarProject.h | 11 +-- libraries/fbx/src/FST.cpp | 35 ++++++--- libraries/fbx/src/FST.h | 5 ++ libraries/fbx/src/FSTReader.cpp | 2 +- libraries/fbx/src/FSTReader.h | 1 + 14 files changed, 177 insertions(+), 60 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/qmldir diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 4d27e6fce4..3dce2c1126 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -7,7 +7,7 @@ import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows import "../dialogs" -import "avatarPackager" +import "./avatarPackager" 1.0 import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { @@ -29,32 +29,46 @@ Windows.ScrollingWindow { height: pane.height width: pane.width + Rectangle { + id: modalOverlay + anchors.fill: parent + z: 20 + color: "#aa031b33" + visible: false + + // This mouse area captures the cursor events while the modalOverlay is active + MouseArea { + anchors.fill: parent + propagateComposedEvents: false; + hoverEnabled: true; + } + } + AvatarApp.MessageBox { id: popup anchors.fill: parent visible: false } - // FIXME: modal overlay does not show Column { id: avatarPackager anchors.fill: parent state: "main" states: [ State { - name: "main" + name: AvatarPackagerState.main PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, State { - name: "createProject" + name: AvatarPackagerState.createProject PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } PropertyChanges { target: createAvatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } }, State { - name: "project" + name: AvatarPackagerState.project PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } @@ -67,6 +81,8 @@ Windows.ScrollingWindow { } ] + property alias showModalOverlay: modalOverlay.visible + AvatarPackagerHeader { id: avatarPackagerHeader onBackButtonClicked: { @@ -119,7 +135,7 @@ Windows.ScrollingWindow { text: qsTr("New Project") colorScheme: root.colorScheme onClicked: { - avatarPackager.state = "createProject"; + avatarPackager.state = AvatarPackagerState.createProject; } } @@ -133,7 +149,7 @@ Windows.ScrollingWindow { color: hifi.buttons.blue colorScheme: root.colorScheme onClicked: { - // TODO: make the dialog modal + avatarPackager.showModalOverlay = true; let browser = desktop.fileDialog({ selectDirectory: false, dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), @@ -142,14 +158,15 @@ Windows.ScrollingWindow { }); browser.canceled.connect(function() { - + avatarPackager.showModalOverlay = false; }); browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); if (currentAvatarProject) { - avatarPackager.state = "project"; + avatarPackager.state = AvatarPackagerState.project; + avatarPackager.showModalOverlay = false; } }); } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index 8498d20858..d7725b250e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -8,6 +8,7 @@ Rectangle { color: "#404040" height: content === defaultContent ? 0 : 74 + visible: content !== defaultContent width: parent.width property var content: Item { id: defaultContent } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 6dcb1267d4..84096e352c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -65,7 +65,7 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: 16 anchors.verticalCenter: faq.verticalCenter - text: qsTr("FAQ") + text: qsTr("Docs") color: "white" } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml new file mode 100644 index 0000000000..f12edf4952 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -0,0 +1,9 @@ +pragma Singleton +import QtQuick 2.6 + +Item { + id: singleton + readonly property string main: "main" + readonly property string project: "project" + readonly property string createProject: "createProject" +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index 4d1f745fa5..41d33e6058 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -27,7 +27,7 @@ Item { Window.alert('Failed to create project') return; } - avatarPackager.state = "project"; + avatarPackager.state = AvatarPackagerState.project; } } } @@ -37,6 +37,26 @@ Item { height: parent.height width: parent.width + + property var errorMessages: QtObject { + readonly property string fileExists: "A folder with that name already exists at that location. Please choose a different project name or location." + } + + RalewayRegular { + id: errorMessage + visible: text !== "" + text: "" + color: "#EA4C5F"; + wrapMode: Text.WordWrap + size: 20 + anchors { + top: createAvatarColumns.bottom + bottom: parent.bottom + left: parent.left + right: parent.right + } + } + Column { id: createAvatarColumns anchors.left: parent.left @@ -45,6 +65,8 @@ Item { spacing: 17 + property string defaultFileBrowserPath: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + ProjectInputControl { id: name label: "Name" @@ -57,9 +79,8 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: true - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Project Location" - text: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) onTextChanged: { //TODO: valid folder? Does project with name exist here already? } @@ -71,11 +92,13 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: false - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseFilter: "Avatar Model File (*.fbx)" browseTitle: "Open Avatar Model (.fbx)" onTextChanged: { - //TODO: try to get texture folder from fbx if none is set? + if (avatarModel.text !== "") { + textureFolder.browseDir = fileDialogHelper.pathToUrl(avatarModel.text.split('/')[0]); + } } } @@ -85,7 +108,7 @@ Item { colorScheme: root.colorScheme browseEnabled: true browseFolder: true - browseDir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH) + browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Texture Folder" onTextChanged: { //TODO: valid folder? @@ -93,16 +116,5 @@ Item { } } } - RalewayRegular { - text: "A folder with that name already exists at that location. Please choose a different project name or location." - color: "#EA4C5F"; - wrapMode: Text.WordWrap - size: 20 - anchors { - top: createAvatarColumns.bottom - bottom: parent.bottom - left: parent.left - right: parent.right - } - } + } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 472db47c2f..664acd6f22 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -55,7 +55,7 @@ Column { text: qsTr("Browse") colorScheme: root.colorScheme onClicked: { - // TODO: make the dialog modal + avatarPackager.showModalOverlay = true; let browser = desktop.fileDialog({ selectDirectory: browseFolder, dir: browseDir, @@ -64,11 +64,12 @@ Column { }); browser.canceled.connect(function() { - + avatarPackager.showModalOverlay = false; }); browser.selectedFile.connect(function(fileUrl) { input.text = fileDialogHelper.urlToPath(fileUrl); + avatarPackager.showModalOverlay = false; }); } } diff --git a/interface/resources/qml/hifi/avatarPackager/qmldir b/interface/resources/qml/hifi/avatarPackager/qmldir new file mode 100644 index 0000000000..4204b6d89f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/qmldir @@ -0,0 +1,2 @@ +module AvatarPackager +singleton AvatarPackagerState 1.0 AvatarPackagerState.qml diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index ba3089aa2f..c7f15d616c 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -30,6 +30,9 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType(); qRegisterMetaType(); }); + + QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath()); + defaultProjectsDir.mkpath("."); } bool AvatarPackager::open() { @@ -57,7 +60,7 @@ AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } - _currentAvatarProject = AvatarProject::createAvatarProject(avatarProjectName, avatarModelPath); + _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 4a14479cce..901c0621fb 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -21,6 +21,7 @@ #include "FBXSerializer.h" #include +#include #include "scripting/HMDScriptingInterface.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { @@ -36,17 +37,25 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { return project; } -AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath) { +AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { if (!isValidNewProjectName(avatarProjectName)) { return nullptr; } - QDir dir(getDefaultProjectsPath() + "/" + avatarProjectName); - if (!dir.mkpath(".")) { + QDir projectDir(projectsFolder + "/" + avatarProjectName); + if (!projectDir.mkpath(".")) { + return nullptr; + } + QDir projectTexturesDir(projectDir.path() + "/textures"); + if (!projectTexturesDir.mkpath(".")) { + return nullptr; + } + QDir projectScriptsDir(projectDir.path() + "/scripts"); + if (!projectScriptsDir.mkpath(".")) { return nullptr; } const auto fileName = QFileInfo(avatarModelPath).fileName(); - const auto newModelPath = dir.absoluteFilePath(fileName); - const auto newFSTPath = dir.absoluteFilePath("avatar.fst"); + const auto newModelPath = projectDir.absoluteFilePath(fileName); + const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst"); QFile::copy(avatarModelPath, newModelPath); QFileInfo fbxInfo(newModelPath); @@ -66,10 +75,41 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa qDebug() << "Error reading: " << error; return nullptr; } - //TODO: copy/fix textures here: + QStringList textures{}; - FST* fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); + auto addTextureToList = [&textures](hfm::Texture texture) mutable { + if (!texture.filename.isEmpty() && texture.content.isEmpty() && !textures.contains(texture.filename)) { + textures << texture.filename; + } + }; + foreach(const HFMMaterial mat, hfmModel->materials) { + addTextureToList(mat.normalTexture); + addTextureToList(mat.albedoTexture); + addTextureToList(mat.opacityTexture); + addTextureToList(mat.glossTexture); + addTextureToList(mat.roughnessTexture); + addTextureToList(mat.specularTexture); + addTextureToList(mat.metallicTexture); + addTextureToList(mat.emissiveTexture); + addTextureToList(mat.occlusionTexture); + addTextureToList(mat.scatteringTexture); + addTextureToList(mat.lightmapTexture); + } + + QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder); + + for (const auto& texture : textures) { + QString sourcePath = textureDir.path() + "/" + texture; + QString targetPath = projectTexturesDir.path() + "/" + texture; + + QFileInfo sourceTexturePath(sourcePath); + if (sourceTexturePath.exists()) { + QFile::copy(sourcePath, targetPath); + } + } + + auto fst = FST::createFSTFromModel(newFSTPath, newModelPath, *hfmModel); fst->setName(avatarProjectName); if (!fst->write()) { @@ -79,6 +119,22 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& avatarProjectNa return new AvatarProject(fst); } +QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { + QStringList result{}; + constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; + if (!scriptsDir.exists()) { + return result; + } + + for (auto& script : scriptsDir.entryInfoList({}, flags)) { + if (script.fileName().endsWith(".js")) { + result.push_back("scripts/" + script.fileName()); + } + } + + return result; +} + bool AvatarProject::isValidNewProjectName(const QString& projectName) { QDir dir(getDefaultProjectsPath() + "/" + projectName); return !dir.exists(); @@ -92,6 +148,9 @@ AvatarProject::AvatarProject(FST* fst) { auto fileInfo = QFileInfo(getFSTPath()); _directory = fileInfo.absoluteDir(); + _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts"))); + _fst->write(); + //_projectFiles = _directory.entryList(); refreshProjectFiles(); @@ -125,8 +184,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); - // TODO(thoys) uncomment this - //_fst->write(); + _fst->write(); } }); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index b164e67e0d..07a6713557 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -36,11 +36,6 @@ class AvatarProject : public QObject { Q_PROPERTY(QString name READ getProjectName) public: - Q_INVOKABLE bool write() { - // Write FST here - return false; - } - Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); @@ -48,7 +43,8 @@ public: * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); - static AvatarProject* createAvatarProject(const QString& avatarProjectName, const QString& avatarModelPath); + static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, + const QString& avatarModelPath, const QString& textureFolder); static bool isValidNewProjectName(const QString& projectName); @@ -61,7 +57,7 @@ private: AvatarProject(FST* fst); ~AvatarProject() { - // TODO: cleanup FST / AvatarProjectUploader etc. + _fst->deleteLater(); } Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } @@ -73,6 +69,7 @@ private: void refreshProjectFiles(); void appendDirectory(QString prefix, QDir dir); + QStringList getScriptPaths(const QDir& scriptsDir); FST* _fst; diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 2631fe951e..af00428a51 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -16,14 +16,23 @@ #include FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { - if (data.contains(NAME_FIELD)) { - _name = data[NAME_FIELD].toString(); - data.remove(NAME_FIELD); - } - if (data.contains(FILENAME_FIELD)) { - _modelPath = data[FILENAME_FIELD].toString(); - data.remove(FILENAME_FIELD); + auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable { + if (data.contains(propertyID)) { + targetProperty = data[propertyID].toString(); + data.remove(propertyID); + } + }; + setValueFromFSTData(NAME_FIELD, _name); + setValueFromFSTData(FILENAME_FIELD, _modelPath); + setValueFromFSTData(MARKETPLACE_ID_FIELD, _marketplaceID); + + if (data.contains(SCRIPT_FIELD)) { + QVariantList scripts = data.values(SCRIPT_FIELD); + for (const auto& script : scripts) { + _scriptPaths.push_back(script.toString()); + } + data.remove(SCRIPT_FIELD); } _other = data; @@ -42,10 +51,8 @@ FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm:: hfmModel.blendshapeChannelNames.contains("Squint_Right")); mapping.insert(NAME_FIELD, QFileInfo(fstPath).baseName()); - QDir root(modelFilePath); - mapping.insert(FILENAME_FIELD, root.relativeFilePath(fstPath)); + mapping.insert(FILENAME_FIELD, QFileInfo(modelFilePath).fileName()); mapping.insert(TEXDIR_FIELD, "textures"); - mapping.insert(SCRIPT_FIELD, "scripts"); // mixamo/autodesk defaults mapping.insert(SCALE_FIELD, 1.0); @@ -150,9 +157,13 @@ void FST::setModelPath(const QString& modelPath) { QVariantHash FST::getMapping() { QVariantHash mapping; - mapping.insertMulti(NAME_FIELD, _name); - mapping.insertMulti(FILENAME_FIELD, _modelPath); mapping.unite(_other); + mapping.insert(NAME_FIELD, _name); + mapping.insert(FILENAME_FIELD, _modelPath); + mapping.insert(MARKETPLACE_ID_FIELD, _marketplaceID); + for (const auto& scriptPath : _scriptPaths) { + mapping.insertMulti(SCRIPT_FIELD, scriptPath); + } return mapping; } diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 83bb1e1933..813d4f3bc5 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -41,6 +41,9 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } + QStringList getScriptPaths() const { return _scriptPaths; } + void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } + QString getPath() { return _fstPath; } QVariantHash getMapping(); @@ -58,6 +61,8 @@ private: QString _modelPath{}; QUuid _marketplaceID{}; + QStringList _scriptPaths{}; + QVariantHash _other{}; }; diff --git a/libraries/fbx/src/FSTReader.cpp b/libraries/fbx/src/FSTReader.cpp index 75596862d2..43806560dc 100644 --- a/libraries/fbx/src/FSTReader.cpp +++ b/libraries/fbx/src/FSTReader.cpp @@ -84,7 +84,7 @@ void FSTReader::writeVariant(QBuffer& buffer, QVariantHash::const_iterator& it) QByteArray FSTReader::writeMapping(const QVariantHash& mapping) { static const QStringList PREFERED_ORDER = QStringList() << NAME_FIELD << TYPE_FIELD << SCALE_FIELD << FILENAME_FIELD - << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD + << MARKETPLACE_ID_FIELD << TEXDIR_FIELD << SCRIPT_FIELD << JOINT_FIELD << FREE_JOINT_FIELD << BLENDSHAPE_FIELD << JOINT_INDEX_FIELD; QBuffer buffer; buffer.open(QIODevice::WriteOnly); diff --git a/libraries/fbx/src/FSTReader.h b/libraries/fbx/src/FSTReader.h index 00244877b3..993d7c3148 100644 --- a/libraries/fbx/src/FSTReader.h +++ b/libraries/fbx/src/FSTReader.h @@ -18,6 +18,7 @@ static const QString NAME_FIELD = "name"; static const QString TYPE_FIELD = "type"; static const QString FILENAME_FIELD = "filename"; +static const QString MARKETPLACE_ID_FIELD = "marketplaceID"; static const QString TEXDIR_FIELD = "texdir"; static const QString LOD_FIELD = "lod"; static const QString JOINT_INDEX_FIELD = "jointIndex"; From 748135eef90174326d5b7ae097511a82bfbdb93b Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Fri, 21 Dec 2018 11:43:20 -0800 Subject: [PATCH 12/43] Fix login-required size and projectFiles warning --- interface/resources/qml/hifi/AvatarPackager.qml | 2 +- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 2 +- interface/src/avatar/AvatarProject.h | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 4d27e6fce4..7e76418e33 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -63,7 +63,7 @@ Windows.ScrollingWindow { name: "project-upload" PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } PropertyChanges { target: avatarUploader; visible: true } - PropertyChanges { target: avatarPackagerFooter; color: "blue"; visible: false } + PropertyChanges { target: avatarPackagerFooter; visible: false } } ] diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 286ebb23ba..5f84452cc1 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -185,7 +185,7 @@ Item { id: loginRequiredMessage visible: !Account.loggedIn - height: loginRequiredTextRow.height + 20 + height: !Account.loggedIn ? loginRequiredTextRow.height + 20 : 0 anchors { bottom: parent.bottom diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index b164e67e0d..6910e2af10 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -28,7 +28,7 @@ class AvatarProject : public QObject { Q_OBJECT Q_PROPERTY(FST* fst READ getFST) - Q_PROPERTY(QStringList projectFiles MEMBER _projectFiles) + Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) @@ -43,6 +43,7 @@ public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); + Q_INVOKABLE QStringList getProjectFiles() const { return _projectFiles; } /** * returns the AvatarProject or a nullptr on failure. From b717188ed08711fb5df08b51262d401bd5571a66 Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Sat, 22 Dec 2018 21:57:09 -0800 Subject: [PATCH 13/43] Update avatar project styling --- .../avatarPackager/AvatarPackagerFooter.qml | 21 +++++- .../qml/hifi/avatarPackager/AvatarProject.qml | 69 +++++++++++++------ .../qml/hifi/avatarPackager/Style.qml | 20 ++++++ interface/src/avatar/AvatarProject.cpp | 16 ++++- interface/src/avatar/AvatarProject.h | 18 +++-- libraries/avatars/src/ProjectFile.h | 11 +++ 6 files changed, 123 insertions(+), 32 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/Style.qml create mode 100644 libraries/avatars/src/ProjectFile.h diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index d7725b250e..e1d9396e04 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -18,8 +18,25 @@ Rectangle { property var background: Rectangle { anchors.fill: parent color: "#404040" - // TODO Use a shadow instead / border is just here for element debug purposes - border.width: 2; + + Rectangle { + id: topBorder1 + + anchors.top: parent.top + + color: "#252525" + height: 1 + width: parent.width + } + Rectangle { + id: topBorder2 + + anchors.top: topBorder1.bottom + + color: "#575757" + height: 1 + width: parent.width + } } } \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 5f84452cc1..669748e7c9 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -8,11 +8,14 @@ import QtQuick.Controls 2.2 as Original import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 + Item { id: root HifiConstants { id: hifi } + Style { id: style } + property int colorScheme; property var uploader: undefined; @@ -21,6 +24,7 @@ Item { anchors.margins: 10 property var footer: Item { + id: uploadFooter anchors.fill: parent anchors.rightMargin: 17 HifiControls.Button { @@ -91,7 +95,6 @@ Item { }; popup.open(); - //popup.forceActiveFocus(); } function showConfirmCreateNewPopup(confirmCallback) { @@ -102,30 +105,40 @@ Item { popup.button1text = 'CANCEL'; popup.button2text = 'CONFIRM'; - popup.onButton1Clicked = function() { popup.close() }; + popup.onButton1Clicked = function() { + popup.close() + }; popup.onButton2Clicked = function() { popup.close(); uploadNew(); }; popup.open(); - //popup.forceActiveFocus(); } - RalewaySemiBold { - id: avatarFBXNameLabel - size: 14 + RalewayRegular { + id: infoMessage + + color: 'white' + size: 20 + anchors.left: parent.left + anchors.right: parent.right anchors.top: parent.top - anchors.topMargin: 25 - anchors.bottomMargin: 25 - text: qsTr("FBX file here") + + anchors.bottomMargin: 24 + + wrapMode: Text.Wrap + + text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes." } HifiControls.Button { id: openFolderButton + + visible: false width: parent.width - anchors.top: avatarFBXNameLabel.bottom + anchors.top: infoMessage.bottom anchors.topMargin: 10 text: qsTr("Open Project Folder") colorScheme: root.colorScheme @@ -140,7 +153,8 @@ Item { visible: false - color: "white" + color: "#6A6A6A" + anchors.top: openFolderButton.bottom anchors.left: parent.left anchors.right: parent.right @@ -155,27 +169,39 @@ Item { model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles delegate: Rectangle { width: parent.width - height: fileText.implicitHeight + 10 - color: (index % 2 == 0) ? "white" : "grey" - Text { + height: fileText.implicitHeight + 8 + color: (index % 2 == 0) ? "#6A6A6A" : "grey" + RalewaySemiBold { id: fileText - anchors.top: parent.top + 5 - anchors.left: parent.left + 5 + size: 16 + elide: Text.ElideLeft + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 4 + width: parent.width - 10 + color: "white" text: modelData } } } } - Text { + RalewayRegular { id: showFilesText + color: 'white' + linkColor: style.colors.blueHighlight + visible: AvatarPackagerCore.currentAvatarProject !== null anchors.bottom: loginRequiredMessage.top anchors.bottomMargin: 10 - font.pointSize: 12 + size: 20 + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. " + (fileList.visible ? "Hide" : "Show") + " list" onLinkActivated: fileList.visible = !fileList.visible @@ -214,19 +240,20 @@ Item { width: implicitWidth - font.pointSize: 20 + size: 48 text: "+" color: "black" } - Text { + RalewayRegular { id: loginWarningText anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: 16 anchors.left: loginWarningGlyph.right anchors.right: parent.right text: "Please login to upload your avatar to High Fidelity hosting." - font.pointSize: 12 + size: 18 wrapMode: Text.Wrap } } diff --git a/interface/resources/qml/hifi/avatarPackager/Style.qml b/interface/resources/qml/hifi/avatarPackager/Style.qml new file mode 100644 index 0000000000..a1dcc8f0c1 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/Style.qml @@ -0,0 +1,20 @@ +import QtQuick 2.5 +import QtQuick.Window 2.2 + +import "../../stylesUit" 1.0 + +QtObject { + readonly property QtObject colors: QtObject { + readonly property color lightGrayBackground: "#f2f2f2" + readonly property color black: "#000000" + readonly property color white: "#ffffff" + readonly property color blueHighlight: "#00b4ef" + readonly property color inputFieldBackground: "#d4d4d4" + readonly property color yellowishOrange: "#ffb017" + readonly property color blueAccent: "#0093c5" + readonly property color greenHighlight: "#1fc6a6" + readonly property color lightGray: "#afafaf" + readonly property color redHighlight: "#ea4c5f" + readonly property color orangeAccent: "#ff6309" + } +} diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 901c0621fb..2a2ec7c1cb 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,7 +162,7 @@ void AvatarProject::appendDirectory(QString prefix, QDir dir) { for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { //_projectFiles.append(prefix + "/" + entry.fileName()); - _projectFiles.append(entry.absoluteFilePath()); + _projectFiles.append({ entry.absoluteFilePath(), prefix + "/" + entry.fileName() }); } else if (entry.isDir()) { appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); } @@ -174,13 +174,25 @@ void AvatarProject::refreshProjectFiles() { appendDirectory("", _directory); } +QStringList AvatarProject::getProjectFiles() const { + QStringList paths; + for (auto& path : _projectFiles) { + paths.append(path.relativePath); + } + return paths; +} + MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { QUuid itemID; if (updateExisting) { itemID = _fst->getMarketplaceID(); } + QStringList projectFilePaths; + for (auto& path : _projectFiles) { + projectFilePaths.append(path.absolutePath); + } auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, - _projectFiles); + projectFilePaths); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 3b4a4b74f9..d5cd7762a1 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -14,6 +14,7 @@ #define hifi_AvatarProject_h #include "MarketplaceItemUploader.h" +#include "ProjectFile.h" #include "FST.h" #include @@ -38,14 +39,16 @@ class AvatarProject : public QObject { public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); Q_INVOKABLE void openInInventory(); - Q_INVOKABLE QStringList getProjectFiles() const { return _projectFiles; } + Q_INVOKABLE QStringList getProjectFiles() const; /** * returns the AvatarProject or a nullptr on failure. */ static AvatarProject* openAvatarProject(const QString& path); - static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, - const QString& avatarModelPath, const QString& textureFolder); + static AvatarProject* createAvatarProject(const QString& projectsFolder, + const QString& avatarProjectName, + const QString& avatarModelPath, + const QString& textureFolder); static bool isValidNewProjectName(const QString& projectName); @@ -53,13 +56,14 @@ public: return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; } +signals: + void projectFilesChanged(); + private: AvatarProject(const QString& fstPath, const QByteArray& data); AvatarProject(FST* fst); - ~AvatarProject() { - _fst->deleteLater(); - } + ~AvatarProject() { _fst->deleteLater(); } Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } @@ -75,7 +79,7 @@ private: FST* _fst; QDir _directory; - QStringList _projectFiles{}; + QList _projectFiles{}; QString _projectPath; }; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h new file mode 100644 index 0000000000..df92513a1b --- /dev/null +++ b/libraries/avatars/src/ProjectFile.h @@ -0,0 +1,11 @@ +#ifndef hifi_AvatarProjectFile_h +#define hifi_AvatarProjectFile_h + +class ProjectFilePath { + Q_GADGET; +public: + QString absolutePath; + QString relativePath; +}; + +#endif // hifi_AvatarProjectFile_h \ No newline at end of file From 8c56e35f69f71d0d648cb8b313b2b7407307da1c Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Sun, 23 Dec 2018 00:13:01 -0800 Subject: [PATCH 14/43] Add granular status to avatar upload --- .../qml/hifi/avatarPackager/AvatarProject.qml | 4 +- .../avatarPackager/AvatarProjectUpload.qml | 163 +++++++----------- .../src/avatar/MarketplaceItemUploader.cpp | 29 ++-- .../src/avatar/MarketplaceItemUploader.h | 10 +- 4 files changed, 93 insertions(+), 113 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 669748e7c9..81afacf28c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,8 +25,10 @@ Item { property var footer: Item { id: uploadFooter + anchors.fill: parent anchors.rightMargin: 17 + HifiControls.Button { id: uploadButton enabled: Account.loggedIn @@ -64,7 +66,7 @@ Item { root.uploader.uploadProgress.connect(function(uploaded, total) { console.log("Uploader progress: " + uploaded + " / " + total); }); - root.uploader.completed.connect(function() { + root.uploader.finished.connect(function() { try { var response = JSON.parse(root.uploader.responseData); console.log("Uploader complete! " + response); diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index bbeca6ab3b..6e011b1ec7 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -28,7 +28,7 @@ Item { onVisibleChanged: { console.log("Visibility changed"); if (visible) { - root.uploader.completed.connect(function() { + root.uploader.finished.connect(function() { console.log("Did complete"); backToProjectTimer.start(); }); @@ -36,127 +36,94 @@ Item { } Item { - visible: !!root.uploader && !root.uploader.complete + id: uploadStatus + + visible: !!root.uploader anchors.fill: parent - AnimatedImage { - id: uploadSpinner + Item { + id: statusItem - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + width: parent.width + height: 128 + + AnimatedImage { + id: uploadSpinner + + visible: !!root.uploader && !root.uploader.complete + + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + + source: "../../../icons/loader-snake-64-w.gif" + playing: true + z: 10000 } - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 - } - } + HiFiGlyphs { + id: errorIcon + visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 - Item { - id: failureScreen + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } - visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 - - anchors.fill: parent - - HiFiGlyphs { - id: errorIcon - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + size: 128 + text: "w" + color: "red" } - size: 128 - text: "w" - color: "red" - } + HiFiGlyphs { + id: successIcon - Column { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: errorIcon.bottom - Text { - anchors.horizontalCenter: parent.horizontalCenter + visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 - text: "Error" - font.pointSize: 24 + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } - color: "white" - } - Text { - text: "Your avatar has not been uploaded." - font.pointSize: 16 - - anchors.horizontalCenter: parent.horizontalCenter - - color: "white" + size: 128 + text: "\ue01a" + color: "#1FC6A6" } } - } + Item { + id: statusRows - Item { - id: successScreen + anchors.top: statusItem.bottom - visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + AvatarUploadStatusItem { + id: statusCategories + text: "Retreiving categories" - anchors.fill: parent - - HiFiGlyphs { - id: successIcon - - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter + state: root.uploader.state == 1 ? "running" : (root.uploader.state > 1 ? "success" : (root.uploader.error ? "fail" : "")) } + AvatarUploadStatusItem { + id: statusUploading + anchors.top: statusCategories.bottom + text: "Uploading data" - size: 128 - text: "\ue01a" - color: "#1FC6A6" - } - - Column { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: successIcon.bottom - - Text { - id: successText - - anchors.horizontalCenter: parent.horizontalCenter - - text: "Congratulations!" - font.pointSize: 24 - - color: "white" + state: root.uploader.state == 2 ? "running" : (root.uploader.state > 2 ? "success" : (root.uploader.error ? "fail" : "")) } - Text { - text: "Your avatar has been uploaded." - font.pointSize: 16 + // TODO add waiting for response + //AvatarUploadStatusItem { + //id: statusResponse + //text: "Waiting for completion" + //} + AvatarUploadStatusItem { + id: statusInventory + anchors.top: statusUploading.bottom + text: "Waiting for inventory" - anchors.horizontalCenter: parent.horizontalCenter - - color: "white" + state: root.uploader.state == 3 ? "running" : (root.uploader.state > 3 ? "success" : (root.uploader.error ? "fail" : "")) } } - HifiControls.Button { - width: implicitWidth - height: implicitHeight - - anchors.bottom: parent.bottom - anchors.right: parent.right - - text: "View in Inventory" - - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: function() { - console.log("Opening in inventory"); - - AvatarPackagerCore.currentAvatarProject.openInInventory(); - } - } } Column { @@ -165,8 +132,6 @@ Item { visible: false Text { - id: uploadStatus - text: "Uploading" color: "white" diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 1559f359a7..0a14ea9af4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -39,15 +39,25 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, } void MarketplaceItemUploader::setState(State newState) { + Q_ASSERT(newState != _state); qDebug() << "Setting uploader state to: " << newState; _state = newState; emit stateChanged(newState); if (newState == State::Complete) { emit completed(); + emit finished(); } } +void MarketplaceItemUploader::setError(Error error) { + Q_ASSERT(_error == Error::None); + + _error = error; + emit errorChanged(error); + emit finished(); +} + void MarketplaceItemUploader::send() { doGetCategories(); } @@ -105,14 +115,12 @@ void MarketplaceItemUploader::doGetCategories() { qDebug() << "Done " << success << id; if (!success) { qWarning() << "Failed to find marketplace category id"; - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } else { doUploadAvatar(); } } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } }); } @@ -132,8 +140,7 @@ void MarketplaceItemUploader::doUploadAvatar() { QuaZipFile zipFile{ &zip }; if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { qWarning() << "Could not open zip file:" << zipFile.getZipError(); - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); return; } QFile file{ filePath }; @@ -204,14 +211,13 @@ void MarketplaceItemUploader::doUploadAvatar() { if (status == "success") { _marketplaceID = QUuid::fromString(doc["data"].toObject()["marketplace_id"].toString()); _itemVersion = doc["data"].toObject()["version"].toDouble(); + setState(State::WaitingForInventory); doWaitForInventory(); } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } } else { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } }); @@ -282,8 +288,7 @@ void MarketplaceItemUploader::doWaitForInventory() { } else { qDebug() << "Failed to find item in inventory"; if (_numRequestsForInventory > 8) { - _error = Error::Unknown; - setState(State::Complete); + setError(Error::Unknown); } else { QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 9cfd531aca..1b1589af96 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -23,7 +23,7 @@ class MarketplaceItemUploader : public QObject { Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) Q_PROPERTY(State state READ getState NOTIFY stateChanged) - Q_PROPERTY(Error error READ getError) + Q_PROPERTY(Error error READ getError NOTIFY errorChanged) Q_PROPERTY(QString responseData READ getResponseData) public: enum class Error @@ -51,11 +51,14 @@ public: Q_INVOKABLE void send(); + void setError(Error error); + QString getResponseData() const { return _responseData; } void setState(State newState); State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } + QUuid getMarketplaceID() const { return _marketplaceID; } Error getError() const { return _error; } @@ -63,7 +66,12 @@ public: signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); void completed(); + void stateChanged(State newState); + void errorChanged(Error error); + + // Triggered when the upload has finished, either succesfully completing, or stopping with an error + void finished(); private: void doGetCategories(); From 1da179dc04973c7c5b3d7d8055d97e55ad46230e Mon Sep 17 00:00:00 2001 From: Ryan Huffman Date: Thu, 27 Dec 2018 00:13:45 -0800 Subject: [PATCH 15/43] Update avatar packager styling --- .../resources/images/loader-snake-128.png | Bin 0 -> 463 bytes .../resources/qml/hifi/AvatarPackager.qml | 1 + .../qml/hifi/avatarPackager/AvatarProject.qml | 159 +++++++++++++++--- .../avatarPackager/AvatarProjectUpload.qml | 94 +++++++---- .../avatarPackager/AvatarUploadStatusItem.qml | 96 +++++++++++ .../qml/hifi/avatarPackager/LoadingCircle.qml | 20 +++ .../qml/hifi/avatarapp/MessageBox.qml | 16 ++ interface/src/avatar/AvatarProject.h | 3 +- .../src/avatar/MarketplaceItemUploader.cpp | 23 ++- .../src/avatar/MarketplaceItemUploader.h | 6 +- libraries/fbx/src/FST.cpp | 5 + libraries/fbx/src/FST.h | 6 +- 12 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 interface/resources/images/loader-snake-128.png create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml diff --git a/interface/resources/images/loader-snake-128.png b/interface/resources/images/loader-snake-128.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ee57766488aed3627111d0dd6a1f0bb2a7dbaf GIT binary patch literal 463 zcmV;=0WkiFP)w!o@;$>0gGA+Fq)u?0a-$u1CxnfTGIa^5mSWSEh$GnaXjy&^`e zPrPbH%)n^08iN;ay=%^xyPw!L}2gYuE!%c-{nAkChx8*Uz>Z2ojhOq=5uHLPX`@xu3{>6{a~(gmcGG% zq9U~swXRF%aQMzSpPDWV0wFHD7~H-YW8$pIiA3Vu_ynabg<_e%Q|SNz002ovPDHLk FV1jQ+#bE#d literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 0dea524991..46fd98daab 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -48,6 +48,7 @@ Windows.ScrollingWindow { id: popup anchors.fill: parent visible: false + closeOnClickOutside: true } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 81afacf28c..d4a67f31fb 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -19,35 +19,130 @@ Item { property int colorScheme; property var uploader: undefined; + property bool hasSuccessfullyUploaded: true; + visible: false anchors.fill: parent anchors.margins: 10 property var footer: Item { - id: uploadFooter - anchors.fill: parent - anchors.rightMargin: 17 - HifiControls.Button { - id: uploadButton - enabled: Account.loggedIn - //width: parent.width - //anchors.bottom: parent.bottom - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - text: qsTr("Upload") - color: hifi.buttons.blue - colorScheme: root.colorScheme - width: 133 - height: 40 - onClicked: function() { - if (AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID()) { - showConfirmUploadPopup(uploadNew, uploadUpdate); - } else { + Item { + id: uploadFooter + + visible: !root.uploader || root.finished || root.uploader.state !== 4 + + anchors.fill: parent + anchors.rightMargin: 17 + + HifiControls.Button { + id: uploadButton + + visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + enabled: Account.loggedIn + //width: parent.width + //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + text: qsTr("Upload") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: function() { uploadNew(); } } + HifiControls.Button { + id: updateButton + + visible: AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + enabled: Account.loggedIn + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + text: qsTr("Update") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 134 + height: 40 + onClicked: function() { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } + } + Item { + anchors.fill: parent + visible: root.hasSuccessfullyUploaded + + HifiControls.Button { + enabled: Account.loggedIn + + anchors.verticalCenter: parent.verticalCenter + anchors.right: viewInInventoryButton.left + anchors.rightMargin: 16 + + text: qsTr("Update") + color: hifi.buttons.white + colorScheme: root.colorScheme + width: 134 + height: 40 + onClicked: function() { + showConfirmUploadPopup(uploadNew, uploadUpdate); + } + } + HifiControls.Button { + id: viewInInventoryButton + + enabled: Account.loggedIn + + width: 168 + height: 40 + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + text: qsTr("View in Inventory") + color: hifi.buttons.blue + colorScheme: root.colorScheme + + onClicked: AvatarPackagerCore.currentAvatarProject.openInInventory() + } + } + } + + Rectangle { + id: uploadingItemFooter + + anchors.fill: parent + anchors.topMargin: 1 + visible: !!root.uploader && !root.finished && root.uploader.state === 4 + + color: "#00B4EF" + + LoadingCircle { + id: runningImage + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: 16 + + width: 28 + height: 28 + } + RalewayRegular { + id: stepText + + size: 20 + + anchors.verticalCenter: parent.verticalCenter + anchors.left: runningImage.right + anchors.leftMargin: 16 + + text: "Adding item to Inventory" + color: "white" + } } } @@ -66,7 +161,10 @@ Item { root.uploader.uploadProgress.connect(function(uploaded, total) { console.log("Uploader progress: " + uploaded + " / " + total); }); - root.uploader.finished.connect(function() { + root.uploader.completed.connect(function() { + root.hasSuccessfullyUploaded = true; + }); + root.uploader.finishedChanged.connect(function() { try { var response = JSON.parse(root.uploader.responseData); console.log("Uploader complete! " + response); @@ -121,6 +219,25 @@ Item { RalewayRegular { id: infoMessage + states: [ + State { + when: root.hasSuccessfullyUploaded + name: "upload-success" + PropertyChanges { + target: infoMessage + text: "Your avatar has been uploaded to our servers. You can modify the project files and update it again to make changes on the uploaded avatar." + } + }, + State { + name: "has-previous-success" + when: !!AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID + PropertyChanges { + target: infoMessage + text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes." + } + } + ] + color: 'white' size: 20 @@ -132,7 +249,7 @@ Item { wrapMode: Text.Wrap - text: "Click \"Update\" to overwrite the hosted files and update the avatar in your inventory. You will have to “Wear” the avatar again to see changes." + text: "You can upload your files to our servers to always access them, and to make your avatar visible to other users." } HifiControls.Button { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 6e011b1ec7..8b80df3d95 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -8,28 +8,35 @@ import QtQuick.Controls 2.2 as Original import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 - Item { id: uploadingScreen property var root: undefined - //visible: !!root.uploader visible: false anchors.fill: parent Timer { id: backToProjectTimer - interval: 2000 + interval: 5000 running: false repeat: false - onTriggered: avatarPackager.state = "project" + onTriggered: { + if (avatarPackager.state =="project-upload") { + avatarPackager.state = "project" + } + } } + function stateChangedCallback(newState) { + if (newState >= 4) { + root.uploader.stateChanged.disconnect(stateChangedCallback) + backToProjectTimer.start(); + } + } onVisibleChanged: { - console.log("Visibility changed"); if (visible) { - root.uploader.finished.connect(function() { - console.log("Did complete"); + root.uploader.stateChanged.connect(stateChangedCallback); + root.uploader.finishedChanged.connect(function() { backToProjectTimer.start(); }); } @@ -46,48 +53,62 @@ Item { id: statusItem width: parent.width - height: 128 + height: 192 - AnimatedImage { + states: [ + State { + name: "success" + when: !!root.uploader && root.uploader.state >= 4 && root.uploader.error === 0 + PropertyChanges { target: uploadSpinner; visible: false } + PropertyChanges { target: errorIcon; visible: false } + PropertyChanges { target: successIcon; visible: true } + }, + State { + name: "error" + when: !!root.uploader && root.uploader.finished && root.uploader.error !== 0 + PropertyChanges { target: uploadSpinner; visible: false } + PropertyChanges { target: errorIcon; visible: true } + PropertyChanges { target: successIcon; visible: false } + } + ] + + LoadingCircle { id: uploadSpinner - visible: !!root.uploader && !root.uploader.complete + visible: true anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - - source: "../../../icons/loader-snake-64-w.gif" - playing: true - z: 10000 } HiFiGlyphs { id: errorIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error !== 0 + + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "w" - color: "red" + color: "#EA4C5F" } HiFiGlyphs { id: successIcon - visible: !!root.uploader && root.uploader.complete && root.uploader.error === 0 + visible: false anchors { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } - size: 128 + size: 164 text: "\ue01a" color: "#1FC6A6" } @@ -96,34 +117,49 @@ Item { id: statusRows anchors.top: statusItem.bottom + anchors.left: parent.left + anchors.leftMargin: 12 AvatarUploadStatusItem { id: statusCategories + uploader: root.uploader text: "Retreiving categories" - state: root.uploader.state == 1 ? "running" : (root.uploader.state > 1 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 1 } AvatarUploadStatusItem { id: statusUploading + uploader: root.uploader anchors.top: statusCategories.bottom text: "Uploading data" - state: root.uploader.state == 2 ? "running" : (root.uploader.state > 2 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 2 } - // TODO add waiting for response - //AvatarUploadStatusItem { - //id: statusResponse - //text: "Waiting for completion" - //} AvatarUploadStatusItem { - id: statusInventory + id: statusResponse + uploader: root.uploader anchors.top: statusUploading.bottom - text: "Waiting for inventory" + text: "Waiting for response" - state: root.uploader.state == 3 ? "running" : (root.uploader.state > 3 ? "success" : (root.uploader.error ? "fail" : "")) + uploaderState: 3 } } + RalewayRegular { + visible: root.uploader.error + + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.bottomMargin: 16 + + size: 28 + wrapMode: Text.Wrap + color: "white" + text: "We couldn't upload your avatar at this time. Please try again later." + } } Column { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml new file mode 100644 index 0000000000..d93afbd4e8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -0,0 +1,96 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +Item { + id: root + + height: 48 + + property string text: "NO STEP TEXT" + property int uploaderState; + property var uploader; + + state: root.uploader.state > uploaderState + ? "success" + : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : "")) + + states: [ + State { + name: "running" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: runningImage; visible: true; playing: true } + }, + State { + name: "fail" + PropertyChanges { target: stepText; color: "#EA4C5F" } + PropertyChanges { target: failGlyph; visible: true } + }, + State { + name: "success" + PropertyChanges { target: stepText; color: "white" } + PropertyChanges { target: successGlyph; visible: true } + } + ] + + Item { + id: statusItem + + width: 48 + height: parent.height + + AnimatedImage { + id: runningImage + + visible: false + + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + width: 32 + height: 32 + + source: "../../../icons/loader-snake-64-w.gif" + playing: false + } + HiFiGlyphs { + id: successGlyph + + visible: false + + width: implicitWidth + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + size: 48 + text: "\ue01a" + color: "#1FC6A6" + } + HiFiGlyphs { + id: failGlyph + + visible: false + + width: implicitWidth + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + + size: 48 + text: "+" + color: "#EA4C5F" + } + } + RalewayRegular { + id: stepText + + anchors.left: statusItem.right + anchors.verticalCenter: parent.verticalCenter + + text: root.text + size: 28 + color: "#777777" + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml new file mode 100644 index 0000000000..f6ba81a96f --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -0,0 +1,20 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtGraphicalEffects 1.0 + +Image { + id: root + + width: 128 + height: 128 + + source: "../../../images/loader-snake-128.png" + + RotationAnimation on rotation { + duration: 2000 + loops: Animation.Infinite + from: 0 + to: 360 + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarapp/MessageBox.qml b/interface/resources/qml/hifi/avatarapp/MessageBox.qml index 1834364fe4..88f7f888cb 100644 --- a/interface/resources/qml/hifi/avatarapp/MessageBox.qml +++ b/interface/resources/qml/hifi/avatarapp/MessageBox.qml @@ -23,6 +23,8 @@ Rectangle { property string button2color: hifi.buttons.blue; property string button2text: '' + property bool closeOnClickOutside: false; + property var onButton2Clicked; property var onButton1Clicked; property var onLinkClicked; @@ -56,6 +58,11 @@ Rectangle { anchors.fill: parent; propagateComposedEvents: false; hoverEnabled: true; + onClicked: { + if (closeOnClickOutside) { + root.close() + } + } } Rectangle { @@ -68,6 +75,15 @@ Rectangle { console.debug('mainContainer: height = ', height) } + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + onClicked: function(ev) { + ev.accepted = true; + } + } + anchors.centerIn: parent color: "white" diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index d5cd7762a1..6da9f710cc 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -34,7 +34,7 @@ class AvatarProject : public QObject { Q_PROPERTY(QString projectFolderPath READ getProjectPath) Q_PROPERTY(QString projectFSTPath READ getFSTPath) Q_PROPERTY(QString projectFBXPath READ getFBXPath) - Q_PROPERTY(QString name READ getProjectName) + Q_PROPERTY(QString name READ getProjectName NOTIFY nameChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); @@ -57,6 +57,7 @@ public: } signals: + void nameChanged(); void projectFilesChanged(); private: diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 0a14ea9af4..31dcf8e9a0 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -39,6 +39,8 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, } void MarketplaceItemUploader::setState(State newState) { + Q_ASSERT(_state != State::Complete); + Q_ASSERT(_error == Error::None); Q_ASSERT(newState != _state); qDebug() << "Setting uploader state to: " << newState; @@ -46,16 +48,17 @@ void MarketplaceItemUploader::setState(State newState) { emit stateChanged(newState); if (newState == State::Complete) { emit completed(); - emit finished(); + emit finishedChanged(); } } void MarketplaceItemUploader::setError(Error error) { + Q_ASSERT(_state != State::Complete); Q_ASSERT(_error == Error::None); _error = error; emit errorChanged(error); - emit finished(); + emit finishedChanged(); } void MarketplaceItemUploader::send() { @@ -179,7 +182,6 @@ void MarketplaceItemUploader::doUploadAvatar() { { "description", _description }, { "root_file_key", _rootFilename }, { "category_ids", QJsonArray({ 5 }) }, - //{ "attributions", QJsonArray({ QJsonObject{ { "name", "" }, { "link", "" } } }) }, { "license", 0 }, { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); @@ -197,14 +199,21 @@ void MarketplaceItemUploader::doUploadAvatar() { reply = networkAccessManager.put(request, doc.toJson()); } - connect(reply, &QNetworkReply::uploadProgress, this, &MarketplaceItemUploader::uploadProgress); + connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) { + if (_state == State::UploadingAvatar) { + emit uploadProgress(bytesSent, bytesTotal); + if (bytesSent >= bytesTotal) { + setState(State::WaitingForUploadResponse); + } + } + }); connect(reply, &QNetworkReply::finished, this, [this, reply]() { - //_responseData = reply->readAll(); + _responseData = reply->readAll(); + qWarning() << "Finished request " << _responseData; + auto error = reply->error(); if (error == QNetworkReply::NoError) { - _responseData = reply->readAll(); - qWarning() << "Finished request " << _responseData; auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); auto status = doc.object()["status"].toString(); diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 1b1589af96..4b8b675255 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -21,6 +21,8 @@ class QNetworkReply; class MarketplaceItemUploader : public QObject { Q_OBJECT + Q_PROPERTY(bool finished READ getFinished NOTIFY finishedChanged) + Q_PROPERTY(bool complete READ getComplete NOTIFY stateChanged) Q_PROPERTY(State state READ getState NOTIFY stateChanged) Q_PROPERTY(Error error READ getError NOTIFY errorChanged) @@ -38,6 +40,7 @@ public: Idle, GettingCategories, UploadingAvatar, + WaitingForUploadResponse, WaitingForInventory, Complete }; @@ -62,6 +65,7 @@ public: QUuid getMarketplaceID() const { return _marketplaceID; } Error getError() const { return _error; } + bool getFinished() const { return _state == State::Complete || _error != Error::None; } signals: void uploadProgress(qint64 bytesSent, qint64 bytesTotal); @@ -71,7 +75,7 @@ signals: void errorChanged(Error error); // Triggered when the upload has finished, either succesfully completing, or stopping with an error - void finished(); + void finishedChanged(); private: void doGetCategories(); diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index af00428a51..f0e444ba33 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -175,3 +175,8 @@ bool FST::write() { fst.write(FSTReader::writeMapping(getMapping())); return true; } + +void FST::setMarketplaceID(QUuid marketplaceID) { + _marketplaceID = marketplaceID; + emit marketplaceIDChanged(); +} diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 813d4f3bc5..6fd654987e 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -24,6 +24,7 @@ class FST : public QObject { Q_PROPERTY(QString name READ getName WRITE setName NOTIFY nameChanged) Q_PROPERTY(QString modelPath READ getModelPath WRITE setModelPath NOTIFY modelPathChanged) Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) + Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged) public: FST(const QString& fstPath, QVariantHash data); @@ -37,9 +38,9 @@ public: QString getModelPath() const { return _modelPath; } void setModelPath(const QString& modelPath); - Q_INVOKABLE bool hasMarketplaceID() const { return !_marketplaceID.isNull(); } + Q_INVOKABLE bool getHasMarketplaceID() const { return !_marketplaceID.isNull(); } QUuid getMarketplaceID() const { return _marketplaceID; } - void setMarketplaceID(QUuid marketplaceID) { _marketplaceID = marketplaceID; } + void setMarketplaceID(QUuid marketplaceID); QStringList getScriptPaths() const { return _scriptPaths; } void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } @@ -53,6 +54,7 @@ public: signals: void nameChanged(const QString& name); void modelPathChanged(const QString& modelPath); + void marketplaceIDChanged(); private: QString _fstPath; From 2f32458f72311a694e479325c2dfe4a2cdaea4c4 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 27 Dec 2018 19:52:56 +0100 Subject: [PATCH 16/43] recent projects --- .../resources/qml/hifi/AvatarPackager.qml | 29 ++++++- .../avatarPackager/AvatarPackagerHeader.qml | 1 + .../hifi/avatarPackager/AvatarProjectCard.qml | 85 +++++++++++++++++++ .../avatarPackager/AvatarProjectUpload.qml | 2 +- .../avatarPackager/CreateAvatarProject.qml | 50 ++++++----- .../avatarPackager/ProjectInputControl.qml | 1 + interface/src/avatar/AvatarPackager.cpp | 31 +++++++ interface/src/avatar/AvatarPackager.h | 68 +++++++++++++++ interface/src/avatar/AvatarProject.cpp | 11 +-- interface/src/avatar/AvatarProject.h | 15 ++-- .../src/avatar/MarketplaceItemUploader.cpp | 4 +- 11 files changed, 259 insertions(+), 38 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 46fd98daab..686bdd28da 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -33,7 +33,7 @@ Windows.ScrollingWindow { id: modalOverlay anchors.fill: parent z: 20 - color: "#aa031b33" + color: "#a15d5d5d" visible: false // This mouse area captures the cursor events while the modalOverlay is active @@ -70,7 +70,7 @@ Windows.ScrollingWindow { }, State { name: AvatarPackagerState.project - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } PropertyChanges { target: avatarProject; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } }, @@ -136,6 +136,7 @@ Windows.ScrollingWindow { text: qsTr("New Project") colorScheme: root.colorScheme onClicked: { + createAvatarProject.clearInputs(); avatarPackager.state = AvatarPackagerState.createProject; } } @@ -173,7 +174,10 @@ Windows.ScrollingWindow { } } } + + Flow { + visible: AvatarPackagerCore.recentProjects.length === 0 anchors { fill: parent topMargin: 18 @@ -190,6 +194,27 @@ Windows.ScrollingWindow { color: "white" text: qsTr("To learn more about using this tool, visit our docs") } + + + } + + Column { + visible: AvatarPackagerCore.recentProjects.length > 0 + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + spacing: 10 + + Repeater { + model: AvatarPackagerCore.recentProjects + AvatarProjectCard { + title: modelData.name + path: modelData.path + } + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 84096e352c..663d4d0f3a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -13,6 +13,7 @@ Rectangle { property alias title: title.text property alias faqEnabled: faq.visible property alias backButtonEnabled: back.visible + property bool canRename: false; signal backButtonClicked RalewaySemiBold { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml new file mode 100644 index 0000000000..be1363850e --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -0,0 +1,85 @@ +import QtQuick 2.0 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + + +Item { + id: projectCard + height: 80 + width: parent.width + + property alias title: title.text + property alias path: path.text + + property color textColor: "#E3E3E3" + property color hoverTextColor: "#121212" + property color pressedTextColor: "#121212" + + property color backgroundColor: "#121212" + property color hoverBackgroundColor: "#E3E3E3" + property color pressedBackgroundColor: "#6A6A6A" + + state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal") + states: [ + State { + name: "normal" + PropertyChanges { target: background; color: backgroundColor } + PropertyChanges { target: title; color: textColor } + PropertyChanges { target: path; color: textColor } + }, + State { + name: "hover" + PropertyChanges { target: background; color: hoverBackgroundColor } + PropertyChanges { target: title; color: hoverTextColor } + PropertyChanges { target: path; color: hoverTextColor } + }, + State { + name: "pressed" + PropertyChanges { target: background; color: pressedBackgroundColor } + PropertyChanges { target: title; color: pressedTextColor } + PropertyChanges { target: path; color: pressedTextColor } + } + ] + + Rectangle { + id: background + width: parent.width + height: parent.height + color: "#121212" + radius: 4 + + RalewayBold { + id: title + anchors { + top: parent.top + topMargin: 13 + left: parent.left + leftMargin: 16 + } + text: "" + size: 16 + } + + RalewayRegular { + id: path + anchors { + top: title.bottom + left: parent.left + leftMargin: 32 + } + text: "<path missing>" + size: 20 + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + AvatarPackagerCore.openAvatarProject(path.text); + avatarPackager.state = "project"; + } + } + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 8b80df3d95..e71d8a4f2f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -178,4 +178,4 @@ Item { } } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index 41d33e6058..a5d335feba 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -22,9 +22,10 @@ Item { height: 30 width: 133 text: qsTr("Create") + enabled: false onClicked: { if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { - Window.alert('Failed to create project') + Window.alert('Failed to create project'); return; } avatarPackager.state = AvatarPackagerState.project; @@ -37,21 +38,39 @@ Item { height: parent.height width: parent.width + function clearInputs() { + name.text = projectLocation.text = avatarModel.text = textureFolder.text = ""; + } - property var errorMessages: QtObject { - readonly property string fileExists: "A folder with that name already exists at that location. Please choose a different project name or location." + function checkErrors() { + let newErrorMessageText = ""; + + let projectName = name.text; + let projectFolder = projectLocation.text; + + let hasProjectNameError = projectName !== "" && projectFolder !== "" && !AvatarPackagerCore.isValidNewProjectName(projectFolder, projectName); + + if (hasProjectNameError) { + newErrorMessageText = "A folder with that name already exists at that location. Please choose a different project name or location."; + } + + name.error = projectLocation.error = hasProjectNameError; + errorMessage.text = newErrorMessageText; + createButton.enabled = newErrorMessageText === "" && requiredFieldsFilledIn(); + } + + function requiredFieldsFilledIn() { + return name.text !== "" && projectLocation.text !== "" && avatarModel.text !== ""; } RalewayRegular { id: errorMessage visible: text !== "" text: "" - color: "#EA4C5F"; + color: "#EA4C5F" wrapMode: Text.WordWrap size: 20 anchors { - top: createAvatarColumns.bottom - bottom: parent.bottom left: parent.left right: parent.right } @@ -59,6 +78,7 @@ Item { Column { id: createAvatarColumns + anchors.top: errorMessage.visible ? errorMessage.bottom : parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: 10 @@ -71,6 +91,7 @@ Item { id: name label: "Name" colorScheme: root.colorScheme + onTextChanged: checkErrors() } ProjectInputControl { @@ -81,9 +102,7 @@ Item { browseFolder: true browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Project Location" - onTextChanged: { - //TODO: valid folder? Does project with name exist here already? - } + onTextChanged: checkErrors() } ProjectInputControl { @@ -95,25 +114,18 @@ Item { browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseFilter: "Avatar Model File (*.fbx)" browseTitle: "Open Avatar Model (.fbx)" - onTextChanged: { - if (avatarModel.text !== "") { - textureFolder.browseDir = fileDialogHelper.pathToUrl(avatarModel.text.split('/')[0]); - } - } + onTextChanged: checkErrors() } ProjectInputControl { id: textureFolder - label: "Specify Texture Folder" + label: "Specify Texture Folder <i> - optional</i>" colorScheme: root.colorScheme browseEnabled: true browseFolder: true browseDir: text !== "" ? fileDialogHelper.pathToUrl(text) : createAvatarColumns.defaultFileBrowserPath browseTitle: "Texture Folder" - onTextChanged: { - //TODO: valid folder? - - } + onTextChanged: checkErrors() } } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 664acd6f22..2ac4a37d02 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -22,6 +22,7 @@ Column { property string browseTitle: "Open file" property string browseDir: "" property alias text: input.text + property alias error: input.error property int colorScheme diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index c7f15d616c..d8aadeb4e0 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -31,6 +31,8 @@ AvatarPackager::AvatarPackager() { qRegisterMetaType<AvatarProject*>(); }); + recentProjectsFromVariantList(_recentProjectsSetting.get()); + QDir defaultProjectsDir(AvatarProject::getDefaultProjectsPath()); defaultProjectsDir.mkpath("."); } @@ -50,17 +52,46 @@ AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFST _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); + if (_currentAvatarProject) { + addRecentProject(avatarProjectFSTPath, _currentAvatarProject->getProjectName()); + } qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); return _currentAvatarProject; } +void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { + const int MAX_RECENT_PROJECTS = 5; + auto removeProjects = QVector<RecentAvatarProject>(); + for (auto project : _recentProjects) { + if (project.getProjectFSTPath() == fstPath) { + removeProjects.append(project); + } + } + for (const auto removeProject : removeProjects) { + _recentProjects.removeOne(removeProject); + } + + RecentAvatarProject newRecentProject = RecentAvatarProject(projectName, fstPath); + _recentProjects.prepend(newRecentProject); + + while (_recentProjects.size() > MAX_RECENT_PROJECTS) { + _recentProjects.pop_back(); + } + + _recentProjectsSetting.set(recentProjectsToVariantList()); + emit recentProjectsChanged(); +} + AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); + if (_currentAvatarProject) { + addRecentProject(_currentAvatarProject->getFSTPath(), _currentAvatarProject->getProjectName()); + } qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 13a42a73d0..8cf641dbaa 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -19,27 +19,95 @@ #include "FileDialogHelper.h" #include "avatar/AvatarProject.h" +#include "SettingHandle.h" + +class RecentAvatarProject { +public: + RecentAvatarProject() { + + } + + RecentAvatarProject(QString projectName, QString projectFSTPath) { + _projectName = projectName; + _projectFSTPath = projectFSTPath; + } + RecentAvatarProject(const RecentAvatarProject& other) { + _projectName = other._projectName; + _projectFSTPath = other._projectFSTPath; + } + + QString getProjectName() const { return _projectName; } + + QString getProjectFSTPath() const { return _projectFSTPath; } + + bool operator==(const RecentAvatarProject& other) const { + return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath; + } + +private: + QString _projectName; + QString _projectFSTPath; + +}; + +inline QDebug operator<<(QDebug debug, const RecentAvatarProject& recentAvatarProject) { + debug << "[recentAvatarProject:" << recentAvatarProject.getProjectFSTPath() << "]"; + return debug; +} + +Q_DECLARE_METATYPE(RecentAvatarProject); + +Q_DECLARE_METATYPE(QVector<RecentAvatarProject>); class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY Q_PROPERTY(AvatarProject* currentAvatarProject READ getAvatarProject NOTIFY avatarProjectChanged) Q_PROPERTY(QString AVATAR_PROJECTS_PATH READ getAvatarProjectsPath CONSTANT) + Q_PROPERTY(QVariantList recentProjects READ getRecentProjects NOTIFY recentProjectsChanged) public: AvatarPackager(); bool open(); Q_INVOKABLE AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { return AvatarProject::isValidNewProjectName(projectPath, projectName); } signals: void avatarProjectChanged(); + void recentProjectsChanged(); private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } + Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(); } + + void addRecentProject(QString fstPath, QString projectName); AvatarProject* _currentAvatarProject{ nullptr }; + QVector<RecentAvatarProject> _recentProjects; + QVariantList recentProjectsToVariantList() { + QVariantList result; + for (const auto& project : _recentProjects) { + QVariantMap projectVariant; + projectVariant.insert("name", project.getProjectName()); + projectVariant.insert("path", project.getProjectFSTPath()); + result.append(projectVariant); + } + + return result; + } + + void recentProjectsFromVariantList(QVariantList projectsVariant) { + _recentProjects.clear(); + for (const auto& projectVariant : projectsVariant) { + auto map = projectVariant.toMap(); + _recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString())); + } + } + + + Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 2a2ec7c1cb..038ded64d8 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -15,13 +15,11 @@ #include <QFile> #include <QFileInfo> -#include <QDebug> #include <QQmlEngine> #include <QTimer> #include "FBXSerializer.h" #include <ui/TabletScriptingInterface.h> -#include <graphics/TextureMap.h> #include "scripting/HMDScriptingInterface.h" AvatarProject* AvatarProject::openAvatarProject(const QString& path) { @@ -38,7 +36,7 @@ AvatarProject* AvatarProject::openAvatarProject(const QString& path) { } AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { - if (!isValidNewProjectName(avatarProjectName)) { + if (!isValidNewProjectName(projectsFolder, avatarProjectName)) { return nullptr; } QDir projectDir(projectsFolder + "/" + avatarProjectName); @@ -135,8 +133,11 @@ QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { return result; } -bool AvatarProject::isValidNewProjectName(const QString& projectName) { - QDir dir(getDefaultProjectsPath() + "/" + projectName); +bool AvatarProject::isValidNewProjectName(const QString& projectPath, const QString& projectName) { + if (projectPath.trimmed().isEmpty() || projectName.trimmed().isEmpty()) { + return false; + } + QDir dir(projectPath + "/" + projectName); return !dir.exists(); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 6da9f710cc..506dd7d40b 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -17,12 +17,9 @@ #include "ProjectFile.h" #include "FST.h" -#include <QDir> #include <QObject> #include <QDir> -#include <QFileInfo> #include <QVariantHash> -#include <QUuid> #include <QStandardPaths> class AvatarProject : public QObject { @@ -41,6 +38,11 @@ public: Q_INVOKABLE void openInInventory(); Q_INVOKABLE QStringList getProjectFiles() const; + Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } + Q_INVOKABLE QString getProjectPath() const { return _projectPath; } + Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } + Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } + /** * returns the AvatarProject or a nullptr on failure. */ @@ -50,7 +52,7 @@ public: const QString& avatarModelPath, const QString& textureFolder); - static bool isValidNewProjectName(const QString& projectName); + static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); static QString getDefaultProjectsPath() { return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/High Fidelity Projects"; @@ -66,11 +68,6 @@ private: ~AvatarProject() { _fst->deleteLater(); } - Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } - Q_INVOKABLE QString getProjectPath() const { return _projectPath; } - Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } - Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } - FST* getFST() { return _fst; } void refreshProjectFiles(); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 31dcf8e9a0..ebb3ccdf53 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -15,8 +15,8 @@ #include <DependencyManager.h> #include <QBuffer> -#include <quazip5\quazip.h> -#include <quazip5\quazipfile.h> +#include <quazip5/quazip.h> +#include <quazip5/quazipfile.h> #include <qtimer.h> #include <QFile> From 882975f01ca90aa24ba8012b34b357b41ee61cbf Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 12:43:43 -0800 Subject: [PATCH 17/43] Update icons in avatar packager --- .../resources/icons/checkmark-stroke.svg | 4 +++ .../resources/icons/loader-snake-256-wf.gif | Bin 0 -> 27304 bytes .../resources/icons/loader-snake-256.gif | Bin 0 -> 26070 bytes .../qml/hifi/avatarPackager/AvatarProject.qml | 8 ++++-- .../hifi/avatarPackager/AvatarProjectCard.qml | 1 + .../avatarPackager/AvatarProjectUpload.qml | 25 +++++++++++------- .../avatarPackager/AvatarUploadStatusItem.qml | 15 +++++------ .../qml/hifi/avatarPackager/LoadingCircle.qml | 14 ++++------ 8 files changed, 37 insertions(+), 30 deletions(-) create mode 100644 interface/resources/icons/checkmark-stroke.svg create mode 100644 interface/resources/icons/loader-snake-256-wf.gif create mode 100644 interface/resources/icons/loader-snake-256.gif diff --git a/interface/resources/icons/checkmark-stroke.svg b/interface/resources/icons/checkmark-stroke.svg new file mode 100644 index 0000000000..cc343c421b --- /dev/null +++ b/interface/resources/icons/checkmark-stroke.svg @@ -0,0 +1,4 @@ +<svg width="149" height="150" viewBox="0 0 149 150" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M74.3055 0C115.543 0 149 33.5916 149 74.6047C149 116.008 115.543 149.6 74.3055 149.6C33.4569 149.6 0 116.008 0 74.6047C0 33.5916 33.4569 0 74.3055 0ZM74.3055 139.054C109.708 139.054 138.496 110.149 138.496 74.6047C138.496 39.4507 109.708 10.5462 74.3055 10.5462C39.2924 10.5462 10.5039 39.4507 10.5039 74.6047C10.5039 110.149 39.2924 139.054 74.3055 139.054Z" fill="#1FC6A6"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M65.3575 89.8376L106.595 43.3562C110.874 38.2784 119.044 45.3092 114.376 50.387L70.0259 100.384C68.0807 102.727 64.9684 102.727 63.0233 101.165L35.0128 78.9008C29.9554 74.6042 36.569 66.4016 41.6264 70.6982L65.3575 89.8376Z" fill="#1FC6A6"/> +</svg> diff --git a/interface/resources/icons/loader-snake-256-wf.gif b/interface/resources/icons/loader-snake-256-wf.gif new file mode 100644 index 0000000000000000000000000000000000000000..c0d5eec1ef6b83e3994bdaa1ee6e033e6e32f78d GIT binary patch literal 27304 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 literal 0 HcmV?d00001 diff --git a/interface/resources/icons/loader-snake-256.gif b/interface/resources/icons/loader-snake-256.gif new file mode 100644 index 0000000000000000000000000000000000000000..ebcbf54bd7759edfde7cab91d0a3e1be78c8390e GIT binary patch literal 26070 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 aJ^ES?TC8C)Fcoo0^4Grm_&0EHWd8?&-`BDL literal 0 HcmV?d00001 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index d4a67f31fb..babe3cda84 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -25,6 +25,8 @@ Item { anchors.fill: parent anchors.margins: 10 + function reset() { hasSuccessfullyUploaded = false } + property var footer: Item { anchors.fill: parent @@ -41,10 +43,10 @@ Item { visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn - //width: parent.width - //anchors.bottom: parent.bottom + anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right + text: qsTr("Upload") color: hifi.buttons.blue colorScheme: root.colorScheme @@ -130,6 +132,8 @@ Item { width: 28 height: 28 + + white: true } RalewayRegular { id: stepText diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index be1363850e..e31115efe5 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -78,6 +78,7 @@ Item { hoverEnabled: true onClicked: { AvatarPackagerCore.openAvatarProject(path.text); + avatarProject.reset(); avatarPackager.state = "project"; } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index e71d8a4f2f..f02dc5a218 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -72,7 +72,7 @@ Item { } ] - LoadingCircle { + AnimatedImage { id: uploadSpinner visible: true @@ -81,6 +81,12 @@ Item { horizontalCenter: parent.horizontalCenter verticalCenter: parent.verticalCenter } + + width: 164 + height: 164 + + source: "../../../icons/loader-snake-256.gif" + playing: true } HiFiGlyphs { @@ -94,23 +100,22 @@ Item { } size: 164 - text: "w" + text: "+" color: "#EA4C5F" } - HiFiGlyphs { + Image { id: successIcon visible: false - anchors { - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter - size: 164 - text: "\ue01a" - color: "#1FC6A6" + width: 148 + height: 148 + + source: "../../../icons/checkmark-stroke.svg" } } Item { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index d93afbd4e8..ca01e453e9 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -42,7 +42,7 @@ Item { width: 48 height: parent.height - AnimatedImage { + LoadingCircle { id: runningImage visible: false @@ -52,22 +52,19 @@ Item { width: 32 height: 32 - - source: "../../../icons/loader-snake-64-w.gif" - playing: false } - HiFiGlyphs { + Image { id: successGlyph visible: false - width: implicitWidth anchors.verticalCenter: parent.verticalCenter anchors.horizontalCenter: parent.horizontalCenter - size: 48 - text: "\ue01a" - color: "#1FC6A6" + width: 30 + height: 30 + + source: "../../../icons/checkmark-stroke.svg" } HiFiGlyphs { id: failGlyph diff --git a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml index f6ba81a96f..a1fac72ae4 100644 --- a/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml +++ b/interface/resources/qml/hifi/avatarPackager/LoadingCircle.qml @@ -3,18 +3,14 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtGraphicalEffects 1.0 -Image { +AnimatedImage { id: root width: 128 height: 128 - source: "../../../images/loader-snake-128.png" + property bool white: false - RotationAnimation on rotation { - duration: 2000 - loops: Animation.Infinite - from: 0 - to: 360 - } -} \ No newline at end of file + source: white ? "../../../icons/loader-snake-256-wf.gif" : "../../../icons/loader-snake-256.gif" + playing: true +} From 1a38abe23014f924cd74989df6883b56f89563f2 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Thu, 27 Dec 2018 21:47:10 +0100 Subject: [PATCH 18/43] - add jointIndexes to new - attempt to fix OSX / linux build - ability to actually load a recent project (previously was only able to load the top recent project) --- interface/resources/qml/hifi/AvatarPackager.qml | 9 ++++++--- .../hifi/avatarPackager/AvatarProjectCard.qml | 5 +++-- interface/src/avatar/AvatarPackager.h | 16 ++++------------ interface/src/avatar/AvatarProject.h | 2 +- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- libraries/fbx/src/FST.cpp | 6 ++++++ 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 686bdd28da..79c130fadb 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -84,6 +84,11 @@ Windows.ScrollingWindow { property alias showModalOverlay: modalOverlay.visible + function openProject(path) { + AvatarPackagerCore.openAvatarProject(path); + avatarPackager.state = "project"; + } + AvatarPackagerHeader { id: avatarPackagerHeader onBackButtonClicked: { @@ -175,7 +180,6 @@ Windows.ScrollingWindow { } } - Flow { visible: AvatarPackagerCore.recentProjects.length === 0 anchors { @@ -194,8 +198,6 @@ Windows.ScrollingWindow { color: "white" text: qsTr("To learn more about using this tool, visit our docs") } - - } Column { @@ -213,6 +215,7 @@ Windows.ScrollingWindow { AvatarProjectCard { title: modelData.name path: modelData.path + onOpen: avatarPackager.openProject(modelData.path) } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index be1363850e..7c29991792 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -20,6 +20,8 @@ Item { property color hoverBackgroundColor: "#E3E3E3" property color pressedBackgroundColor: "#6A6A6A" + signal open; + state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal") states: [ State { @@ -77,8 +79,7 @@ Item { anchors.fill: parent hoverEnabled: true onClicked: { - AvatarPackagerCore.openAvatarProject(path.text); - avatarPackager.state = "project"; + open(); } } } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 8cf641dbaa..57cbf046a7 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -23,9 +23,8 @@ class RecentAvatarProject { public: - RecentAvatarProject() { - - } + RecentAvatarProject() = default; + RecentAvatarProject(QString projectName, QString projectFSTPath) { _projectName = projectName; @@ -36,6 +35,8 @@ public: _projectFSTPath = other._projectFSTPath; } + ~RecentAvatarProject() = default; + QString getProjectName() const { return _projectName; } QString getProjectFSTPath() const { return _projectFSTPath; } @@ -50,15 +51,6 @@ private: }; -inline QDebug operator<<(QDebug debug, const RecentAvatarProject& recentAvatarProject) { - debug << "[recentAvatarProject:" << recentAvatarProject.getProjectFSTPath() << "]"; - return debug; -} - -Q_DECLARE_METATYPE(RecentAvatarProject); - -Q_DECLARE_METATYPE(QVector<RecentAvatarProject>); - class AvatarPackager : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 506dd7d40b..e950fd7379 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -24,7 +24,7 @@ class AvatarProject : public QObject { Q_OBJECT - Q_PROPERTY(FST* fst READ getFST) + Q_PROPERTY(FST* fst READ getFST CONSTANT) Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index ebb3ccdf53..6bc499c0fb 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -33,7 +33,7 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QUuid marketplaceID, QStringList filePaths) : _title(title), - _description(description), _rootFilename(rootFilename), _filePaths(filePaths), _marketplaceID(marketplaceID) { + _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { qWarning() << "File paths: " << _filePaths.join(", "); //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); } diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index f0e444ba33..5d3737319f 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -74,6 +74,12 @@ FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm:: mapping.insert(JOINT_FIELD, joints); + QVariantHash jointIndices; + for (int i = 0; i < hfmModel.joints.size(); i++) { + jointIndices.insert(hfmModel.joints.at(i).name, QString::number(i)); + } + mapping.insert(JOINT_INDEX_FIELD, jointIndices); + mapping.insertMulti(FREE_JOINT_FIELD, "LeftArm"); mapping.insertMulti(FREE_JOINT_FIELD, "LeftForeArm"); mapping.insertMulti(FREE_JOINT_FIELD, "RightArm"); From 89b03a3cdcce5fdee5282fc29f0fe171df8a1867 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 14:30:44 -0800 Subject: [PATCH 19/43] Add info popup box for avatar project files --- .../resources/qml/hifi/AvatarPackager.qml | 42 +++++++ .../qml/hifi/avatarPackager/AvatarProject.qml | 45 +------ .../qml/hifi/avatarPackager/InfoBox.qml | 112 ++++++++++++++++++ 3 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/InfoBox.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index cd1bfb34f1..b3535395db 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -29,6 +29,48 @@ Windows.ScrollingWindow { height: pane.height width: pane.width + InfoBox { + id: fileListPopup + + title: "List of Files" + + content: Rectangle { + id: fileList + + color: "#404040" + + anchors.fill: parent + anchors.topMargin: 10 + anchors.bottomMargin: 10 + anchors.leftMargin: 29 + anchors.rightMargin: 29 + + ListView { + anchors.fill: parent + model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles + delegate: Rectangle { + width: parent.width + height: fileText.implicitHeight + 8 + color: "#404040" + RalewaySemiBold { + id: fileText + size: 16 + elide: Text.ElideLeft + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 4 + width: parent.width - 10 + color: "white" + text: modelData + } + } + } + } + } + Rectangle { id: modalOverlay anchors.fill: parent diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index babe3cda84..ffe012258a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -271,47 +271,6 @@ Item { } } - Rectangle { - id: fileList - - visible: false - - color: "#6A6A6A" - - anchors.top: openFolderButton.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: showFilesText.top - //anchors.bottom: uploadButton.top - anchors.topMargin: 10 - anchors.bottomMargin: 10 - height: 1000 - - ListView { - anchors.fill: parent - model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Rectangle { - width: parent.width - height: fileText.implicitHeight + 8 - color: (index % 2 == 0) ? "#6A6A6A" : "grey" - RalewaySemiBold { - id: fileText - size: 16 - elide: Text.ElideLeft - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - anchors.topMargin: 4 - width: parent.width - 10 - color: "white" - text: modelData - } - } - } - } - RalewayRegular { id: showFilesText @@ -325,9 +284,9 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in the project. <a href='toggle'>" + (fileList.visible ? "Hide" : "Show") + " list</a>" + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>Show list</a>" - onLinkActivated: fileList.visible = !fileList.visible + onLinkActivated: fileListPopup.open() } Rectangle { diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml new file mode 100644 index 0000000000..89f5d5c7f8 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -0,0 +1,112 @@ +import Hifi 1.0 as Hifi +import QtQuick 2.5 +import stylesUit 1.0 +import controlsUit 1.0 as HifiControlsUit +import "../../controls" as HifiControls + +Rectangle { + id: root; + visible: false; + color: Qt.rgba(.34, .34, .34, 0.6); + z: 999; + + anchors.fill: parent + + property alias title: titleText.text + property alias content: loader.sourceComponent + + property bool closeOnClickOutside: false; + + function open() { + visible = true; + } + + function close() { + visible = false; + } + + HifiConstants { + id: hifi + } + + // 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; + hoverEnabled: true; + onClicked: { + if (closeOnClickOutside) { + root.close() + } + } + } + + Rectangle { + id: mainContainer; + + width: Math.max(parent.width * 0.8, 400) + height: parent.height * 0.6 + + MouseArea { + anchors.fill: parent; + propagateComposedEvents: false; + hoverEnabled: true; + onClicked: function(ev) { + ev.accepted = true; + } + } + + anchors.centerIn: parent + + color: "#252525" + + // TextStyle1 + RalewaySemiBold { + id: titleText + size: 24 + color: "white" + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 30 + + text: "Title not defined" + } + + Item { + anchors.topMargin: 10 + anchors.top: titleText.bottom + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: button.top + + Loader { + id: loader + anchors.fill: parent + } + } + + Item { + id: button + + height: 40 + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.bottomMargin: 12 + + HifiControlsUit.Button { + anchors.centerIn: parent + + text: "CLOSE" + onClicked: close() + + color: hifi.buttons.noneBorderlessWhite; + colorScheme: hifi.colorSchemes.dark; + } + } + + } +} From 9e7f89e2b0cee6943130358fc57cdc8abf2bd7d1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 14:38:46 -0800 Subject: [PATCH 20/43] Update link text for avatar project files --- interface/resources/qml/hifi/avatarPackager/AvatarProject.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index ffe012258a..96daf2c0ca 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -284,7 +284,7 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>Show list</a>" + text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" onLinkActivated: fileListPopup.open() } From c21d0378005f98cef70ea5fef8903b63f804dc83 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 17:51:27 -0800 Subject: [PATCH 21/43] Fix marketplace uploader not working with projects larger than 128MB --- .../resources/qml/hifi/AvatarPackager.qml | 12 ++-- interface/src/avatar/AvatarProject.cpp | 13 ++--- .../src/avatar/MarketplaceItemUploader.cpp | 56 +++++++++---------- .../src/avatar/MarketplaceItemUploader.h | 8 ++- libraries/avatars/src/ProjectFile.h | 2 + 5 files changed, 45 insertions(+), 46 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index b3535395db..ff275923ac 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -127,9 +127,12 @@ Windows.ScrollingWindow { property alias showModalOverlay: modalOverlay.visible function openProject(path) { - AvatarPackagerCore.openAvatarProject(path); - avatarProject.reset(); - avatarPackager.state = "project"; + let project = AvatarPackagerCore.openAvatarProject(path); + if (project) { + avatarProject.reset(); + avatarPackager.state = "project"; + } + return project; } AvatarPackagerHeader { @@ -213,9 +216,8 @@ Windows.ScrollingWindow { browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = AvatarPackagerCore.openAvatarProject(fstFilePath); + let currentAvatarProject = avatarPackager.openProject(fstFilePath); if (currentAvatarProject) { - avatarPackager.state = AvatarPackagerState.project; avatarPackager.showModalOverlay = false; } }); diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 038ded64d8..17ed4b6921 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,10 +162,9 @@ void AvatarProject::appendDirectory(QString prefix, QDir dir) { constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { - //_projectFiles.append(prefix + "/" + entry.fileName()); - _projectFiles.append({ entry.absoluteFilePath(), prefix + "/" + entry.fileName() }); + _projectFiles.append({ entry.absoluteFilePath(), prefix + entry.fileName() }); } else if (entry.isDir()) { - appendDirectory(prefix + dir.dirName() + "/", entry.absoluteFilePath()); + appendDirectory(prefix + entry.fileName() + "/", entry.absoluteFilePath()); } } } @@ -188,12 +187,8 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { if (updateExisting) { itemID = _fst->getMarketplaceID(); } - QStringList projectFilePaths; - for (auto& path : _projectFiles) { - projectFilePaths.append(path.absolutePath); - } - auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), itemID, - projectFilePaths); + auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), + itemID, _projectFiles); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { _fst->setMarketplaceID(uploader->getMarketplaceID()); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 6bc499c0fb..33cfb85c8e 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -31,18 +31,15 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QString description, QString rootFilename, QUuid marketplaceID, - QStringList filePaths) : + QList<ProjectFilePath> filePaths) : _title(title), _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { - qWarning() << "File paths: " << _filePaths.join(", "); - //_marketplaceID = QUuid::fromString(QLatin1String("{50dbd62f-cb6b-4be4-afb8-1ef8bd2dffa8}")); } void MarketplaceItemUploader::setState(State newState) { Q_ASSERT(_state != State::Complete); Q_ASSERT(_error == Error::None); Q_ASSERT(newState != _state); - qDebug() << "Setting uploader state to: " << newState; _state = newState; emit stateChanged(newState); @@ -113,13 +110,12 @@ void MarketplaceItemUploader::doGetCategories() { }; bool success; - int id; - std::tie(success, id) = extractCategoryID(); - qDebug() << "Done " << success << id; + std::tie(success, _categoryID) = extractCategoryID(); if (!success) { qWarning() << "Failed to find marketplace category id"; setError(Error::Unknown); } else { + qDebug() << "Marketplace Avatar category ID is" << _categoryID; doUploadAvatar(); } } else { @@ -133,24 +129,26 @@ void MarketplaceItemUploader::doUploadAvatar() { //buffer.open(QIODevice::WriteOnly); QuaZip zip{ &buffer }; if (!zip.open(QuaZip::Mode::mdAdd)) { - qWarning() << "Failed to open zip!!"; + qWarning() << "Failed to open zip"; + setError(Error::Unknown); + return; } for (auto& filePath : _filePaths) { - qWarning() << "Zipping: " << filePath; - QFileInfo fileInfo{ filePath }; + qWarning() << "Zipping: " << filePath.absolutePath << filePath.relativePath; + QFileInfo fileInfo{ filePath.absolutePath }; QuaZipFile zipFile{ &zip }; - if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileInfo.fileName()))) { + if (!zipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(filePath.relativePath))) { qWarning() << "Could not open zip file:" << zipFile.getZipError(); setError(Error::Unknown); return; } - QFile file{ filePath }; + QFile file{ filePath.absolutePath }; if (file.open(QIODevice::ReadOnly)) { zipFile.write(file.readAll()); } else { - qWarning() << "Failed to open: " << filePath; + qWarning() << "Failed to open: " << filePath.absolutePath; } file.close(); zipFile.close(); @@ -175,28 +173,26 @@ void MarketplaceItemUploader::doUploadAvatar() { } auto accountManager = DependencyManager::get<AccountManager>(); auto request = accountManager->createRequest(path, AccountManagerAuth::Required); - qWarning() << "Request url is: " << request.url(); - - QJsonObject root{ { "marketplace_item", - QJsonObject{ { "title", _title }, - { "description", _description }, - { "root_file_key", _rootFilename }, - { "category_ids", QJsonArray({ 5 }) }, - { "license", 0 }, - { "files", QString::fromLatin1(_fileData.toBase64()) } } } }; request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json"); - QJsonDocument doc{ root }; - qWarning() << "data: " << doc.toJson(); + // TODO(huffman) add JSON escaping + auto escapeJson = [](QString str) -> QString { return str; }; + + QString jsonString = "{\"marketplace_item\":{"; + jsonString += "\"title\":\"" + escapeJson(_title) + "\""; + jsonString += ",\"description\":\"" + escapeJson(_description) + "\""; + jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\""; + jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]"; + jsonString += ",\"license\":0"; + jsonString += ",\"files\":\"" + QString::fromLatin1(_fileData.toBase64()) + "\"}}"; - _fileData.toBase64(); QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); QNetworkReply* reply{ nullptr }; if (creating) { - reply = networkAccessManager.post(request, doc.toJson()); + reply = networkAccessManager.post(request, jsonString.toUtf8()); } else { - reply = networkAccessManager.put(request, doc.toJson()); + reply = networkAccessManager.put(request, jsonString.toUtf8()); } connect(reply, &QNetworkReply::uploadProgress, this, [this](float bytesSent, float bytesTotal) { @@ -210,11 +206,9 @@ void MarketplaceItemUploader::doUploadAvatar() { connect(reply, &QNetworkReply::finished, this, [this, reply]() { _responseData = reply->readAll(); - qWarning() << "Finished request " << _responseData; auto error = reply->error(); if (error == QNetworkReply::NoError) { - auto doc = QJsonDocument::fromJson(_responseData.toLatin1()); auto status = doc.object()["status"].toString(); if (status == "success") { @@ -223,9 +217,11 @@ void MarketplaceItemUploader::doUploadAvatar() { setState(State::WaitingForInventory); doWaitForInventory(); } else { + qWarning() << "Got error response while uploading avatar: " << _responseData; setError(Error::Unknown); } } else { + qWarning() << "Got error while uploading avatar: " << reply->error() << reply->errorString() << _responseData; setError(Error::Unknown); } }); @@ -265,7 +261,7 @@ void MarketplaceItemUploader::doWaitForInventory() { } auto data = root["data"]; if (!data.isObject()) { - return false; + return false; } auto assets = data.toObject()["assets"]; if (!assets.isArray()) { diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 4b8b675255..4fb1713b7d 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -13,6 +13,8 @@ #ifndef hifi_MarketplaceItemUploader_h #define hifi_MarketplaceItemUploader_h +#include "ProjectFile.h" + #include <QObject> #include <QUuid> @@ -50,7 +52,7 @@ public: QString description, QString rootFilename, QUuid marketplaceID, - QStringList filePaths); + QList<ProjectFilePath> filePaths); Q_INVOKABLE void send(); @@ -91,13 +93,15 @@ private: QString _description; QString _rootFilename; QUuid _marketplaceID; + int _categoryID; int _itemVersion; QString _responseData; int _numRequestsForInventory{ 0 }; - QStringList _filePaths; + QString _rootFilePath; + QList<ProjectFilePath> _filePaths; QByteArray _fileData; }; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h index df92513a1b..82930a3464 100644 --- a/libraries/avatars/src/ProjectFile.h +++ b/libraries/avatars/src/ProjectFile.h @@ -1,6 +1,8 @@ #ifndef hifi_AvatarProjectFile_h #define hifi_AvatarProjectFile_h +#include <QObject> + class ProjectFilePath { Q_GADGET; public: From 83a60a541b14723e542da8544607db5f5744121e Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 18:53:07 -0800 Subject: [PATCH 22/43] Fix styling and flow issues in avatar uploader --- .../resources/qml/hifi/AvatarPackager.qml | 2 + .../qml/hifi/avatarPackager/AvatarProject.qml | 2 +- .../avatarPackager/AvatarProjectUpload.qml | 46 ++++++++++++++++--- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index ff275923ac..1300184591 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -45,6 +45,8 @@ Windows.ScrollingWindow { anchors.leftMargin: 29 anchors.rightMargin: 29 + clip: true + ListView { anchors.fill: parent model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 96daf2c0ca..c94c9570ae 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -229,7 +229,7 @@ Item { name: "upload-success" PropertyChanges { target: infoMessage - text: "Your avatar has been uploaded to our servers. You can modify the project files and update it again to make changes on the uploaded avatar." + text: "Your avatar has been successfully uploaded to our servers. Make changes to your avatar by editing and uploading the project files." } }, State { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index f02dc5a218..b21bd38070 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -17,7 +17,7 @@ Item { Timer { id: backToProjectTimer - interval: 5000 + interval: 2000 running: false repeat: false onTriggered: { @@ -37,7 +37,9 @@ Item { if (visible) { root.uploader.stateChanged.connect(stateChangedCallback); root.uploader.finishedChanged.connect(function() { - backToProjectTimer.start(); + if (root.uploader.error === 0) { + backToProjectTimer.start(); + } }); } } @@ -53,7 +55,7 @@ Item { id: statusItem width: parent.width - height: 192 + height: 256 states: [ State { @@ -69,6 +71,8 @@ Item { PropertyChanges { target: uploadSpinner; visible: false } PropertyChanges { target: errorIcon; visible: true } PropertyChanges { target: successIcon; visible: false } + PropertyChanges { target: errorFooter; visible: true } + PropertyChanges { target: errorMessage; visible: true } } ] @@ -99,7 +103,7 @@ Item { verticalCenter: parent.verticalCenter } - size: 164 + size: 315 text: "+" color: "#EA4C5F" } @@ -151,20 +155,48 @@ Item { } RalewayRegular { - visible: root.uploader.error + id: errorMessage + + visible: false anchors.left: parent.left anchors.right: parent.right - anchors.bottom: parent.bottom + anchors.bottom: errorFooter.top anchors.leftMargin: 16 anchors.rightMargin: 16 - anchors.bottomMargin: 16 + anchors.bottomMargin: 32 size: 28 wrapMode: Text.Wrap color: "white" text: "We couldn't upload your avatar at this time. Please try again later." } + AvatarPackagerFooter { + id: errorFooter + + anchors.bottom: parent.bottom + visible: false + + content: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: backButton + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + text: qsTr("Back") + color: hifi.buttons.blue + colorScheme: root.colorScheme + width: 133 + height: 40 + onClicked: function() { + avatarPackager.state = "project" + } + } + } + } } Column { From 8f70865cf71df1cbcec7fc73d91f084a3bd0e5e2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 21:02:56 -0800 Subject: [PATCH 23/43] Fix marketplace upload finishing before inventory item is valid --- interface/src/avatar/MarketplaceItemUploader.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 33cfb85c8e..45a13f04c4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -275,9 +275,10 @@ void MarketplaceItemUploader::doWaitForInventory() { } if (id == _marketplaceID) { auto version = assetObject["version"]; - if (version.isDouble()) { + auto valid = assetObject["valid"]; + if (version.isDouble() && valid.isBool()) { int versionInt = version.toDouble(); - if (versionInt >= _itemVersion) { + if ((int)version.toDouble() >= _itemVersion && valid.toBool()) { return true; } } From 24aaeee5fd82d7f29fe00c9986ccdd487c54ab40 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 27 Dec 2018 22:55:54 -0800 Subject: [PATCH 24/43] Disable back button on avatar upload page --- .../resources/qml/hifi/AvatarPackager.qml | 4 ++-- .../avatarPackager/AvatarPackagerHeader.qml | 13 ++++++----- .../avatarPackager/AvatarProjectUpload.qml | 23 ++----------------- interface/src/avatar/AvatarProject.cpp | 2 +- 4 files changed, 12 insertions(+), 30 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml index 1300184591..2492746627 100644 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ b/interface/resources/qml/hifi/AvatarPackager.qml @@ -102,7 +102,7 @@ Windows.ScrollingWindow { states: [ State { name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonEnabled: false } + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonVisible: false } PropertyChanges { target: avatarPackagerMain; visible: true } PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } }, @@ -120,7 +120,7 @@ Windows.ScrollingWindow { }, State { name: "project-upload" - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name } + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false } PropertyChanges { target: avatarUploader; visible: true } PropertyChanges { target: avatarPackagerFooter; visible: false } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 663d4d0f3a..5b01005edd 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -4,7 +4,7 @@ import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 Rectangle { - id: avatarPackagerHeader + id: root width: parent.width height: 74 @@ -12,13 +12,14 @@ Rectangle { property alias title: title.text property alias faqEnabled: faq.visible - property alias backButtonEnabled: back.visible + property bool backButtonVisible: true // If false, is not visible and does not take up space + property bool backButtonEnabled: true // If false, is not visible but does not affect space property bool canRename: false; signal backButtonClicked RalewaySemiBold { id: back - visible: true + visible: backButtonEnabled && backButtonVisible size: 28 anchors.top: parent.top anchors.bottom: parent.bottom @@ -29,7 +30,7 @@ Rectangle { color: "white" MouseArea { anchors.fill: parent - onClicked: avatarPackagerHeader.backButtonClicked() + onClicked: root.backButtonClicked() hoverEnabled: true onEntered: { state = "hovering" } onExited: { state = "" } @@ -50,8 +51,8 @@ Rectangle { size: 28 anchors.top: parent.top anchors.bottom: parent.bottom - anchors.left: back.visible ? back.right : parent.left - anchors.leftMargin: back.visible ? 11 : 21 + anchors.left: root.backButtonVisible ? back.right : parent.left + anchors.leftMargin: root.backButtonVisible ? 11 : 21 anchors.verticalCenter: title.verticalCenter text: qsTr("Avatar Packager") color: "white" diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index b21bd38070..2031392ca2 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -47,8 +47,6 @@ Item { Item { id: uploadStatus - visible: !!root.uploader - anchors.fill: parent Item { @@ -60,14 +58,14 @@ Item { states: [ State { name: "success" - when: !!root.uploader && root.uploader.state >= 4 && root.uploader.error === 0 + when: root.uploader.state >= 4 && root.uploader.error === 0 PropertyChanges { target: uploadSpinner; visible: false } PropertyChanges { target: errorIcon; visible: false } PropertyChanges { target: successIcon; visible: true } }, State { name: "error" - when: !!root.uploader && root.uploader.finished && root.uploader.error !== 0 + when: root.uploader.finished && root.uploader.error !== 0 PropertyChanges { target: uploadSpinner; visible: false } PropertyChanges { target: errorIcon; visible: true } PropertyChanges { target: successIcon; visible: false } @@ -198,21 +196,4 @@ Item { } } } - - Column { - id: debugInfo - - visible: false - - Text { - text: "Uploading" - color: "white" - - } - Text { - text: "State: " + (!!root.uploader ? root.uploader.state : " NONE") - color: "white" - } - } - } diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 17ed4b6921..09d60163b6 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -187,7 +187,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { if (updateExisting) { itemID = _fst->getMarketplaceID(); } - auto uploader = new MarketplaceItemUploader(getProjectName(), "Empty description", QFileInfo(getFSTPath()).fileName(), + auto uploader = new MarketplaceItemUploader(getProjectName(), "", QFileInfo(getFSTPath()).fileName(), itemID, _projectFiles); connect(uploader, &MarketplaceItemUploader::completed, this, [this, uploader]() { if (uploader->getError() == MarketplaceItemUploader::Error::None) { From cb33a91a34366f04ac286cb342ea63cff4b3d2ca Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Sat, 29 Dec 2018 03:31:56 +0100 Subject: [PATCH 25/43] - rename functionality - avatar packager works in tablet now --- .../resources/qml/hifi/AvatarPackager.qml | 276 ----------------- .../qml/hifi/AvatarPackagerWindow.qml | 32 ++ .../hifi/avatarPackager/AvatarPackagerApp.qml | 288 ++++++++++++++++++ .../avatarPackager/AvatarPackagerHeader.qml | 102 ++++++- .../avatarPackager/AvatarPackagerState.qml | 1 + .../qml/hifi/avatarPackager/AvatarProject.qml | 2 +- .../hifi/avatarPackager/AvatarProjectCard.qml | 7 + .../avatarPackager/AvatarProjectUpload.qml | 10 +- .../avatarPackager/AvatarUploadStatusItem.qml | 8 +- .../avatarPackager/CreateAvatarProject.qml | 3 +- .../avatarPackager/ProjectInputControl.qml | 2 +- .../qml/hifi/tablet/AvatarPackager.qml | 15 + interface/src/avatar/AvatarPackager.cpp | 88 ++++-- interface/src/avatar/AvatarPackager.h | 33 +- interface/src/avatar/AvatarProject.cpp | 25 +- interface/src/avatar/AvatarProject.h | 15 +- libraries/fbx/src/FST.cpp | 4 +- libraries/fbx/src/FST.h | 4 +- 18 files changed, 543 insertions(+), 372 deletions(-) delete mode 100644 interface/resources/qml/hifi/AvatarPackager.qml create mode 100644 interface/resources/qml/hifi/AvatarPackagerWindow.qml create mode 100644 interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml create mode 100644 interface/resources/qml/hifi/tablet/AvatarPackager.qml diff --git a/interface/resources/qml/hifi/AvatarPackager.qml b/interface/resources/qml/hifi/AvatarPackager.qml deleted file mode 100644 index 2492746627..0000000000 --- a/interface/resources/qml/hifi/AvatarPackager.qml +++ /dev/null @@ -1,276 +0,0 @@ -import QtQuick 2.6 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 -import "../controlsUit" 1.0 as HifiControls -import "../stylesUit" 1.0 -import "../windows" as Windows -import "../dialogs" -import "./avatarPackager" 1.0 -import "avatarapp" 1.0 as AvatarApp - -Windows.ScrollingWindow { - id: root - objectName: "AvatarPackager" - width: 480 - height: 706 - title: "Avatar Packager" - resizable: false - opacity: parent.opacity - destroyOnHidden: true - implicitWidth: 384; implicitHeight: 640 - minSize: Qt.vector2d(480, 706) - - HifiConstants { id: hifi } - - Item { - id: windowContent - height: pane.height - width: pane.width - - InfoBox { - id: fileListPopup - - title: "List of Files" - - content: Rectangle { - id: fileList - - color: "#404040" - - anchors.fill: parent - anchors.topMargin: 10 - anchors.bottomMargin: 10 - anchors.leftMargin: 29 - anchors.rightMargin: 29 - - clip: true - - ListView { - anchors.fill: parent - model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles - delegate: Rectangle { - width: parent.width - height: fileText.implicitHeight + 8 - color: "#404040" - RalewaySemiBold { - id: fileText - size: 16 - elide: Text.ElideLeft - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - anchors.leftMargin: 16 - anchors.rightMargin: 16 - anchors.topMargin: 4 - width: parent.width - 10 - color: "white" - text: modelData - } - } - } - } - } - - Rectangle { - id: modalOverlay - anchors.fill: parent - z: 20 - color: "#a15d5d5d" - visible: false - - // This mouse area captures the cursor events while the modalOverlay is active - MouseArea { - anchors.fill: parent - propagateComposedEvents: false; - hoverEnabled: true; - } - } - - AvatarApp.MessageBox { - id: popup - anchors.fill: parent - visible: false - closeOnClickOutside: true - } - - Column { - id: avatarPackager - anchors.fill: parent - state: "main" - states: [ - State { - name: AvatarPackagerState.main - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); faqEnabled: true; backButtonVisible: false } - PropertyChanges { target: avatarPackagerMain; visible: true } - PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } - }, - State { - name: AvatarPackagerState.createProject - PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } - PropertyChanges { target: createAvatarProject; visible: true } - PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } - }, - State { - name: AvatarPackagerState.project - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } - PropertyChanges { target: avatarProject; visible: true } - PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } - }, - State { - name: "project-upload" - PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false } - PropertyChanges { target: avatarUploader; visible: true } - PropertyChanges { target: avatarPackagerFooter; visible: false } - } - ] - - property alias showModalOverlay: modalOverlay.visible - - function openProject(path) { - let project = AvatarPackagerCore.openAvatarProject(path); - if (project) { - avatarProject.reset(); - avatarPackager.state = "project"; - } - return project; - } - - AvatarPackagerHeader { - id: avatarPackagerHeader - onBackButtonClicked: { - avatarPackager.state = "main" - } - } - - Item { - height: pane.height - avatarPackagerHeader.height - avatarPackagerFooter.height - width: pane.width - - Rectangle { - anchors.fill: parent - color: "#404040" - } - - AvatarProject { - id: avatarProject - colorScheme: root.colorScheme - anchors.fill: parent - } - - AvatarProjectUpload { - id: avatarUploader - anchors.fill: parent - root: avatarProject - } - - CreateAvatarProject { - id: createAvatarProject - colorScheme: root.colorScheme - anchors.fill: parent - } - - Item { - id: avatarPackagerMain - visible: false - anchors.fill: parent - - property var footer: Item { - anchors.fill: parent - anchors.rightMargin: 17 - HifiControls.Button { - id: createProjectButton - anchors.verticalCenter: parent.verticalCenter - anchors.right: openProjectButton.left - anchors.rightMargin: 22 - height: 40 - width: 134 - text: qsTr("New Project") - colorScheme: root.colorScheme - onClicked: { - createAvatarProject.clearInputs(); - avatarPackager.state = AvatarPackagerState.createProject; - } - } - - HifiControls.Button { - id: openProjectButton - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - height: 40 - width: 133 - text: qsTr("Open Project") - color: hifi.buttons.blue - colorScheme: root.colorScheme - onClicked: { - avatarPackager.showModalOverlay = true; - let browser = desktop.fileDialog({ - selectDirectory: false, - dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), - filter: "Avatar Project FST Files (*.fst)", - title: "Open Project (.fst)", - }); - - browser.canceled.connect(function() { - avatarPackager.showModalOverlay = false; - }); - - browser.selectedFile.connect(function(fileUrl) { - let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = avatarPackager.openProject(fstFilePath); - if (currentAvatarProject) { - avatarPackager.showModalOverlay = false; - } - }); - } - } - } - - Flow { - visible: AvatarPackagerCore.recentProjects.length === 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - RalewayRegular { - size: 20 - color: "white" - text: qsTr("Use a custom avatar to express your identity") - } - RalewayRegular { - size: 20 - color: "white" - text: qsTr("To learn more about using this tool, visit our docs") - } - } - - Column { - visible: AvatarPackagerCore.recentProjects.length > 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - spacing: 10 - - Repeater { - model: AvatarPackagerCore.recentProjects - AvatarProjectCard { - title: modelData.name - path: modelData.path - onOpen: avatarPackager.openProject(modelData.path) - } - } - } - } - } - AvatarPackagerFooter { - id: avatarPackagerFooter - } - } - } -} diff --git a/interface/resources/qml/hifi/AvatarPackagerWindow.qml b/interface/resources/qml/hifi/AvatarPackagerWindow.qml new file mode 100644 index 0000000000..9d434ef97c --- /dev/null +++ b/interface/resources/qml/hifi/AvatarPackagerWindow.qml @@ -0,0 +1,32 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../controlsUit" 1.0 as HifiControls +import "../stylesUit" 1.0 +import "../windows" as Windows +import "../controls" 1.0 +import "../dialogs" +import "avatarPackager" 1.0 +import "avatarapp" 1.0 as AvatarApp + +Windows.ScrollingWindow { + id: root + objectName: "AvatarPackager" + width: 480 + height: 706 + title: "Avatar Packager" + resizable: false + opacity: parent.opacity + destroyOnHidden: true + implicitWidth: 384; implicitHeight: 640 + minSize: Qt.vector2d(480, 706) + + HifiConstants { id: hifi } + + AvatarPackagerApp { + height: pane.height + width: pane.width + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml new file mode 100644 index 0000000000..2b21b4d938 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -0,0 +1,288 @@ +import QtQuick 2.6 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 +import QtQml.Models 2.1 +import QtGraphicalEffects 1.0 +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 +import "../../windows" as Windows +import "../../controls" 1.0 +import "../../dialogs" +import "../avatarapp" 1.0 as AvatarApp + +Item { + HifiConstants { id: hifi } + + property alias desktopObject: avatarPackager.desktopObject + + id: windowContent + // height: pane ? pane.height : parent.width + // width: pane ? pane.width : parent.width + + + MouseArea { + anchors.fill: parent + onClicked: { + unfocusser.forceActiveFocus(); + } + Item { + id: unfocusser + visible: false + } + } + + InfoBox { + id: fileListPopup + + title: "List of Files" + + content: Rectangle { + id: fileList + + color: "#404040" + + anchors.fill: parent + anchors.topMargin: 10 + anchors.bottomMargin: 10 + anchors.leftMargin: 29 + anchors.rightMargin: 29 + + clip: true + + ListView { + anchors.fill: parent + model: AvatarPackagerCore.currentAvatarProject === null ? [] : AvatarPackagerCore.currentAvatarProject.projectFiles + delegate: Rectangle { + width: parent.width + height: fileText.implicitHeight + 8 + color: "#404040" + RalewaySemiBold { + id: fileText + size: 16 + elide: Text.ElideLeft + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 16 + anchors.rightMargin: 16 + anchors.topMargin: 4 + width: parent.width - 10 + color: "white" + text: modelData + } + } + } + } + } + + Rectangle { + id: modalOverlay + anchors.fill: parent + z: 20 + color: "#a15d5d5d" + visible: false + + // This mouse area captures the cursor events while the modalOverlay is active + MouseArea { + anchors.fill: parent + propagateComposedEvents: false; + hoverEnabled: true; + } + } + + AvatarApp.MessageBox { + id: popup + anchors.fill: parent + visible: false + closeOnClickOutside: true + } + + Column { + id: avatarPackager + anchors.fill: parent + state: "main" + states: [ + State { + name: AvatarPackagerState.main + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Avatar Packager"); docsEnabled: true; backButtonVisible: false } + PropertyChanges { target: avatarPackagerMain; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarPackagerMain.footer } + }, + State { + name: AvatarPackagerState.createProject + PropertyChanges { target: avatarPackagerHeader; title: qsTr("Create Project") } + PropertyChanges { target: createAvatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: createAvatarProject.footer } + }, + State { + name: AvatarPackagerState.project + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; canRename: true } + PropertyChanges { target: avatarProject; visible: true } + PropertyChanges { target: avatarPackagerFooter; content: avatarProject.footer } + }, + State { + name: AvatarPackagerState.projectUpload + PropertyChanges { target: avatarPackagerHeader; title: AvatarPackagerCore.currentAvatarProject.name; backButtonEnabled: false } + PropertyChanges { target: avatarUploader; visible: true } + PropertyChanges { target: avatarPackagerFooter; visible: false } + } + ] + + property alias showModalOverlay: modalOverlay.visible + + property var desktopObject: desktop + + function openProject(path) { + let project = AvatarPackagerCore.openAvatarProject(path); + if (project) { + avatarProject.reset(); + avatarPackager.state = AvatarPackagerState.project; + } + return project; + } + + function openDocs() { + Qt.openUrlExternally("https://docs.highfidelity.com/create/avatars/create-avatars#how-to-package-your-avatar"); + } + + AvatarPackagerHeader { + id: avatarPackagerHeader + colorScheme: root.colorScheme + onBackButtonClicked: { + avatarPackager.state = AvatarPackagerState.main; + } + onDocsButtonClicked: { + avatarPackager.openDocs(); + } + } + + Item { + height: windowContent.height - avatarPackagerHeader.height - avatarPackagerFooter.height + width: windowContent.width + + Rectangle { + anchors.fill: parent + color: "#404040" + } + + AvatarProject { + id: avatarProject + colorScheme: root.colorScheme + anchors.fill: parent + } + + AvatarProjectUpload { + id: avatarUploader + anchors.fill: parent + root: avatarProject + } + + CreateAvatarProject { + id: createAvatarProject + colorScheme: root.colorScheme + anchors.fill: parent + } + + Item { + id: avatarPackagerMain + visible: false + anchors.fill: parent + + property var footer: Item { + anchors.fill: parent + anchors.rightMargin: 17 + HifiControls.Button { + id: createProjectButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: openProjectButton.left + anchors.rightMargin: 22 + height: 40 + width: 134 + text: qsTr("New Project") + colorScheme: root.colorScheme + onClicked: { + createAvatarProject.clearInputs(); + avatarPackager.state = AvatarPackagerState.createProject; + } + } + + HifiControls.Button { + id: openProjectButton + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + height: 40 + width: 133 + text: qsTr("Open Project") + color: hifi.buttons.blue + colorScheme: root.colorScheme + onClicked: { + avatarPackager.showModalOverlay = true; + + let browser = avatarPackager.desktopObject.fileDialog({ + selectDirectory: false, + dir: fileDialogHelper.pathToUrl(AvatarPackagerCore.AVATAR_PROJECTS_PATH), + filter: "Avatar Project FST Files (*.fst)", + title: "Open Project (.fst)", + }); + + browser.canceled.connect(function() { + avatarPackager.showModalOverlay = false; + }); + + browser.selectedFile.connect(function(fileUrl) { + let fstFilePath = fileDialogHelper.urlToPath(fileUrl); + let currentAvatarProject = avatarPackager.openProject(fstFilePath); + if (currentAvatarProject) { + avatarPackager.showModalOverlay = false; + } + }); + } + } + } + + Flow { + visible: AvatarPackagerCore.recentProjects.length === 0 + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("Use a custom avatar to express your identity") + } + RalewayRegular { + size: 20 + color: "white" + text: qsTr("To learn more about using this tool, visit our docs") + } + } + + Column { + visible: AvatarPackagerCore.recentProjects.length > 0 + anchors { + fill: parent + topMargin: 18 + leftMargin: 16 + rightMargin: 16 + } + spacing: 10 + + Repeater { + model: AvatarPackagerCore.recentProjects + AvatarProjectCard { + title: modelData.name + path: modelData.projectPath + onOpen: avatarPackager.openProject(modelData.path) + } + } + } + } + } + AvatarPackagerFooter { + id: avatarPackagerFooter + } + } +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 5b01005edd..7037aa9d92 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -11,11 +11,18 @@ Rectangle { color: "#252525" property alias title: title.text - property alias faqEnabled: faq.visible + property alias docsEnabled: docs.visible property bool backButtonVisible: true // If false, is not visible and does not take up space property bool backButtonEnabled: true // If false, is not visible but does not affect space - property bool canRename: false; + property bool canRename: false + property int colorScheme + + property color textColor: "white" + property color hoverTextColor: "gray" + property color pressedTextColor: "#6A6A6A" + signal backButtonClicked + signal docsButtonClicked RalewaySemiBold { id: back @@ -27,7 +34,7 @@ Rectangle { anchors.leftMargin: 16 anchors.verticalCenter: back.verticalCenter text: "◀" - color: "white" + color: textColor MouseArea { anchors.fill: parent onClicked: root.backButtonClicked() @@ -37,37 +44,102 @@ Rectangle { states: [ State { name: "hovering" - PropertyChanges { - target: back - color: "gray" - } + PropertyChanges { target: back; color: hoverTextColor } } ] } } - - RalewaySemiBold { - id: title - size: 28 + Item { + id: titleArea anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: root.backButtonVisible ? back.right : parent.left anchors.leftMargin: root.backButtonVisible ? 11 : 21 anchors.verticalCenter: title.verticalCenter - text: qsTr("Avatar Packager") - color: "white" + anchors.right: docs.left + states: [ + State { + name: "renaming" + PropertyChanges { target: title; visible: false } + PropertyChanges { target: titleInputArea; visible: true } + } + ] + + RalewaySemiBold { + id: title + size: 28 + anchors.fill: parent + text: qsTr("Avatar Packager") + color: "white" + + MouseArea { + anchors.fill: parent + onClicked: { + if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) { + return; + } + + titleArea.state = "renaming"; + titleInput.text = AvatarPackagerCore.currentAvatarProject.name; + titleInput.selectAll(); + titleInput.forceActiveFocus(Qt.MouseFocusReason); + } + } + } + Item { + id: titleInputArea + visible: false + anchors.fill: parent + + HifiControls.TextField { + id: titleInput + anchors.fill: parent + text: "" + colorScheme: root.colorScheme + font.family: "Fira Sans" + font.pixelSize: 28 + z: 200 + onFocusChanged: { + if (titleArea.state === "renaming" && !titleArea.focus) { + //titleArea.state = ""; + accepted(); + } + } + Keys.onPressed: { + if (event.key === Qt.Key_Escape) { + titleArea.state = ""; + } + } + onAccepted: { + if (acceptableInput) { + //AvatarPackagerCore.renameProject(text); + console.warn(text); + AvatarPackagerCore.currentAvatarProject.name = text; + console.warn(AvatarPackagerCore.currentAvatarProject.name); + + } + titleArea.state = ""; + } + } + } } RalewaySemiBold { - id: faq + id: docs visible: false size: 28 anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right anchors.rightMargin: 16 - anchors.verticalCenter: faq.verticalCenter + anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") color: "white" + MouseArea { + anchors.fill: parent + onClicked: { + docsButtonClicked(); + } + } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml index f12edf4952..c81173a080 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerState.qml @@ -6,4 +6,5 @@ Item { readonly property string main: "main" readonly property string project: "project" readonly property string createProject: "createProject" + readonly property string projectUpload: "projectUpload" } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index c94c9570ae..a5a2263346 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -178,7 +178,7 @@ Item { } }); root.uploader.send(); - avatarPackager.state = "project-upload"; + avatarPackager.state = AvatarPackagerState.projectUpload; } function showConfirmUploadPopup() { diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 736de2019c..5496711d44 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -53,11 +53,14 @@ Item { RalewayBold { id: title + elide: "ElideRight" anchors { top: parent.top topMargin: 13 left: parent.left leftMargin: 16 + right: parent.right + rightMargin: 16 } text: "<title missing>" size: 16 @@ -69,7 +72,11 @@ Item { top: title.bottom left: parent.left leftMargin: 32 + right: background.right + rightMargin: 16 } + elide: "ElideLeft" + horizontalAlignment: Text.AlignRight text: "<path missing>" size: 20 } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index 2031392ca2..c1d1a98158 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -21,8 +21,8 @@ Item { running: false repeat: false onTriggered: { - if (avatarPackager.state =="project-upload") { - avatarPackager.state = "project" + if (avatarPackager.state === AvatarPackagerState.projectUpload) { + avatarPackager.state = AvatarPackagerState.project; } } } @@ -130,7 +130,7 @@ Item { AvatarUploadStatusItem { id: statusCategories uploader: root.uploader - text: "Retreiving categories" + text: "Retrieving categories" uploaderState: 1 } @@ -189,8 +189,8 @@ Item { colorScheme: root.colorScheme width: 133 height: 40 - onClicked: function() { - avatarPackager.state = "project" + onClicked: { + avatarPackager.state = AvatarPackagerState.project; } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index ca01e453e9..4749e912c6 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,9 +14,9 @@ Item { property int uploaderState; property var uploader; - state: root.uploader.state > uploaderState - ? "success" - : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : "")) + state: root.uploader === undefined ? "" : + (root.uploader.state > uploaderState ? "success" + : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) states: [ State { @@ -90,4 +90,4 @@ Item { size: 28 color: "#777777" } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index a5d335feba..d6f530a196 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -119,7 +119,7 @@ Item { ProjectInputControl { id: textureFolder - label: "Specify Texture Folder <i> - optional</i>" + label: "Specify Texture Folder - <i>Optional</i>" colorScheme: root.colorScheme browseEnabled: true browseFolder: true @@ -128,5 +128,4 @@ Item { onTextChanged: checkErrors() } } - } diff --git a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml index 2ac4a37d02..f0a3aac8a7 100644 --- a/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml +++ b/interface/resources/qml/hifi/avatarPackager/ProjectInputControl.qml @@ -57,7 +57,7 @@ Column { colorScheme: root.colorScheme onClicked: { avatarPackager.showModalOverlay = true; - let browser = desktop.fileDialog({ + let browser = avatarPackager.desktopObject.fileDialog({ selectDirectory: browseFolder, dir: browseDir, filter: browseFilter, diff --git a/interface/resources/qml/hifi/tablet/AvatarPackager.qml b/interface/resources/qml/hifi/tablet/AvatarPackager.qml new file mode 100644 index 0000000000..c1c234dd73 --- /dev/null +++ b/interface/resources/qml/hifi/tablet/AvatarPackager.qml @@ -0,0 +1,15 @@ +import QtQuick 2.0 +import "../avatarPackager" 1.0 + +Item { + id: root + width: 480 + height: 706 + + AvatarPackagerApp { + width: parent.width + height: parent.height + + desktopObject: tabletRoot + } +} diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index d8aadeb4e0..eef574a8b5 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -11,6 +11,8 @@ #include "AvatarPackager.h" +#include "Application.h" + #include <QQmlContext> #include <QQmlEngine> #include <QUrl> @@ -19,8 +21,9 @@ #include "ModelSelector.h" #include <avatar/MarketplaceItemUploader.h> -#include <thread> #include <mutex> +#include "scripting/HMDScriptingInterface.h" +#include "ui/TabletScriptingInterface.h" std::once_flag setupQMLTypesFlag; AvatarPackager::AvatarPackager() { @@ -38,31 +41,32 @@ AvatarPackager::AvatarPackager() { } bool AvatarPackager::open() { - static const QUrl url{ "hifi/AvatarPackager.qml" }; + static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { context->setContextProperty("AvatarPackagerCore", this); }; - DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); + + static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; + auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>(); + auto tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); + auto hmd = DependencyManager::get<HMDScriptingInterface>(); + + if (tablet->getToolbarMode()) { + DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); + } else { + static const QUrl url("hifi/tablet/AvatarPackager.qml"); + if (!tablet->isPathLoaded(url)) { + tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this); + tablet->pushOntoStack(url); + } + } return true; } -AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { - if (_currentAvatarProject) { - _currentAvatarProject->deleteLater(); - } - _currentAvatarProject = AvatarProject::openAvatarProject(avatarProjectFSTPath); - if (_currentAvatarProject) { - addRecentProject(avatarProjectFSTPath, _currentAvatarProject->getProjectName()); - } - qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; - QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); - emit avatarProjectChanged(); - return _currentAvatarProject; -} - -void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { +void AvatarPackager::addCurrentProjectToRecentProjects() { const int MAX_RECENT_PROJECTS = 5; + const QString& fstPath = _currentAvatarProject->getFSTPath(); auto removeProjects = QVector<RecentAvatarProject>(); for (auto project : _recentProjects) { if (project.getProjectFSTPath() == fstPath) { @@ -73,27 +77,61 @@ void AvatarPackager::addRecentProject(QString fstPath, QString projectName) { _recentProjects.removeOne(removeProject); } - RecentAvatarProject newRecentProject = RecentAvatarProject(projectName, fstPath); + const auto newRecentProject = RecentAvatarProject(_currentAvatarProject->getProjectName(), fstPath); _recentProjects.prepend(newRecentProject); while (_recentProjects.size() > MAX_RECENT_PROJECTS) { _recentProjects.pop_back(); } - _recentProjectsSetting.set(recentProjectsToVariantList()); + _recentProjectsSetting.set(recentProjectsToVariantList(false)); emit recentProjectsChanged(); } +QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) { + QVariantList result; + for (const auto& project : _recentProjects) { + QVariantMap projectVariant; + projectVariant.insert("name", project.getProjectName()); + projectVariant.insert("path", project.getProjectFSTPath()); + if (includeProjectPaths) { + projectVariant.insert("projectPath", project.getProjectPath()); + } + result.append(projectVariant); + } + + return result; +} +void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) { + _recentProjects.clear(); + for (const auto& projectVariant : projectsVariant) { + auto map = projectVariant.toMap(); + _recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString())); + } +} + +AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { + setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath)); + return _currentAvatarProject; +} + AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { + setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder)); + return _currentAvatarProject; +} + +void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) { + if (avatarProject == _currentAvatarProject) { + return; + } if (_currentAvatarProject) { _currentAvatarProject->deleteLater(); } - _currentAvatarProject = AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder); + _currentAvatarProject = avatarProject; if (_currentAvatarProject) { - addRecentProject(_currentAvatarProject->getFSTPath(), _currentAvatarProject->getProjectName()); + addCurrentProjectToRecentProjects(); + connect(_currentAvatarProject, &AvatarProject::nameChanged, this, &AvatarPackager::addCurrentProjectToRecentProjects); + QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); } - qDebug() << "_currentAvatarProject has" << (QQmlEngine::objectOwnership(_currentAvatarProject) == QQmlEngine::CppOwnership ? "CPP" : "JS") << "OWNERSHIP"; - QQmlEngine::setObjectOwnership(_currentAvatarProject, QQmlEngine::CppOwnership); emit avatarProjectChanged(); - return _currentAvatarProject; } diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 57cbf046a7..343176497f 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -35,12 +35,14 @@ public: _projectFSTPath = other._projectFSTPath; } - ~RecentAvatarProject() = default; - QString getProjectName() const { return _projectName; } QString getProjectFSTPath() const { return _projectFSTPath; } + QString getProjectPath() const { + return QFileInfo(_projectFSTPath).absoluteDir().absolutePath(); + } + bool operator==(const RecentAvatarProject& other) const { return _projectName == other._projectName && _projectFSTPath == other._projectFSTPath; } @@ -72,31 +74,18 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } - Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(); } + Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(true); } - void addRecentProject(QString fstPath, QString projectName); + void setAvatarProject(AvatarProject* avatarProject); + + void addCurrentProjectToRecentProjects(); AvatarProject* _currentAvatarProject{ nullptr }; QVector<RecentAvatarProject> _recentProjects; - QVariantList recentProjectsToVariantList() { - QVariantList result; - for (const auto& project : _recentProjects) { - QVariantMap projectVariant; - projectVariant.insert("name", project.getProjectName()); - projectVariant.insert("path", project.getProjectFSTPath()); - result.append(projectVariant); - } - - return result; - } - void recentProjectsFromVariantList(QVariantList projectsVariant) { - _recentProjects.clear(); - for (const auto& projectVariant : projectsVariant) { - auto map = projectVariant.toMap(); - _recentProjects.append(RecentAvatarProject(map.value("name").toString(), map.value("path").toString())); - } - } + QVariantList recentProjectsToVariantList(bool includeProjectPaths); + + void recentProjectsFromVariantList(QVariantList projectsVariant); Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 09d60163b6..0c29bcf906 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -81,18 +81,18 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, } }; - foreach(const HFMMaterial mat, hfmModel->materials) { - addTextureToList(mat.normalTexture); - addTextureToList(mat.albedoTexture); - addTextureToList(mat.opacityTexture); - addTextureToList(mat.glossTexture); - addTextureToList(mat.roughnessTexture); - addTextureToList(mat.specularTexture); - addTextureToList(mat.metallicTexture); - addTextureToList(mat.emissiveTexture); - addTextureToList(mat.occlusionTexture); - addTextureToList(mat.scatteringTexture); - addTextureToList(mat.lightmapTexture); + foreach(const HFMMaterial material, hfmModel->materials) { + addTextureToList(material.normalTexture); + addTextureToList(material.albedoTexture); + addTextureToList(material.opacityTexture); + addTextureToList(material.glossTexture); + addTextureToList(material.roughnessTexture); + addTextureToList(material.specularTexture); + addTextureToList(material.metallicTexture); + addTextureToList(material.emissiveTexture); + addTextureToList(material.occlusionTexture); + addTextureToList(material.scatteringTexture); + addTextureToList(material.lightmapTexture); } QDir textureDir(textureFolder.isEmpty() ? fbxInfo.absoluteDir() : textureFolder); @@ -152,7 +152,6 @@ AvatarProject::AvatarProject(FST* fst) { _fst->setScriptPaths(getScriptPaths(QDir(_directory.path() + "/scripts"))); _fst->write(); - //_projectFiles = _directory.entryList(); refreshProjectFiles(); _projectPath = fileInfo.absoluteDir().absolutePath(); diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index e950fd7379..469324004d 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -28,10 +28,10 @@ class AvatarProject : public QObject { Q_PROPERTY(QStringList projectFiles READ getProjectFiles NOTIFY projectFilesChanged) - Q_PROPERTY(QString projectFolderPath READ getProjectPath) - Q_PROPERTY(QString projectFSTPath READ getFSTPath) - Q_PROPERTY(QString projectFBXPath READ getFBXPath) - Q_PROPERTY(QString name READ getProjectName NOTIFY nameChanged) + Q_PROPERTY(QString projectFolderPath READ getProjectPath CONSTANT) + Q_PROPERTY(QString projectFSTPath READ getFSTPath CONSTANT) + Q_PROPERTY(QString projectFBXPath READ getFBXPath CONSTANT) + Q_PROPERTY(QString name READ getProjectName WRITE setProjectName NOTIFY nameChanged) public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); @@ -39,6 +39,13 @@ public: Q_INVOKABLE QStringList getProjectFiles() const; Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } + Q_INVOKABLE void setProjectName(const QString& newProjectName) { + if (newProjectName.trimmed().length() > 0) { + _fst->setName(newProjectName); + _fst->write(); + emit nameChanged(); + } + } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 5d3737319f..9510ca5962 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -15,7 +15,7 @@ #include <QFileInfo> #include <hfm/HFM.h> -FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { +FST::FST(QString fstPath, QVariantHash data) : _fstPath(std::move(fstPath)) { auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable { if (data.contains(propertyID)) { @@ -38,7 +38,7 @@ FST::FST(const QString& fstPath, QVariantHash data) : _fstPath(fstPath) { _other = data; } -FST* FST::createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel) { +FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel) { QVariantHash mapping; // mixamo files - in the event that a mixamo file was edited by some other tool, it's likely the applicationName will diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 6fd654987e..6104130512 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -26,9 +26,9 @@ class FST : public QObject { Q_PROPERTY(QUuid marketplaceID READ getMarketplaceID) Q_PROPERTY(bool hasMarketplaceID READ getHasMarketplaceID NOTIFY marketplaceIDChanged) public: - FST(const QString& fstPath, QVariantHash data); + FST(QString fstPath, QVariantHash data); - static FST* createFSTFromModel(QString fstPath, QString modelFilePath, const hfm::Model& hfmModel); + static FST* createFSTFromModel(const QString& fstPath, const QString& modelFilePath, const hfm::Model& hfmModel); QString absoluteModelPath() const; From d29233872e147c47983c737b996b781caa425568 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 14:59:47 -0800 Subject: [PATCH 26/43] Remove avatar packager from Android builds --- interface/src/Menu.cpp | 2 ++ interface/src/avatar/MarketplaceItemUploader.cpp | 14 +++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 8b43753cad..87b1542648 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -150,10 +150,12 @@ Menu::Menu() { qApp, SLOT(packageModel())); // Edit > Avatar Packager +#ifndef Q_OS_ANDROID action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager); connect(action, &QAction::triggered, [] { DependencyManager::get<AvatarPackager>()->open(); }); +#endif // Edit > Reload All Content addActionToQMenuAndActionHash(editMenu, MenuOption::ReloadContent, 0, qApp, SLOT(reloadResourceCaches())); diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 45a13f04c4..00c8299c62 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -14,11 +14,14 @@ #include <AccountManager.h> #include <DependencyManager.h> -#include <QBuffer> +#ifndef Q_OS_ANDROID #include <quazip5/quazip.h> #include <quazip5/quazipfile.h> +#endif + +#include <QTimer> +#include <QBuffer> -#include <qtimer.h> #include <QFile> #include <QFileInfo> @@ -125,8 +128,12 @@ void MarketplaceItemUploader::doGetCategories() { } void MarketplaceItemUploader::doUploadAvatar() { +#ifdef Q_OS_ANDROID + qWarning() << "Marketplace uploading is not supported on Android"; + setError(Error::Unknown); + return; +#else QBuffer buffer{ &_fileData }; - //buffer.open(QIODevice::WriteOnly); QuaZip zip{ &buffer }; if (!zip.open(QuaZip::Mode::mdAdd)) { qWarning() << "Failed to open zip"; @@ -227,6 +234,7 @@ void MarketplaceItemUploader::doUploadAvatar() { }); setState(State::UploadingAvatar); +#endif } void MarketplaceItemUploader::doWaitForInventory() { From 41effbf8624a1d7ea0d13b18ee0e638b443aa34e Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 15:01:00 -0800 Subject: [PATCH 27/43] Remove unused variable in MarketplaceItemUploader.cpp --- interface/src/avatar/MarketplaceItemUploader.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 00c8299c62..8b97358ba4 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -285,7 +285,6 @@ void MarketplaceItemUploader::doWaitForInventory() { auto version = assetObject["version"]; auto valid = assetObject["valid"]; if (version.isDouble() && valid.isBool()) { - int versionInt = version.toDouble(); if ((int)version.toDouble() >= _itemVersion && valid.toBool()) { return true; } From 61ac9d005045c560b324c1c954d1fee711270c37 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Wed, 2 Jan 2019 16:47:09 -0800 Subject: [PATCH 28/43] Update various styling in Avatar Packager --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 59 ++++++++++++++----- .../avatarPackager/AvatarPackagerHeader.qml | 35 ++++------- .../hifi/avatarPackager/AvatarProjectCard.qml | 13 +++- 3 files changed, 69 insertions(+), 38 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 2b21b4d938..0a678dafee 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -146,6 +146,8 @@ Item { } AvatarPackagerHeader { + z: 100 + id: avatarPackagerHeader colorScheme: root.colorScheme onBackButtonClicked: { @@ -260,22 +262,49 @@ Item { } } - Column { - visible: AvatarPackagerCore.recentProjects.length > 0 - anchors { - fill: parent - topMargin: 18 - leftMargin: 16 - rightMargin: 16 - } - spacing: 10 + Item { + anchors.fill: parent - Repeater { - model: AvatarPackagerCore.recentProjects - AvatarProjectCard { - title: modelData.name - path: modelData.projectPath - onOpen: avatarPackager.openProject(modelData.path) + visible: AvatarPackagerCore.recentProjects.length > 0 + + RalewayRegular { + id: recentProjectsText + + color: 'white' + + visible: AvatarPackagerCore.currentAvatarProject !== null + + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: 16 + anchors.leftMargin: 16 + + size: 20 + + text: "Recent Projects" + + onLinkActivated: fileListPopup.open() + } + + Column { + anchors { + left: parent.left + right: parent.right + bottom: parent.bottom + top: recentProjectsText.bottom + topMargin: 16 + leftMargin: 16 + rightMargin: 16 + } + spacing: 10 + + Repeater { + model: AvatarPackagerCore.recentProjects + AvatarProjectCard { + title: modelData.name + path: modelData.projectPath + onOpen: avatarPackager.openProject(modelData.path) + } } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 7037aa9d92..d0b06ea15f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -2,8 +2,9 @@ import QtQuick 2.6 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 +import "../avatarapp" 1.0 -Rectangle { +ShadowRectangle { id: root width: parent.width @@ -24,30 +25,21 @@ Rectangle { signal backButtonClicked signal docsButtonClicked - RalewaySemiBold { + RalewayButton { id: back + visible: backButtonEnabled && backButtonVisible + size: 28 anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 16 anchors.verticalCenter: back.verticalCenter + text: "◀" - color: textColor - MouseArea { - anchors.fill: parent - onClicked: root.backButtonClicked() - hoverEnabled: true - onEntered: { state = "hovering" } - onExited: { state = "" } - states: [ - State { - name: "hovering" - PropertyChanges { target: back; color: hoverTextColor } - } - ] - } + + onClicked: root.backButtonClicked() } Item { id: titleArea @@ -124,7 +116,7 @@ Rectangle { } } - RalewaySemiBold { + RalewayButton { id: docs visible: false size: 28 @@ -133,13 +125,12 @@ Rectangle { anchors.right: parent.right anchors.rightMargin: 16 anchors.verticalCenter: docs.verticalCenter + text: qsTr("Docs") color: "white" - MouseArea { - anchors.fill: parent - onClicked: { - docsButtonClicked(); - } + + onClicked: { + docsButtonClicked(); } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index 5496711d44..a758d3936a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -1,4 +1,5 @@ import QtQuick 2.0 +import QtGraphicalEffects 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 @@ -63,7 +64,7 @@ Item { rightMargin: 16 } text: "<title missing>" - size: 16 + size: 24 } RalewayRegular { @@ -88,4 +89,14 @@ Item { onClicked: open() } } + + DropShadow { + id: shadow + anchors.fill: background + radius: 4 + horizontalOffset: 0 + verticalOffset: 4 + color: Qt.rgba(0, 0, 0, 0.25) + source: background + } } From 9c96f7bd01107a13c879bd5b88d6357668f3adfe Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 08:21:52 -0800 Subject: [PATCH 29/43] Add RalewayButton --- .../qml/hifi/avatarPackager/RalewayButton.qml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 interface/resources/qml/hifi/avatarPackager/RalewayButton.qml diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml new file mode 100644 index 0000000000..e7134b6934 --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -0,0 +1,68 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +import TabletScriptingInterface 1.0 + +RalewaySemiBold { + id: root + + text: "no text" + + signal clicked() + + color: "white" + + property var hoverColor: "#AFAFAF" + property var pressedColor: "#575757" + + MouseArea { + id: mouseArea + + anchors.fill: parent + + hoverEnabled: true + + onClicked: { + Tablet.playSound(TabletEnums.ButtonClick); + root.clicked() + } + + property string lastState: "" + + states: [ + State { + name: "" + StateChangeScript { + script: { + mouseArea.lastState = mouseArea.state + } + } + }, + State { + name: "pressed" + when: mouseArea.containsMouse && mouseArea.pressed + PropertyChanges { target: root; color: pressedColor } + StateChangeScript { + script: { + mouseArea.lastState = mouseArea.state + } + } + }, + State { + name: "hovering" + when: mouseArea.containsMouse + PropertyChanges { target: root; color: hoverColor } + StateChangeScript { + script: { + if (mouseArea.lastState == "") { + Tablet.playSound(TabletEnums.ButtonHover); + } + mouseArea.lastState = mouseArea.state + } + } + } + ] + } +} \ No newline at end of file From c13badcbc8f32eec08fb5456187c44e58a215933 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 10:20:13 -0800 Subject: [PATCH 30/43] Fix project rename focus and create project not initializing --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 3 +- .../avatarPackager/AvatarPackagerHeader.qml | 8 +-- .../qml/hifi/avatarPackager/ClickableArea.qml | 63 +++++++++++++++++++ .../avatarPackager/CreateAvatarProject.qml | 1 + .../qml/hifi/avatarPackager/InfoBox.qml | 6 ++ .../qml/hifi/avatarPackager/RalewayButton.qml | 56 +++-------------- 6 files changed, 80 insertions(+), 57 deletions(-) create mode 100644 interface/resources/qml/hifi/avatarPackager/ClickableArea.qml diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 0a678dafee..6e2e352b51 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -22,6 +22,7 @@ Item { MouseArea { anchors.fill: parent + onClicked: { unfocusser.forceActiveFocus(); } @@ -272,8 +273,6 @@ Item { color: 'white' - visible: AvatarPackagerCore.currentAvatarProject !== null - anchors.top: parent.top anchors.left: parent.left anchors.topMargin: 16 diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index d0b06ea15f..b40e9cc264 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -92,8 +92,7 @@ ShadowRectangle { font.pixelSize: 28 z: 200 onFocusChanged: { - if (titleArea.state === "renaming" && !titleArea.focus) { - //titleArea.state = ""; + if (titleArea.state === "renaming" && !focus) { accepted(); } } @@ -104,11 +103,7 @@ ShadowRectangle { } onAccepted: { if (acceptableInput) { - //AvatarPackagerCore.renameProject(text); - console.warn(text); AvatarPackagerCore.currentAvatarProject.name = text; - console.warn(AvatarPackagerCore.currentAvatarProject.name); - } titleArea.state = ""; } @@ -127,7 +122,6 @@ ShadowRectangle { anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") - color: "white" onClicked: { docsButtonClicked(); diff --git a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml new file mode 100644 index 0000000000..f49c98bc6a --- /dev/null +++ b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml @@ -0,0 +1,63 @@ +import QtQuick 2.6 + +import "../../controlsUit" 1.0 as HifiControls +import "../../stylesUit" 1.0 + +import TabletScriptingInterface 1.0 + +Item { + id: root + + readonly property bool pressed: mouseArea.state == "pressed" + readonly property bool hovered: mouseArea.state == "hovering" + + signal clicked() + + MouseArea { + id: mouseArea + + anchors.fill: parent + + hoverEnabled: true + + onClicked: { + root.focus = true + Tablet.playSound(TabletEnums.ButtonClick); + root.clicked() + } + + property string lastState: "" + + states: [ + State { + name: "" + StateChangeScript { + script: { + mouseArea.lastState = mouseArea.state + } + } + }, + State { + name: "pressed" + when: mouseArea.containsMouse && mouseArea.pressed + StateChangeScript { + script: { + mouseArea.lastState = mouseArea.state + } + } + }, + State { + name: "hovering" + when: mouseArea.containsMouse + StateChangeScript { + script: { + if (mouseArea.lastState == "") { + Tablet.playSound(TabletEnums.ButtonHover); + } + mouseArea.lastState = mouseArea.state + } + } + } + ] + } +} \ No newline at end of file diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index d6f530a196..b72cbd42df 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -28,6 +28,7 @@ Item { Window.alert('Failed to create project'); return; } + avatarProject.reset(); avatarPackager.state = AvatarPackagerState.project; } } diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml index 89f5d5c7f8..4c7718c2ac 100644 --- a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -17,6 +17,12 @@ Rectangle { property bool closeOnClickOutside: false; + onVisibleChanged: { + if (visible) { + focus = true; + } + } + function open() { visible = true; } diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index e7134b6934..edbc31b24f 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -8,61 +8,21 @@ import TabletScriptingInterface 1.0 RalewaySemiBold { id: root - text: "no text" - - signal clicked() - - color: "white" + anchors.fill: textItem + property var idleColor: "white" property var hoverColor: "#AFAFAF" property var pressedColor: "#575757" - MouseArea { - id: mouseArea + color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor); - anchors.fill: parent + signal clicked() - hoverEnabled: true + ClickableArea { + id: clickable - onClicked: { - Tablet.playSound(TabletEnums.ButtonClick); - root.clicked() - } + anchors.fill: root - property string lastState: "" - - states: [ - State { - name: "" - StateChangeScript { - script: { - mouseArea.lastState = mouseArea.state - } - } - }, - State { - name: "pressed" - when: mouseArea.containsMouse && mouseArea.pressed - PropertyChanges { target: root; color: pressedColor } - StateChangeScript { - script: { - mouseArea.lastState = mouseArea.state - } - } - }, - State { - name: "hovering" - when: mouseArea.containsMouse - PropertyChanges { target: root; color: hoverColor } - StateChangeScript { - script: { - if (mouseArea.lastState == "") { - Tablet.playSound(TabletEnums.ButtonHover); - } - mouseArea.lastState = mouseArea.state - } - } - } - ] + onClicked: root.clicked() } } \ No newline at end of file From 7953507cb8dbb1b06abfd471557124dcfe3cd7ad Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 11:59:16 -0800 Subject: [PATCH 31/43] Update avatar project rename to have audio and highlighting --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 7 ++--- .../avatarPackager/AvatarPackagerHeader.qml | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 6e2e352b51..ee78f4f934 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -11,15 +11,12 @@ import "../../dialogs" import "../avatarapp" 1.0 as AvatarApp Item { + id: windowContent + HifiConstants { id: hifi } property alias desktopObject: avatarPackager.desktopObject - id: windowContent - // height: pane ? pane.height : parent.width - // width: pane ? pane.width : parent.width - - MouseArea { anchors.fill: parent diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index b40e9cc264..845fdeb99f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -11,7 +11,7 @@ ShadowRectangle { height: 74 color: "#252525" - property alias title: title.text + property string title: qsTr("Avatar Packager") property alias docsEnabled: docs.visible property bool backButtonVisible: true // If false, is not visible and does not take up space property bool backButtonEnabled: true // If false, is not visible but does not affect space @@ -43,6 +43,7 @@ ShadowRectangle { } Item { id: titleArea + anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: root.backButtonVisible ? back.right : parent.left @@ -57,15 +58,31 @@ ShadowRectangle { } ] - RalewaySemiBold { + Item { id: title - size: 28 anchors.fill: parent - text: qsTr("Avatar Packager") - color: "white" - MouseArea { + RalewaySemiBold { + id: titleNotRenameable + + visible: !root.canRename + + size: 28 anchors.fill: parent + text: root.title + color: "white" + } + + RalewayButton { + id: titleRenameable + + visible: root.canRename + enabled: root.canRename + + size: 28 + anchors.fill: parent + text: root.title + onClicked: { if (!root.canRename || AvatarPackagerCore.currentAvatarProject === null) { return; From ecc578c2dd4979a97cc4505a9e407178e4971693 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Thu, 3 Jan 2019 22:00:22 +0100 Subject: [PATCH 32/43] - error messages - style changes --- .../qml/hifi/AvatarPackagerWindow.qml | 8 -- .../hifi/avatarPackager/AvatarPackagerApp.qml | 114 +++++++++++++++--- .../avatarPackager/AvatarPackagerFooter.qml | 3 +- .../avatarPackager/AvatarPackagerHeader.qml | 5 - .../qml/hifi/avatarPackager/AvatarProject.qml | 6 +- .../avatarPackager/AvatarProjectUpload.qml | 5 +- .../avatarPackager/CreateAvatarProject.qml | 7 +- .../qml/hifi/avatarPackager/InfoBox.qml | 18 +-- interface/src/avatar/AvatarPackager.cpp | 54 +++++---- interface/src/avatar/AvatarPackager.h | 8 +- interface/src/avatar/AvatarProject.cpp | 65 ++++++++-- interface/src/avatar/AvatarProject.h | 32 ++++- 12 files changed, 242 insertions(+), 83 deletions(-) diff --git a/interface/resources/qml/hifi/AvatarPackagerWindow.qml b/interface/resources/qml/hifi/AvatarPackagerWindow.qml index 9d434ef97c..82bcd3fa40 100644 --- a/interface/resources/qml/hifi/AvatarPackagerWindow.qml +++ b/interface/resources/qml/hifi/AvatarPackagerWindow.qml @@ -1,15 +1,7 @@ import QtQuick 2.6 -import QtQuick.Controls 2.2 -import QtQuick.Layouts 1.3 -import QtQml.Models 2.1 -import QtGraphicalEffects 1.0 -import "../controlsUit" 1.0 as HifiControls import "../stylesUit" 1.0 import "../windows" as Windows -import "../controls" 1.0 -import "../dialogs" import "avatarPackager" 1.0 -import "avatarapp" 1.0 as AvatarApp Windows.ScrollingWindow { id: root diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 0a678dafee..fb7987ca76 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -3,9 +3,9 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import QtQml.Models 2.1 import QtGraphicalEffects 1.0 +import Hifi.AvatarPackager.AvatarProjectStatus 1.0 import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 -import "../../windows" as Windows import "../../controls" 1.0 import "../../dialogs" import "../avatarapp" 1.0 as AvatarApp @@ -16,9 +16,6 @@ Item { property alias desktopObject: avatarPackager.desktopObject id: windowContent - // height: pane ? pane.height : parent.width - // width: pane ? pane.width : parent.width - MouseArea { anchors.fill: parent @@ -75,6 +72,37 @@ Item { } } + InfoBox { + id: errorPopup + + property string errorMessage; + + boxWidth: 380 + boxHeight: 293 + + content: RalewayRegular { + + id: bodyMessage + + anchors.fill: parent + anchors.bottomMargin: 10 + anchors.leftMargin: 29 + anchors.rightMargin: 29 + + size: 20 + color: "white" + text: errorPopup.errorMessage + width: parent.width + wrapMode: Text.WordWrap + } + + function show(title, message) { + errorPopup.title = title; + errorMessage = message; + errorPopup.open(); + } + } + Rectangle { id: modalOverlay anchors.fill: parent @@ -85,8 +113,8 @@ Item { // This mouse area captures the cursor events while the modalOverlay is active MouseArea { anchors.fill: parent - propagateComposedEvents: false; - hoverEnabled: true; + propagateComposedEvents: false + hoverEnabled: true } } @@ -133,12 +161,58 @@ Item { property var desktopObject: desktop function openProject(path) { - let project = AvatarPackagerCore.openAvatarProject(path); - if (project) { - avatarProject.reset(); - avatarPackager.state = AvatarPackagerState.project; + let status = AvatarPackagerCore.openAvatarProject(path); + if (status !== AvatarProjectStatus.SUCCESS) { + displayErrorMessage(status); + return status; } - return project; + avatarProject.reset(); + avatarPackager.state = AvatarPackagerState.project; + return status; + } + + function displayErrorMessage(status) { + if (status === AvatarProjectStatus.SUCCESS) { + return; + } + switch (status) { + case AvatarProjectStatus.ERROR_CREATE_PROJECT_NAME: + errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location."); + break; + case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES: + errorPopup.show("Project Folders Creation Error", "There was a problem during the creation of the Avatar Project directories. Please select a project location with write permissions."); + break; + case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL: + errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify if it exist at the specified location."); + break; + case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL: + errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file. Please verify if you have read permissions at the specified location."); + break; + case AvatarProjectStatus.ERROR_CREATE_READ_MODEL: + errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please verify if the model file is supported by High Fidelity."); + break; + case AvatarProjectStatus.ERROR_CREATE_WRITE_FST: + errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file."); + break; + case AvatarProjectStatus.ERROR_OPEN_INVALID_FILE_TYPE: + errorPopup.show("Invalid Project Path", "The avatar packager can only open FST files."); + break; + case AvatarProjectStatus.ERROR_OPEN_PROJECT_FOLDER: + errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location."); + break; + case AvatarProjectStatus.ERROR_OPEN_FIND_FST: + errorPopup.show("File Missing", "We cannot find the project file (avatar.fst) in the folder. Please locate it and move to the project folder."); + break; + case AvatarProjectStatus.ERROR_OPEN_OPEN_FST: + errorPopup.show("File Read Error", "We cannot read the project file (avatar.fst). Please make sure that it is not in use by another program."); + break; + case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL: + errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the folder. Please locate it and move to the project folder."); + break; + default: + errorPopup.show("Error Message Missing", "Error message missing for status " + status); + } + } function openDocs() { @@ -233,10 +307,8 @@ Item { browser.selectedFile.connect(function(fileUrl) { let fstFilePath = fileDialogHelper.urlToPath(fileUrl); - let currentAvatarProject = avatarPackager.openProject(fstFilePath); - if (currentAvatarProject) { - avatarPackager.showModalOverlay = false; - } + avatarPackager.showModalOverlay = false; + avatarPackager.openProject(fstFilePath); }); } } @@ -253,12 +325,20 @@ Item { RalewayRegular { size: 20 color: "white" - text: qsTr("Use a custom avatar to express your identity") + text: "Use a custom avatar of your choice." + width: parent.width + wrapMode: Text.WordWrap } RalewayRegular { size: 20 color: "white" - text: qsTr("To learn more about using this tool, visit our docs") + text: "<a href='javascript:void'>Visit our docs</a> to learn more about using the packager." + linkColor: "#00B4EF" + width: parent.width + wrapMode: Text.WordWrap + onLinkActivated: { + avatarPackager.openDocs(); + } } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml index e1d9396e04..31e05672d2 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerFooter.qml @@ -38,5 +38,4 @@ Rectangle { width: parent.width } } - -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index d0b06ea15f..cd6fdb72fe 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -93,7 +93,6 @@ ShadowRectangle { z: 200 onFocusChanged: { if (titleArea.state === "renaming" && !titleArea.focus) { - //titleArea.state = ""; accepted(); } } @@ -104,11 +103,7 @@ ShadowRectangle { } onAccepted: { if (acceptableInput) { - //AvatarPackagerCore.renameProject(text); - console.warn(text); AvatarPackagerCore.currentAvatarProject.name = text; - console.warn(AvatarPackagerCore.currentAvatarProject.name); - } titleArea.state = ""; } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a5a2263346..8f7a4be481 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -52,7 +52,7 @@ Item { colorScheme: root.colorScheme width: 133 height: 40 - onClicked: function() { + onClicked: { uploadNew(); } } @@ -70,7 +70,7 @@ Item { colorScheme: root.colorScheme width: 134 height: 40 - onClicked: function() { + onClicked: { showConfirmUploadPopup(uploadNew, uploadUpdate); } } @@ -90,7 +90,7 @@ Item { colorScheme: root.colorScheme width: 134 height: 40 - onClicked: function() { + onClicked: { showConfirmUploadPopup(uploadNew, uploadUpdate); } } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml index c1d1a98158..68f465f514 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectUpload.qml @@ -29,10 +29,11 @@ Item { function stateChangedCallback(newState) { if (newState >= 4) { - root.uploader.stateChanged.disconnect(stateChangedCallback) + root.uploader.stateChanged.disconnect(stateChangedCallback); backToProjectTimer.start(); } } + onVisibleChanged: { if (visible) { root.uploader.stateChanged.connect(stateChangedCallback); @@ -120,6 +121,7 @@ Item { source: "../../../icons/checkmark-stroke.svg" } } + Item { id: statusRows @@ -169,6 +171,7 @@ Item { color: "white" text: "We couldn't upload your avatar at this time. Please try again later." } + AvatarPackagerFooter { id: errorFooter diff --git a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml index d6f530a196..1e9a3d9e84 100644 --- a/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/CreateAvatarProject.qml @@ -2,6 +2,8 @@ import QtQuick 2.6 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 +import Hifi.AvatarPackager.AvatarProjectStatus 1.0 + import "../../controlsUit" 1.0 as HifiControls import "../../stylesUit" 1.0 @@ -24,8 +26,9 @@ Item { text: qsTr("Create") enabled: false onClicked: { - if (!AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text)) { - Window.alert('Failed to create project'); + let status = AvatarPackagerCore.createAvatarProject(projectLocation.text, name.text, avatarModel.text, textureFolder.text); + if (status !== AvatarProjectStatus.SUCCESS) { + avatarPackager.displayErrorMessage(status); return; } avatarPackager.state = AvatarPackagerState.project; diff --git a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml index 89f5d5c7f8..301386acfa 100644 --- a/interface/resources/qml/hifi/avatarPackager/InfoBox.qml +++ b/interface/resources/qml/hifi/avatarPackager/InfoBox.qml @@ -5,9 +5,9 @@ import controlsUit 1.0 as HifiControlsUit import "../../controls" as HifiControls Rectangle { - id: root; - visible: false; - color: Qt.rgba(.34, .34, .34, 0.6); + id: root + visible: false + color: Qt.rgba(.34, .34, .34, 0.6) z: 999; anchors.fill: parent @@ -17,6 +17,9 @@ Rectangle { property bool closeOnClickOutside: false; + property alias boxWidth: mainContainer.width + property alias boxHeight: mainContainer.height + function open() { visible = true; } @@ -44,15 +47,15 @@ Rectangle { } Rectangle { - id: mainContainer; + id: mainContainer width: Math.max(parent.width * 0.8, 400) height: parent.height * 0.6 MouseArea { - anchors.fill: parent; - propagateComposedEvents: false; - hoverEnabled: true; + anchors.fill: parent + propagateComposedEvents: false + hoverEnabled: true onClicked: function(ev) { ev.accepted = true; } @@ -107,6 +110,5 @@ Rectangle { colorScheme: hifi.colorSchemes.dark; } } - } } diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index eef574a8b5..941aff6943 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -22,7 +22,6 @@ #include <avatar/MarketplaceItemUploader.h> #include <mutex> -#include "scripting/HMDScriptingInterface.h" #include "ui/TabletScriptingInterface.h" std::once_flag setupQMLTypesFlag; @@ -32,6 +31,14 @@ AvatarPackager::AvatarPackager() { qmlRegisterType<MarketplaceItemUploader>(); qRegisterMetaType<AvatarPackager*>(); qRegisterMetaType<AvatarProject*>(); + qRegisterMetaType<AvatarProjectStatus::AvatarProjectStatus>(); + qmlRegisterUncreatableMetaObject( + AvatarProjectStatus::staticMetaObject, + "Hifi.AvatarPackager.AvatarProjectStatus", + 1, 0, + "AvatarProjectStatus", + "Error: only enums" + ); }); recentProjectsFromVariantList(_recentProjectsSetting.get()); @@ -41,39 +48,39 @@ AvatarPackager::AvatarPackager() { } bool AvatarPackager::open() { - static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; - const auto packageModelDialogCreated = [=](QQmlContext* context, QObject* newObject) { context->setContextProperty("AvatarPackagerCore", this); }; static const QString SYSTEM_TABLET = "com.highfidelity.interface.tablet.system"; - auto tabletScriptingInterface = DependencyManager::get<TabletScriptingInterface>(); - auto tablet = dynamic_cast<TabletProxy*>(tabletScriptingInterface->getTablet(SYSTEM_TABLET)); - auto hmd = DependencyManager::get<HMDScriptingInterface>(); + auto tablet = dynamic_cast<TabletProxy*>(DependencyManager::get<TabletScriptingInterface>()->getTablet(SYSTEM_TABLET)); if (tablet->getToolbarMode()) { + static const QUrl url{ "hifi/AvatarPackagerWindow.qml" }; DependencyManager::get<OffscreenUi>()->show(url, "AvatarPackager", packageModelDialogCreated); - } else { - static const QUrl url("hifi/tablet/AvatarPackager.qml"); - if (!tablet->isPathLoaded(url)) { - tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this); - tablet->pushOntoStack(url); - } + return true; } - return true; + + static const QUrl url{ "hifi/tablet/AvatarPackager.qml" }; + if (!tablet->isPathLoaded(url)) { + tablet->getTabletSurface()->getSurfaceContext()->setContextProperty("AvatarPackagerCore", this); + tablet->pushOntoStack(url); + return true; + } + + return false; } void AvatarPackager::addCurrentProjectToRecentProjects() { const int MAX_RECENT_PROJECTS = 5; const QString& fstPath = _currentAvatarProject->getFSTPath(); auto removeProjects = QVector<RecentAvatarProject>(); - for (auto project : _recentProjects) { + for (const auto& project : _recentProjects) { if (project.getProjectFSTPath() == fstPath) { removeProjects.append(project); } } - for (const auto removeProject : removeProjects) { + for (const auto& removeProject : removeProjects) { _recentProjects.removeOne(removeProject); } @@ -110,14 +117,19 @@ void AvatarPackager::recentProjectsFromVariantList(QVariantList projectsVariant) } } -AvatarProject* AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { - setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath)); - return _currentAvatarProject; +AvatarProjectStatus::AvatarProjectStatus AvatarPackager::openAvatarProject(const QString& avatarProjectFSTPath) { + AvatarProjectStatus::AvatarProjectStatus status; + setAvatarProject(AvatarProject::openAvatarProject(avatarProjectFSTPath, status)); + return status; } -AvatarProject* AvatarPackager::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { - setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder)); - return _currentAvatarProject; +AvatarProjectStatus::AvatarProjectStatus AvatarPackager::createAvatarProject(const QString& projectsFolder, + const QString& avatarProjectName, + const QString& avatarModelPath, + const QString& textureFolder) { + AvatarProjectStatus::AvatarProjectStatus status; + setAvatarProject(AvatarProject::createAvatarProject(projectsFolder, avatarProjectName, avatarModelPath, textureFolder, status)); + return status; } void AvatarPackager::setAvatarProject(AvatarProject* avatarProject) { diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 343176497f..c9c5b9d312 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -63,8 +63,12 @@ public: AvatarPackager(); bool open(); - Q_INVOKABLE AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder); - Q_INVOKABLE AvatarProject* openAvatarProject(const QString& avatarProjectFSTPath); + Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus createAvatarProject(const QString& projectsFolder, + const QString& avatarProjectName, + const QString& avatarModelPath, + const QString& textureFolder); + + Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath); Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { return AvatarProject::isValidNewProjectName(projectPath, projectName); } signals: diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 0c29bcf906..e049dc0ba3 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -22,55 +22,98 @@ #include <ui/TabletScriptingInterface.h> #include "scripting/HMDScriptingInterface.h" -AvatarProject* AvatarProject::openAvatarProject(const QString& path) { +AvatarProject* AvatarProject::openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status) { + status = AvatarProjectStatus::NONE; + if (!path.toLower().endsWith(".fst")) { + status = AvatarProjectStatus::ERROR_OPEN_INVALID_FILE_TYPE; return nullptr; } - QFile file{ path }; + + QFileInfo fstFileInfo{ path }; + if (!fstFileInfo.absoluteDir().exists()) { + status = AvatarProjectStatus::ERROR_OPEN_PROJECT_FOLDER; + return nullptr; + } + + if (!fstFileInfo.exists()) { + status = AvatarProjectStatus::ERROR_OPEN_FIND_FST; + return nullptr; + } + + QFile file{ fstFileInfo.filePath() }; if (!file.open(QIODevice::ReadOnly)) { + status = AvatarProjectStatus::ERROR_OPEN_OPEN_FST; return nullptr; } + const auto project = new AvatarProject(path, file.readAll()); + + QFileInfo fbxFileInfo{ project->getFBXPath() }; + if (!fbxFileInfo.exists()) { + project->deleteLater(); + status = AvatarProjectStatus::ERROR_OPEN_FIND_MODEL; + return nullptr; + } + QQmlEngine::setObjectOwnership(project, QQmlEngine::CppOwnership); + status = AvatarProjectStatus::SUCCESS; return project; } -AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, const QString& textureFolder) { +AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, + const QString& avatarModelPath, const QString& textureFolder, + AvatarProjectStatus::AvatarProjectStatus& status) { + status = AvatarProjectStatus::NONE; + if (!isValidNewProjectName(projectsFolder, avatarProjectName)) { + status = AvatarProjectStatus::ERROR_CREATE_PROJECT_NAME; return nullptr; } + QDir projectDir(projectsFolder + "/" + avatarProjectName); if (!projectDir.mkpath(".")) { + status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } + QDir projectTexturesDir(projectDir.path() + "/textures"); if (!projectTexturesDir.mkpath(".")) { + status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } + QDir projectScriptsDir(projectDir.path() + "/scripts"); if (!projectScriptsDir.mkpath(".")) { + status = AvatarProjectStatus::ERROR_CREATE_CREATING_DIRECTORIES; return nullptr; } + const auto fileName = QFileInfo(avatarModelPath).fileName(); const auto newModelPath = projectDir.absoluteFilePath(fileName); const auto newFSTPath = projectDir.absoluteFilePath("avatar.fst"); QFile::copy(avatarModelPath, newModelPath); - QFileInfo fbxInfo(newModelPath); - QFile fbx(fbxInfo.filePath()); - if (!fbxInfo.exists() || !fbxInfo.isFile() || !fbx.open(QIODevice::ReadOnly)) { - // TODO: Can't open model FBX (throw error here) + QFileInfo fbxInfo{ newModelPath }; + if (!fbxInfo.exists() || !fbxInfo.isFile()) { + status = AvatarProjectStatus::ERROR_CREATE_FIND_MODEL; + return nullptr; + } + + QFile fbx{ fbxInfo.filePath() }; + if (!fbx.open(QIODevice::ReadOnly)) { + status = AvatarProjectStatus::ERROR_CREATE_OPEN_MODEL; return nullptr; } std::shared_ptr<hfm::Model> hfmModel; try { - qDebug() << "Reading FBX file : " << fbxInfo.filePath(); const QByteArray fbxContents = fbx.readAll(); hfmModel = FBXSerializer().read(fbxContents, QVariantHash(), fbxInfo.filePath()); } catch (const QString& error) { - qDebug() << "Error reading: " << error; + Q_UNUSED(error) + status = AvatarProjectStatus::ERROR_CREATE_READ_MODEL; return nullptr; } QStringList textures{}; @@ -111,9 +154,11 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, fst->setName(avatarProjectName); if (!fst->write()) { + status = AvatarProjectStatus::ERROR_CREATE_WRITE_FST; return nullptr; } + status = AvatarProjectStatus::SUCCESS; return new AvatarProject(fst); } @@ -157,7 +202,7 @@ AvatarProject::AvatarProject(FST* fst) { _projectPath = fileInfo.absoluteDir().absolutePath(); } -void AvatarProject::appendDirectory(QString prefix, QDir dir) { +void AvatarProject::appendDirectory(const QString& prefix, const QDir& dir) { constexpr auto flags = QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; for (auto& entry : dir.entryInfoList({}, flags)) { if (entry.isFile()) { diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 469324004d..c422c14d62 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -22,6 +22,27 @@ #include <QVariantHash> #include <QStandardPaths> +namespace AvatarProjectStatus { + Q_NAMESPACE + enum AvatarProjectStatus { + NONE, + SUCCESS, + ERROR_CREATE_PROJECT_NAME, + ERROR_CREATE_CREATING_DIRECTORIES, + ERROR_CREATE_FIND_MODEL, + ERROR_CREATE_OPEN_MODEL, + ERROR_CREATE_READ_MODEL, + ERROR_CREATE_WRITE_FST, + ERROR_OPEN_INVALID_FILE_TYPE, + ERROR_OPEN_PROJECT_FOLDER, + ERROR_OPEN_FIND_FST, + ERROR_OPEN_OPEN_FST, + ERROR_OPEN_FIND_MODEL + }; + Q_ENUM_NS(AvatarProjectStatus) +} + + class AvatarProject : public QObject { Q_OBJECT Q_PROPERTY(FST* fst READ getFST CONSTANT) @@ -48,16 +69,19 @@ public: } Q_INVOKABLE QString getProjectPath() const { return _projectPath; } Q_INVOKABLE QString getFSTPath() const { return _fst->getPath(); } - Q_INVOKABLE QString getFBXPath() const { return _fst->getModelPath(); } + Q_INVOKABLE QString getFBXPath() const { + return QDir::cleanPath(QDir(_projectPath).absoluteFilePath(_fst->getModelPath())); + } /** * returns the AvatarProject or a nullptr on failure. */ - static AvatarProject* openAvatarProject(const QString& path); + static AvatarProject* openAvatarProject(const QString& path, AvatarProjectStatus::AvatarProjectStatus& status); static AvatarProject* createAvatarProject(const QString& projectsFolder, const QString& avatarProjectName, const QString& avatarModelPath, - const QString& textureFolder); + const QString& textureFolder, + AvatarProjectStatus::AvatarProjectStatus& status); static bool isValidNewProjectName(const QString& projectPath, const QString& projectName); @@ -78,7 +102,7 @@ private: FST* getFST() { return _fst; } void refreshProjectFiles(); - void appendDirectory(QString prefix, QDir dir); + void appendDirectory(const QString& prefix, const QDir& dir); QStringList getScriptPaths(const QDir& scriptsDir); FST* _fst; From 7ca7e0f2345744c86af72805dece85de57744935 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 13:45:00 -0800 Subject: [PATCH 33/43] Revert unintended changes in avatar packager --- interface/src/Application.cpp | 2 +- interface/src/avatar/AvatarManager.cpp | 4 ++-- scripts/system/html/js/entityProperties.js | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index b8324874b1..a0edc7f5ff 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -466,7 +466,7 @@ public: // Don't actually crash in debug builds, in case this apparent deadlock is simply from // the developer actively debugging code #ifdef NDEBUG - //deadlockDetectionCrash(); + deadlockDetectionCrash(); #endif } } diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 1a6b510ea1..7ca18ca258 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -536,7 +536,6 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents // my avatar. (Other user machines will make a similar analysis and inject sound for their collisions.) if (collision.idA.isNull() || collision.idB.isNull()) { auto myAvatar = getMyAvatar(); - myAvatar->collisionWithEntity(collision); auto collisionSound = myAvatar->getCollisionSound(); if (collisionSound) { const auto characterController = myAvatar->getCharacterController(); @@ -572,8 +571,9 @@ void AvatarManager::handleCollisionEvents(const CollisionEvents& collisionEvents auto injector = AudioInjector::playSoundAndDelete(collisionSound, options); _collisionInjectors.emplace_back(injector); } + myAvatar->collisionWithEntity(collision); + return; } - return; } } } diff --git a/scripts/system/html/js/entityProperties.js b/scripts/system/html/js/entityProperties.js index d4b60f1814..78ef8ac313 100644 --- a/scripts/system/html/js/entityProperties.js +++ b/scripts/system/html/js/entityProperties.js @@ -112,15 +112,6 @@ const GROUPS = [ type: "color", propertyID: "color", }, - { - label: "Alpha", - type: "", - type: "number", - min: 0, - max: 1, - step: 0.001, - propertyID: "alpha", - }, ] }, { From 847496048fda4ecc623028efda5490bb4dd88d75 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Thu, 3 Jan 2019 13:51:45 -0800 Subject: [PATCH 34/43] Fix waiting-for-inventory sticking around on avatar uploader --- .../qml/hifi/avatarPackager/AvatarProject.qml | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index a5a2263346..7a0f7b6750 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -17,7 +17,7 @@ Item { Style { id: style } property int colorScheme; - property var uploader: undefined; + property var uploader: null; property bool hasSuccessfullyUploaded: true; @@ -25,7 +25,10 @@ Item { anchors.fill: parent anchors.margins: 10 - function reset() { hasSuccessfullyUploaded = false } + function reset() { + hasSuccessfullyUploaded = false; + uploader = null; + } property var footer: Item { anchors.fill: parent @@ -151,32 +154,22 @@ Item { } function uploadNew() { - console.log("Uploading new"); upload(false); } function uploadUpdate() { - console.log("Uploading update"); upload(true); } + Connections { + target: root.uploader + onStateChanged: { + root.hasSuccessfullyUploaded = newState >= 4; + } + } + function upload(updateExisting) { root.uploader = AvatarPackagerCore.currentAvatarProject.upload(updateExisting); console.log("uploader: "+ root.uploader); - root.uploader.uploadProgress.connect(function(uploaded, total) { - console.log("Uploader progress: " + uploaded + " / " + total); - }); - root.uploader.completed.connect(function() { - root.hasSuccessfullyUploaded = true; - }); - root.uploader.finishedChanged.connect(function() { - try { - var response = JSON.parse(root.uploader.responseData); - console.log("Uploader complete! " + response); - uploadStatus.text = response.status; - } catch (e) { - console.log("Error parsing JSON: " + root.uploader.reponseData); - } - }); root.uploader.send(); avatarPackager.state = AvatarPackagerState.projectUpload; } From f27ee1767cca4b790734473246239ce9b8b44d81 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 08:43:43 -0800 Subject: [PATCH 35/43] Fix rename not working correctly in AvatarPackager --- .../resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 04a4d2e41a..845fdeb99f 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -109,7 +109,7 @@ ShadowRectangle { font.pixelSize: 28 z: 200 onFocusChanged: { - if (titleArea.state === "renaming" && !titleArea.focus) { + if (titleArea.state === "renaming" && !focus) { accepted(); } } From c2ceeb3d7696a4d982ecf3b7327da8b56114b691 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:41:31 -0800 Subject: [PATCH 36/43] Apply AvatarPackager code review cleanup --- .../avatarPackager/AvatarUploadStatusItem.qml | 17 +++++++++++++---- .../qml/hifi/avatarPackager/ClickableArea.qml | 2 +- .../qml/hifi/avatarPackager/RalewayButton.qml | 2 +- interface/src/avatar/AvatarProject.cpp | 8 ++++---- interface/src/avatar/AvatarProject.h | 2 +- .../src/avatar/MarketplaceItemUploader.cpp | 11 ++++++++--- interface/src/avatar/MarketplaceItemUploader.h | 17 +++++++---------- libraries/fbx/src/FST.cpp | 6 ++++-- libraries/fbx/src/FST.h | 4 ++-- libraries/networking/src/AccountManager.h | 7 +++---- 10 files changed, 44 insertions(+), 32 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index 4749e912c6..1e48264b3a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,25 +14,34 @@ Item { property int uploaderState; property var uploader; +/* state: root.uploader === undefined ? "" : (root.uploader.state > uploaderState ? "success" : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) + */ states: [ State { - name: "running" + name: "" + when: root.uploader === null + }, + State { + name: "success" + when: root.uploader.state > uploaderState PropertyChanges { target: stepText; color: "white" } - PropertyChanges { target: runningImage; visible: true; playing: true } + PropertyChanges { target: successGlyph; visible: true } }, State { name: "fail" + when: root.uploader.error !== 0 PropertyChanges { target: stepText; color: "#EA4C5F" } PropertyChanges { target: failGlyph; visible: true } }, State { - name: "success" + name: "running" + when: root.uploader.state === uploaderState PropertyChanges { target: stepText; color: "white" } - PropertyChanges { target: successGlyph; visible: true } + PropertyChanges { target: runningImage; visible: true; playing: true } } ] diff --git a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml index ebe38364ec..0f7b201f72 100644 --- a/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml +++ b/interface/resources/qml/hifi/avatarPackager/ClickableArea.qml @@ -60,4 +60,4 @@ Item { } ] } -} \ No newline at end of file +} diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index edbc31b24f..18cce8138f 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -25,4 +25,4 @@ RalewaySemiBold { onClicked: root.clicked() } -} \ No newline at end of file +} diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index e049dc0ba3..34da80587b 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -162,7 +162,7 @@ AvatarProject* AvatarProject::createAvatarProject(const QString& projectsFolder, return new AvatarProject(fst); } -QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) { +QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const { QStringList result{}; constexpr auto flags = QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot | QDir::Hidden; if (!scriptsDir.exists()) { @@ -244,6 +244,8 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { } void AvatarProject::openInInventory() { + constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; + auto tablet = dynamic_cast<TabletProxy*>( DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); @@ -251,9 +253,7 @@ void AvatarProject::openInInventory() { auto name = getProjectName(); // I'm not a fan of this, but it's the only current option. - QTimer::singleShot(1000, [name]() { - auto tablet = dynamic_cast<TabletProxy*>( - DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); + QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index c422c14d62..2a655409e1 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -103,7 +103,7 @@ private: void refreshProjectFiles(); void appendDirectory(const QString& prefix, const QDir& dir); - QStringList getScriptPaths(const QDir& scriptsDir); + QStringList getScriptPaths(const QDir& scriptsDir) const; FST* _fst; diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 8b97358ba4..543617fc56 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -36,7 +36,10 @@ MarketplaceItemUploader::MarketplaceItemUploader(QString title, QUuid marketplaceID, QList<ProjectFilePath> filePaths) : _title(title), - _description(description), _rootFilename(rootFilename), _marketplaceID(marketplaceID), _filePaths(filePaths) { + _description(description), + _rootFilename(rootFilename), + _marketplaceID(marketplaceID), + _filePaths(filePaths) { } void MarketplaceItemUploader::setState(State newState) { @@ -299,11 +302,13 @@ void MarketplaceItemUploader::doWaitForInventory() { if (success) { setState(State::Complete); } else { + constexpr int MAX_INVENTORY_REQUESTS { 8 }; + constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 }; qDebug() << "Failed to find item in inventory"; - if (_numRequestsForInventory > 8) { + if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) { setError(Error::Unknown); } else { - QTimer::singleShot(5000, [this]() { doWaitForInventory(); }); + QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); }); } } }); diff --git a/interface/src/avatar/MarketplaceItemUploader.h b/interface/src/avatar/MarketplaceItemUploader.h index 4fb1713b7d..998413da88 100644 --- a/interface/src/avatar/MarketplaceItemUploader.h +++ b/interface/src/avatar/MarketplaceItemUploader.h @@ -30,21 +30,19 @@ class MarketplaceItemUploader : public QObject { Q_PROPERTY(Error error READ getError NOTIFY errorChanged) Q_PROPERTY(QString responseData READ getResponseData) public: - enum class Error - { + enum class Error { None, - Unknown + Unknown, }; Q_ENUM(Error); - enum class State - { + enum class State { Idle, GettingCategories, UploadingAvatar, WaitingForUploadResponse, WaitingForInventory, - Complete + Complete, }; Q_ENUM(State); @@ -63,7 +61,6 @@ public: State getState() const { return _state; } bool getComplete() const { return _state == State::Complete; } - QUuid getMarketplaceID() const { return _marketplaceID; } Error getError() const { return _error; } @@ -86,8 +83,8 @@ private: QNetworkReply* _reply; - State _state{ State::Idle }; - Error _error{ Error::None }; + State _state { State::Idle }; + Error _error { Error::None }; QString _title; QString _description; @@ -98,7 +95,7 @@ private: QString _responseData; - int _numRequestsForInventory{ 0 }; + int _numRequestsForInventory { 0 }; QString _rootFilePath; QList<ProjectFilePath> _filePaths; diff --git a/libraries/fbx/src/FST.cpp b/libraries/fbx/src/FST.cpp index 9510ca5962..7828037c74 100644 --- a/libraries/fbx/src/FST.cpp +++ b/libraries/fbx/src/FST.cpp @@ -15,6 +15,8 @@ #include <QFileInfo> #include <hfm/HFM.h> +constexpr float DEFAULT_SCALE { 1.0f }; + FST::FST(QString fstPath, QVariantHash data) : _fstPath(std::move(fstPath)) { auto setValueFromFSTData = [&data] (const QString& propertyID, auto &targetProperty) mutable { @@ -55,7 +57,7 @@ FST* FST::createFSTFromModel(const QString& fstPath, const QString& modelFilePat mapping.insert(TEXDIR_FIELD, "textures"); // mixamo/autodesk defaults - mapping.insert(SCALE_FIELD, 1.0); + mapping.insert(SCALE_FIELD, DEFAULT_SCALE); QVariantHash joints = mapping.value(JOINT_FIELD).toHash(); joints.insert("jointEyeLeft", hfmModel.jointIndices.contains("jointEyeLeft") ? "jointEyeLeft" : (hfmModel.jointIndices.contains("EyeLeft") ? "EyeLeft" : "LeftEye")); @@ -161,7 +163,7 @@ void FST::setModelPath(const QString& modelPath) { emit modelPathChanged(modelPath); } -QVariantHash FST::getMapping() { +QVariantHash FST::getMapping() const { QVariantHash mapping; mapping.unite(_other); mapping.insert(NAME_FIELD, _name); diff --git a/libraries/fbx/src/FST.h b/libraries/fbx/src/FST.h index 6104130512..0f4c1ecd3a 100644 --- a/libraries/fbx/src/FST.h +++ b/libraries/fbx/src/FST.h @@ -45,9 +45,9 @@ public: QStringList getScriptPaths() const { return _scriptPaths; } void setScriptPaths(QStringList scriptPaths) { _scriptPaths = scriptPaths; } - QString getPath() { return _fstPath; } + QString getPath() const { return _fstPath; } - QVariantHash getMapping(); + QVariantHash getMapping() const; bool write(); diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index 77f20472fa..b8c3bf0cc4 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -40,11 +40,10 @@ public: }; namespace AccountManagerAuth { -enum Type -{ +enum Type { None, Required, - Optional + Optional, }; } @@ -157,7 +156,7 @@ private: bool _isWaitingForTokenRefresh{ false }; bool _isAgent{ false }; - bool _isWaitingForKeypairResponse{ false }; + bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; QUuid _sessionID{ QUuid::createUuid() }; From f38a469e659db7a96a9864a1e2052549c4b92891 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:55:21 -0800 Subject: [PATCH 37/43] Update avatar packager to force focus on inventory when opened --- interface/src/avatar/AvatarProject.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 34da80587b..20556ce5ed 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -250,10 +250,12 @@ void AvatarProject::openInInventory() { DependencyManager::get<TabletScriptingInterface>()->getTablet("com.highfidelity.interface.tablet.system")); tablet->loadQMLSource("hifi/commerce/wallet/Wallet.qml"); DependencyManager::get<HMDScriptingInterface>()->openTablet(); + tablet->getTabletRoot()->forceActiveFocus(); auto name = getProjectName(); // I'm not a fan of this, but it's the only current option. QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); + }); } From b680e9c52ae175497ad7892884dfb331853a37d8 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 09:59:17 -0800 Subject: [PATCH 38/43] Update avatar packager with CR feedback --- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- libraries/avatars/src/ProjectFile.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 543617fc56..938485353c 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -173,7 +173,7 @@ void MarketplaceItemUploader::doUploadAvatar() { qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - QString path = "/api/v1/marketplace/items"; + static const QString path = "/api/v1/marketplace/items"; bool creating = true; if (!_marketplaceID.isNull()) { creating = false; diff --git a/libraries/avatars/src/ProjectFile.h b/libraries/avatars/src/ProjectFile.h index 82930a3464..4040eb1ce5 100644 --- a/libraries/avatars/src/ProjectFile.h +++ b/libraries/avatars/src/ProjectFile.h @@ -10,4 +10,4 @@ public: QString relativePath; }; -#endif // hifi_AvatarProjectFile_h \ No newline at end of file +#endif // hifi_AvatarProjectFile_h From 6d5b9a88e416360b192886207c70a721b2ea43f2 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 11:29:07 -0800 Subject: [PATCH 39/43] Remove dead code in AvatarUploadStatusItem --- .../qml/hifi/avatarPackager/AvatarUploadStatusItem.qml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml index 1e48264b3a..70a0ea0672 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarUploadStatusItem.qml @@ -14,12 +14,6 @@ Item { property int uploaderState; property var uploader; -/* - state: root.uploader === undefined ? "" : - (root.uploader.state > uploaderState ? "success" - : (root.uploader.error !== 0 ? "fail" : (root.uploader.state === uploaderState ? "running" : ""))) - */ - states: [ State { name: "" From 51ce19e026fd0ec9677aedfe46ef8b867145c6d1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 12:08:23 -0800 Subject: [PATCH 40/43] Fix modified const var in MarketplaceItemUploader --- interface/src/avatar/MarketplaceItemUploader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 938485353c..543617fc56 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -173,7 +173,7 @@ void MarketplaceItemUploader::doUploadAvatar() { qDebug() << "Finished zipping, size: " << (buffer.size() / (1000.0f)) << "KB"; - static const QString path = "/api/v1/marketplace/items"; + QString path = "/api/v1/marketplace/items"; bool creating = true; if (!_marketplaceID.isNull()) { creating = false; From 63172ec87ba3116c574e70e0b2fa3458525059c1 Mon Sep 17 00:00:00 2001 From: Ryan Huffman <ryanhuffman@gmail.com> Date: Fri, 4 Jan 2019 14:21:10 -0800 Subject: [PATCH 41/43] Fix marketplace item update failing on submitted items --- interface/src/avatar/AvatarProject.cpp | 1 - interface/src/avatar/MarketplaceItemUploader.cpp | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 20556ce5ed..a38bdcd693 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -256,6 +256,5 @@ void AvatarProject::openInInventory() { // I'm not a fan of this, but it's the only current option. QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); - }); } diff --git a/interface/src/avatar/MarketplaceItemUploader.cpp b/interface/src/avatar/MarketplaceItemUploader.cpp index 543617fc56..53b37eba4f 100644 --- a/interface/src/avatar/MarketplaceItemUploader.cpp +++ b/interface/src/avatar/MarketplaceItemUploader.cpp @@ -190,7 +190,12 @@ void MarketplaceItemUploader::doUploadAvatar() { QString jsonString = "{\"marketplace_item\":{"; jsonString += "\"title\":\"" + escapeJson(_title) + "\""; - jsonString += ",\"description\":\"" + escapeJson(_description) + "\""; + + // Items cannot have their description updated after they have been submitted. + if (creating) { + jsonString += ",\"description\":\"" + escapeJson(_description) + "\""; + } + jsonString += ",\"root_file_key\":\"" + escapeJson(_rootFilename) + "\""; jsonString += ",\"category_ids\":[" + QStringLiteral("%1").arg(_categoryID) + "]"; jsonString += ",\"license\":0"; @@ -300,12 +305,13 @@ void MarketplaceItemUploader::doWaitForInventory() { success = isAssetAvailable(); } if (success) { + qDebug() << "Found item in inventory"; setState(State::Complete); } else { constexpr int MAX_INVENTORY_REQUESTS { 8 }; constexpr int TIME_BETWEEN_INVENTORY_REQUESTS_MS { 5000 }; - qDebug() << "Failed to find item in inventory"; if (_numRequestsForInventory > MAX_INVENTORY_REQUESTS) { + qDebug() << "Failed to find item in inventory"; setError(Error::Unknown); } else { QTimer::singleShot(TIME_BETWEEN_INVENTORY_REQUESTS_MS, [this]() { doWaitForInventory(); }); From 5d40e1e4801e2a4e4f2eca034b5f7d0590aa7543 Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Fri, 4 Jan 2019 23:39:04 +0100 Subject: [PATCH 42/43] - CR/style fixes - Moved the old Avatar Packager tool to Developer -> Avatar -> .. --- .../hifi/avatarPackager/AvatarPackagerApp.qml | 16 +++++++------- .../avatarPackager/AvatarPackagerHeader.qml | 3 --- .../qml/hifi/avatarPackager/AvatarProject.qml | 22 +++++++++---------- .../hifi/avatarPackager/AvatarProjectCard.qml | 2 +- .../qml/hifi/avatarPackager/RalewayButton.qml | 10 ++++----- interface/src/Menu.cpp | 6 ++--- interface/src/avatar/AvatarPackager.cpp | 2 +- interface/src/avatar/AvatarPackager.h | 12 +++++----- interface/src/avatar/AvatarProject.cpp | 7 +++--- interface/src/avatar/AvatarProject.h | 2 +- 10 files changed, 38 insertions(+), 44 deletions(-) diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml index 9d3a347a11..b4293d5eee 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerApp.qml @@ -76,7 +76,7 @@ Item { InfoBox { id: errorPopup - property string errorMessage; + property string errorMessage boxWidth: 380 boxHeight: 293 @@ -181,16 +181,16 @@ Item { errorPopup.show("Project Folder Already Exists", "A folder with that name already exists at that location. Please choose a different project name or location."); break; case AvatarProjectStatus.ERROR_CREATE_CREATING_DIRECTORIES: - errorPopup.show("Project Folders Creation Error", "There was a problem during the creation of the Avatar Project directories. Please select a project location with write permissions."); + errorPopup.show("Project Folders Creation Error", "There was a problem creating the Avatar Project directory. Please check the project location and try again."); break; case AvatarProjectStatus.ERROR_CREATE_FIND_MODEL: - errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify if it exist at the specified location."); + errorPopup.show("Cannot Find Model File", "There was a problem while trying to find the specified model file. Please verify that it exists at the specified location."); break; case AvatarProjectStatus.ERROR_CREATE_OPEN_MODEL: - errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file. Please verify if you have read permissions at the specified location."); + errorPopup.show("Cannot Open Model File", "There was a problem while trying to open the specified model file."); break; case AvatarProjectStatus.ERROR_CREATE_READ_MODEL: - errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please verify if the model file is supported by High Fidelity."); + errorPopup.show("Error Read Model File", "There was a problem while trying to read the specified model file. Please check that the file is a valid FBX file and try again."); break; case AvatarProjectStatus.ERROR_CREATE_WRITE_FST: errorPopup.show("Error Writing Project File", "There was a problem while trying to write the FST file."); @@ -202,13 +202,13 @@ Item { errorPopup.show("Project Missing", "Project folder cannot be found. Please locate the folder and copy/move it to its original location."); break; case AvatarProjectStatus.ERROR_OPEN_FIND_FST: - errorPopup.show("File Missing", "We cannot find the project file (avatar.fst) in the folder. Please locate it and move to the project folder."); + errorPopup.show("File Missing", "We cannot find the project file (.fst) in the project folder. Please locate it and move it to the project folder."); break; case AvatarProjectStatus.ERROR_OPEN_OPEN_FST: - errorPopup.show("File Read Error", "We cannot read the project file (avatar.fst). Please make sure that it is not in use by another program."); + errorPopup.show("File Read Error", "We cannot read the project file (.fst)."); break; case AvatarProjectStatus.ERROR_OPEN_FIND_MODEL: - errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the folder. Please locate it and move to the project folder."); + errorPopup.show("File Missing", "We cannot find the avatar model file (.fbx) in the project folder. Please locate it and move it to the project folder."); break; default: errorPopup.show("Error Message Missing", "Error message missing for status " + status); diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml index 845fdeb99f..25201bf81e 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarPackagerHeader.qml @@ -35,7 +35,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.left: parent.left anchors.leftMargin: 16 - anchors.verticalCenter: back.verticalCenter text: "◀" @@ -48,7 +47,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.left: root.backButtonVisible ? back.right : parent.left anchors.leftMargin: root.backButtonVisible ? 11 : 21 - anchors.verticalCenter: title.verticalCenter anchors.right: docs.left states: [ State { @@ -136,7 +134,6 @@ ShadowRectangle { anchors.bottom: parent.bottom anchors.right: parent.right anchors.rightMargin: 16 - anchors.verticalCenter: docs.verticalCenter text: qsTr("Docs") diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml index 59dd1ac5c9..85ef821a4a 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProject.qml @@ -16,10 +16,10 @@ Item { Style { id: style } - property int colorScheme; - property var uploader: null; + property int colorScheme + property var uploader: null - property bool hasSuccessfullyUploaded: true; + property bool hasSuccessfullyUploaded: true visible: false anchors.fill: parent @@ -44,7 +44,7 @@ Item { HifiControls.Button { id: uploadButton - visible: !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + visible: AvatarPackagerCore.currentAvatarProject && !AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn anchors.verticalCenter: parent.verticalCenter @@ -62,7 +62,7 @@ Item { HifiControls.Button { id: updateButton - visible: AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded + visible: AvatarPackagerCore.currentAvatarProject && AvatarPackagerCore.currentAvatarProject.fst.hasMarketplaceID && !root.hasSuccessfullyUploaded enabled: Account.loggedIn anchors.verticalCenter: parent.verticalCenter @@ -175,9 +175,9 @@ Item { } function showConfirmUploadPopup() { - popup.titleText = 'Overwrite Avatar' + popup.titleText = 'Overwrite Avatar'; popup.bodyText = 'You have previously uploaded the avatar file from this project.' + - ' This will overwrite that avatar and you won’t be able to access the older version.' + ' This will overwrite that avatar and you won’t be able to access the older version.'; popup.button1text = 'CREATE NEW'; popup.button2text = 'OVERWRITE'; @@ -185,7 +185,7 @@ Item { popup.onButton2Clicked = function() { popup.close(); uploadUpdate(); - } + }; popup.onButton1Clicked = function() { popup.close(); showConfirmCreateNewPopup(); @@ -195,9 +195,9 @@ Item { } function showConfirmCreateNewPopup(confirmCallback) { - popup.titleText = 'Create New' + popup.titleText = 'Create New'; popup.bodyText = 'This will upload your current files with the same avatar name.' + - ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?' + ' You will lose the ability to update the previously uploaded avatar. Are you sure you want to continue?'; popup.button1text = 'CANCEL'; popup.button2text = 'CONFIRM'; @@ -277,7 +277,7 @@ Item { size: 20 - text: AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" + text: AvatarPackagerCore.currentAvatarProject ? AvatarPackagerCore.currentAvatarProject.projectFiles.length + " files in project. <a href='toggle'>See list</a>" : "" onLinkActivated: fileListPopup.open() } diff --git a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml index a758d3936a..25222c814c 100644 --- a/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml +++ b/interface/resources/qml/hifi/avatarPackager/AvatarProjectCard.qml @@ -21,7 +21,7 @@ Item { property color hoverBackgroundColor: "#E3E3E3" property color pressedBackgroundColor: "#6A6A6A" - signal open; + signal open state: mouseArea.pressed ? "pressed" : (mouseArea.containsMouse ? "hover" : "normal") states: [ diff --git a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml index 18cce8138f..86742ddccd 100644 --- a/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml +++ b/interface/resources/qml/hifi/avatarPackager/RalewayButton.qml @@ -8,13 +8,11 @@ import TabletScriptingInterface 1.0 RalewaySemiBold { id: root - anchors.fill: textItem + property color idleColor: "white" + property color hoverColor: "#AFAFAF" + property color pressedColor: "#575757" - property var idleColor: "white" - property var hoverColor: "#AFAFAF" - property var pressedColor: "#575757" - - color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor); + color: clickable.hovered ? root.hoverColor : (clickable.pressed ? root.pressedColor : root.idleColor) signal clicked() diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 87b1542648..810e21daf5 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -145,10 +145,6 @@ Menu::Menu() { assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets()); } - // Edit > Package Avatar as .fst... - addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0, - qApp, SLOT(packageModel())); - // Edit > Avatar Packager #ifndef Q_OS_ANDROID action = addActionToQMenuAndActionHash(editMenu, MenuOption::AvatarPackager); @@ -654,6 +650,8 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::ShowTrackedObjects, 0, false, qApp, SLOT(setShowTrackedObjects(bool))); + addActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::PackageModel, 0, qApp, SLOT(packageModel())); + // Developer > Hands >>> MenuWrapper* handOptionsMenu = developerMenu->addMenu("Hands"); addCheckableActionToQMenuAndActionHash(handOptionsMenu, MenuOption::DisplayHandTargets, 0, false, diff --git a/interface/src/avatar/AvatarPackager.cpp b/interface/src/avatar/AvatarPackager.cpp index 941aff6943..fa70eee374 100644 --- a/interface/src/avatar/AvatarPackager.cpp +++ b/interface/src/avatar/AvatarPackager.cpp @@ -95,7 +95,7 @@ void AvatarPackager::addCurrentProjectToRecentProjects() { emit recentProjectsChanged(); } -QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) { +QVariantList AvatarPackager::recentProjectsToVariantList(bool includeProjectPaths) const { QVariantList result; for (const auto& project : _recentProjects) { QVariantMap projectVariant; diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index c9c5b9d312..4416ec5806 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -69,7 +69,9 @@ public: const QString& textureFolder); Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath); - Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { return AvatarProject::isValidNewProjectName(projectPath, projectName); } + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { + return AvatarProject::isValidNewProjectName(projectPath, projectName); + } signals: void avatarProjectChanged(); @@ -78,21 +80,21 @@ signals: private: Q_INVOKABLE AvatarProject* getAvatarProject() const { return _currentAvatarProject; }; Q_INVOKABLE QString getAvatarProjectsPath() const { return AvatarProject::getDefaultProjectsPath(); } - Q_INVOKABLE QVariantList getRecentProjects() { return recentProjectsToVariantList(true); } + Q_INVOKABLE QVariantList getRecentProjects() const { return recentProjectsToVariantList(true); } void setAvatarProject(AvatarProject* avatarProject); void addCurrentProjectToRecentProjects(); - AvatarProject* _currentAvatarProject{ nullptr }; + AvatarProject* _currentAvatarProject { nullptr }; QVector<RecentAvatarProject> _recentProjects; - QVariantList recentProjectsToVariantList(bool includeProjectPaths); + QVariantList recentProjectsToVariantList(bool includeProjectPaths) const; void recentProjectsFromVariantList(QVariantList projectsVariant); - Setting::Handle<QVariantList> _recentProjectsSetting{ "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; + Setting::Handle<QVariantList> _recentProjectsSetting { "io.highfidelity.avatarPackager.recentProjects", QVariantList() }; }; #endif // hifi_AvatarPackager_h diff --git a/interface/src/avatar/AvatarProject.cpp b/interface/src/avatar/AvatarProject.cpp index 20556ce5ed..728917e673 100644 --- a/interface/src/avatar/AvatarProject.cpp +++ b/interface/src/avatar/AvatarProject.cpp @@ -169,8 +169,8 @@ QStringList AvatarProject::getScriptPaths(const QDir& scriptsDir) const { return result; } - for (auto& script : scriptsDir.entryInfoList({}, flags)) { - if (script.fileName().endsWith(".js")) { + for (const auto& script : scriptsDir.entryInfoList({}, flags)) { + if (script.fileName().toLower().endsWith(".js")) { result.push_back("scripts/" + script.fileName()); } } @@ -243,7 +243,7 @@ MarketplaceItemUploader* AvatarProject::upload(bool updateExisting) { return uploader; } -void AvatarProject::openInInventory() { +void AvatarProject::openInInventory() const { constexpr int TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS { 1000 }; auto tablet = dynamic_cast<TabletProxy*>( @@ -256,6 +256,5 @@ void AvatarProject::openInInventory() { // I'm not a fan of this, but it's the only current option. QTimer::singleShot(TIME_TO_WAIT_FOR_INVENTORY_TO_OPEN_MS, [name, tablet]() { tablet->sendToQml(QVariantMap({ { "method", "updatePurchases" }, { "filterText", name } })); - }); } diff --git a/interface/src/avatar/AvatarProject.h b/interface/src/avatar/AvatarProject.h index 2a655409e1..1710282a3e 100644 --- a/interface/src/avatar/AvatarProject.h +++ b/interface/src/avatar/AvatarProject.h @@ -56,7 +56,7 @@ class AvatarProject : public QObject { public: Q_INVOKABLE MarketplaceItemUploader* upload(bool updateExisting); - Q_INVOKABLE void openInInventory(); + Q_INVOKABLE void openInInventory() const; Q_INVOKABLE QStringList getProjectFiles() const; Q_INVOKABLE QString getProjectName() const { return _fst->getName(); } From 94146ab997daba74c94b09392e914fbfc484454b Mon Sep 17 00:00:00 2001 From: Thijs Wenker <me@thoys.nl> Date: Fri, 4 Jan 2019 23:59:22 +0100 Subject: [PATCH 43/43] CR fixes --- interface/src/avatar/AvatarPackager.h | 2 +- libraries/networking/src/AccountManager.h | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/interface/src/avatar/AvatarPackager.h b/interface/src/avatar/AvatarPackager.h index 4416ec5806..ec954a60d7 100644 --- a/interface/src/avatar/AvatarPackager.h +++ b/interface/src/avatar/AvatarPackager.h @@ -69,7 +69,7 @@ public: const QString& textureFolder); Q_INVOKABLE AvatarProjectStatus::AvatarProjectStatus openAvatarProject(const QString& avatarProjectFSTPath); - Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) { + Q_INVOKABLE bool isValidNewProjectName(const QString& projectPath, const QString& projectName) const { return AvatarProject::isValidNewProjectName(projectPath, projectName); } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index b8c3bf0cc4..ca2b826c98 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -153,15 +153,15 @@ private: QUrl _authURL; DataServerAccountInfo _accountInfo; - bool _isWaitingForTokenRefresh{ false }; - bool _isAgent{ false }; + bool _isWaitingForTokenRefresh { false }; + bool _isAgent { false }; bool _isWaitingForKeypairResponse { false }; QByteArray _pendingPrivateKey; - QUuid _sessionID{ QUuid::createUuid() }; + QUuid _sessionID { QUuid::createUuid() }; - bool _limitedCommerce{ false }; + bool _limitedCommerce { false }; }; #endif // hifi_AccountManager_h