diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 712c01aa02..6694168f2b 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -523,6 +523,14 @@ QProgressBar::chunk { background-color: #444444; } +#gemCatalogMenuButton { + qproperty-flat: true; + max-width:36px; + min-width:36px; + max-height:24px; + min-height:24px; +} + #GemCatalogHeaderLabel { font-size: 12px; color: #FFFFFF; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 0ecea215bf..5da163bafe 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -8,14 +8,13 @@ #include #include -#include - #include #include #include #include -#include #include +#include +#include namespace O3DE::ProjectManager { @@ -406,7 +405,6 @@ namespace O3DE::ProjectManager CartButton* cartButton = new CartButton(gemModel, downloadController); hLayout->addWidget(cartButton); - hLayout->addSpacing(16); // Separating line @@ -418,9 +416,9 @@ namespace O3DE::ProjectManager hLayout->addSpacing(16); QMenu* gemMenu = new QMenu(this); - m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos")); - - connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); }); + gemMenu->addAction( tr("Show Gem Repos"), [this]() { emit OpenGemsRepo(); }); + gemMenu->addSeparator(); + gemMenu->addAction( tr("Add Existing Gem"), [this]() { emit AddGem(); }); QPushButton* gemMenuButton = new QPushButton(this); gemMenuButton->setObjectName("gemCatalogMenuButton"); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index fa381e54ae..19adec5607 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -8,24 +8,23 @@ #pragma once -#include - #if !defined(Q_MOC_RUN) +#include #include #include #include #include -#include - #include -#include -#include -#include -#include -#include -#include +#include #endif +QT_FORWARD_DECLARE_CLASS(QPushButton) +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QVBoxLayout) +QT_FORWARD_DECLARE_CLASS(QHBoxLayout) +QT_FORWARD_DECLARE_CLASS(QHideEvent) +QT_FORWARD_DECLARE_CLASS(QMoveEvent) + namespace O3DE::ProjectManager { class CartOverlayWidget @@ -87,12 +86,11 @@ namespace O3DE::ProjectManager void ReinitForProject(); signals: + void AddGem(); void OpenGemsRepo(); - + private: AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr; inline constexpr static int s_height = 60; - - QAction* m_openGemReposAction = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 69c5844e84..b145363460 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -19,6 +19,10 @@ #include #include #include +#include +#include +#include +#include namespace O3DE::ProjectManager { @@ -74,6 +78,7 @@ namespace O3DE::ProjectManager void GemCatalogScreen::ReinitForProject(const QString& projectPath) { m_gemModel->clear(); + m_gemsToRegisterWithProject.clear(); FillModel(projectPath); if (m_filterWidget) @@ -90,6 +95,47 @@ namespace O3DE::ProjectManager connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); + connect( + m_headerWidget, &GemCatalogHeaderWidget::AddGem, + [&]() + { + EngineInfo engineInfo; + QString defaultPath; + + AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + engineInfo = engineInfoResult.GetValue(); + defaultPath = engineInfo.m_defaultGemsFolder; + } + + if (defaultPath.isEmpty()) + { + defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + } + + QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath)); + if (!directory.isEmpty()) + { + // register the gem to the o3de_manifest.json and to the project after the user confirms + // project creation/update + auto registerResult = PythonBindingsInterface::Get()->RegisterGem(directory); + if(!registerResult) + { + QMessageBox::critical(this, tr("Failed to add gem"), registerResult.GetError().c_str()); + } + else + { + m_gemsToRegisterWithProject.insert(directory); + AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(directory); + if (gemInfoResult) + { + m_gemModel->AddGem(gemInfoResult.GetValue()); + m_gemModel->UpdateGemDependencies(); + } + } + } + }); // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ @@ -251,6 +297,12 @@ namespace O3DE::ProjectManager return EnableDisableGemsResult::Failed; } + + // register external gems that were added with relative paths + if (m_gemsToRegisterWithProject.contains(gemPath)) + { + pythonBindings->RegisterGem(QDir(projectPath).relativeFilePath(gemPath), projectPath); + } } for (const QModelIndex& modelIndex : toBeRemoved) diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 62528ba942..8e9f31c710 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -18,6 +18,8 @@ #include #include #include +#include +#include #endif namespace O3DE::ProjectManager @@ -70,5 +72,6 @@ namespace O3DE::ProjectManager GemFilterWidget* m_filterWidget = nullptr; DownloadController* m_downloadController = nullptr; bool m_notificationsEnabled = true; + QSet m_gemsToRegisterWithProject; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index bc8773b0c8..f001195e85 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -556,6 +556,47 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(gemNames)); } + AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath) + { + bool registrationResult = false; + auto result = ExecuteWithLockErrorHandling( + [&] + { + auto externalProjectPath = projectPath.isEmpty() ? pybind11::none() : QString_To_Py_Path(projectPath); + auto pythonRegistrationResult = m_register.attr("register")( + pybind11::none(), // engine_path + pybind11::none(), // project_path + QString_To_Py_Path(gemPath), // gem folder + pybind11::none(), // external subdirectory + pybind11::none(), // template_path + pybind11::none(), // restricted folder + pybind11::none(), // repo uri + pybind11::none(), // default_engines_folder + pybind11::none(), // default_projects_folder + pybind11::none(), // default_gems_folder + pybind11::none(), // default_templates_folder + pybind11::none(), // default_restricted_folder + pybind11::none(), // default_third_party_folder + pybind11::none(), // external_subdir_engine_path + externalProjectPath // external_subdir_project_path + ); + + // Returns an exit code so boolify it then invert result + registrationResult = !pythonRegistrationResult.cast(); + }); + + if (!result.IsSuccess()) + { + return AZ::Failure(result.GetError().c_str()); + } + else if (!registrationResult) + { + return AZ::Failure(AZStd::string::format("Failed to register gem path %s", gemPath.toUtf8().constData())); + } + + return AZ::Success(); + } + bool PythonBindings::AddProject(const QString& path) { bool registrationResult = false; diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 638ce6b1d4..9d2c8c850f 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -42,6 +42,7 @@ namespace O3DE::ProjectManager AZ::Outcome, AZStd::string> GetEngineGemInfos() override; AZ::Outcome, AZStd::string> GetAllGemInfos(const QString& projectPath) override; AZ::Outcome, AZStd::string> GetEnabledGemNames(const QString& projectPath) override; + AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) override; // Project AZ::Outcome CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 19442540a4..6b9ac39213 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -91,6 +91,14 @@ namespace O3DE::ProjectManager */ virtual AZ::Outcome, AZStd::string> GetEnabledGemNames(const QString& projectPath) = 0; + /** + * Registers the gem to the specified project, or to the o3de_manifest.json if no project path is given + * @param gemPath the path to the gem + * @param projectPath the path to the project. If empty, will register the external path in o3de_manifest.json + * @return An outcome with the success flag as well as an error message in case of a failure. + */ + virtual AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) = 0; + // Projects diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index 0de161177c..6ad54a11f3 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -596,7 +596,7 @@ def register(engine_path: pathlib.Path = None, :param default_third_party_folder: default 3rd party cache folder :param external_subdir_engine_path: Path to the engine to use when registering an external subdirectory. The registration occurs in the engine.json file in this case - :param external_subdir_engine_path: Path to the project to use when registering an external subdirectory. + :param external_subdir_project_path: Path to the project to use when registering an external subdirectory. The registrations occurs in the project.json in this case :param remove: add/remove the entries :param force: force update of the engine_path for specified "engine_name" from the engine.json file