Browse Source

Project Manager Gem Dependencies (#4132)

* Fix engine API change and add gem dependencies

Signed-off-by: AMZN-alexpete <[email protected]>

* Add GemCatalog dependency test

Signed-off-by: AMZN-alexpete <[email protected]>

* Clarify display name and fix missing const

Signed-off-by: AMZN-alexpete <[email protected]>

* Moving a couple helper functions into private scope

Signed-off-by: AMZN-alexpete <[email protected]>

* Update gem count when unselecting a gem #4074

This addresses the following issue https://github.com/o3de/o3de/issues/4074

Signed-off-by: AMZN-alexpete <[email protected]>

* Active/Inactive filter and dependency tooltips

Signed-off-by: AMZN-alexpete <[email protected]>

* Accessors for previously added and dependencies

Signed-off-by: AMZN-alexpete <[email protected]>

* Cart displays gem dependency changes

Signed-off-by: AMZN-alexpete <[email protected]>

* Shorten titles to fit in summary popup

Signed-off-by: AMZN-alexpete <[email protected]>

* Remove QString::number

Signed-off-by: AMZN-alexpete <[email protected]>

* Remove extra space

Signed-off-by: AMZN-alexpete <[email protected]>

* Consolidate source model accesor helpers

Signed-off-by: AMZN-alexpete <[email protected]>

* Addressing minor feedback

Signed-off-by: AMZN-alexpete <[email protected]>

* Remove unused local variable

Signed-off-by: AMZN-alexpete <[email protected]>
Alex Peterson 4 years ago
parent
commit
43244d30e1

+ 102 - 64
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -7,6 +7,7 @@
  */
 
 #include <GemCatalog/GemCatalogHeaderWidget.h>
+#include <AzCore/std/functional.h>
 #include <QHBoxLayout>
 #include <QMouseEvent>
 #include <QLabel>
@@ -23,7 +24,7 @@ namespace O3DE::ProjectManager
 
         m_layout = new QVBoxLayout();
         m_layout->setSpacing(0);
-        m_layout->setMargin(0);
+        m_layout->setMargin(5);
         m_layout->setAlignment(Qt::AlignTop);
         setLayout(m_layout);
 
@@ -41,74 +42,111 @@ namespace O3DE::ProjectManager
         hLayout->addWidget(closeButton);
         m_layout->addLayout(hLayout);
 
-        // enabled
-        {
-            m_enabledWidget = new QWidget();
-            m_enabledWidget->setFixedWidth(s_width);
-            m_layout->addWidget(m_enabledWidget);
-
-            QVBoxLayout* layout = new QVBoxLayout();
-            layout->setAlignment(Qt::AlignTop);
-            m_enabledWidget->setLayout(layout);
-
-            m_enabledLabel = new QLabel();
-            m_enabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
-            layout->addWidget(m_enabledLabel);
-            m_enabledTagContainer = new TagContainerWidget();
-            layout->addWidget(m_enabledTagContainer);
-        }
+        // added
+        CreateGemSection( tr("Gem to be activated"), tr("Gems to be activated"), [=]
+            {
+                QVector<QModelIndex> gems;
+                const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/false);
 
-        // disabled
-        {
-            m_disabledWidget = new QWidget();
-            m_disabledWidget->setFixedWidth(s_width);
-            m_layout->addWidget(m_disabledWidget);
-
-            QVBoxLayout* layout = new QVBoxLayout();
-            layout->setAlignment(Qt::AlignTop);
-            m_disabledWidget->setLayout(layout);
-
-            m_disabledLabel = new QLabel();
-            m_disabledLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
-            layout->addWidget(m_disabledLabel);
-            m_disabledTagContainer = new TagContainerWidget();
-            layout->addWidget(m_disabledTagContainer);
-        }
+                // don't include gems that were already active because they were dependencies
+                for (const QModelIndex& modelIndex : toBeAdded)
+                {
+                    if (!GemModel::WasPreviouslyAddedDependency(modelIndex))
+                    {
+                        gems.push_back(modelIndex);
+                    }
+                }
+                return gems;
+            });
 
-        setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
+        // removed
+        CreateGemSection( tr("Gem to be deactivated"), tr("Gems to be deactivated"), [=]
+            {
+                QVector<QModelIndex> gems;
+                const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/false);
+
+                // don't include gems that are still active because they are dependencies
+                for (const QModelIndex& modelIndex : toBeAdded)
+                {
+                    if (!GemModel::IsAddedDependency(modelIndex))
+                    {
+                        gems.push_back(modelIndex);
+                    }
+                }
+                return gems;
+            });
+
+        // added dependencies 
+        CreateGemSection( tr("Dependency to be activated"), tr("Dependencies to be activated"), [=]
+            {
+                QVector<QModelIndex> dependencies;
+                const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true);
+
+                // only include gems that are dependencies and not explicitly added 
+                for (const QModelIndex& modelIndex : toBeAdded)
+                {
+                    if (GemModel::IsAddedDependency(modelIndex) && !GemModel::IsAdded(modelIndex))
+                    {
+                        dependencies.push_back(modelIndex);
+                    }
+                }
+                return dependencies;
+            });
 
-        Update();
-        connect(gemModel, &GemModel::dataChanged, this, [=]
+        // removed dependencies 
+        CreateGemSection( tr("Dependency to be deactivated"), tr("Dependencies to be deactivated"), [=]
             {
-                Update();
+                QVector<QModelIndex> dependencies;
+                const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true);
+
+                // don't include gems that were explicitly removed - those are listed in a different section
+                for (const QModelIndex& modelIndex : toBeRemoved)
+                {
+                    if (!GemModel::WasPreviouslyAdded(modelIndex))
+                    {
+                        dependencies.push_back(modelIndex);
+                    }
+                }
+                return dependencies;
             });
+
+        setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog);
     }
 
