diff --git a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp index 4be876e79f..72ffa686c1 100644 --- a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp @@ -12,6 +12,7 @@ #include + #include #include #include @@ -58,19 +59,15 @@ namespace O3DE::ProjectManager m_overlayLabel->setText(text); } - ProjectButton::ProjectButton(const QString& projectName, QWidget* parent) + ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent) : QFrame(parent) - , m_projectName(projectName) - , m_projectImagePath(":/Resources/DefaultProjectImage.png") + , m_projectInfo(projectInfo) { - Setup(); - } + if (m_projectInfo.m_imagePath.isEmpty()) + { + m_projectInfo.m_imagePath = ":/DefaultProjectImage.png"; + } - ProjectButton::ProjectButton(const QString& projectName, const QString& projectImage, QWidget* parent) - : QFrame(parent) - , m_projectName(projectName) - , m_projectImagePath(projectImage) - { Setup(); } @@ -85,20 +82,22 @@ namespace O3DE::ProjectManager m_projectImageLabel = new LabelButton(this); m_projectImageLabel->setFixedSize(s_projectImageWidth, s_projectImageHeight); + m_projectImageLabel->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); vLayout->addWidget(m_projectImageLabel); - m_projectImageLabel->setPixmap(QPixmap(m_projectImagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding)); + m_projectImageLabel->setPixmap( + QPixmap(m_projectInfo.m_imagePath).scaled(m_projectImageLabel->size(), Qt::KeepAspectRatioByExpanding)); QMenu* newProjectMenu = new QMenu(this); m_editProjectAction = newProjectMenu->addAction(tr("Edit Project Settings...")); - -#ifdef SHOW_ALL_PROJECT_ACTIONS - m_editProjectGemsAction = newProjectMenu->addAction(tr("Cutomize Gems...")); newProjectMenu->addSeparator(); m_copyProjectAction = newProjectMenu->addAction(tr("Duplicate")); newProjectMenu->addSeparator(); m_removeProjectAction = newProjectMenu->addAction(tr("Remove from O3DE")); - m_deleteProjectAction = newProjectMenu->addAction(tr("Delete the Project")); + m_deleteProjectAction = newProjectMenu->addAction(tr("Delete this Project")); + +#ifdef SHOW_ALL_PROJECT_ACTIONS + m_editProjectGemsAction = newProjectMenu->addAction(tr("Cutomize Gems...")); #endif QFrame* footer = new QFrame(this); @@ -106,7 +105,7 @@ namespace O3DE::ProjectManager hLayout->setContentsMargins(0, 0, 0, 0); footer->setLayout(hLayout); { - QLabel* projectNameLabel = new QLabel(m_projectName, this); + QLabel* projectNameLabel = new QLabel(m_projectInfo.m_displayName, this); hLayout->addWidget(projectNameLabel); QPushButton* projectMenuButton = new QPushButton(this); @@ -117,14 +116,14 @@ namespace O3DE::ProjectManager vLayout->addWidget(footer); - connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectName); }); - connect(m_editProjectAction, &QAction::triggered, [this]() { emit EditProject(m_projectName); }); + connect(m_projectImageLabel, &LabelButton::triggered, [this]() { emit OpenProject(m_projectInfo.m_path); }); + connect(m_editProjectAction, &QAction::triggered, [this]() { emit EditProject(m_projectInfo.m_path); }); + connect(m_copyProjectAction, &QAction::triggered, [this]() { emit CopyProject(m_projectInfo.m_path); }); + connect(m_removeProjectAction, &QAction::triggered, [this]() { emit RemoveProject(m_projectInfo.m_path); }); + connect(m_deleteProjectAction, &QAction::triggered, [this]() { emit DeleteProject(m_projectInfo.m_path); }); #ifdef SHOW_ALL_PROJECT_ACTIONS - connect(m_editProjectGemsAction, &QAction::triggered, [this]() { emit EditProjectGems(m_projectName); }); - connect(m_copyProjectAction, &QAction::triggered, [this]() { emit CopyProject(m_projectName); }); - connect(m_removeProjectAction, &QAction::triggered, [this]() { emit RemoveProject(m_projectName); }); - connect(m_deleteProjectAction, &QAction::triggered, [this]() { emit DeleteProject(m_projectName); }); + connect(m_editProjectGemsAction, &QAction::triggered, [this]() { emit EditProjectGems(m_projectInfo.m_path); }); #endif } diff --git a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h index 671debf6d0..e82b56b3fa 100644 --- a/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h +++ b/Code/Tools/ProjectManager/Source/ProjectButtonWidget.h @@ -13,7 +13,8 @@ #pragma once #if !defined(Q_MOC_RUN) -#include +#include + #include #endif @@ -52,8 +53,7 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - explicit ProjectButton(const QString& projectName, QWidget* parent = nullptr); - explicit ProjectButton(const QString& projectName, const QString& projectImage, QWidget* parent = nullptr); + explicit ProjectButton(const ProjectInfo& m_projectInfo, QWidget* parent = nullptr); ~ProjectButton() = default; void SetButtonEnabled(bool enabled); @@ -70,8 +70,7 @@ namespace O3DE::ProjectManager private: void Setup(); - QString m_projectName; - QString m_projectImagePath; + ProjectInfo m_projectInfo; LabelButton* m_projectImageLabel; QAction* m_editProjectAction; QAction* m_editProjectGemsAction; diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.cpp b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp new file mode 100644 index 0000000000..526e745d82 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/ProjectUtils.cpp @@ -0,0 +1,196 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ + +#include +#include + +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + namespace ProjectUtils + { + static bool WarnDirectoryOverwrite(const QString& path, QWidget* parent) + { + if (!QDir(path).isEmpty()) + { + QMessageBox::StandardButton warningResult = QMessageBox::warning( + parent, + QObject::tr("Overwrite Directory"), + QObject::tr("Directory is not empty! Are you sure you want to overwrite it?"), + QMessageBox::No | QMessageBox::Yes + ); + + if (warningResult != QMessageBox::Yes) + { + return false; + } + } + + return true; + } + + static bool IsDirectoryDescedent(const QString& possibleAncestorPath, const QString& possibleDecedentPath) + { + QDir ancestor(possibleAncestorPath); + QDir descendent(possibleDecedentPath); + + do + { + if (ancestor == descendent) + { + return false; + } + + descendent.cdUp(); + } + while (!descendent.isRoot()); + + return true; + } + + static bool CopyDirectory(const QString& origPath, const QString& newPath) + { + QDir original(origPath); + if (!original.exists()) + { + return false; + } + + for (QString directory : original.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + QString newDirectoryPath = newPath + QDir::separator() + directory; + original.mkpath(newDirectoryPath); + + if (!CopyDirectory(origPath + QDir::separator() + directory, newDirectoryPath)) + { + return false; + } + } + + for (QString file : original.entryList(QDir::Files)) + { + if (!QFile::copy(origPath + QDir::separator() + file, newPath + QDir::separator() + file)) + return false; + } + + return true; + } + + bool AddProjectDialog(QWidget* parent) + { + QString path = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(parent, QObject::tr("Select Project Directory"))); + if (!path.isEmpty()) + { + return RegisterProject(path); + } + + return false; + } + + bool RegisterProject(const QString& path) + { + return PythonBindingsInterface::Get()->AddProject(path); + } + + bool UnregisterProject(const QString& path) + { + return PythonBindingsInterface::Get()->RemoveProject(path); + } + + bool CopyProjectDialog(const QString& origPath, QWidget* parent) + { + bool copyResult = false; + + QDir parentOrigDir(origPath); + parentOrigDir.cdUp(); + QString newPath = QDir::toNativeSeparators( + QFileDialog::getExistingDirectory(parent, QObject::tr("Select New Project Directory"), parentOrigDir.path())); + if (!newPath.isEmpty()) + { + if (!WarnDirectoryOverwrite(newPath, parent)) + { + return false; + } + + // TODO: Block UX and Notify User they need to wait + + copyResult = CopyProject(origPath, newPath); + } + + return copyResult; + } + + bool CopyProject(const QString& origPath, const QString& newPath) + { + // Disallow copying from or into subdirectory + if (!IsDirectoryDescedent(origPath, newPath) || !IsDirectoryDescedent(newPath, origPath)) + { + return false; + } + + if (!CopyDirectory(origPath, newPath)) + { + // Cleanup whatever mess was made + DeleteProjectFiles(newPath, true); + return false; + } + + if (!RegisterProject(newPath)) + { + DeleteProjectFiles(newPath, true); + } + + return true; + } + + bool DeleteProjectFiles(const QString& path, bool force) + { + QDir projectDirectory(path); + if (projectDirectory.exists()) + { + // Check if there is an actual project hereor just force it + if (force || PythonBindingsInterface::Get()->GetProject(path).IsSuccess()) + { + return projectDirectory.removeRecursively(); + } + } + + return false; + } + + bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent) + { + if (!WarnDirectoryOverwrite(newPath, parent) || !UnregisterProject(origPath)) + { + return false; + } + + QDir directory; + if (directory.rename(origPath, newPath)) + { + return directory.rename(origPath, newPath); + } + + if (!RegisterProject(newPath)) + { + return false; + } + + return true; + } + + } // namespace ProjectUtils +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectUtils.h b/Code/Tools/ProjectManager/Source/ProjectUtils.h new file mode 100644 index 0000000000..5982bff634 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/ProjectUtils.h @@ -0,0 +1,28 @@ +/* + * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or + * its licensors. + * + * For complete copyright and license terms please see the LICENSE at the root of this + * distribution (the "License"). All use of this software is governed by the License, + * or, if provided, by the license below or the license accompanying this file. Do not + * remove or modify any license notices. This file is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + */ +#pragma once + +#include + +namespace O3DE::ProjectManager +{ + namespace ProjectUtils + { + bool AddProjectDialog(QWidget* parent = nullptr); + bool RegisterProject(const QString& path); + bool UnregisterProject(const QString& path); + bool CopyProjectDialog(const QString& origPath, QWidget* parent = nullptr); + bool CopyProject(const QString& origPath, const QString& newPath); + bool DeleteProjectFiles(const QString& path, bool force = false); + bool MoveProject(const QString& origPath, const QString& newPath, QWidget* parent = nullptr); + } // namespace ProjectUtils +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp index 5f1c0e2b36..dd2e411ec5 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectsScreen.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include @@ -65,9 +66,6 @@ namespace O3DE::ProjectManager m_stack->addWidget(m_projectsContent); vLayout->addWidget(m_stack); - - connect(m_createNewProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleNewProjectButton); - connect(m_addExistingProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleAddProjectButton); } QFrame* ProjectsScreen::CreateFirstTimeContent() @@ -167,28 +165,27 @@ namespace O3DE::ProjectManager #endif { ProjectButton* projectButton; + QString projectPreviewPath = project.m_path + m_projectPreviewImagePath; QFileInfo doesPreviewExist(projectPreviewPath); if (doesPreviewExist.exists() && doesPreviewExist.isFile()) { - projectButton = new ProjectButton(project.m_projectName, projectPreviewPath, this); - } - else - { - projectButton = new ProjectButton(project.m_projectName, this); + project.m_imagePath = projectPreviewPath; } + projectButton = new ProjectButton(project, this); + flowLayout->addWidget(projectButton); connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject); connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject); - - #ifdef DISPLAY_PROJECT_DEV_DATA - connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems); connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject); connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject); connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject); - #endif + +#ifdef SHOW_ALL_PROJECT_ACTIONS + connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems); +#endif } layout->addWidget(projectsScrollArea); @@ -242,7 +239,11 @@ namespace O3DE::ProjectManager } void ProjectsScreen::HandleAddProjectButton() { - // Do nothing for now + if (ProjectUtils::AddProjectDialog(this)) + { + emit ResetScreenRequest(ProjectManagerScreen::Projects); + emit ChangeScreenRequest(ProjectManagerScreen::Projects); + } } void ProjectsScreen::HandleOpenProject(const QString& projectPath) { @@ -300,18 +301,36 @@ namespace O3DE::ProjectManager emit NotifyCurrentProject(projectPath); emit ChangeScreenRequest(ProjectManagerScreen::GemCatalog); } - void ProjectsScreen::HandleCopyProject([[maybe_unused]] const QString& projectPath) + void ProjectsScreen::HandleCopyProject(const QString& projectPath) { // Open file dialog and choose location for copied project then register copy with O3DE + if (ProjectUtils::CopyProjectDialog(projectPath, this)) + { + emit ResetScreenRequest(ProjectManagerScreen::Projects); + emit ChangeScreenRequest(ProjectManagerScreen::Projects); + } } - void ProjectsScreen::HandleRemoveProject([[maybe_unused]] const QString& projectPath) + void ProjectsScreen::HandleRemoveProject(const QString& projectPath) { - // Unregister Project from O3DE + // Unregister Project from O3DE and reload projects + if (ProjectUtils::UnregisterProject(projectPath)) + { + emit ResetScreenRequest(ProjectManagerScreen::Projects); + emit ChangeScreenRequest(ProjectManagerScreen::Projects); + } } - void ProjectsScreen::HandleDeleteProject([[maybe_unused]] const QString& projectPath) + void ProjectsScreen::HandleDeleteProject(const QString& projectPath) { - // Remove project from 03DE and delete from disk - ProjectsScreen::HandleRemoveProject(projectPath); + QMessageBox::StandardButton warningResult = QMessageBox::warning( + this, tr("Delete Project"), tr("Are you sure?\nProject will be removed from O3DE and directory will be deleted!"), + QMessageBox::No | QMessageBox::Yes); + + if (warningResult == QMessageBox::Yes) + { + // Remove project from O3DE and delete from disk + HandleRemoveProject(projectPath); + ProjectUtils::DeleteProjectFiles(projectPath); + } } void ProjectsScreen::NotifyCurrentScreen() diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 8c79a153c8..9279ad1291 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -379,13 +379,13 @@ namespace O3DE::ProjectManager pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString(); auto registrationResult = m_registration.attr("register")( - enginePath, // engine_path - pybind11::none(), // project_path - pybind11::none(), // gem_path - pybind11::none(), // template_path - pybind11::none(), // restricted_path - pybind11::none(), // repo_uri - pybind11::none(), // default_engines_folder + enginePath, // engine_path + pybind11::none(), // project_path + pybind11::none(), // gem_path + pybind11::none(), // template_path + pybind11::none(), // restricted_path + pybind11::none(), // repo_uri + pybind11::none(), // default_engines_folder defaultProjectsFolder, defaultGemsFolder, defaultTemplatesFolder @@ -456,6 +456,51 @@ namespace O3DE::ProjectManager } } + bool PythonBindings::AddProject(const QString& path) + { + bool registrationResult = false; + bool result = ExecuteWithLock( + [&] + { + pybind11::str projectPath = path.toStdString(); + auto pythonRegistrationResult = m_registration.attr("register")(pybind11::none(), projectPath); + + // Returns an exit code so boolify it then invert result + registrationResult = !pythonRegistrationResult.cast(); + }); + + return result && registrationResult; + } + + bool PythonBindings::RemoveProject(const QString& path) + { + bool registrationResult = false; + bool result = ExecuteWithLock( + [&] + { + pybind11::str projectPath = path.toStdString(); + auto pythonRegistrationResult = m_registration.attr("register")( + pybind11::none(), // engine_path + projectPath, // project_path + pybind11::none(), // gem_path + pybind11::none(), // template_path + pybind11::none(), // restricted_path + pybind11::none(), // repo_uri + pybind11::none(), // default_engines_folder + pybind11::none(), // default_gems_folder + pybind11::none(), // default_templates_folder + pybind11::none(), // default_restricted_folder + pybind11::none(), // default_restricted_folder + true // remove + ); + + // Returns an exit code so boolify it then invert result + registrationResult = !pythonRegistrationResult.cast(); + }); + + return result && registrationResult; + } + AZ::Outcome PythonBindings::CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) { ProjectInfo createdProjectInfo; @@ -600,7 +645,7 @@ namespace O3DE::ProjectManager pybind11::none(), // gem_target pybind11::none(), // project_name pyProjectPath - ); + ); }); return result; @@ -618,7 +663,7 @@ namespace O3DE::ProjectManager pybind11::none(), // gem_target pybind11::none(), // project_name pyProjectPath - ); + ); }); return result; diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 892e13a65b..fb2303c495 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -46,6 +46,8 @@ namespace O3DE::ProjectManager AZ::Outcome CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; AZ::Outcome GetProject(const QString& path) override; AZ::Outcome> GetProjects() override; + bool AddProject(const QString& path) override; + bool RemoveProject(const QString& path) override; bool UpdateProject(const ProjectInfo& projectInfo) override; bool AddGemToProject(const QString& gemPath, const QString& projectPath) override; bool RemoveGemFromProject(const QString& gemPath, const QString& projectPath) override; diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index b5c8f1a76a..a58eea0fe6 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -88,6 +88,20 @@ namespace O3DE::ProjectManager * @return an outcome with ProjectInfos on success */ virtual AZ::Outcome> GetProjects() = 0; + + /** + * Adds existing project on disk + * @param path the absolute path to the project + * @return true on success, false on failure + */ + virtual bool AddProject(const QString& path) = 0; + + /** + * Adds existing project on disk + * @param path the absolute path to the project + * @return true on success, false on failure + */ + virtual bool RemoveProject(const QString& path) = 0; /** * Update a project diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 223465f3c8..feaea4c172 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -36,6 +36,8 @@ set(FILES Source/PythonBindingsInterface.h Source/ProjectInfo.h Source/ProjectInfo.cpp + Source/ProjectUtils.h + Source/ProjectUtils.cpp Source/NewProjectSettingsScreen.h Source/NewProjectSettingsScreen.cpp Source/CreateProjectCtrl.h