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: ""
+ 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 - optional"
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();
});
+ 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();
+ 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);
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 _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 _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
#include
-#include
#include
#include
#include "FBXSerializer.h"
#include
-#include
#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
#include
#include
-#include
#include
-#include
#include
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
#include
-#include
-#include
+#include
+#include
#include
#include