-    void CartOverlayWidget::Update()
+    void CartOverlayWidget::CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices)
     {
-        const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
-        if (toBeAdded.isEmpty())
-        {
-            m_enabledWidget->hide();
-        }
-        else
-        {
-            m_enabledTagContainer->Update(ConvertFromModelIndices(toBeAdded));
-            m_enabledLabel->setText(QString("%1 %2").arg(QString::number(toBeAdded.size()), tr("Gems to be enabled")));
-            m_enabledWidget->show();
-        }
+        QWidget* widget = new QWidget();
+        widget->setFixedWidth(s_width);
+        m_layout->addWidget(widget);
 
-        const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
-        if (toBeRemoved.isEmpty())
-        {
-            m_disabledWidget->hide();
-        }
-        else
+        QVBoxLayout* layout = new QVBoxLayout();
+        layout->setAlignment(Qt::AlignTop);
+        widget->setLayout(layout);
+
+        QLabel* label = new QLabel();
+        label->setObjectName("GemCatalogCartOverlaySectionLabel");
+        layout->addWidget(label);
+
+        TagContainerWidget* tagContainer = new TagContainerWidget();
+        layout->addWidget(tagContainer);
+
+        auto update = [=]()
         {
-            m_disabledTagContainer->Update(ConvertFromModelIndices(toBeRemoved));
-            m_disabledLabel->setText(QString("%1 %2").arg(QString::number(toBeRemoved.size()), tr("Gems to be disabled")));
-            m_disabledWidget->show();
-        }
+            const QVector<QModelIndex> tagIndices = getTagIndices();
+            if (tagIndices.isEmpty())
+            {
+                widget->hide();
+            }
+            else
+            {
+                tagContainer->Update(ConvertFromModelIndices(tagIndices));
+                label->setText(QString("%1 %2").arg(tagIndices.size()).arg(tagIndices.size() == 1 ? singularTitle : pluralTitle));
+                widget->show();
+            }
+        };
+
+        connect(m_gemModel, &GemModel::dataChanged, this, update); 
+        update();
     }
 
     QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector<QModelIndex>& gems) const
@@ -154,15 +192,15 @@ namespace O3DE::ProjectManager
         // Adjust the label text whenever the model gets updated.
         connect(gemModel, &GemModel::dataChanged, [=]
             {
-                const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
-                const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
+                const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true);
+                const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true);
 
                 const int count = toBeAdded.size() + toBeRemoved.size();
                 m_countLabel->setText(QString::number(count));
 
                 m_dropDownButton->setVisible(!toBeAdded.isEmpty() || !toBeRemoved.isEmpty());
 
-                // Automatically close the overlay window in case there are no gems to be enabled or disabled anymore.
+                // Automatically close the overlay window in case there are no gems to be activated or deactivated anymore.
                 if (m_cartOverlay && toBeAdded.isEmpty() && toBeRemoved.isEmpty())
                 {
                     m_cartOverlay->deleteLater();
@@ -186,8 +224,8 @@ namespace O3DE::ProjectManager
 
     void CartButton::ShowOverlay()
     {
-        const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
-        const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved();
+        const QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true);
+        const QVector<QModelIndex> toBeRemoved = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true);
         if (toBeAdded.isEmpty() && toBeRemoved.isEmpty())
         {
             return;

+ 5 - 9
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h

@@ -8,6 +8,8 @@
 
 #pragma once
 
+#include <AzCore/std/function/function_fwd.h>
+
 #if !defined(Q_MOC_RUN)
 #include <AzQtComponents/Components/SearchLineEdit.h>
 #include <GemCatalog/GemModel.h>
@@ -30,22 +32,16 @@ namespace O3DE::ProjectManager
 
     public:
         CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr);
-        void Update();
 
     private:
         QStringList ConvertFromModelIndices(const QVector<QModelIndex>& gems) const;
 
+        using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>;
+        void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices);
+
         QVBoxLayout* m_layout = nullptr;
         GemModel* m_gemModel = nullptr;
 
-        QWidget* m_enabledWidget = nullptr;
-        QLabel* m_enabledLabel = nullptr;
-        TagContainerWidget* m_enabledTagContainer = nullptr;
-
-        QWidget* m_disabledWidget = nullptr;
-        QLabel* m_disabledLabel = nullptr;
-        TagContainerWidget* m_disabledTagContainer = nullptr;
-
         inline constexpr static int s_width = 240;
     };
 

+ 2 - 0
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -100,6 +100,8 @@ namespace O3DE::ProjectManager
                 m_gemModel->AddGem(gemInfo);
             }
 
+            m_gemModel->UpdateGemDependencies();
+
             // Gather enabled gems for the given project.
             auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath);
             if (enabledGemNamesResult.IsSuccess())

+ 52 - 40
Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp

@@ -226,13 +226,20 @@ namespace O3DE::ProjectManager
         QVector<int> elementCounts;
         const int totalGems = m_gemModel->rowCount();
         const int selectedGemTotal = m_gemModel->TotalAddedGems();
+        const int enabledGemTotal = m_gemModel->TotalAddedGems(/*includeDependencies=*/true);
 
-        elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Unselected));
+        elementNames.push_back(GemSortFilterProxyModel::GetGemSelectedString(GemSortFilterProxyModel::GemSelected::Unselected));
         elementCounts.push_back(totalGems - selectedGemTotal);
 
-        elementNames.push_back(GemSortFilterProxyModel::GetGemStatusString(GemSortFilterProxyModel::GemStatus::Selected));
+        elementNames.push_back(GemSortFilterProxyModel::GetGemSelectedString(GemSortFilterProxyModel::GemSelected::Selected));
         elementCounts.push_back(selectedGemTotal);
 
+        elementNames.push_back(GemSortFilterProxyModel::GetGemActiveString(GemSortFilterProxyModel::GemActive::Inactive));
+        elementCounts.push_back(totalGems - enabledGemTotal);
+
+        elementNames.push_back(GemSortFilterProxyModel::GetGemActiveString(GemSortFilterProxyModel::GemActive::Active));
+        elementCounts.push_back(enabledGemTotal);
+
         bool wasCollapsed = false;
         if (m_statusFilter)
         {
@@ -253,48 +260,53 @@ namespace O3DE::ProjectManager
         m_statusFilter->deleteLater();
         m_statusFilter = filterWidget;
 
-        const GemSortFilterProxyModel::GemStatus currentFilterState = m_filterProxyModel->GetGemStatus();
         const QList<QAbstractButton*> buttons = m_statusFilter->GetButtonGroup()->buttons();
-        for (int statusFilterIndex = 0; statusFilterIndex < buttons.size(); ++statusFilterIndex)
-        {
-            const GemSortFilterProxyModel::GemStatus gemStatus = static_cast<GemSortFilterProxyModel::GemStatus>(statusFilterIndex);
-            QAbstractButton* button = buttons[statusFilterIndex];
 
-            if (static_cast<GemSortFilterProxyModel::GemStatus>(statusFilterIndex) == currentFilterState)
+        QAbstractButton* unselectedButton = buttons[0];
+        QAbstractButton* selectedButton = buttons[1];
+        unselectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Unselected); 
+        selectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Selected); 
+
+        auto updateGemSelection = [=]([[maybe_unused]] bool checked)
+        {
+            if (unselectedButton->isChecked() && !selectedButton->isChecked())
+            {
+                m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Unselected);
+            }
+            else if (!unselectedButton->isChecked() && selectedButton->isChecked())
             {
-                button->setChecked(true);
+                m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Selected);
             }
