From 2f32458f72311a694e479325c2dfe4a2cdaea4c4 Mon Sep 17 00:00:00 2001 From: Thijs Wenker Date: Thu, 27 Dec 2018 19:52:56 +0100 Subject: [PATCH] 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>