From d5ae98496ca8abc139c7cf30ccac04dc8cae45ae Mon Sep 17 00:00:00 2001 From: nggieber Date: Tue, 9 Nov 2021 10:26:37 -0800 Subject: [PATCH 1/5] Full UI Built for Updating and Uninstalling Gems Signed-off-by: nggieber --- .../Resources/ProjectManager.qss | 46 ++++++++++++++ .../Source/GemCatalog/GemCatalogScreen.cpp | 42 ++++++++++--- .../Source/GemCatalog/GemCatalogScreen.h | 3 + .../Source/GemCatalog/GemInfo.h | 2 +- .../Source/GemCatalog/GemInspector.cpp | 32 +++++++++- .../Source/GemCatalog/GemInspector.h | 9 ++- .../Source/GemCatalog/GemUninstallDialog.cpp | 60 +++++++++++++++++++ .../Source/GemCatalog/GemUninstallDialog.h | 25 ++++++++ .../Source/GemCatalog/GemUpdateDialog.cpp | 60 +++++++++++++++++++ .../Source/GemCatalog/GemUpdateDialog.h | 25 ++++++++ .../project_manager_files.cmake | 4 ++ 11 files changed, 297 insertions(+), 11 deletions(-) create mode 100644 Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp create mode 100644 Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h create mode 100644 Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp create mode 100644 Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index d3ec066be7..426f409581 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -563,6 +563,52 @@ QProgressBar::chunk { margin-top:5px; } +#gemCatalogUpdateGemButton, +#gemCatalogUninstallGemButton +{ + qproperty-flat: true; + min-height:24px; + max-height:24px; + border-radius: 3px; + text-align:center; + font-size:12px; + font-weight:600; +} + +#gemCatalogUpdateGemButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #888888, stop: 1.0 #555555); +} +#gemCatalogUpdateGemButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #999999, stop: 1.0 #666666); +} +#gemCatalogUpdateGemButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #555555, stop: 1.0 #777777); +} + +#footer > #gemCatalogUninstallGemButton, +#gemCatalogUninstallGemButton { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #E32C27, stop: 1.0 #951D21); +} +#footer > #gemCatalogUninstallGemButton:hover, +#gemCatalogUninstallGemButton:hover { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #FD3129, stop: 1.0 #AF2221); +} +#footer > #gemCatalogUninstallGemButton:pressed, +#gemCatalogUninstallGemButton:pressed { + background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, + stop: 0 #951D1F, stop: 1.0 #C92724); +} + +#gemCatalogDialogSubTitle { + font-size:14px; + font-weight:600; +} + /************** Filter Tag widget **************/ #FilterTagWidgetTextLabel { diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 1a3dc03230..1395dce764 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -12,7 +12,10 @@ #include #include #include +#include +#include #include + #include #include #include @@ -55,6 +58,8 @@ namespace O3DE::ProjectManager m_gemInspector->setFixedWidth(240); connect(m_gemInspector, &GemInspector::TagClicked, this, &GemCatalogScreen::SelectGem); + connect(m_gemInspector, &GemInspector::UpdateGem, this, &GemCatalogScreen::UpdateGem); + connect(m_gemInspector, &GemInspector::UninstallGem, this, &GemCatalogScreen::UninstallGem); QWidget* filterWidget = new QWidget(this); filterWidget->setFixedWidth(240); @@ -104,9 +109,10 @@ namespace O3DE::ProjectManager // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ - QModelIndex firstModelIndex = m_gemListView->model()->index(0,0); - m_gemListView->selectionModel()->select(firstModelIndex, QItemSelectionModel::ClearAndSelect); - }); + QModelIndex firstModelIndex = m_gemModel->index(0, 0); // m_gemListView->model()->index(0,0); + //m_gemListView->selectionModel()->select(firstModelIndex, QItemSelectionModel::ClearAndSelect); + m_gemModel->GetSelectionModel()->select(firstModelIndex, QItemSelectionModel::ClearAndSelect); + }); } void GemCatalogScreen::OnAddGemClicked() @@ -170,7 +176,7 @@ namespace O3DE::ProjectManager notification = GemModel::GetDisplayName(modelIndex); if (numChangedDependencies > 0) { - notification += " " + tr("and") + " "; + notification += tr(" and "); } if (added && GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded) { @@ -178,15 +184,15 @@ namespace O3DE::ProjectManager } } - if (numChangedDependencies == 1 ) + if (numChangedDependencies == 1) { - notification += "1 Gem " + tr("dependency"); + notification += tr("1 Gem dependency"); } else if (numChangedDependencies > 1) { - notification += QString("%1 Gem ").arg(numChangedDependencies) + tr("dependencies"); + notification += tr("%1 Gem %2").arg(QString(numChangedDependencies), tr("dependencies")); } - notification += " " + (added ? tr("activated") : tr("deactivated")); + notification += (added ? tr(" activated") : tr(" deactivated")); AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); toastConfiguration.m_customIconImage = ":/gem.svg"; @@ -210,6 +216,26 @@ namespace O3DE::ProjectManager m_gemListView->scrollTo(proxyIndex); } + void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex) + { + const QString selectedGemName = m_gemModel->GetDisplayName(modelIndex); + GemUpdateDialog* confirmUpdateDialog = new GemUpdateDialog(selectedGemName, this); + if (confirmUpdateDialog->exec() == QDialog::Accepted) + { + // Update Gem + } + } + + void GemCatalogScreen::UninstallGem(const QModelIndex& modelIndex) + { + const QString selectedGemName = m_gemModel->GetDisplayName(modelIndex); + GemUninstallDialog* confirmUninstallDialog = new GemUninstallDialog(selectedGemName, this); + if (confirmUninstallDialog->exec() == QDialog::Accepted) + { + // Uninstall Gem + } + } + void GemCatalogScreen::hideEvent(QHideEvent* event) { ScreenWidget::hideEvent(event); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 55fbb1befc..5a5a782254 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -49,6 +49,8 @@ namespace O3DE::ProjectManager void OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies); void OnAddGemClicked(); void SelectGem(const QString& gemName); + void UpdateGem(const QModelIndex& modelIndex); + void UninstallGem(const QModelIndex& modelIndex); protected: void hideEvent(QHideEvent* event) override; @@ -62,6 +64,7 @@ namespace O3DE::ProjectManager private: void FillModel(const QString& projectPath); + QModelIndex GetCurrentlySelectedGem(); AZStd::unique_ptr m_notificationsView; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h index 12cce5a4ea..bef9f6cd99 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h @@ -57,7 +57,7 @@ namespace O3DE::ProjectManager UnknownDownloadStatus = -1, NotDownloaded, Downloading, - Downloaded, + Downloaded }; static QString GetDownloadStatusString(DownloadStatus status); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp index b0b8cca29a..7b6947c040 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp @@ -14,6 +14,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -70,6 +71,8 @@ namespace O3DE::ProjectManager void GemInspector::Update(const QModelIndex& modelIndex) { + m_curModelIndex = modelIndex; + if (!modelIndex.isValid()) { m_mainWidget->hide(); @@ -122,6 +125,18 @@ namespace O3DE::ProjectManager m_lastUpdatedLabel->setText(tr("Last Updated: %1").arg(m_model->GetLastUpdated(modelIndex))); m_binarySizeLabel->setText(tr("Binary Size: %1 KB").arg(m_model->GetBinarySizeInKB(modelIndex))); + // Update and Uninstall buttons + if (m_model->GetGemOrigin(modelIndex) == GemInfo::Remote && m_model->GetDownloadStatus(modelIndex) == GemInfo::Downloaded) + { + m_updateGemButton->show(); + m_uninstallGemButton->show(); + } + else + { + m_updateGemButton->hide(); + m_uninstallGemButton->hide(); + } + m_mainWidget->adjustSize(); m_mainWidget->show(); } @@ -222,7 +237,7 @@ namespace O3DE::ProjectManager // Depending gems m_dependingGems = new GemsSubWidget(); - connect(m_dependingGems, &GemsSubWidget::TagClicked, this, [=](const QString& tag){ emit TagClicked(tag); }); + connect(m_dependingGems, &GemsSubWidget::TagClicked, this, [this](const QString& tag){ emit TagClicked(tag); }); m_mainLayout->addWidget(m_dependingGems); m_mainLayout->addSpacing(20); @@ -233,5 +248,20 @@ namespace O3DE::ProjectManager m_versionLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor); m_lastUpdatedLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor); m_binarySizeLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor); + + m_mainLayout->addSpacing(20); + + // Update and Uninstall buttons + m_updateGemButton = new QPushButton(tr("Update Gem")); + m_updateGemButton->setObjectName("gemCatalogUpdateGemButton"); + m_mainLayout->addWidget(m_updateGemButton); + connect(m_updateGemButton, &QPushButton::clicked, this , [this]{ emit UpdateGem(m_curModelIndex); }); + + m_mainLayout->addSpacing(10); + + m_uninstallGemButton = new QPushButton(tr("Uninstall Gem")); + m_uninstallGemButton->setObjectName("gemCatalogUninstallGemButton"); + m_mainLayout->addWidget(m_uninstallGemButton); + connect(m_uninstallGemButton, &QPushButton::clicked, this , [this]{ emit UninstallGem(m_curModelIndex); }); } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h index c6548527ab..4fb2375d6b 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h @@ -16,11 +16,12 @@ #include #include -#include #endif QT_FORWARD_DECLARE_CLASS(QVBoxLayout) QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QSpacerItem) +QT_FORWARD_DECLARE_CLASS(QPushButton) namespace O3DE::ProjectManager { @@ -45,6 +46,8 @@ namespace O3DE::ProjectManager signals: void TagClicked(const QString& tag); + void UpdateGem(const QModelIndex& modelIndex); + void UninstallGem(const QModelIndex& modelIndex); private slots: void OnSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected); @@ -55,6 +58,7 @@ namespace O3DE::ProjectManager GemModel* m_model = nullptr; QWidget* m_mainWidget = nullptr; QVBoxLayout* m_mainLayout = nullptr; + QModelIndex m_curModelIndex; // General info (top) section QLabel* m_nameLabel = nullptr; @@ -77,5 +81,8 @@ namespace O3DE::ProjectManager QLabel* m_versionLabel = nullptr; QLabel* m_lastUpdatedLabel = nullptr; QLabel* m_binarySizeLabel = nullptr; + + QPushButton* m_updateGemButton = nullptr; + QPushButton* m_uninstallGemButton = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp new file mode 100644 index 0000000000..6838741417 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + GemUninstallDialog::GemUninstallDialog(const QString& gemName, QWidget* parent) + : QDialog(parent) + { + setWindowTitle(tr("Uninstall Remote Gem")); + setObjectName("GemUninstallDialog"); + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setMargin(30); + layout->setAlignment(Qt::AlignTop); + setLayout(layout); + + // Body + QLabel* subTitleLabel = new QLabel(tr("Are you sure you want to uninstall %1?").arg(gemName)); + subTitleLabel->setObjectName("gemCatalogDialogSubTitle"); + layout->addWidget(subTitleLabel); + + layout->addSpacing(10); + + QLabel* bodyLabel = new QLabel(tr("The Gem and its related files will be uninstalled. This does not affect the Gem’s repository. " + "You can reinstall this Gem from the Catalog, but its contents may be subject to change.")); + bodyLabel->setWordWrap(true); + bodyLabel->setFixedWidth(440); + layout->addWidget(bodyLabel); + + layout->addSpacing(60); + + // Buttons + QDialogButtonBox* dialogButtons = new QDialogButtonBox(); + dialogButtons->setObjectName("footer"); + layout->addWidget(dialogButtons); + + QPushButton* cancelButton = dialogButtons->addButton(tr("Cancel"), QDialogButtonBox::RejectRole); + cancelButton->setProperty("secondary", true); + QPushButton* uninstallButton = dialogButtons->addButton(tr("Uninstall Gem"), QDialogButtonBox::ApplyRole); + uninstallButton->setObjectName("gemCatalogUninstallGemButton"); + + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); + connect(uninstallButton, &QPushButton::clicked, this, &QDialog::accept); + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h new file mode 100644 index 0000000000..9e3f4c3f3b --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +namespace O3DE::ProjectManager +{ + class GemUninstallDialog + : public QDialog + { + Q_OBJECT // AUTOMOC + public: + explicit GemUninstallDialog(const QString& gemName, QWidget *parent = nullptr); + ~GemUninstallDialog() = default; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp new file mode 100644 index 0000000000..8d8490ab40 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +#include +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + GemUpdateDialog::GemUpdateDialog(const QString& gemName, QWidget* parent) + : QDialog(parent) + { + setWindowTitle(tr("Update Remote Gem")); + setObjectName("GemUpdateDialog"); + setAttribute(Qt::WA_DeleteOnClose); + setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setMargin(30); + layout->setAlignment(Qt::AlignTop); + setLayout(layout); + + // Body + QLabel* subTitleLabel = new QLabel(tr("Update to the latest version of %1?").arg(gemName)); + subTitleLabel->setObjectName("gemCatalogDialogSubTitle"); + layout->addWidget(subTitleLabel); + + layout->addSpacing(10); + + QLabel* bodyLabel = new QLabel(tr("The latest version of this Gem may not be compatible with your engine. " + "Updating this Gem will remove any local changes made to this Gem, " + "and may remove old features that are in use.")); + bodyLabel->setWordWrap(true); + bodyLabel->setFixedWidth(440); + layout->addWidget(bodyLabel); + + layout->addSpacing(60); + + // Buttons + QDialogButtonBox* dialogButtons = new QDialogButtonBox(); + dialogButtons->setObjectName("footer"); + layout->addWidget(dialogButtons); + + QPushButton* cancelButton = dialogButtons->addButton(tr("Cancel"), QDialogButtonBox::RejectRole); + cancelButton->setProperty("secondary", true); + QPushButton* updateButton = dialogButtons->addButton(tr("Update Gem"), QDialogButtonBox::ApplyRole); + + connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); + connect(updateButton, &QPushButton::clicked, this, &QDialog::accept); + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h new file mode 100644 index 0000000000..1a2813d1a2 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +namespace O3DE::ProjectManager +{ + class GemUpdateDialog + : public QDialog + { + Q_OBJECT // AUTOMOC + public : + explicit GemUpdateDialog(const QString& gemName, QWidget* parent = nullptr); + ~GemUpdateDialog() = default; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index e2e35717f6..fcfae2f336 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -96,6 +96,10 @@ set(FILES Source/GemCatalog/GemListHeaderWidget.cpp Source/GemCatalog/GemModel.h Source/GemCatalog/GemModel.cpp + Source/GemCatalog/GemUninstallDialog.h + Source/GemCatalog/GemUninstallDialog.cpp + Source/GemCatalog/GemUpdateDialog.h + Source/GemCatalog/GemUpdateDialog.cpp Source/GemCatalog/GemDependenciesDialog.h Source/GemCatalog/GemDependenciesDialog.cpp Source/GemCatalog/GemRequirementDialog.h From f470351d0c5a32ad9859c6d111d00338f4738311 Mon Sep 17 00:00:00 2001 From: nggieber Date: Wed, 10 Nov 2021 12:33:24 -0800 Subject: [PATCH 2/5] Hook up Gem Updating and Uninstallation and fix lots of minor bugs between gem catalog and gem repos Signed-off-by: nggieber --- .../ProjectManager/Source/DownloadWorker.cpp | 2 +- .../GemCatalog/GemCatalogHeaderWidget.cpp | 1 + .../GemCatalog/GemCatalogHeaderWidget.h | 1 + .../Source/GemCatalog/GemCatalogScreen.cpp | 83 +++++++++++++++++-- .../Source/GemCatalog/GemInfo.h | 1 + .../Source/GemCatalog/GemInspector.cpp | 2 +- .../Source/GemCatalog/GemModel.cpp | 25 ++++++ .../Source/GemCatalog/GemModel.h | 4 +- .../Source/GemCatalog/GemUninstallDialog.cpp | 4 +- .../Source/GemCatalog/GemUpdateDialog.cpp | 18 ++-- .../Source/GemCatalog/GemUpdateDialog.h | 2 +- .../ProjectManager/Source/PythonBindings.cpp | 69 ++++++++++----- .../ProjectManager/Source/PythonBindings.h | 7 +- .../Source/PythonBindingsInterface.h | 41 +++++---- scripts/o3de/o3de/repo.py | 13 +-- scripts/o3de/o3de/utils.py | 6 +- 16 files changed, 206 insertions(+), 73 deletions(-) diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp index 9bda1b34cc..5f5c64987b 100644 --- a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp @@ -25,7 +25,7 @@ namespace O3DE::ProjectManager m_downloadProgress = downloadProgress; emit UpdateProgress(downloadProgress); }; - AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress); + AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress, true); if (gemInfoResult.IsSuccess()) { emit Done(""); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 8c875e4846..def02ea9ba 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -429,6 +429,7 @@ namespace O3DE::ProjectManager hLayout->addSpacing(16); QMenu* gemMenu = new QMenu(this); + gemMenu->addAction( tr("Refresh"), [this]() { emit RefreshGems(); }); gemMenu->addAction( tr("Show Gem Repos"), [this]() { emit OpenGemsRepo(); }); gemMenu->addSeparator(); gemMenu->addAction( tr("Add Existing Gem"), [this]() { emit AddGem(); }); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index 6da78cce7a..a0ed7c70f9 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -89,6 +89,7 @@ namespace O3DE::ProjectManager signals: void AddGem(); void OpenGemsRepo(); + void RefreshGems(); private: AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 29f73e5651..6758010cf5 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -50,6 +51,7 @@ namespace O3DE::ProjectManager vLayout->addWidget(m_headerWidget); connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); + connect(m_headerWidget, &GemCatalogHeaderWidget::RefreshGems, this, &GemCatalogScreen::Refresh); connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo); connect(m_headerWidget, &GemCatalogHeaderWidget::AddGem, this, &GemCatalogScreen::OnAddGemClicked); connect(m_downloadController, &DownloadController::Done, this, &GemCatalogScreen::OnGemDownloadResult); @@ -235,6 +237,9 @@ namespace O3DE::ProjectManager // temporary, until we can refresh filter counts m_proxyModel->ResetFilters(); m_filterWidget->ResetAllFilters(); + + // Reselect the same selection to proc UI updates + m_proxyModel->GetSelectionModel()->select(m_proxyModel->GetSelectionModel()->selection(), QItemSelectionModel::Select); } void GemCatalogScreen::OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies) @@ -300,21 +305,83 @@ namespace O3DE::ProjectManager void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex) { - const QString selectedGemName = m_gemModel->GetDisplayName(modelIndex); - GemUpdateDialog* confirmUpdateDialog = new GemUpdateDialog(selectedGemName, this); + const QString selectedGemName = m_gemModel->GetName(modelIndex); + const QString selectedGemLastUpdate = m_gemModel->GetLastUpdated(modelIndex); + const QString selectedDisplayGemName = m_gemModel->GetDisplayName(modelIndex); + const QString selectedGemRepoUri = m_gemModel->GetRepoUri(modelIndex); + + // Refresh gem repo + if (!selectedGemRepoUri.isEmpty()) + { + AZ::Outcome refreshResult = PythonBindingsInterface::Get()->RefreshGemRepo(selectedGemRepoUri); + if (refreshResult.IsSuccess()) + { + Refresh(); + } + else + { + QMessageBox::critical( + this, tr("Operation failed"), + tr("Failed to refresh gem repo %1
Error:
%2").arg(selectedGemRepoUri, refreshResult.GetError().c_str())); + } + } + // If repo uri isn't specified warn user that repo might not be refreshed + else + { + int result = QMessageBox::warning( + this, tr("Gem Repo Unspecified"), + tr("The repo for %1 is unspecfied. Repo cannot be automatically refreshed. " + "Please ensure this gem's repo is refreshed before attempting to update.") + .arg(selectedDisplayGemName), + QMessageBox::Cancel, QMessageBox::Ok); + + // Allow user to cancel update to manually refresh repo + if (result != QMessageBox::Ok) + { + return; + } + } + + // Check if there is an update avaliable now that repo is refreshed + bool updateAvaliable = PythonBindingsInterface::Get()->IsGemUpdateAvaliable(selectedGemName, selectedGemLastUpdate); + + GemUpdateDialog* confirmUpdateDialog = new GemUpdateDialog(selectedGemName, updateAvaliable, this); if (confirmUpdateDialog->exec() == QDialog::Accepted) { - // Update Gem + m_downloadController->AddGemDownload(selectedGemName); } } void GemCatalogScreen::UninstallGem(const QModelIndex& modelIndex) { - const QString selectedGemName = m_gemModel->GetDisplayName(modelIndex); - GemUninstallDialog* confirmUninstallDialog = new GemUninstallDialog(selectedGemName, this); + const QString selectedDisplayGemName = m_gemModel->GetDisplayName(modelIndex); + + GemUninstallDialog* confirmUninstallDialog = new GemUninstallDialog(selectedDisplayGemName, this); if (confirmUninstallDialog->exec() == QDialog::Accepted) { - // Uninstall Gem + const QString selectedGemPath = m_gemModel->GetPath(modelIndex); + + // Unregister the gem + auto unregisterResult = PythonBindingsInterface::Get()->RegisterGem(selectedGemPath, {}, /*remove*/true); + if (!unregisterResult) + { + QMessageBox::critical(this, tr("Failed to unregister gem"), unregisterResult.GetError().c_str()); + } + else + { + // Remove gem from model + m_gemModel->removeRow(modelIndex.row()); + + // Delete uninstalled gem directory + if (!ProjectUtils::DeleteProjectFiles(selectedGemPath, /*force*/true)) + { + QMessageBox::critical( + this, tr("Failed to remove gem directory"), tr("Could not delete gem directory at:
%1").arg(selectedGemPath)); + } + + // Show undownloaded remote gem again + Refresh(); + } } } @@ -351,8 +418,10 @@ namespace O3DE::ProjectManager { // Add all available gems to the model. const QVector& allGemInfos = allGemInfosResult.GetValue(); - for (const GemInfo& gemInfo : allGemInfos) + for (GemInfo gemInfo : allGemInfos) { + // Mark as downloaded because this gem was registered with an existing directory + gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded; m_gemModel->AddGem(gemInfo); } diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h index bef9f6cd99..b3b29d6ee6 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h @@ -85,6 +85,7 @@ namespace O3DE::ProjectManager QString m_licenseLink; QString m_directoryLink; QString m_documentationLink; + QString m_repoUri; QString m_version = "Unknown Version"; QString m_lastUpdatedDate = "Unknown Date"; int m_binarySizeInKB = 0; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp index f0f5adebaa..52e078eacd 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp @@ -238,7 +238,7 @@ namespace O3DE::ProjectManager // Depending gems m_dependingGems = new GemsSubWidget(); - connect(m_dependingGems, &GemsSubWidget::TagClicked, this, [this](const QString& tag){ emit TagClicked(tag); }); + connect(m_dependingGems, &GemsSubWidget::TagClicked, this, [this](const Tag& tag){ emit TagClicked(tag); }); m_mainLayout->addWidget(m_dependingGems); m_mainLayout->addSpacing(20); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 88c54de0b3..886b9e57c7 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -61,6 +61,7 @@ namespace O3DE::ProjectManager item->setData(gemInfo.m_downloadStatus, RoleDownloadStatus); item->setData(gemInfo.m_licenseText, RoleLicenseText); item->setData(gemInfo.m_licenseLink, RoleLicenseLink); + item->setData(gemInfo.m_repoUri, RoleRepoUri); appendRow(item); @@ -255,6 +256,11 @@ namespace O3DE::ProjectManager return modelIndex.data(RoleLicenseLink).toString(); } + QString GemModel::GetRepoUri(const QModelIndex& modelIndex) + { + return modelIndex.data(RoleRepoUri).toString(); + } + GemModel* GemModel::GetSourceModel(QAbstractItemModel* model) { GemSortFilterProxyModel* proxyModel = qobject_cast(model); @@ -369,11 +375,30 @@ namespace O3DE::ProjectManager void GemModel::OnRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { + bool selectedRowRemoved = false; for (int i = first; i <= last; ++i) { QModelIndex modelIndex = index(i, 0, parent); const QString& gemName = GetName(modelIndex); m_nameToIndexMap.remove(gemName); + + if (GetSelectionModel()->isRowSelected(i)) + { + selectedRowRemoved = true; + } + } + + // Select a valid row if currently selected row was removed + if (selectedRowRemoved) + { + for (const QModelIndex& index : m_nameToIndexMap) + { + if (index.isValid()) + { + GetSelectionModel()->select(index, QItemSelectionModel::ClearAndSelect); + break; + } + } } } diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index e25a1c7703..87a718d8c7 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -51,7 +51,8 @@ namespace O3DE::ProjectManager RoleRequirement, RoleDownloadStatus, RoleLicenseText, - RoleLicenseLink + RoleLicenseLink, + RoleRepoUri }; void AddGem(const GemInfo& gemInfo); @@ -80,6 +81,7 @@ namespace O3DE::ProjectManager static QString GetRequirement(const QModelIndex& modelIndex); static QString GetLicenseText(const QModelIndex& modelIndex); static QString GetLicenseLink(const QModelIndex& modelIndex); + static QString GetRepoUri(const QModelIndex& modelIndex); static GemModel* GetSourceModel(QAbstractItemModel* model); static const GemModel* GetSourceModel(const QAbstractItemModel* model); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp index 6838741417..6b2f14e551 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp @@ -39,10 +39,10 @@ namespace O3DE::ProjectManager QLabel* bodyLabel = new QLabel(tr("The Gem and its related files will be uninstalled. This does not affect the Gem’s repository. " "You can reinstall this Gem from the Catalog, but its contents may be subject to change.")); bodyLabel->setWordWrap(true); - bodyLabel->setFixedWidth(440); + bodyLabel->setFixedSize(QSize(440, 80)); layout->addWidget(bodyLabel); - layout->addSpacing(60); + layout->addSpacing(40); // Buttons QDialogButtonBox* dialogButtons = new QDialogButtonBox(); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp index 8d8490ab40..dab4a2a0fe 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp @@ -16,7 +16,7 @@ namespace O3DE::ProjectManager { - GemUpdateDialog::GemUpdateDialog(const QString& gemName, QWidget* parent) + GemUpdateDialog::GemUpdateDialog(const QString& gemName, bool updateAvaliable, QWidget* parent) : QDialog(parent) { setWindowTitle(tr("Update Remote Gem")); @@ -30,20 +30,23 @@ namespace O3DE::ProjectManager setLayout(layout); // Body - QLabel* subTitleLabel = new QLabel(tr("Update to the latest version of %1?").arg(gemName)); + QLabel* subTitleLabel = new QLabel(tr("%1Update to the latest version of %2?").arg( + updateAvaliable ? "" : tr("Force "), gemName)); subTitleLabel->setObjectName("gemCatalogDialogSubTitle"); layout->addWidget(subTitleLabel); layout->addSpacing(10); - QLabel* bodyLabel = new QLabel(tr("The latest version of this Gem may not be compatible with your engine. " + QLabel* bodyLabel = new QLabel(tr("%1The latest version of this Gem may not be compatible with your engine. " "Updating this Gem will remove any local changes made to this Gem, " - "and may remove old features that are in use.")); + "and may remove old features that are in use.").arg( + updateAvaliable ? "" : tr("No update detected for Gem. " + "This will force a redownload of the gem anyways. "))); bodyLabel->setWordWrap(true); - bodyLabel->setFixedWidth(440); + bodyLabel->setFixedSize(QSize(440, 80)); layout->addWidget(bodyLabel); - layout->addSpacing(60); + layout->addSpacing(40); // Buttons QDialogButtonBox* dialogButtons = new QDialogButtonBox(); @@ -52,7 +55,8 @@ namespace O3DE::ProjectManager QPushButton* cancelButton = dialogButtons->addButton(tr("Cancel"), QDialogButtonBox::RejectRole); cancelButton->setProperty("secondary", true); - QPushButton* updateButton = dialogButtons->addButton(tr("Update Gem"), QDialogButtonBox::ApplyRole); + QPushButton* updateButton = + dialogButtons->addButton(tr("%1Update Gem").arg(updateAvaliable ? "" : tr("Force ")), QDialogButtonBox::ApplyRole); connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject); connect(updateButton, &QPushButton::clicked, this, &QDialog::accept); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h index 1a2813d1a2..cf34abfb3d 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.h @@ -19,7 +19,7 @@ namespace O3DE::ProjectManager { Q_OBJECT // AUTOMOC public : - explicit GemUpdateDialog(const QString& gemName, QWidget* parent = nullptr); + explicit GemUpdateDialog(const QString& gemName, bool updateAvaliable = true, QWidget* parent = nullptr); ~GemUpdateDialog() = default; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 021066e1c7..ef7daa761e 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -560,7 +560,7 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(gemNames)); } - AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath) + AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath, bool remove) { bool registrationResult = false; auto result = ExecuteWithLockErrorHandling( @@ -582,7 +582,8 @@ namespace O3DE::ProjectManager pybind11::none(), // default_restricted_folder pybind11::none(), // default_third_party_folder pybind11::none(), // external_subdir_engine_path - externalProjectPath // external_subdir_project_path + externalProjectPath, // external_subdir_project_path + remove // remove ); // Returns an exit code so boolify it then invert result @@ -715,6 +716,7 @@ namespace O3DE::ProjectManager gemInfo.m_documentationLink = Py_To_String_Optional(data, "documentation_url", ""); gemInfo.m_licenseText = Py_To_String_Optional(data, "license", "Unspecified License"); gemInfo.m_licenseLink = Py_To_String_Optional(data, "license_url", ""); + gemInfo.m_repoUri = Py_To_String_Optional(data, "repo_uri", ""); if (gemInfo.m_creator.contains("Open 3D Engine")) { @@ -728,6 +730,11 @@ namespace O3DE::ProjectManager { gemInfo.m_gemOrigin = GemInfo::GemOrigin::Remote; } + // If no origin was provided this cannot be remote and would be specified if O3DE so it should be local + else + { + gemInfo.m_gemOrigin = GemInfo::GemOrigin::Local; + } // As long Base Open3DEngine gems are installed before first startup non-remote gems will be downloaded if (gemInfo.m_gemOrigin != GemInfo::GemOrigin::Remote) @@ -1166,7 +1173,34 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(gemRepos)); } - AZ::Outcome PythonBindings::DownloadGem(const QString& gemName, std::function gemProgressCallback) + AZ::Outcome, AZStd::string> PythonBindings::GetAllGemRepoGemsInfos() + { + QVector gemInfos; + AZ::Outcome result = ExecuteWithLockErrorHandling( + [&] + { + auto gemPaths = m_repo.attr("get_gem_json_paths_from_all_cached_repos")(); + + if (pybind11::isinstance(gemPaths)) + { + for (auto path : gemPaths) + { + GemInfo gemInfo = GemInfoFromPath(path, pybind11::none()); + gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded; + gemInfos.push_back(gemInfo); + } + } + }); + + if (!result.IsSuccess()) + { + return AZ::Failure(result.GetError()); + } + + return AZ::Success(AZStd::move(gemInfos)); + } + + AZ::Outcome PythonBindings::DownloadGem(const QString& gemName, std::function gemProgressCallback, bool force) { // This process is currently limited to download a single gem at a time. bool downloadSucceeded = false; @@ -1179,7 +1213,7 @@ namespace O3DE::ProjectManager QString_To_Py_String(gemName), // gem name pybind11::none(), // destination path false, // skip auto register - false, // force + force, // force overwrite pybind11::cpp_function( [this, gemProgressCallback](int progress) { @@ -1209,30 +1243,19 @@ namespace O3DE::ProjectManager m_requestCancelDownload = true; } - AZ::Outcome, AZStd::string> PythonBindings::GetAllGemRepoGemsInfos() + bool PythonBindings::IsGemUpdateAvaliable(const QString& gemName, const QString& lastUpdated) { - QVector gemInfos; - AZ::Outcome result = ExecuteWithLockErrorHandling( + bool updateAvaliableResult = false; + bool result = ExecuteWithLock( [&] { - auto gemPaths = m_repo.attr("get_gem_json_paths_from_all_cached_repos")(); + auto pyGemName = QString_To_Py_String(gemName); + auto pyLastUpdated = QString_To_Py_String(lastUpdated); + auto pythonUpdateAvaliableResult = m_download.attr("is_o3de_gem_update_available")(pyGemName, pyLastUpdated); - if (pybind11::isinstance(gemPaths)) - { - for (auto path : gemPaths) - { - GemInfo gemInfo = GemInfoFromPath(path, pybind11::none()); - gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded; - gemInfos.push_back(gemInfo); - } - } + updateAvaliableResult = pythonUpdateAvaliableResult.cast(); }); - if (!result.IsSuccess()) - { - return AZ::Failure(result.GetError()); - } - - return AZ::Success(AZStd::move(gemInfos)); + return result && updateAvaliableResult; } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index 4375d56d02..8a22344596 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -42,7 +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; + AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}, bool remove = false) override; // Project AZ::Outcome CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; @@ -64,9 +64,10 @@ namespace O3DE::ProjectManager bool AddGemRepo(const QString& repoUri) override; bool RemoveGemRepo(const QString& repoUri) override; AZ::Outcome, AZStd::string> GetAllGemRepoInfos() override; - AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) override; - void CancelDownload() override; AZ::Outcome, AZStd::string> GetAllGemRepoGemsInfos() override; + AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback, bool force = false) override; + void CancelDownload() override; + bool IsGemUpdateAvaliable(const QString& gemName, const QString& lastUpdated) override; private: AZ_DISABLE_COPY_MOVE(PythonBindings); diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 1134804f1f..07d4551c60 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -94,10 +94,11 @@ namespace O3DE::ProjectManager /** * 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 + * @param projectPath the path to the project. If empty, will register the external path in o3de_manifest.json + * @param remove Unregister instead of registering this gem * @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; + virtual AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}, bool remove = false) = 0; // Projects @@ -209,24 +210,34 @@ namespace O3DE::ProjectManager */ virtual AZ::Outcome, AZStd::string> GetAllGemRepoInfos() = 0; - /** - * Downloads and registers a Gem. - * @param gemName the name of the Gem to download - * @param gemProgressCallback a callback function that is called with an int percentage download value - * @return an outcome with a string error message on failure. - */ - virtual AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) = 0; - - /** - * Cancels the current download. - */ - virtual void CancelDownload() = 0; - /** * Gathers all gem infos for all gems registered from repos. * @return A list of gem infos. */ virtual AZ::Outcome, AZStd::string> GetAllGemRepoGemsInfos() = 0; + + /** + * Downloads and registers a Gem. + * @param gemName the name of the Gem to download. + * @param gemProgressCallback a callback function that is called with an int percentage download value. + * @param force should we forcibly overwrite the old version of the gem. + * @return an outcome with a string error message on failure. + */ + virtual AZ::Outcome DownloadGem( + const QString& gemName, std::function gemProgressCallback, bool force = false) = 0; + + /** + * Cancels the current download. + */ + virtual void CancelDownload() = 0; + + /** + * Checks if there is an update avaliable for a gem on a repo. + * @param gemName the name of the gem to check. + * @param lastUpdated last time the gem was update. + * @return true if update is avaliable, false if not. + */ + virtual bool IsGemUpdateAvaliable(const QString& gemName, const QString& lastUpdated) = 0; }; using PythonBindingsInterface = AZ::Interface; diff --git a/scripts/o3de/o3de/repo.py b/scripts/o3de/o3de/repo.py index a6b1505761..1462015630 100644 --- a/scripts/o3de/o3de/repo.py +++ b/scripts/o3de/o3de/repo.py @@ -74,9 +74,9 @@ def process_add_o3de_repo(file_name: str or pathlib.Path, manifest_json_uri = f'{o3de_object_uri}/{manifest_json}' manifest_json_sha256 = hashlib.sha256(manifest_json_uri.encode()) cache_file = cache_folder / str(manifest_json_sha256.hexdigest() + '.json') - if not cache_file.is_file(): + if cache_file.is_file(): parsed_uri = urllib.parse.urlparse(manifest_json_uri) - download_file_result = utils.download_file(parsed_uri, cache_file) + download_file_result = utils.download_file(parsed_uri, cache_file, True) if download_file_result != 0: return download_file_result @@ -96,7 +96,7 @@ def process_add_o3de_repo(file_name: str or pathlib.Path, cache_file = cache_folder / str(manifest_json_sha256.hexdigest() + '.json') if cache_file.is_file(): cache_file.unlink() - download_file_result = utils.download_file(parsed_uri, cache_file) + download_file_result = utils.download_file(parsed_uri, cache_file, True) if download_file_result != 0: return download_file_result @@ -165,7 +165,7 @@ def refresh_repo(repo_uri: str, repo_sha256 = hashlib.sha256(parsed_uri.geturl().encode()) cache_file = cache_folder / str(repo_sha256.hexdigest() + '.json') - download_file_result = utils.download_file(parsed_uri, cache_file) + download_file_result = utils.download_file(parsed_uri, cache_file, True) if download_file_result != 0: return download_file_result @@ -178,12 +178,7 @@ def refresh_repo(repo_uri: str, def refresh_repos() -> int: json_data = manifest.load_o3de_manifest() - - # clear the cache cache_folder = manifest.get_o3de_cache_folder() - shutil.rmtree(cache_folder) - cache_folder = manifest.get_o3de_cache_folder() # will recreate it - result = 0 # set will stop circular references diff --git a/scripts/o3de/o3de/utils.py b/scripts/o3de/o3de/utils.py index 56f9ae4bcd..71929af7b5 100755 --- a/scripts/o3de/o3de/utils.py +++ b/scripts/o3de/o3de/utils.py @@ -117,7 +117,7 @@ def backup_folder(folder: str or pathlib.Path) -> None: if backup_folder_name.is_dir(): renamed = True -def download_file(parsed_uri, download_path: pathlib.Path, force_overwrite, download_progress_callback = None) -> int: +def download_file(parsed_uri, download_path: pathlib.Path, force_overwrite: bool = False, download_progress_callback = None) -> int: """ :param parsed_uri: uniform resource identifier to zip file to download :param download_path: location path on disk to download file @@ -128,7 +128,7 @@ def download_file(parsed_uri, download_path: pathlib.Path, force_overwrite, down logger.warn(f'File already downloaded to {download_path}.') else: try: - shutil.rmtree(download_path) + os.unlink(download_path) except OSError: logger.error(f'Could not remove existing download path {download_path}.') return 1 @@ -162,7 +162,7 @@ def download_zip_file(parsed_uri, download_zip_path: pathlib.Path, download_prog :param parsed_uri: uniform resource identifier to zip file to download :param download_zip_path: path to output zip file """ - download_file_result = download_file(parsed_uri, download_zip_path, download_progress_callback) + download_file_result = download_file(parsed_uri, download_zip_path, True, download_progress_callback) if download_file_result != 0: return download_file_result From d9443ec42ce8039f3e7c75a26835b1ab8c12426a Mon Sep 17 00:00:00 2001 From: nggieber Date: Wed, 10 Nov 2021 15:29:20 -0800 Subject: [PATCH 3/5] Addressed some PR feedback and continue filtering with same search string even after refresh Signed-off-by: nggieber --- Code/Tools/ProjectManager/Source/DownloadWorker.cpp | 4 +++- .../Source/GemCatalog/GemCatalogScreen.cpp | 10 +++++----- .../Source/GemCatalog/GemSortFilterProxyModel.cpp | 7 +++++-- .../Source/GemCatalog/GemSortFilterProxyModel.h | 2 +- .../Source/GemCatalog/GemUpdateDialog.cpp | 6 +++--- scripts/o3de/o3de/repo.py | 10 +++++----- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp index 5f5c64987b..a71a8dd486 100644 --- a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp @@ -25,7 +25,9 @@ namespace O3DE::ProjectManager m_downloadProgress = downloadProgress; emit UpdateProgress(downloadProgress); }; - AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress, true); + AZ::Outcome gemInfoResult = + PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress, /*force*/true); + if (gemInfoResult.IsSuccess()) { emit Done(""); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 3a513fdc1d..cff9a28db3 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -106,7 +106,7 @@ namespace O3DE::ProjectManager FillModel(projectPath); - m_proxyModel->ResetFilters(); + m_proxyModel->ResetFilters(false); m_proxyModel->sort(/*column=*/0); if (m_filterWidget) @@ -235,7 +235,7 @@ namespace O3DE::ProjectManager m_proxyModel->sort(/*column=*/0); // temporary, until we can refresh filter counts - m_proxyModel->ResetFilters(); + m_proxyModel->ResetFilters(false); m_filterWidget->ResetAllFilters(); // Reselect the same selection to proc UI updates @@ -323,15 +323,15 @@ namespace O3DE::ProjectManager { QMessageBox::critical( this, tr("Operation failed"), - tr("Failed to refresh gem repo %1
Error:
%2").arg(selectedGemRepoUri, refreshResult.GetError().c_str())); + tr("Failed to refresh gem repository %1
Error:
%2").arg(selectedGemRepoUri, refreshResult.GetError().c_str())); } } // If repo uri isn't specified warn user that repo might not be refreshed else { int result = QMessageBox::warning( - this, tr("Gem Repo Unspecified"), - tr("The repo for %1 is unspecfied. Repo cannot be automatically refreshed. " + this, tr("Gem Repository Unspecified"), + tr("The repo for %1 is unspecfied. Repository cannot be automatically refreshed. " "Please ensure this gem's repo is refreshed before attempting to update.") .arg(selectedDisplayGemName), QMessageBox::Cancel, QMessageBox::Ok); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp index 32d0e2fee9..a32492cf1e 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp @@ -204,9 +204,12 @@ namespace O3DE::ProjectManager emit OnInvalidated(); } - void GemSortFilterProxyModel::ResetFilters() + void GemSortFilterProxyModel::ResetFilters(bool clearSearchString) { - m_searchString.clear(); + if (clearSearchString) + { + m_searchString.clear(); + } m_gemSelectedFilter = GemSelected::NoFilter; m_gemActiveFilter = GemActive::NoFilter; m_gemOriginFilter = {}; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h index ab739e62f9..0c58d66ccf 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h @@ -70,7 +70,7 @@ namespace O3DE::ProjectManager void SetFeatures(const QSet& features) { m_featureFilter = features; InvalidateFilter(); } void InvalidateFilter(); - void ResetFilters(); + void ResetFilters(bool clearSearchString = true); signals: void OnInvalidated(); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp index dab4a2a0fe..3e68c597de 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp @@ -30,8 +30,8 @@ namespace O3DE::ProjectManager setLayout(layout); // Body - QLabel* subTitleLabel = new QLabel(tr("%1Update to the latest version of %2?").arg( - updateAvaliable ? "" : tr("Force "), gemName)); + QLabel* subTitleLabel = new QLabel(tr("%1 to the latest version of %2?").arg( + updateAvaliable ? tr("Update") : tr("Force update"), gemName)); subTitleLabel->setObjectName("gemCatalogDialogSubTitle"); layout->addWidget(subTitleLabel); @@ -46,7 +46,7 @@ namespace O3DE::ProjectManager bodyLabel->setFixedSize(QSize(440, 80)); layout->addWidget(bodyLabel); - layout->addSpacing(40); + layout->addSpacing(); // Buttons QDialogButtonBox* dialogButtons = new QDialogButtonBox(); diff --git a/scripts/o3de/o3de/repo.py b/scripts/o3de/o3de/repo.py index 1462015630..c8fac38605 100644 --- a/scripts/o3de/o3de/repo.py +++ b/scripts/o3de/o3de/repo.py @@ -74,11 +74,11 @@ def process_add_o3de_repo(file_name: str or pathlib.Path, manifest_json_uri = f'{o3de_object_uri}/{manifest_json}' manifest_json_sha256 = hashlib.sha256(manifest_json_uri.encode()) cache_file = cache_folder / str(manifest_json_sha256.hexdigest() + '.json') - if cache_file.is_file(): - parsed_uri = urllib.parse.urlparse(manifest_json_uri) - download_file_result = utils.download_file(parsed_uri, cache_file, True) - if download_file_result != 0: - return download_file_result + + parsed_uri = urllib.parse.urlparse(manifest_json_uri) + download_file_result = utils.download_file(parsed_uri, cache_file, True) + if download_file_result != 0: + return download_file_result # Having a repo is also optional repo_list = [] From d71196102f875150b03cd0c9c550fbdc356bb989 Mon Sep 17 00:00:00 2001 From: nggieber Date: Thu, 11 Nov 2021 10:20:46 -0800 Subject: [PATCH 4/5] More PR feedback changes Signed-off-by: nggieber --- .../Source/GemCatalog/GemCatalogScreen.cpp | 6 ++---- .../Source/GemCatalog/GemUpdateDialog.cpp | 4 ++-- .../ProjectManager/Source/PythonBindings.cpp | 21 ++++++++++++++++--- .../ProjectManager/Source/PythonBindings.h | 4 +++- .../Source/PythonBindingsInterface.h | 11 ++++++++-- 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index cff9a28db3..7004fe24d4 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -363,7 +363,7 @@ namespace O3DE::ProjectManager const QString selectedGemPath = m_gemModel->GetPath(modelIndex); // Unregister the gem - auto unregisterResult = PythonBindingsInterface::Get()->RegisterGem(selectedGemPath, {}, /*remove*/true); + auto unregisterResult = PythonBindingsInterface::Get()->UnregisterGem(selectedGemPath); if (!unregisterResult) { QMessageBox::critical(this, tr("Failed to unregister gem"), unregisterResult.GetError().c_str()); @@ -419,10 +419,8 @@ namespace O3DE::ProjectManager { // Add all available gems to the model. const QVector& allGemInfos = allGemInfosResult.GetValue(); - for (GemInfo gemInfo : allGemInfos) + for (const GemInfo& gemInfo : allGemInfos) { - // Mark as downloaded because this gem was registered with an existing directory - gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded; m_gemModel->AddGem(gemInfo); } diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp index 3e68c597de..82d205aab5 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUpdateDialog.cpp @@ -41,12 +41,12 @@ namespace O3DE::ProjectManager "Updating this Gem will remove any local changes made to this Gem, " "and may remove old features that are in use.").arg( updateAvaliable ? "" : tr("No update detected for Gem. " - "This will force a redownload of the gem anyways. "))); + "This will force a re-download of the gem. "))); bodyLabel->setWordWrap(true); bodyLabel->setFixedSize(QSize(440, 80)); layout->addWidget(bodyLabel); - layout->addSpacing(); + layout->addSpacing(40); // Buttons QDialogButtonBox* dialogButtons = new QDialogButtonBox(); diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index ef7daa761e..e0208a266c 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -515,7 +515,11 @@ namespace O3DE::ProjectManager auto pyProjectPath = QString_To_Py_Path(projectPath); for (auto path : m_manifest.attr("get_all_gems")(pyProjectPath)) { - gems.push_back(GemInfoFromPath(path, pyProjectPath)); + GemInfo gemInfo = GemInfoFromPath(path, pyProjectPath); + // Mark as downloaded because this gem was registered with an existing directory + gemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded; + + gems.push_back(AZStd::move(gemInfo)); } }); if (!result.IsSuccess()) @@ -560,7 +564,7 @@ namespace O3DE::ProjectManager return AZ::Success(AZStd::move(gemNames)); } - AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath, bool remove) + AZ::Outcome PythonBindings::GemRegistration(const QString& gemPath, const QString& projectPath, bool remove) { bool registrationResult = false; auto result = ExecuteWithLockErrorHandling( @@ -596,12 +600,23 @@ namespace O3DE::ProjectManager } else if (!registrationResult) { - return AZ::Failure(AZStd::string::format("Failed to register gem path %s", gemPath.toUtf8().constData())); + return AZ::Failure(AZStd::string::format( + "Failed to %s gem path %s", remove ? "unregister" : "register", gemPath.toUtf8().constData())); } return AZ::Success(); } + AZ::Outcome PythonBindings::RegisterGem(const QString& gemPath, const QString& projectPath) + { + return GemRegistration(gemPath, projectPath); + } + + AZ::Outcome PythonBindings::UnregisterGem(const QString& gemPath, const QString& projectPath) + { + return GemRegistration(gemPath, projectPath, /*remove*/true); + } + 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 8a22344596..d258898fae 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -42,7 +42,8 @@ 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 = {}, bool remove = false) override; + AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) override; + AZ::Outcome UnregisterGem(const QString& gemPath, const QString& projectPath = {}) override; // Project AZ::Outcome CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo) override; @@ -78,6 +79,7 @@ namespace O3DE::ProjectManager GemRepoInfo GetGemRepoInfo(pybind11::handle repoUri); ProjectInfo ProjectInfoFromPath(pybind11::handle path); ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path, pybind11::handle pyProjectPath); + AZ::Outcome GemRegistration(const QString& gemPath, const QString& projectPath, bool remove = false); bool RegisterThisEngine(); bool StopPython(); diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 07d4551c60..6c8b0c89a6 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -95,10 +95,17 @@ namespace O3DE::ProjectManager * 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 - * @param remove Unregister instead of registering this gem * @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 = {}, bool remove = false) = 0; + virtual AZ::Outcome RegisterGem(const QString& gemPath, const QString& projectPath = {}) = 0; + + /** + * Unregisters the gem from the specified project, or from 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 unregister 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 UnregisterGem(const QString& gemPath, const QString& projectPath = {}) = 0; // Projects From 2b248ac4859a4d35bed04b6bd4d21a38316c5edc Mon Sep 17 00:00:00 2001 From: nggieber Date: Thu, 11 Nov 2021 12:09:24 -0800 Subject: [PATCH 5/5] Fix merge issue and validation issue Signed-off-by: nggieber --- Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp | 4 +++- .../ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp index 52e078eacd..26844b43e6 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp @@ -127,7 +127,9 @@ namespace O3DE::ProjectManager m_binarySizeLabel->setText(tr("Binary Size: %1").arg(binarySize ? tr("%1 KB").arg(binarySize) : tr("Unknown"))); // Update and Uninstall buttons - if (m_model->GetGemOrigin(modelIndex) == GemInfo::Remote && m_model->GetDownloadStatus(modelIndex) == GemInfo::Downloaded) + if (m_model->GetGemOrigin(modelIndex) == GemInfo::Remote && + (m_model->GetDownloadStatus(modelIndex) == GemInfo::Downloaded || + m_model->GetDownloadStatus(modelIndex) == GemInfo::DownloadSuccessful)) { m_updateGemButton->show(); m_uninstallGemButton->show(); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp index 6b2f14e551..1408e29b6d 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp @@ -36,7 +36,7 @@ namespace O3DE::ProjectManager layout->addSpacing(10); - QLabel* bodyLabel = new QLabel(tr("The Gem and its related files will be uninstalled. This does not affect the Gem’s repository. " + QLabel* bodyLabel = new QLabel(tr("The Gem and its related files will be uninstalled. This does not affect the Gem's repository. " "You can reinstall this Gem from the Catalog, but its contents may be subject to change.")); bodyLabel->setWordWrap(true); bodyLabel->setFixedSize(QSize(440, 80));