+            else
+            {
+                m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::NoFilter);
+            }
+        };
+        connect(unselectedButton, &QAbstractButton::toggled, this, updateGemSelection);
+        connect(selectedButton, &QAbstractButton::toggled, this, updateGemSelection);
 
-            connect(
-                button, &QAbstractButton::toggled, this,
-                [=](bool checked)
-                {
-                    GemSortFilterProxyModel::GemStatus filterStatus = m_filterProxyModel->GetGemStatus();
-                    if (checked)
-                    {
-                        if (filterStatus == GemSortFilterProxyModel::GemStatus::NoFilter)
-                        {
-                            filterStatus = gemStatus;
-                        }
-                        else
-                        {
-                            filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter;
-                        }
-                    }
-                    else
-                    {
-                        if (filterStatus != gemStatus)
-                        {
-                            filterStatus = static_cast<GemSortFilterProxyModel::GemStatus>(!gemStatus);
-                        }
-                        else
-                        {
-                            filterStatus = GemSortFilterProxyModel::GemStatus::NoFilter;
-                        }
-                    }
-                    m_filterProxyModel->SetGemStatus(filterStatus);
-                });
-        }
+        QAbstractButton* inactiveButton = buttons[2];
+        QAbstractButton* activeButton = buttons[3];
+        inactiveButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Inactive); 
+        activeButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Active); 
+
+        auto updateGemActive = [=]([[maybe_unused]] bool checked)
+        {
+            if (inactiveButton->isChecked() && !activeButton->isChecked())
+            {
+                m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::Inactive);
+            }
+            else if (!inactiveButton->isChecked() && activeButton->isChecked())
+            {
+                m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::Active);
+            }
+            else
+            {
+                m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::NoFilter);
+            }
+        };
+        connect(inactiveButton, &QAbstractButton::toggled, this, updateGemActive);
+        connect(activeButton, &QAbstractButton::toggled, this, updateGemActive);
     }
 
     void GemFilterWidget::AddGemOriginFilter()
@@ -487,7 +499,7 @@ namespace O3DE::ProjectManager
             const QString& feature = elementNames[i];
             QAbstractButton* button = buttons[i];
 
-            // Adjust the proxy model and enable or disable the clicked feature used for filtering.
+            // Adjust the proxy model and enable the clicked feature used for filtering.
             connect(button, &QAbstractButton::toggled, this, [=](bool checked)
                 {
                     QSet<QString> features = m_filterProxyModel->GetFeatures();

+ 1 - 2
Code/Tools/ProjectManager/Source/GemCatalog/GemInfo.h

@@ -75,8 +75,7 @@ namespace O3DE::ProjectManager
         QString m_version = "Unknown Version";
         QString m_lastUpdatedDate = "Unknown Date";
         int m_binarySizeInKB = 0;
-        QStringList m_dependingGemUuids;
-        QStringList m_conflictingGemUuids;
+        QStringList m_dependencies;
     };
 } // namespace O3DE::ProjectManager
 

+ 2 - 7
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp

@@ -83,9 +83,8 @@ namespace O3DE::ProjectManager
             m_reqirementsTextLabel->hide();
         }
 
-        // Depending and conflicting gems
+        // Depending gems
         m_dependingGems->Update("Depending Gems", "The following Gems will be automatically enabled with this Gem.", m_model->GetDependingGemNames(modelIndex));
-        m_conflictingGems->Update("Conflicting Gems", "The following Gems will be automatically disabled with this Gem.", m_model->GetConflictingGemNames(modelIndex));
 
         // Additional information
         m_versionLabel->setText(QString("Gem Version: %1").arg(m_model->GetVersion(modelIndex)));
@@ -173,15 +172,11 @@ namespace O3DE::ProjectManager
 
         m_mainLayout->addSpacing(20);
 
-        // Depending and conflicting gems
+        // Depending gems
         m_dependingGems = new GemsSubWidget();
         m_mainLayout->addWidget(m_dependingGems);
         m_mainLayout->addSpacing(20);
 
-        m_conflictingGems = new GemsSubWidget();
-        m_mainLayout->addWidget(m_conflictingGems);
-        m_mainLayout->addSpacing(20);
-
         // Additional information
         QLabel* additionalInfoLabel = CreateStyledLabel(m_mainLayout, 14, s_headerColor);
         additionalInfoLabel->setText("Additional Information");

+ 0 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h

@@ -78,7 +78,6 @@ namespace O3DE::ProjectManager
 
         // Depending and conflicting gems
         GemsSubWidget* m_dependingGems = nullptr;
-        GemsSubWidget* m_conflictingGems = nullptr;
 
         // Additional information
         QLabel* m_versionLabel = nullptr;

+ 77 - 4
Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp

@@ -8,9 +8,14 @@
 
 #include <GemCatalog/GemItemDelegate.h>
 #include <GemCatalog/GemModel.h>
+#include <GemCatalog/GemSortFilterProxyModel.h>
 #include <QEvent>
+#include <QAbstractItemView>
 #include <QPainter>
 #include <QMouseEvent>
+#include <QHelpEvent>
+#include <QToolTip>
+#include <QHoverEvent>
 
 namespace O3DE::ProjectManager
 {
@@ -149,8 +154,7 @@ namespace O3DE::ProjectManager
                 return true;
             }
         }
-
-        if (event->type() == QEvent::MouseButtonPress)
+        else if (event->type() == QEvent::MouseButtonPress )
         {
             QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
 
@@ -169,6 +173,69 @@ namespace O3DE::ProjectManager
         return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
     }
 
