From 1989316cac4d723048d85a31327ccfab2e04dd21 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Fri, 22 Oct 2021 09:11:28 -0700 Subject: [PATCH] Added toast notifications to the gem catalog --- .../Components/ToastNotification.cpp | 33 +++++++++ .../Components/ToastNotification.h | 3 + .../Components/ToastNotification.ui | 2 +- .../ToastNotificationConfiguration.h | 1 + .../Notifications/ToastNotificationsView.cpp | 10 +++ .../UI/Notifications/ToastNotificationsView.h | 3 + Code/Tools/ProjectManager/CMakeLists.txt | 2 +- .../Resources/ProjectManager.qrc | 1 + .../Resources/ProjectManager.qss | 18 +++++ Code/Tools/ProjectManager/Resources/gem.svg | 3 + .../Source/GemCatalog/GemCatalogScreen.cpp | 73 +++++++++++++++++++ .../Source/GemCatalog/GemCatalogScreen.h | 16 +++- .../Source/GemCatalog/GemModel.cpp | 35 ++++++++- .../Source/GemCatalog/GemModel.h | 3 + 14 files changed, 196 insertions(+), 7 deletions(-) create mode 100644 Code/Tools/ProjectManager/Resources/gem.svg diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp index 670607900f..f79f355ccb 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace AzQtComponents { @@ -27,6 +28,13 @@ namespace AzQtComponents setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_DeleteOnClose); + m_borderRadius = toastConfiguration.m_borderRadius; + if (m_borderRadius > 0) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + setAttribute(Qt::WA_TranslucentBackground); + } + m_ui->setupUi(this); QIcon toastIcon; @@ -53,6 +61,13 @@ namespace AzQtComponents m_ui->titleLabel->setText(toastConfiguration.m_title); m_ui->mainLabel->setText(toastConfiguration.m_description); + // hide the optional description if none is provided so the title is centered vertically + if (toastConfiguration.m_description.isEmpty()) + { + m_ui->mainLabel->setVisible(false); + m_ui->verticalLayout->removeWidget(m_ui->mainLabel); + } + m_lifeSpan.setInterval(aznumeric_cast(toastConfiguration.m_duration.count())); m_closeOnClick = toastConfiguration.m_closeOnClick; @@ -68,6 +83,24 @@ namespace AzQtComponents { } + void ToastNotification::paintEvent(QPaintEvent* event) + { + if (m_borderRadius > 0) + { + QPainter p(this); + p.setPen(Qt::transparent); + QColor painterColor; + painterColor.setRgbF(0, 0, 0, 255); + p.setBrush(painterColor); + p.setRenderHint(QPainter::Antialiasing); + p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius); + } + else + { + QDialog::paintEvent(event); + } + } + void ToastNotification::ShowToastAtCursor() { QPoint globalCursorPos = QCursor::pos(); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h index eaf8a0751d..7f2701a803 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h @@ -52,6 +52,8 @@ namespace AzQtComponents void mousePressEvent(QMouseEvent* mouseEvent) override; bool eventFilter(QObject* object, QEvent* event) override; + void paintEvent(QPaintEvent* event) override; + public slots: void StartTimer(); void FadeOut(); @@ -65,6 +67,7 @@ namespace AzQtComponents bool m_closeOnClick; QTimer m_lifeSpan; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_fadeDuration; diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui index 107281bdc2..7aefddbd98 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui @@ -191,7 +191,7 @@ - 40 + 20 20 diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h index 05999017e4..5ace9d1be2 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h @@ -37,6 +37,7 @@ namespace AzQtComponents QString m_title; QString m_description; QString m_customIconImage; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp index 0ef0bf9a7b..e039230783 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp @@ -177,4 +177,14 @@ namespace AzToolsFramework DisplayQueuedNotification(); } } + + void ToastNotificationsView::SetOffset(const QPoint& offset) + { + m_offset = offset; + } + + void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint) + { + m_anchorPoint = anchorPoint; + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h index c4220331d8..e13f129467 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h @@ -50,6 +50,9 @@ namespace AzToolsFramework void OnShow(); void UpdateToastPosition(); + void SetOffset(const QPoint& offset); + void SetAnchorPoint(const QPointF& anchorPoint); + private: ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration); void DisplayQueuedNotification(); diff --git a/Code/Tools/ProjectManager/CMakeLists.txt b/Code/Tools/ProjectManager/CMakeLists.txt index 28c1871616..d34abcbc6c 100644 --- a/Code/Tools/ProjectManager/CMakeLists.txt +++ b/Code/Tools/ProjectManager/CMakeLists.txt @@ -43,7 +43,7 @@ ly_add_target( 3rdParty::pybind11 AZ::AzCore AZ::AzFramework - AZ::AzQtComponents + AZ::AzToolsFramework ) ly_add_target( diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc index aeaf9a9248..8dd7e4c9b5 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -40,5 +40,6 @@ Delete.svg Download.svg in_progress.gif + gem.svg diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 298d2a749a..712c01aa02 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -61,6 +61,24 @@ QTabBar::tab:focus { color: #4082eb; } +#ToastNotification { + background-color: black; + border-radius: 20px; + border:1px solid #dddddd; + qproperty-minimumSize: 100px 50px; +} + +#ToastNotification #icon_frame { + border-radius: 4px; + qproperty-minimumSize: 44px 20px; +} + +#ToastNotification #iconLabel { + qproperty-minimumSize: 30px 20px; + qproperty-maximumSize: 30px 20px; + margin-left: 6px; +} + /************** General (Forms) **************/ #formLineEditWidget, diff --git a/Code/Tools/ProjectManager/Resources/gem.svg b/Code/Tools/ProjectManager/Resources/gem.svg new file mode 100644 index 0000000000..2b688d5db5 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/gem.svg @@ -0,0 +1,3 @@ + + + diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index cbe36400cf..69c5844e84 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -66,6 +66,9 @@ namespace O3DE::ProjectManager hLayout->addWidget(filterWidget); hLayout->addLayout(middleVLayout); hLayout->addWidget(m_gemInspector); + + m_notificationsView = AZStd::make_unique(this, AZ_CRC("GemCatalogNotificationsView")); + m_notificationsView->SetOffset(QPoint(10, 70)); } void GemCatalogScreen::ReinitForProject(const QString& projectPath) @@ -86,6 +89,7 @@ namespace O3DE::ProjectManager m_headerWidget->ReinitForProject(); connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); + connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ @@ -94,6 +98,72 @@ namespace O3DE::ProjectManager }); } + void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies) + { + if (m_notificationsEnabled) + { + bool added = GemModel::IsAdded(modelIndex); + bool dependency = GemModel::IsAddedDependency(modelIndex); + + bool gemStateChanged = (added && !dependency) || (!added && !dependency); + if (!gemStateChanged && !numChangedDependencies) + { + // no actual changes made + return; + } + + QString notification; + if (gemStateChanged) + { + notification = GemModel::GetDisplayName(modelIndex); + if (numChangedDependencies > 0) + { + notification += " " + tr("and") + " "; + } + } + + if (numChangedDependencies == 1 ) + { + notification += "1 Gem " + tr("dependency"); + } + else if (numChangedDependencies > 1) + { + notification += QString("%d Gem ").arg(numChangedDependencies) + tr("dependencies"); + } + notification += " " + (added ? tr("activated") : tr("deactivated")); + + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); + toastConfiguration.m_customIconImage = ":/gem.svg"; + toastConfiguration.m_borderRadius = 4; + toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000); + m_notificationsView->ShowToastNotification(toastConfiguration); + } + } + + void GemCatalogScreen::hideEvent(QHideEvent* event) + { + ScreenWidget::hideEvent(event); + m_notificationsView->OnHide(); + } + + void GemCatalogScreen::showEvent(QShowEvent* event) + { + ScreenWidget::showEvent(event); + m_notificationsView->OnShow(); + } + + void GemCatalogScreen::resizeEvent(QResizeEvent* event) + { + ScreenWidget::resizeEvent(event); + m_notificationsView->UpdateToastPosition(); + } + + void GemCatalogScreen::moveEvent(QMoveEvent* event) + { + ScreenWidget::moveEvent(event); + m_notificationsView->UpdateToastPosition(); + } + void GemCatalogScreen::FillModel(const QString& projectPath) { AZ::Outcome, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); @@ -107,6 +177,7 @@ namespace O3DE::ProjectManager } m_gemModel->UpdateGemDependencies(); + m_notificationsEnabled = false; // Gather enabled gems for the given project. auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); @@ -133,6 +204,8 @@ namespace O3DE::ProjectManager { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.\n\nError:\n%2").arg(projectPath, enabledGemNamesResult.GetError().c_str())); } + + m_notificationsEnabled = true; } else { diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 456a5fe91c..62528ba942 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -10,6 +10,8 @@ #if !defined(Q_MOC_RUN) #include +#include +#include #include #include #include @@ -41,13 +43,24 @@ namespace O3DE::ProjectManager GemModel* GetGemModel() const { return m_gemModel; } DownloadController* GetDownloadController() const { return m_downloadController; } + public slots: + void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + + protected: + void hideEvent(QHideEvent* event) override; + void showEvent(QShowEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void moveEvent(QMoveEvent* event) override; + private slots: void HandleOpenGemRepo(); - private: + private: void FillModel(const QString& projectPath); + AZStd::unique_ptr m_notificationsView; + GemListView* m_gemListView = nullptr; GemInspector* m_gemInspector = nullptr; GemModel* m_gemModel = nullptr; @@ -56,5 +69,6 @@ namespace O3DE::ProjectManager QVBoxLayout* m_filterWidgetLayout = nullptr; GemFilterWidget* m_filterWidget = nullptr; DownloadController* m_downloadController = nullptr; + bool m_notificationsEnabled = true; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 35491f4ddd..acdef483ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -299,23 +300,50 @@ namespace O3DE::ProjectManager AZ_Assert(gemModel, "Failed to obtain GemModel"); QVector dependencies = gemModel->GatherGemDependencies(modelIndex); + uint32_t numChangedDependencies = 0; + if (IsAdded(modelIndex)) { for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, true); + if (!IsAddedDependency(dependency)) + { + SetIsAddedDependency(*gemModel, dependency, true); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } else { // still a dependency if some added gem depends on this one - SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex)); + bool hasDependentGems = gemModel->HasDependentGems(modelIndex); + if (IsAddedDependency(modelIndex) != hasDependentGems) + { + SetIsAddedDependency(model, modelIndex, hasDependentGems); + } for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency)); + hasDependentGems = gemModel->HasDependentGems(dependency); + if (IsAddedDependency(dependency) != hasDependentGems) + { + SetIsAddedDependency(*gemModel, dependency, hasDependentGems); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } + + gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies); } void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) @@ -488,5 +516,4 @@ namespace O3DE::ProjectManager } return result; } - } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 0d1c225f74..938543eb39 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -77,6 +77,9 @@ namespace O3DE::ProjectManager int TotalAddedGems(bool includeDependencies = false) const; + signals: + void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + private: void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames); void GetAllDependingGems(const QModelIndex& modelIndex, QSet& inOutGems);