+    QString GetGemNameList(const QVector<QModelIndex> modelIndices)
+    {
+        QString gemNameList;
+        for (int i = 0; i < modelIndices.size(); ++i)
+        {
+            if (!gemNameList.isEmpty())
+            {
+                if (i == modelIndices.size() - 1)
+                {
+                    gemNameList.append(" and ");
+                }
+                else
+                {
+                    gemNameList.append(", ");
+                }
+            }
+
+            gemNameList.append(GemModel::GetDisplayName(modelIndices[i]));
+        }
+
+        return gemNameList;
+    }
+
+    bool GemItemDelegate::helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index)
+    {
+        if (event->type() == QEvent::ToolTip)
+        {
+            QRect fullRect, itemRect, contentRect;
+            CalcRects(option, fullRect, itemRect, contentRect);
+            const QRect buttonRect = CalcButtonRect(contentRect);
+            if (buttonRect.contains(event->pos()))
+            {
+                if (!QToolTip::isVisible())
+                {
+                    if(GemModel::IsAddedDependency(index) && !GemModel::IsAdded(index))
+                    {
+                        const GemModel* gemModel = GemModel::GetSourceModel(index.model());
+                        AZ_Assert(gemModel, "Failed to obtain GemModel");
+
+                        // we only want to display the gems that must be de-selected to automatically
+                        // disable this dependency, so don't include any that haven't been selected (added) 
+                        constexpr bool addedOnly = true;
+                        QVector<QModelIndex> dependents = gemModel->GatherDependentGems(index, addedOnly);
+                        QString nameList = GetGemNameList(dependents);
+                        if (!nameList.isEmpty())
+                        {
+                            QToolTip::showText(event->globalPos(), tr("This gem is a dependency of %1.\nTo disable this gem, first disable %1.").arg(nameList));
+                        }
+                    }
+                }
+                return true;
+            }
+            else if (QToolTip::isVisible())
+            {
+                QToolTip::hideText();
+                event->ignore();
+                return true;
+            }
+        }
+
+        return QStyledItemDelegate::helpEvent(event, view, option, index);
+    }
+
     void GemItemDelegate::CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const
     {
         outFullRect = QRect(option.rect);
@@ -260,14 +327,20 @@ namespace O3DE::ProjectManager
         const QRect buttonRect = CalcButtonRect(contentRect);
         QPoint circleCenter;
 
-        const bool isAdded = GemModel::IsAdded(modelIndex);
-        if (isAdded)
+        if (GemModel::IsAdded(modelIndex))
         {
             painter->setBrush(m_buttonEnabledColor);
             painter->setPen(m_buttonEnabledColor);
 
             circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1);
         }
+        else if (GemModel::IsAddedDependency(modelIndex))
+        {
+            painter->setBrush(m_buttonImplicitlyEnabledColor);
+            painter->setPen(m_buttonImplicitlyEnabledColor);
+
+            circleCenter = buttonRect.center() + QPoint(buttonRect.width() / 2 - s_buttonBorderRadius + 1, 1);
+        }
         else
         {
             circleCenter = buttonRect.center() + QPoint(-buttonRect.width() / 2 + s_buttonBorderRadius, 1);

+ 4 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.h

@@ -29,7 +29,6 @@ namespace O3DE::ProjectManager
         ~GemItemDelegate() = default;
 
         void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override;
-        bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) override;
         QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& modelIndex) const override;
 
         // Colors
@@ -39,6 +38,7 @@ namespace O3DE::ProjectManager
         const QColor m_itemBackgroundColor = QColor("#404040"); // Background color of the gem item
         const QColor m_borderColor = QColor("#1E70EB");
         const QColor m_buttonEnabledColor = QColor("#00B931");
+        const QColor m_buttonImplicitlyEnabledColor = QColor("#BCBCBE");
 
         // Item
         inline constexpr static int s_height = 105; // Gem item total height
@@ -65,6 +65,9 @@ namespace O3DE::ProjectManager
         inline constexpr static int s_featureTagSpacing = 7;
 
     protected:
+        bool editorEvent(QEvent* event, QAbstractItemModel* model, const QStyleOptionViewItem& option, const QModelIndex& modelIndex) override;
+        bool helpEvent(QHelpEvent* event, QAbstractItemView* view, const QStyleOptionViewItem& option, const QModelIndex& index) override;
+
         void CalcRects(const QStyleOptionViewItem& option, QRect& outFullRect, QRect& outItemRect, QRect& outContentRect) const;
         QRect GetTextRect(QFont& font, const QString& text, qreal fontSize) const;
         QRect CalcButtonRect(const QRect& contentRect) const;

+ 6 - 3
Code/Tools/ProjectManager/Source/GemCatalog/GemListHeaderWidget.cpp

@@ -60,11 +60,14 @@ namespace O3DE::ProjectManager
         QLabel* showCountLabel = new QLabel();
         showCountLabel->setObjectName("GemCatalogHeaderShowCountLabel");
         topLayout->addWidget(showCountLabel);
-        connect(proxyModel, &GemSortFilterProxyModel::OnInvalidated, this, [=]
-            {
+
+        auto refreshGemCountUI = [=]() {
                 const int numGemsShown = proxyModel->rowCount();
                 showCountLabel->setText(QString(tr("showing %1 Gems")).arg(numGemsShown));
-            });
+            };
+
+        connect(proxyModel, &GemSortFilterProxyModel::OnInvalidated, this, refreshGemCountUI);
+        connect(proxyModel->GetSourceModel(), &GemModel::dataChanged, this, refreshGemCountUI);
 
         topLayout->addSpacing(GemItemDelegate::s_contentMargins.right() + GemItemDelegate::s_borderWidth);
 

+ 20 - 0
Code/Tools/ProjectManager/Source/GemCatalog/GemListView.cpp

@@ -9,9 +9,26 @@
 #include <GemCatalog/GemListView.h>
 #include <GemCatalog/GemItemDelegate.h>
 #include <QStandardItemModel>
+#include <QProxyStyle>
 
 namespace O3DE::ProjectManager
 {
+    class GemListViewProxyStyle : public QProxyStyle
+    {
+    public:
+        using QProxyStyle::QProxyStyle;
+        int styleHint(StyleHint hint, const QStyleOption* option = nullptr, const QWidget* widget = nullptr, QStyleHintReturn* returnData = nullptr) const override
+        {
+            if (hint == QStyle::SH_ToolTip_WakeUpDelay || hint == QStyle::SH_ToolTip_FallAsleepDelay)
+            {
+                // no delay
+                return 0;
+            }
+
+            return QProxyStyle::styleHint(hint, option, widget, returnData);
+        }
+    };
+
     GemListView::GemListView(QAbstractItemModel* model, QItemSelectionModel* selectionModel, QWidget* parent)
         : QListView(parent)
     {
@@ -21,5 +38,8 @@ namespace O3DE::ProjectManager
         setModel(model);
         setSelectionModel(selectionModel);
         setItemDelegate(new GemItemDelegate(model, this));
+
+        // use a custom proxy style so we get immediate tooltips for gem radio buttons
+        setStyle(new GemListViewProxyStyle(this->style()));
     }
 } // namespace O3DE::ProjectManager

+ 210 - 32
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp

@@ -8,6 +8,7 @@
 
 #include <AzCore/std/string/string.h>
 #include <GemCatalog/GemModel.h>
+#include <GemCatalog/GemSortFilterProxyModel.h>
 #include <AzCore/Casting/numeric_cast.h>
 
 namespace O3DE::ProjectManager
@@ -40,8 +41,7 @@ namespace O3DE::ProjectManager
         item->setData(gemInfo.m_isAdded, RoleIsAdded);
         item->setData(gemInfo.m_directoryLink, RoleDirectoryLink);
         item->setData(gemInfo.m_documentationLink, RoleDocLink);
-        item->setData(gemInfo.m_dependingGemUuids, RoleDependingGems);
-        item->setData(gemInfo.m_conflictingGemUuids, RoleConflictingGems);
+        item->setData(gemInfo.m_dependencies, RoleDependingGems);
         item->setData(gemInfo.m_version, RoleVersion);
         item->setData(gemInfo.m_lastUpdatedDate, RoleLastUpdated);
         item->setData(gemInfo.m_binarySizeInKB, RoleBinarySize);
@@ -60,6 +60,39 @@ namespace O3DE::ProjectManager
         clear();
     }
 
+    void GemModel::UpdateGemDependencies()
+    {
+        m_gemDependencyMap.clear();
+        m_gemReverseDependencyMap.clear();
+
+        for (auto iter = m_nameToIndexMap.begin(); iter != m_nameToIndexMap.end(); ++iter)
+        {
+            const QString& key = iter.key();
+            const QModelIndex modelIndex = iter.value();
+            QSet<QModelIndex> dependencies;
+            GetAllDependingGems(modelIndex, dependencies);
+            if (!dependencies.isEmpty())
+            {
+                m_gemDependencyMap.insert(key, dependencies);
+            }
+        }
+
+        for (auto iter = m_gemDependencyMap.begin(); iter != m_gemDependencyMap.end(); ++iter)
+        {
+            const QString& dependant = iter.key();
+            for (const QModelIndex& dependency : iter.value())
+            {
+                const QString& dependencyName = dependency.data(RoleName).toString();
+                if (!m_gemReverseDependencyMap.contains(dependencyName))
+                {
+                    m_gemReverseDependencyMap.insert(dependencyName, QSet<QModelIndex>());
+                }
+
+                m_gemReverseDependencyMap[dependencyName].insert(m_nameToIndexMap[dependant]);
+            }
+        }
+    }
+
     QString GemModel::GetName(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleName).toString();
@@ -125,49 +158,46 @@ namespace O3DE::ProjectManager
         return {};
     }
 
-    void GemModel::FindGemNamesByNameStrings(QStringList& inOutGemNames)
+    void GemModel::FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames)
     {
-        for (QString& dependingGemString : inOutGemNames)
+        for (QString& name : inOutGemNames)
         {
-            QModelIndex modelIndex = FindIndexByNameString(dependingGemString);
+            QModelIndex modelIndex = FindIndexByNameString(name);
             if (modelIndex.isValid())
             {
-                dependingGemString = GetDisplayName(modelIndex);
+                name = GetDisplayName(modelIndex);
             }
         }
     }
 
-    QStringList GemModel::GetDependingGemUuids(const QModelIndex& modelIndex)
+    QStringList GemModel::GetDependingGems(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleDependingGems).toStringList();
     }
 
-    QStringList GemModel::GetDependingGemNames(const QModelIndex& modelIndex)
+    void GemModel::GetAllDependingGems(const QModelIndex& modelIndex, QSet<QModelIndex>& inOutGems)
     {
-        QStringList result = GetDependingGemUuids(modelIndex);
-        if (result.isEmpty())
+        QStringList dependencies = GetDependingGems(modelIndex);
+        for (const QString& dependency : dependencies)
         {
-            return {};
+            QModelIndex dependencyIndex = FindIndexByNameString(dependency);
+            if (!inOutGems.contains(dependencyIndex))
+            {
+                inOutGems.insert(dependencyIndex);
+                GetAllDependingGems(dependencyIndex, inOutGems);
+            }
         }
-
-        FindGemNamesByNameStrings(result);
-        return result;
     }
 
-    QStringList GemModel::GetConflictingGemUuids(const QModelIndex& modelIndex)
-    {
-        return modelIndex.data(RoleConflictingGems).toStringList();
-    }
-
-    QStringList GemModel::GetConflictingGemNames(const QModelIndex& modelIndex)
+    QStringList GemModel::GetDependingGemNames(const QModelIndex& modelIndex)
     {
-        QStringList result = GetConflictingGemUuids(modelIndex);
+        QStringList result = GetDependingGems(modelIndex);
         if (result.isEmpty())
         {
             return {};
         }
 
-        FindGemNamesByNameStrings(result);
+        FindGemDisplayNamesByNameStrings(result);
         return result;
     }
 
@@ -201,29 +231,146 @@ namespace O3DE::ProjectManager
         return modelIndex.data(RoleRequirement).toString();
     }
 
+    GemModel* GemModel::GetSourceModel(QAbstractItemModel* model)
+    {
+        GemSortFilterProxyModel* proxyModel = qobject_cast<GemSortFilterProxyModel*>(model);
+        if (proxyModel)
+        {
+            return proxyModel->GetSourceModel();
+        }
+        else
+        {
+            return qobject_cast<GemModel*>(model);
+        }
+    }
+
+    const GemModel* GemModel::GetSourceModel(const QAbstractItemModel* model)
+    {
+        const GemSortFilterProxyModel* proxyModel = qobject_cast<const GemSortFilterProxyModel*>(model);
+        if (proxyModel)
+        {
+            return proxyModel->GetSourceModel();
+        }
+        else
+        {
+            return qobject_cast<const GemModel*>(model);
+        }
+    }
+
     bool GemModel::IsAdded(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleIsAdded).toBool();
     }
 
+    bool GemModel::IsAddedDependency(const QModelIndex& modelIndex)
+    {
+        return modelIndex.data(RoleIsAddedDependency).toBool();
+    }
+
     void GemModel::SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded)
     {
         model.setData(modelIndex, isAdded, RoleIsAdded);
+
+        UpdateDependencies(model, modelIndex);
+    }
+
+    bool GemModel::HasDependentGems(const QModelIndex& modelIndex) const
+    {
+        QVector<QModelIndex> dependentGems = GatherDependentGems(modelIndex);
+        for (const QModelIndex& dependency : dependentGems)
+        {
+            if (IsAdded(dependency))
+            {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void GemModel::UpdateDependencies(QAbstractItemModel& model, const QModelIndex& modelIndex)
+    {
+        GemModel* gemModel = GetSourceModel(&model);
+        AZ_Assert(gemModel, "Failed to obtain GemModel");
+
+        QVector<QModelIndex> dependencies = gemModel->GatherGemDependencies(modelIndex);
+        if (IsAdded(modelIndex))
+        {
+            for (const QModelIndex& dependency : dependencies)
+            {
+                SetIsAddedDependency(*gemModel, dependency, true);
+            }
+        }
+        else
+        {
+            // still a dependency if some added gem depends on this one 
+            SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex));
+
+            for (const QModelIndex& dependency : dependencies)
+            {
+                SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency));
+            }
+        }
+    }
+
+    void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded)
+    {
+        model.setData(modelIndex, isAdded, RoleIsAddedDependency);
     }
 
     void GemModel::SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded)
     {
         model.setData(modelIndex, wasAdded, RoleWasPreviouslyAdded);
+
+        if (wasAdded)
+        {
+            // update all dependencies
+            GemModel* gemModel = GetSourceModel(&model);
+            AZ_Assert(gemModel, "Failed to obtain GemModel");
+            QVector<QModelIndex> dependencies = gemModel->GatherGemDependencies(modelIndex);
+            for (const QModelIndex& dependency : dependencies)
+            {
+                SetWasPreviouslyAddedDependency(*gemModel, dependency, true);
+            }
+        }
+    }
+
+    void GemModel::SetWasPreviouslyAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded)
+    {
+        model.setData(modelIndex, wasAdded, RoleWasPreviouslyAddedDependency);
     }
 
-    bool GemModel::NeedsToBeAdded(const QModelIndex& modelIndex)
+    bool GemModel::WasPreviouslyAdded(const QModelIndex& modelIndex)
     {
-        return (!modelIndex.data(RoleWasPreviouslyAdded).toBool() && modelIndex.data(RoleIsAdded).toBool());
+        return modelIndex.data(RoleWasPreviouslyAdded).toBool();
     }
 
-    bool GemModel::NeedsToBeRemoved(const QModelIndex& modelIndex)
+    bool GemModel::WasPreviouslyAddedDependency(const QModelIndex& modelIndex)
     {
-        return (modelIndex.data(RoleWasPreviouslyAdded).toBool() && !modelIndex.data(RoleIsAdded).toBool());
+        return modelIndex.data(RoleWasPreviouslyAddedDependency).toBool();
+    }
+
+    bool GemModel::NeedsToBeAdded(const QModelIndex& modelIndex, bool includeDependencies)
+    {
+        bool previouslyAdded = modelIndex.data(RoleWasPreviouslyAdded).toBool();
+        bool added = modelIndex.data(RoleIsAdded).toBool();
+        if (includeDependencies)
+        {
+            previouslyAdded |= modelIndex.data(RoleWasPreviouslyAddedDependency).toBool();
+            added |= modelIndex.data(RoleIsAddedDependency).toBool();
+        }
+        return !previouslyAdded && added;
+    }
+
+    bool GemModel::NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies)
+    {
+        bool previouslyAdded = modelIndex.data(RoleWasPreviouslyAdded).toBool();
+        bool added = modelIndex.data(RoleIsAdded).toBool();
+        if (includeDependencies)
+        {
+            previouslyAdded |= modelIndex.data(RoleWasPreviouslyAddedDependency).toBool();
+            added |= modelIndex.data(RoleIsAddedDependency).toBool();
+        }
+        return previouslyAdded && !added;
     }
 
     bool GemModel::HasRequirement(const QModelIndex& modelIndex)
@@ -244,13 +391,44 @@ namespace O3DE::ProjectManager
         return false;
     }
 
-    QVector<QModelIndex> GemModel::GatherGemsToBeAdded() const
+    QVector<QModelIndex> GemModel::GatherGemDependencies(const QModelIndex& modelIndex) const 
+    {
+        QVector<QModelIndex> result;
+        const QString& gemName = modelIndex.data(RoleName).toString();
+        if (m_gemDependencyMap.contains(gemName))
+        {
+            for (const QModelIndex& dependency : m_gemDependencyMap[gemName])
+            {
+                result.push_back(dependency);
+            }
+        }
+        return result;
+    }
+
+    QVector<QModelIndex> GemModel::GatherDependentGems(const QModelIndex& modelIndex, bool addedOnly) const
+    {
+        QVector<QModelIndex> result;
+        const QString& gemName = modelIndex.data(RoleName).toString();
+        if (m_gemReverseDependencyMap.contains(gemName))
+        {
+            for (const QModelIndex& dependency : m_gemReverseDependencyMap[gemName])
+            {
+                if (!addedOnly || GemModel::IsAdded(dependency))
+                {
+                    result.push_back(dependency);
+                }
+            }
+        }
+        return result;
+    }
+
+    QVector<QModelIndex> GemModel::GatherGemsToBeAdded(bool includeDependencies) const
     {
         QVector<QModelIndex> result;
         for (int row = 0; row < rowCount(); ++row)
         {
             const QModelIndex modelIndex = index(row, 0);
-            if (NeedsToBeAdded(modelIndex))
+            if (NeedsToBeAdded(modelIndex, includeDependencies))
             {
                 result.push_back(modelIndex);
             }
@@ -258,13 +436,13 @@ namespace O3DE::ProjectManager
         return result;
     }
 
-    QVector<QModelIndex> GemModel::GatherGemsToBeRemoved() const
+    QVector<QModelIndex> GemModel::GatherGemsToBeRemoved(bool includeDependencies) const
     {
         QVector<QModelIndex> result;
         for (int row = 0; row < rowCount(); ++row)
         {
             const QModelIndex modelIndex = index(row, 0);
-            if (NeedsToBeRemoved(modelIndex))
+            if (NeedsToBeRemoved(modelIndex, includeDependencies))
             {
                 result.push_back(modelIndex);
             }
@@ -272,13 +450,13 @@ namespace O3DE::ProjectManager
         return result;
     }
 
-    int GemModel::TotalAddedGems() const
+    int GemModel::TotalAddedGems(bool includeDependencies) const
     {
         int result = 0;
         for (int row = 0; row < rowCount(); ++row)
         {
             const QModelIndex modelIndex = index(row, 0);
-            if (IsAdded(modelIndex))
+            if (IsAdded(modelIndex) || (includeDependencies && IsAddedDependency(modelIndex)))
             {
                 ++result;
             }

+ 25 - 10
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h

@@ -28,13 +28,11 @@ namespace O3DE::ProjectManager
 
         void AddGem(const GemInfo& gemInfo);
         void Clear();
+        void UpdateGemDependencies();
 
         QModelIndex FindIndexByNameString(const QString& nameString) const;
-        void FindGemNamesByNameStrings(QStringList& inOutGemNames);
-        QStringList GetDependingGemUuids(const QModelIndex& modelIndex);
         QStringList GetDependingGemNames(const QModelIndex& modelIndex);
-        QStringList GetConflictingGemUuids(const QModelIndex& modelIndex);
-        QStringList GetConflictingGemNames(const QModelIndex& modelIndex);
+        bool HasDependentGems(const QModelIndex& modelIndex) const;
 
         static QString GetName(const QModelIndex& modelIndex);
         static QString GetDisplayName(const QModelIndex& modelIndex);
@@ -51,22 +49,36 @@ namespace O3DE::ProjectManager
         static QStringList GetFeatures(const QModelIndex& modelIndex);
         static QString GetPath(const QModelIndex& modelIndex);
         static QString GetRequirement(const QModelIndex& modelIndex);
+        static GemModel* GetSourceModel(QAbstractItemModel* model);
+        static const GemModel* GetSourceModel(const QAbstractItemModel* model);
 
         static bool IsAdded(const QModelIndex& modelIndex);
+        static bool IsAddedDependency(const QModelIndex& modelIndex);
         static void SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
+        static void SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
         static void SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
-        static bool NeedsToBeAdded(const QModelIndex& modelIndex);
-        static bool NeedsToBeRemoved(const QModelIndex& modelIndex);
+        static bool WasPreviouslyAdded(const QModelIndex& modelIndex);
+        static void SetWasPreviouslyAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
+        static bool WasPreviouslyAddedDependency(const QModelIndex& modelIndex);
+        static bool NeedsToBeAdded(const QModelIndex& modelIndex, bool includeDependencies = false);
+        static bool NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies = false);
         static bool HasRequirement(const QModelIndex& modelIndex);
+        static void UpdateDependencies(QAbstractItemModel& model, const QModelIndex& modelIndex);
 
         bool DoGemsToBeAddedHaveRequirements() const;
 
-        QVector<QModelIndex> GatherGemsToBeAdded() const;
-        QVector<QModelIndex> GatherGemsToBeRemoved() const;
+        QVector<QModelIndex> GatherGemDependencies(const QModelIndex& modelIndex) const;
+        QVector<QModelIndex> GatherDependentGems(const QModelIndex& modelIndex, bool addedOnly = false) const;
+        QVector<QModelIndex> GatherGemsToBeAdded(bool includeDependencies = false) const;
+        QVector<QModelIndex> GatherGemsToBeRemoved(bool includeDependencies = false) const;
 
-        int TotalAddedGems() const;
+        int TotalAddedGems(bool includeDependencies = false) const;
 
     private:
+        void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames);
+        void GetAllDependingGems(const QModelIndex& modelIndex, QSet<QModelIndex>& inOutGems);
+        QStringList GetDependingGems(const QModelIndex& modelIndex);
+
         enum UserRole
         {
             RoleName = Qt::UserRole,
@@ -76,11 +88,12 @@ namespace O3DE::ProjectManager
             RolePlatforms,
             RoleSummary,
             RoleWasPreviouslyAdded,
+            RoleWasPreviouslyAddedDependency,
             RoleIsAdded,
+            RoleIsAddedDependency,
             RoleDirectoryLink,
             RoleDocLink,
             RoleDependingGems,
-            RoleConflictingGems,
             RoleVersion,
             RoleLastUpdated,
             RoleBinarySize,
@@ -92,5 +105,7 @@ namespace O3DE::ProjectManager
 
         QHash<QString, QModelIndex> m_nameToIndexMap;
         QItemSelectionModel* m_selectionModel = nullptr;
+        QHash<QString, QSet<QModelIndex>> m_gemDependencyMap;
+        QHash<QString, QSet<QModelIndex>> m_gemReverseDependencyMap;
     };
 } // namespace O3DE::ProjectManager

+ 30 - 8
Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp

@@ -50,11 +50,21 @@ namespace O3DE::ProjectManager
             }
         }
 
-        // Gem status
-        if (m_gemStatusFilter != GemStatus::NoFilter)
+        // Gem selected 
+        if (m_gemSelectedFilter != GemSelected::NoFilter)
         {
-            const GemStatus sourceGemStatus = static_cast<GemStatus>(GemModel::IsAdded(sourceIndex));
-            if (m_gemStatusFilter != sourceGemStatus)
+            const GemSelected sourceGemStatus = static_cast<GemSelected>(GemModel::IsAdded(sourceIndex));
+            if (m_gemSelectedFilter != sourceGemStatus)
+            {
+                return false;
+            }
+        }
+
+        // Gem enabled
+        if (m_gemActiveFilter != GemActive::NoFilter)
+        {
+            const GemActive sourceGemStatus = static_cast<GemActive>(GemModel::IsAdded(sourceIndex) || GemModel::IsAddedDependency(sourceIndex));
+            if (m_gemActiveFilter != sourceGemStatus)
             {
                 return false;
             }
@@ -148,19 +158,31 @@ namespace O3DE::ProjectManager
         return true;
     }
 
-    QString GemSortFilterProxyModel::GetGemStatusString(GemStatus status)
+    QString GemSortFilterProxyModel::GetGemSelectedString(GemSelected status)
     {
         switch (status)
         {
-        case Unselected:
+        case GemSelected::Unselected:
             return "Unselected";
-        case Selected:
+        case GemSelected::Selected:
             return "Selected";
         default:
-            return "<Unknown Gem Status>";
+            return "<Unknown Selection Status>";
         }
     }
 
+    QString GemSortFilterProxyModel::GetGemActiveString(GemActive status)
+    {
+        switch (status)
+        {
+        case GemActive::Inactive:
+            return "Inactive";
+        case GemActive::Active:
+            return "Active";
+        default:
+            return "<Unknown Active Status>";
+        }
+    }
     void GemSortFilterProxyModel::InvalidateFilter()
     {
         invalidate();

+ 16 - 5
Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h

@@ -25,16 +25,23 @@ namespace O3DE::ProjectManager
         Q_OBJECT // AUTOMOC
 
     public:
-        enum GemStatus
+        enum class GemSelected
         {
             NoFilter = -1,
             Unselected,
             Selected
         };
+        enum class GemActive
+        {
+            NoFilter = -1,
+            Inactive,
+            Active 
+        };
 
         GemSortFilterProxyModel(GemModel* sourceModel, QObject* parent = nullptr);
 
-        static QString GetGemStatusString(GemStatus status);
+        static QString GetGemSelectedString(GemSelected status);
+        static QString GetGemActiveString(GemActive status);
 
         bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
 
@@ -43,8 +50,11 @@ namespace O3DE::ProjectManager
 
         void SetSearchString(const QString& searchString) { m_searchString = searchString; InvalidateFilter(); }
 
-        GemStatus GetGemStatus() const { return m_gemStatusFilter; }
-        void SetGemStatus(GemStatus gemStatus) { m_gemStatusFilter = gemStatus; InvalidateFilter(); }
+        GemSelected GetGemSelected() const { return m_gemSelectedFilter; }
+        void SetGemSelected(GemSelected selected) { m_gemSelectedFilter = selected; InvalidateFilter(); }
+
+        GemActive GetGemActive() const { return m_gemActiveFilter; }
+        void SetGemActive(GemActive enabled) { m_gemActiveFilter = enabled; InvalidateFilter(); }
 
         GemInfo::GemOrigins GetGemOrigins() const { return m_gemOriginFilter; }
         void SetGemOrigins(const GemInfo::GemOrigins& gemOrigins) { m_gemOriginFilter = gemOrigins; InvalidateFilter(); }
@@ -69,7 +79,8 @@ namespace O3DE::ProjectManager
         AzQtComponents::SelectionProxyModel* m_selectionProxyModel = nullptr;
 
         QString m_searchString;
-        GemStatus m_gemStatusFilter = GemStatus::NoFilter;
+        GemSelected m_gemSelectedFilter = GemSelected::NoFilter;
+        GemActive m_gemActiveFilter = GemActive::NoFilter;
         GemInfo::GemOrigins m_gemOriginFilter = {};
         GemInfo::Platforms m_platformFilter = {};
         GemInfo::Types m_typeFilter = {};

+ 8 - 0
Code/Tools/ProjectManager/Source/PythonBindings.cpp

@@ -675,6 +675,14 @@ namespace O3DE::ProjectManager
                     }
                 }
 
+                if (data.contains("dependencies"))
+                {
+                    for (auto dependency : data["dependencies"])
+                    {
+                        gemInfo.m_dependencies.push_back(Py_To_String(dependency));
+                    }
+                }
+
                 QString gemType = Py_To_String_Optional(data, "type", "");
                 if (gemType == "Asset")
                 {

+ 1 - 0
Code/Tools/ProjectManager/project_manager_tests_files.cmake

@@ -11,6 +11,7 @@ set(FILES
     Resources/ProjectManager.qss
     tests/ApplicationTests.cpp
     tests/PythonBindingsTests.cpp
+    tests/GemCatalogTests.cpp
     tests/main.cpp
     tests/UtilsTests.cpp
 )

+ 64 - 0
Code/Tools/ProjectManager/tests/GemCatalogTests.cpp

@@ -0,0 +1,64 @@
+/*
+ * 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 <AzCore/UnitTest/TestTypes.h>
+#include <AzTest/Utils.h>
+#include <GemCatalog/GemModel.h>
+
+
+namespace O3DE::ProjectManager
+{
+    class GemCatalogTests 
+        : public ::UnitTest::ScopedAllocatorSetupFixture
+    {
+    public:
+
+        GemCatalogTests() = default;
+    };
+
+    TEST_F(GemCatalogTests, GemCatalog_Displays_But_Does_Not_Add_Dependencies)
+    {
+        GemModel* gemModel = new GemModel();
+
+        // given 3 gems a,b,c where a depends on b which depends on c
+        GemInfo gemA, gemB, gemC;
+        QModelIndex indexA, indexB, indexC;
+        gemA.m_name = "a";
+        gemB.m_name = "b";
+        gemC.m_name = "c";
+
+        gemA.m_dependencies = QStringList({ "b" });
+        gemB.m_dependencies = QStringList({ "c" });
+
+        gemModel->AddGem(gemA);
+        indexA = gemModel->FindIndexByNameString(gemA.m_name);
+
+        gemModel->AddGem(gemB);
+        indexB = gemModel->FindIndexByNameString(gemB.m_name);
+
+        gemModel->AddGem(gemC);
+        indexC = gemModel->FindIndexByNameString(gemC.m_name);
+
+        gemModel->UpdateGemDependencies();
+
+        EXPECT_FALSE(GemModel::IsAdded(indexA));
+        EXPECT_FALSE(GemModel::IsAddedDependency(indexB) || GemModel::IsAddedDependency(indexC));
+
+        // when a is added
+        GemModel::SetIsAdded(*gemModel, indexA, true);
+
+        // expect b and c are now dependencies of an added gem but not themselves added
+        // cmake will handle dependencies
+        EXPECT_TRUE(GemModel::IsAddedDependency(indexB) && GemModel::IsAddedDependency(indexC));
+        EXPECT_TRUE(!GemModel::IsAdded(indexB) && !GemModel::IsAdded(indexC));
+
+        QVector<QModelIndex> gemsToAdd = gemModel->GatherGemsToBeAdded();
+        EXPECT_TRUE(gemsToAdd.size() == 1);
+        EXPECT_EQ(GemModel::GetName(gemsToAdd.at(0)), gemA.m_name);
+    }
+}