Переглянути джерело

gem versions and engines ux

Signed-off-by: Alex Peterson <[email protected]>
Alex Peterson 2 роки тому
батько
коміт
a771c21ab3

+ 79 - 32
Code/Tools/ProjectManager/Resources/ProjectManager.qss

@@ -17,6 +17,37 @@ QPushButton:focus {
     border:1px solid #1e70eb;
 }
 
+QPushButton[secondary="true"],
+QPushButton[class="Secondary"] {
+    qproperty-flat: true;
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #888888, stop: 1.0 #555555);
+}
+QPushButton[secondary="true"]:hover,
+QPushButton[class="Secondary"]:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #999999, stop: 1.0 #666666);
+}
+QPushButton[secondary="true"]:pressed,
+QPushButton[class="Secondary"]:pressed {
+    qproperty-flat: true;
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #555555, stop: 1.0 #777777);
+}
+
+QPushButton[class="Danger"] {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #E32C27, stop: 1.0 #951D21);
+}
+QPushButton[class="Danger"]:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #FD3129, stop: 1.0 #AF2221);
+}
+QPushButton[class="Danger"]:pressed {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #951D1F, stop: 1.0 #C92724);
+}
+
 QTabBar {
     background-color: transparent;
 }
@@ -298,8 +329,6 @@ QTabBar::tab:focus {
     height: 40px;
 }
 
-
-
 #formErrorLabel {
     color: #ec3030;
     font-size: 14px;
@@ -445,20 +474,37 @@ QTabBar::tab:focus {
                             stop: 0 #0085e2, stop: 1.0 #0e60db);
 }
 
+#footer > QPushButton[class="Secondary"],
 #footer > QPushButton[secondary="true"] {
     margin-right: 10px;
     background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                             stop: 0 #888888, stop: 1.0 #555555);
 }
-#footer > QPushButton[secondary="true"]:hover {
+#footer > QPushButton[secondary="true"]:hover,
+#footer > QPushButton[class="Secondary"]:hover {
     background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                             stop: 0 #999999, stop: 1.0 #666666);
 }
+
+#footer > QPushButton[class="Secondary"]:pressed,
 #footer > QPushButton[secondary="true"]:pressed {
     background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
                             stop: 0 #555555, stop: 1.0 #777777);
 }
 
+#footer > QPushButton[class="Danger"] {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #E32C27, stop: 1.0 #951D21);
+}
+#footer > QPushButton[class="Danger"]:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #FD3129, stop: 1.0 #AF2221);
+}
+#footer > QPushButton[class="Danger"]:pressed {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
+                            stop: 0 #951D1F, stop: 1.0 #C92724);
+}
+
 #dialogSubTitle {
     font-size:14px;
     font-weight:600;
@@ -844,6 +890,15 @@ QProgressBar::chunk {
     min-height:24px;
 }
 
+#GemCatalogRequirements {
+   margin:0 0 10px 0; 
+   padding-left:34px;
+   min-height: 34px;
+   font-size:10px;
+   color:#ddd;
+   background:transparent url(:/Warning.svg) no-repeat left center;
+}
+
 #GemCatalogCartOverlayGemDownloadHeader {
     margin:0;
     padding: 0px;
@@ -883,7 +938,7 @@ QProgressBar::chunk {
     margin-top:5px;
 }
 
-#gemCatalogUpdateGemButton,
+#GemCatalogInspector QPushButton,
 #gemCatalogUninstallGemButton 
 {
     qproperty-flat: true;
@@ -895,34 +950,6 @@ QProgressBar::chunk {
     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);
-}
 
 /************** Filter Tag widget **************/
 
@@ -957,6 +984,26 @@ QProgressBar::chunk {
     background-color: #444444;
 }
 
+#GemCatalogInspector QLabel[class="title"] {
+    font-size: 16px;
+    margin:0;
+}
+
+#GemCatalogInspector QLabel[class="label"] {
+    min-width:50px;
+    max-width:50px;
+    margin:0;
+}
+
+#GemCatalogInspector QLabel[class="value"] {
+    margin:0;
+}
+
+#GemCatalogVersion QPushButton {
+    margin:5px 0 5px 0;
+}
+
+
 /************** Gem Catalog (Filter/left pane) **************/
 
 #GemCatalogFilterWidget {

+ 25 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -347,7 +347,31 @@ namespace O3DE::ProjectManager
         tags.reserve(gems.size());
         for (const QModelIndex& modelIndex : gems)
         {
-            tags.push_back({ GemModel::GetDisplayName(modelIndex), GemModel::GetName(modelIndex) });
+            if(GemModel::IsEngineGem(modelIndex))
+            {
+                // don't show engine gem versions
+                tags.push_back({ GemModel::GetDisplayName(modelIndex), GemModel::GetName(modelIndex) });
+            }
+            else
+            {
+                // show non-engine gem versions if available
+                QString version =  GemModel::GetNewVersion(modelIndex);
+                if (version.isEmpty())
+                {
+                    version =  GemModel::GetVersion(modelIndex);
+                }
+
+                if (version.isEmpty() || version.contains("Unknown", Qt::CaseInsensitive))
+                {
+                    tags.push_back({ GemModel::GetDisplayName(modelIndex), GemModel::GetName(modelIndex) });
+                }
+                else
+                {
+                    const QString& title = QString("%1 %2").arg(GemModel::GetDisplayName(modelIndex), version);
+                    tags.push_back({ title, GemModel::GetName(modelIndex) });
+                }
+            }
+
         }
         return tags;
     }

+ 43 - 76
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -55,8 +55,8 @@ namespace O3DE::ProjectManager
         m_gemModel = new GemModel(this);
         m_proxyModel = new GemSortFilterProxyModel(m_gemModel, this);
 
-        // default to sort by gem name
-        m_proxyModel->setSortRole(GemModel::RoleName);
+        // default to sort by gem display name 
+        m_proxyModel->setSortRole(GemModel::RoleDisplayName);
 
         QVBoxLayout* vLayout = new QVBoxLayout();
         vLayout->setMargin(0);
@@ -85,7 +85,7 @@ namespace O3DE::ProjectManager
         m_rightPanelStack = new QStackedWidget(this);
         m_rightPanelStack->setFixedWidth(sidePanelWidth);
 
-        m_gemInspector = new GemInspector(m_gemModel, m_rightPanelStack);
+        m_gemInspector = new GemInspector(m_gemModel, m_rightPanelStack, m_readOnly);
 
         connect(
             m_gemInspector,
@@ -108,18 +108,26 @@ namespace O3DE::ProjectManager
         GemListHeaderWidget* catalogHeaderWidget = new GemListHeaderWidget(m_proxyModel);
         connect(catalogHeaderWidget, &GemListHeaderWidget::OnRefresh, this, &GemCatalogScreen::Refresh);
 
-        constexpr int minHeaderSectionWidth = 100;
+        constexpr int GemImageHeaderWidth = GemItemDelegate::s_itemMargins.left() + GemPreviewImageWidth +
+                                            AdjustableHeaderWidget::s_headerTextIndent;
+        constexpr int GemVersionHeaderWidth = GemItemDelegate::s_versionSize + GemItemDelegate::s_versionSizeSpacing;
+        constexpr int GemStatusHeaderWidth = GemItemDelegate::s_statusIconSize + GemItemDelegate::s_statusButtonSpacing +
+                                             GemItemDelegate::s_buttonWidth + GemItemDelegate::s_contentMargins.right();
+        constexpr int minHeaderSectionWidth = AZStd::min(GemImageHeaderWidth, AZStd::min(GemVersionHeaderWidth, GemStatusHeaderWidth));
+
+        //constexpr int itemLeftMargin = k
         AdjustableHeaderWidget* listHeaderWidget = new AdjustableHeaderWidget(
-            QStringList{ tr("Gem Image"), tr("Gem Name"), tr("Gem Summary"), tr("Status") },
-            QVector<int>{ GemPreviewImageWidth + AdjustableHeaderWidget::s_headerTextIndent,
-                          -GemPreviewImageWidth - AdjustableHeaderWidget::s_headerTextIndent + GemItemDelegate::s_defaultSummaryStartX - 30,
+            QStringList{ tr("Gem Image"), tr("Gem Name"), tr("Gem Summary"), tr("Version"), tr("Status") },
+            QVector<int>{ GemImageHeaderWidth,
+                          GemItemDelegate::s_defaultSummaryStartX - GemImageHeaderWidth,
                           0, // Section is set to stretch to fit
-                          GemItemDelegate::s_statusIconSize + GemItemDelegate::s_statusButtonSpacing + GemItemDelegate::s_buttonWidth +
-                              GemItemDelegate::s_contentMargins.right() },
+                          GemVersionHeaderWidth,
+                          GemStatusHeaderWidth},
             minHeaderSectionWidth,
             QVector<QHeaderView::ResizeMode>{ QHeaderView::ResizeMode::Fixed,
                                               QHeaderView::ResizeMode::Interactive,
                                               QHeaderView::ResizeMode::Stretch,
+                                              QHeaderView::ResizeMode::Fixed,
                                               QHeaderView::ResizeMode::Fixed },
             this);
 
@@ -368,7 +376,16 @@ namespace O3DE::ProjectManager
             QString notification;
             if (gemStateChanged)
             {
-                notification = GemModel::GetDisplayName(modelIndex);
+                QString version = GemModel::GetNewVersion(modelIndex);
+                if (version.isEmpty())
+                {
+                    notification = GemModel::GetDisplayName(modelIndex);
+                } 
+                else
+                {
+                    notification = QString("%1 %2").arg(GemModel::GetDisplayName(modelIndex), version);
+                }
+
                 if (numChangedDependencies > 0)
                 {
                     notification += tr(" and ");
@@ -562,87 +579,37 @@ namespace O3DE::ProjectManager
         const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath);
         if (allGemInfosResult.IsSuccess())
         {
-            // Add all available gems to the model.
-            const QVector<GemInfo>& allGemInfos = allGemInfosResult.GetValue();
-            for (const GemInfo& gemInfo : allGemInfos)
-            {
-                if (gemInfo.m_name != "${Name}")
-                {
-                    m_gemModel->AddGem(gemInfo);
-                }
-            }
+            m_gemModel->AddGems(allGemInfosResult.GetValue());
 
-            const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
+            const auto& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
             if (allRepoGemInfosResult.IsSuccess())
             {
-                const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
-                for (const GemInfo& gemInfo : allRepoGemInfos)
-                {
-                    // do not add gems that have already been downloaded
-                    if (!m_gemModel->FindIndexByNameString(gemInfo.m_name).isValid())
-                    {
-                        m_gemModel->AddGem(gemInfo);
-                    }
-                }
+                m_gemModel->AddGems(allRepoGemInfosResult.GetValue());
             }
             else
             {
                 QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems from repos.<br><br>Error:<br>%1").arg(allRepoGemInfosResult.GetError().c_str()));
             }
 
+            // we need to update all gem dependencies before activating all the gems for this project
             m_gemModel->UpdateGemDependencies();
 
-            // if we don't have a project path early out
-            if (m_projectPath.isEmpty())
+            if (!m_projectPath.isEmpty())
             {
-                return;
-            }
-
-            m_notificationsEnabled = false;
-
-            // Gather enabled gems for the given project.
-            constexpr bool includeDependencies = false;
-            const auto& enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGems(projectPath, includeDependencies);
-            if (enabledGemNamesResult.IsSuccess())
-            {
-                const auto& enabledGemNames = enabledGemNamesResult.GetValue();
-                for (auto itr = enabledGemNames.cbegin(); itr != enabledGemNames.cend(); itr++)
+                constexpr bool includeDependencies = false;
+                const auto& enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGems(projectPath, includeDependencies);
+                if (enabledGemNamesResult.IsSuccess())
                 {
-                    const QString& gemNameWithSpecifier = itr.key();
-                    const QString& gemPath = itr.value(); 
-
-                    AZ::Dependency<AZ::SemanticVersion::parts_count> dependency;
-                    auto parseOutcome = dependency.ParseVersions({ gemNameWithSpecifier.toUtf8().constData() });
-                    const QString& gemName = parseOutcome ? dependency.GetName().c_str() : gemNameWithSpecifier; 
-
-                    // First, try to find the gem by path
-                    QModelIndex modelIndex = m_gemModel->FindIndexByPath(gemPath);
-                    if (!modelIndex.isValid())
-                    {
-                        // Fall back to lookup by name 
-                        modelIndex = m_gemModel->FindIndexByNameString(gemName);
-                    }
-
-                    if (modelIndex.isValid())
-                    {
-                        GemModel::SetWasPreviouslyAdded(*m_gemModel, modelIndex, true);
-                        GemModel::SetIsAdded(*m_gemModel, modelIndex, true);
-                    }
-                    // ${Name} is a special name used in templates and is not really an error
-                    else if (gemName != "${Name}")
-                    {
-                        AZ_Warning("ProjectManager::GemCatalog", false,
-                            "Cannot find entry for gem with name '%s'. The CMake target name probably does not match the specified name in the gem.json.",
-                            gemName.toUtf8().constData());
-                    }
+                    m_gemModel->ActivateGems(enabledGemNamesResult.GetValue());
+                }
+                else
+                {
+                    QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.<br><br>Error:<br>%2").arg(projectPath, enabledGemNamesResult.GetError().c_str()));
                 }
-            }
-            else
-            {
-                QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.<br><br>Error:<br>%2").arg(projectPath, enabledGemNamesResult.GetError().c_str()));
-            }
 
-            m_notificationsEnabled = true;
+                // sort after activating gems in case the display name for a gem is different for the active version 
+                m_proxyModel->sort(/*column=*/0);
+            }
         }
         else
         {

+ 152 - 68
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp

@@ -14,15 +14,18 @@
 #include <QFrame>
 #include <QLabel>
 #include <QSpacerItem>
+#include <QHBoxLayout>
 #include <QVBoxLayout>
 #include <QIcon>
 #include <QPushButton>
+#include <QComboBox>
 
 namespace O3DE::ProjectManager
 {
-    GemInspector::GemInspector(GemModel* model, QWidget* parent)
+    GemInspector::GemInspector(GemModel* model, QWidget* parent, bool readOnly)
         : QScrollArea(parent)
         , m_model(model)
+        , m_readOnly(readOnly)
     {
         setObjectName("GemCatalogInspector");
         setWidgetResizable(true);
@@ -59,7 +62,7 @@ namespace O3DE::ProjectManager
         Update(selectedIndices[0]);
     }
 
-    void SetLabelElidedText(QLabel* label, QString text, int labelWidth = 0)
+    void SetLabelElidedText(QLabel* label, const QString& text, int labelWidth = 0)
     {
         QFontMetrics nameFontMetrics(label->font());
         if (!labelWidth)
@@ -78,7 +81,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    void GemInspector::Update(const QModelIndex& modelIndex)
+    void GemInspector::Update(const QModelIndex& modelIndex, [[maybe_unused]] const QString& version)
     {
         m_curModelIndex = modelIndex;
 
@@ -87,36 +90,38 @@ namespace O3DE::ProjectManager
             m_mainWidget->hide();
         }
 
+        // use the provided version if available
+        QString displayVersion = version;
+        QString activeVersion = m_model->GetNewVersion(modelIndex);
+        if (activeVersion.isEmpty())
+        {
+            // fallback to the current version
+            activeVersion = m_model->GetVersion(modelIndex);
+        }
+
+        if (displayVersion.isEmpty())
+        {
+            displayVersion = activeVersion;
+        }
+
+        const GemInfo& gemInfo = m_model->GetGemInfo(modelIndex, displayVersion);
+
+        // The gem display name should stay the same
         SetLabelElidedText(m_nameLabel, m_model->GetDisplayName(modelIndex));
-        SetLabelElidedText(m_creatorLabel, m_model->GetCreator(modelIndex));
+        SetLabelElidedText(m_creatorLabel, gemInfo.m_origin);
 
-        m_summaryLabel->setText(m_model->GetSummary(modelIndex));
+        m_summaryLabel->setText(gemInfo.m_summary);
         m_summaryLabel->adjustSize();
 
         // Manually define remaining space to elide text because spacer would like to take all of the space
-        SetLabelElidedText(m_licenseLinkLabel, m_model->GetLicenseText(modelIndex), width() - m_licenseLabel->width() - 35);
-        m_licenseLinkLabel->SetUrl(m_model->GetLicenseLink(modelIndex));
-
-        m_directoryLinkLabel->SetUrl(m_model->GetDirectoryLink(modelIndex));
-        m_documentationLinkLabel->SetUrl(m_model->GetDocLink(modelIndex));
+        SetLabelElidedText(m_licenseLinkLabel, gemInfo.m_licenseText, width() - m_licenseLabel->width() - 35);
+        m_licenseLinkLabel->SetUrl(gemInfo.m_licenseLink);
 
-        if (m_model->HasRequirement(modelIndex))
-        {
-            m_requirementsIconLabel->show();
-            m_requirementsTitleLabel->show();
-            m_requirementsTextLabel->show();
-            m_requirementsMainSpacer->changeSize(0, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
+        m_directoryLinkLabel->SetUrl(gemInfo.m_directoryLink);
+        m_documentationLinkLabel->SetUrl(gemInfo.m_documentationLink);
 
-            m_requirementsTitleLabel->setText(tr("Requirement"));
-            m_requirementsTextLabel->setText(m_model->GetRequirement(modelIndex));
-        }
-        else
-        {
-            m_requirementsIconLabel->hide();
-            m_requirementsTitleLabel->hide();
-            m_requirementsTextLabel->hide();
-            m_requirementsMainSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
-        }
+        m_requirementsTextLabel->setVisible(!gemInfo.m_requirement.isEmpty());
+        m_requirementsTextLabel->setText(gemInfo.m_requirement);
 
         // Depending gems
         const QVector<Tag>& dependingGemTags = m_model->GetDependingGemTags(modelIndex);
@@ -133,25 +138,67 @@ namespace O3DE::ProjectManager
         }
 
         // Additional information
-        m_versionLabel->setText(tr("Gem Version: %1").arg(m_model->GetVersion(modelIndex)));
-        m_lastUpdatedLabel->setText(tr("Last Updated: %1").arg(m_model->GetLastUpdated(modelIndex)));
-        const int binarySize = m_model->GetBinarySizeInKB(modelIndex);
-        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 ||
-             m_model->GetDownloadStatus(modelIndex) == GemInfo::DownloadSuccessful))
+        m_lastUpdatedLabel->setText(tr("Last Updated: %1").arg(gemInfo.m_lastUpdatedDate));
+        m_binarySizeLabel->setText(tr("Binary Size:  %1").arg(gemInfo.m_binarySizeInKB ? tr("%1 KB").arg(gemInfo.m_binarySizeInKB) : tr("Unknown")));
+
+        // Versions
+        disconnect(m_versionComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GemInspector::OnVersionChanged);
+        m_versionComboBox->clear();
+        const bool isEngineGem = gemInfo.IsEngineGem();
+        auto gemVersions = m_model->GetGemVersions(modelIndex);
+        if (isEngineGem || gemVersions.count() < 2)
         {
-            m_updateGemButton->show();
-            m_uninstallGemButton->show();
+            m_versionComboBox->setVisible(false);
+            m_versionLabel->setText(gemInfo.m_version);
+            m_versionLabel->setVisible(true);
+            m_updateVersionButton->setVisible(false);
         }
         else
         {
-            m_updateGemButton->hide();
-            m_uninstallGemButton->hide();
+            m_versionLabel->setVisible(false);
+            m_versionComboBox->setVisible(true);
+            m_versionComboBox->addItems(gemVersions);
+            if (m_versionComboBox->count() == 0)
+            {
+                m_versionComboBox->insertItem(0, "Unknown");
+            }
+
+            auto foundIndex = m_versionComboBox->findText(displayVersion);
+            m_versionComboBox->setCurrentIndex(foundIndex > -1 ? foundIndex : 0);
+
+            bool versionChanged = displayVersion != activeVersion && !m_readOnly && m_model->IsAdded(modelIndex);
+            m_updateVersionButton->setVisible(versionChanged);
+            if (versionChanged)
+            {
+                m_updateVersionButton->setText(tr("Use Version %1").arg(m_versionComboBox->currentText()));
+            }
+            connect(m_versionComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GemInspector::OnVersionChanged);
+
         }
 
+        // Compatible engines
+        m_enginesTitleLabel->setVisible(!isEngineGem);
+        m_enginesLabel->setVisible(!isEngineGem);
+        if (!isEngineGem)
+        {
+            if (gemInfo.m_compatibleEngines.isEmpty())
+            {
+                m_enginesLabel->setText("All");
+            }
+            else
+            {
+                m_enginesLabel->setText(gemInfo.m_compatibleEngines.join("\n"));
+            }
+        }
+
+        const bool isRemote = gemInfo.m_gemOrigin == GemInfo::Remote;
+        const bool isDownloaded = gemInfo.m_downloadStatus == GemInfo::Downloaded ||
+                                  gemInfo.m_downloadStatus == GemInfo::DownloadSuccessful;
+
+        m_updateGemButton->setVisible(isRemote && isDownloaded);
+        m_uninstallGemButton->setVisible(isRemote && isDownloaded);
+        m_editGemButton->setVisible(!isRemote || (isRemote && isDownloaded));
+
         m_mainWidget->adjustSize();
         m_mainWidget->show();
     }
@@ -164,11 +211,63 @@ namespace O3DE::ProjectManager
         return result;
     }
 
+    void GemInspector::OnVersionChanged([[maybe_unused]] int index)
+    {
+        Update(m_curModelIndex, m_versionComboBox->currentText());
+        if (!GemModel::IsAdded(m_curModelIndex))
+        {
+            GemModel::UpdateWithVersion(*m_model, m_curModelIndex, m_versionComboBox->currentText());
+        }
+    }
+
     void GemInspector::InitMainWidget()
     {
         // Gem name, creator and summary
         m_nameLabel = CreateStyledLabel(m_mainLayout, 18, s_headerColor);
         m_creatorLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_headerColor);
+
+        // Version
+        {
+            m_versionWidget = new QWidget();
+            m_versionWidget->setObjectName("GemCatalogVersion");
+            auto versionVLayout = new QVBoxLayout();
+            versionVLayout->setMargin(0);
+            auto versionHLayout = new QHBoxLayout();
+            versionHLayout->setMargin(0);
+            versionVLayout->addLayout(versionHLayout);
+            m_versionWidget->setLayout(versionVLayout);
+            m_mainLayout->addWidget(m_versionWidget);
+
+            auto versionLabelTitle = new QLabel(tr("Version: "));
+            versionLabelTitle->setProperty("class", "label");
+            versionHLayout->addWidget(versionLabelTitle);
+            m_versionLabel = new QLabel();
+            m_versionLabel->setProperty("class", "value");
+            versionHLayout->addWidget(m_versionLabel);
+
+            m_versionComboBox = new QComboBox();
+            versionHLayout->addWidget(m_versionComboBox);
+
+            m_updateVersionButton = new QPushButton(tr("Use Version"));
+            m_updateVersionButton->setProperty("class", "Secondary");
+            versionVLayout->addWidget(m_updateVersionButton);
+            connect(m_updateVersionButton, &QPushButton::clicked, this , [this]{
+                GemModel::SetIsAdded(*m_model, m_curModelIndex, true, m_versionComboBox->currentText());
+                GemModel::UpdateWithVersion(*m_model, m_curModelIndex, m_versionComboBox->currentText());
+                m_updateVersionButton->setVisible(false);
+            });
+
+            auto enginesHLayout = new QHBoxLayout();
+            enginesHLayout->setMargin(0);
+            versionVLayout->addLayout(enginesHLayout);
+            m_enginesTitleLabel = new QLabel(tr("Engines: "));
+            m_enginesTitleLabel->setProperty("class", "label");
+            enginesHLayout->addWidget(m_enginesTitleLabel);
+            m_enginesLabel = new QLabel();
+            m_enginesLabel->setProperty("class", "value");
+            enginesHLayout->addWidget(m_enginesLabel);
+        }
+
         m_mainLayout->addSpacing(5);
 
         // TODO: QLabel seems to have issues determining the right sizeHint() for our font with the given font size.
@@ -226,29 +325,23 @@ namespace O3DE::ProjectManager
         m_mainLayout->addSpacing(10);
 
         // Requirements
-        m_requirementsTitleLabel = GemInspector::CreateStyledLabel(m_mainLayout, 16, s_headerColor);
-
-        QHBoxLayout* requirementsLayout = new QHBoxLayout();
-        requirementsLayout->setAlignment(Qt::AlignTop);
-        requirementsLayout->setMargin(0);
-        requirementsLayout->setSpacing(0);
-
-        m_requirementsIconLabel = new QLabel();
-        m_requirementsIconLabel->setPixmap(QIcon(":/Warning.svg").pixmap(24, 24));
-        requirementsLayout->addWidget(m_requirementsIconLabel);
-
-        m_requirementsTextLabel = GemInspector::CreateStyledLabel(requirementsLayout, 10, s_textColor);
+        m_requirementsTextLabel = new QLabel();
+        m_requirementsTextLabel->setObjectName("GemCatalogRequirements");
         m_requirementsTextLabel->setWordWrap(true);
         m_requirementsTextLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
         m_requirementsTextLabel->setOpenExternalLinks(true);
 
-        QSpacerItem* requirementsSpacer = new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding);
-        requirementsLayout->addSpacerItem(requirementsSpacer);
+        m_mainLayout->addWidget(m_requirementsTextLabel);
 
-        m_mainLayout->addLayout(requirementsLayout);
+        // Additional information
+        auto additionalInfoLabel = new QLabel(tr("Additional Information"));
+        additionalInfoLabel->setProperty("class", "title");
+        m_mainLayout->addWidget(additionalInfoLabel);
+
+        m_lastUpdatedLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor);
+        m_binarySizeLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor);
 
-        m_requirementsMainSpacer = new QSpacerItem(0, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
-        m_mainLayout->addSpacerItem(m_requirementsMainSpacer);
+        m_mainLayout->addSpacing(20);
 
         // Depending gems
         m_dependingGems = new GemsSubWidget();
@@ -257,33 +350,24 @@ namespace O3DE::ProjectManager
         m_dependingGemsSpacer = new QSpacerItem(0, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
         m_mainLayout->addSpacerItem(m_dependingGemsSpacer);
 
-        // Additional information
-        QLabel* additionalInfoLabel = CreateStyledLabel(m_mainLayout, 14, s_headerColor);
-        additionalInfoLabel->setText(tr("Additional Information"));
-
-        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_updateGemButton->setProperty("class", "Secondary");
         m_mainLayout->addWidget(m_updateGemButton);
         connect(m_updateGemButton, &QPushButton::clicked, this , [this]{ emit UpdateGem(m_curModelIndex); });
 
         m_mainLayout->addSpacing(10);
 
         m_editGemButton = new QPushButton(tr("Edit Gem"));
-        m_editGemButton->setObjectName("gemCatalogUpdateGemButton");
+        m_editGemButton->setProperty("class", "Secondary");
         m_mainLayout->addWidget(m_editGemButton);
         connect(m_editGemButton, &QPushButton::clicked, this , [this]{ emit EditGem(m_curModelIndex); });
 
         m_mainLayout->addSpacing(10);
 
         m_uninstallGemButton = new QPushButton(tr("Uninstall Gem"));
-        m_uninstallGemButton->setObjectName("gemCatalogUninstallGemButton");
+        m_uninstallGemButton->setProperty("class", "Danger");
         m_mainLayout->addWidget(m_uninstallGemButton);
         connect(m_uninstallGemButton, &QPushButton::clicked, this , [this]{ emit UninstallGem(m_curModelIndex); });
     }

+ 11 - 5
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h

@@ -22,6 +22,7 @@ QT_FORWARD_DECLARE_CLASS(QVBoxLayout)
 QT_FORWARD_DECLARE_CLASS(QLabel)
 QT_FORWARD_DECLARE_CLASS(QSpacerItem)
 QT_FORWARD_DECLARE_CLASS(QPushButton)
+QT_FORWARD_DECLARE_CLASS(QComboBox)
 
 namespace O3DE::ProjectManager
 {
@@ -31,10 +32,10 @@ namespace O3DE::ProjectManager
         Q_OBJECT
 
     public:
-        explicit GemInspector(GemModel* model, QWidget* parent = nullptr);
+        explicit GemInspector(GemModel* model, QWidget* parent, bool readOnly = false);
         ~GemInspector() = default;
 
-        void Update(const QModelIndex& modelIndex);
+        void Update(const QModelIndex& modelIndex, const QString& version = "");
         static QLabel* CreateStyledLabel(QLayout* layout, int fontSize, const QString& colorCodeString);
 
         // Fonts
@@ -52,10 +53,13 @@ namespace O3DE::ProjectManager
 
     private slots:
         void OnSelectionChanged(const QItemSelection& selected, const QItemSelection& deselected);
+        void OnVersionChanged(int index);
 
     private:
         void InitMainWidget();
 
+        bool m_readOnly = false;
+
         GemModel* m_model = nullptr;
         QWidget* m_mainWidget = nullptr;
         QVBoxLayout* m_mainLayout = nullptr;
@@ -71,20 +75,22 @@ namespace O3DE::ProjectManager
         LinkLabel* m_documentationLinkLabel = nullptr;
 
         // Requirements
-        QLabel* m_requirementsTitleLabel = nullptr;
-        QLabel* m_requirementsIconLabel = nullptr;
         QLabel* m_requirementsTextLabel = nullptr;
-        QSpacerItem* m_requirementsMainSpacer = nullptr;
 
         // Depending gems
         GemsSubWidget* m_dependingGems = nullptr;
         QSpacerItem* m_dependingGemsSpacer = nullptr;
 
         // Additional information
+        QComboBox* m_versionComboBox = nullptr;
+        QWidget* m_versionWidget = nullptr;
         QLabel* m_versionLabel = nullptr;
+        QLabel* m_enginesTitleLabel = nullptr;
+        QLabel* m_enginesLabel = nullptr;
         QLabel* m_lastUpdatedLabel = nullptr;
         QLabel* m_binarySizeLabel = nullptr;
 
+        QPushButton* m_updateVersionButton = nullptr;
         QPushButton* m_updateGemButton = nullptr;
         QPushButton* m_editGemButton = nullptr;
         QPushButton* m_uninstallGemButton = nullptr;

+ 13 - 2
Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp

@@ -91,8 +91,6 @@ namespace O3DE::ProjectManager
         QRect fullRect, itemRect, contentRect;
         CalcRects(options, fullRect, itemRect, contentRect);
 
-        QRect buttonRect = CalcButtonRect(contentRect);
-
         QFont standardFont(options.font);
         standardFont.setPixelSize(static_cast<int>(s_fontSize));
         QFontMetrics standardFontMetrics(standardFont);
@@ -163,6 +161,19 @@ namespace O3DE::ProjectManager
         const QRect summaryRect = CalcSummaryRect(contentRect, hasTags);
         DrawText(summary, painter, summaryRect, standardFont);
 
+        // Gem Version
+        // include the version in the name if it isn't unknown
+        QString gemVersion = GemModel::GetVersion(modelIndex);
+        if (!gemVersion.isEmpty() && !gemVersion.contains("unknown", Qt::CaseInsensitive))
+        {
+            QPair<int, int> versionXBounds = CalcColumnXBounds(HeaderOrder::Version);
+            QRect gemVersionRect{ versionXBounds.first, contentRect.top(), versionXBounds.second - versionXBounds.first, contentRect.height() };
+            painter->setFont(standardFont);
+            gemVersionRect = painter->boundingRect(gemVersionRect, Qt::TextWordWrap | Qt::AlignRight | Qt::AlignVCenter, gemVersion);
+            painter->drawText(gemVersionRect, Qt::TextWordWrap | Qt::AlignRight | Qt::AlignVCenter, gemVersion);
+        }
+
+        QRect buttonRect = CalcButtonRect(contentRect);
         DrawDownloadStatusIcon(painter, contentRect, buttonRect, modelIndex);
         if (!m_readOnly)
         {

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

@@ -51,7 +51,7 @@ namespace O3DE::ProjectManager
 
         // Margin and borders
         inline constexpr static QMargins s_itemMargins = QMargins(/*left=*/16, /*top=*/5, /*right=*/16, /*bottom=*/5); // Item border distances
-        inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/10, /*top=*/12, /*right=*/20, /*bottom=*/12); // Distances of the elements within an item to the item borders
+        inline constexpr static QMargins s_contentMargins = QMargins(/*left=*/10, /*top=*/12, /*right=*/30, /*bottom=*/12); // Distances of the elements within an item to the item borders
         inline constexpr static int s_borderWidth = 4;
         inline constexpr static int s_extraSummarySpacing = s_itemMargins.right();
 
@@ -75,6 +75,10 @@ namespace O3DE::ProjectManager
         inline constexpr static int s_platformTextLineBottomMargin = 5;
         inline constexpr static int s_platformTextWrapAroundLineMaxCount = 2;
 
+        // Version
+        inline constexpr static int s_versionSize = 50;
+        inline constexpr static int s_versionSizeSpacing = 25;
+
         // Status icon
         inline constexpr static int s_statusIconSize = 16;
         inline constexpr static int s_statusButtonSpacing = 5;
@@ -84,6 +88,7 @@ namespace O3DE::ProjectManager
             Preview,
             Name,
             Summary,
+            Version,
             Status
         };
 

+ 271 - 67
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp

@@ -7,10 +7,15 @@
  */
 
 #include <AzCore/std/string/string.h>
+#include <AzCore/IO/Path/Path.h>
+#include <AzCore/Dependency/Dependency.h>
 #include <GemCatalog/GemModel.h>
 #include <GemCatalog/GemSortFilterProxyModel.h>
 #include <AzCore/Casting/numeric_cast.h>
 #include <AzToolsFramework/UI/Notifications/ToastBus.h>
+#include <ProjectUtils.h>
+
+#include <QList>
 
 namespace O3DE::ProjectManager
 {
@@ -27,53 +32,207 @@ namespace O3DE::ProjectManager
         return m_selectionModel;
     }
 
-    QModelIndex GemModel::AddGem(const GemInfo& gemInfo)
+    void SetItemDataFromGemInfo(QStandardItem* item, const GemInfo& gemInfo, bool metaDataOnly = false)
     {
-        if (FindIndexByNameString(gemInfo.m_name).isValid())
+        item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+        item->setData(gemInfo.m_name, GemModel::RoleName);
+        item->setData(gemInfo.m_displayName, GemModel::RoleDisplayName);
+        item->setData(gemInfo.m_origin, GemModel::RoleCreator);
+        item->setData(gemInfo.m_gemOrigin, GemModel::RoleGemOrigin);
+        item->setData(aznumeric_cast<int>(gemInfo.m_platforms), GemModel::RolePlatforms);
+        item->setData(aznumeric_cast<int>(gemInfo.m_types), GemModel::RoleTypes);
+        item->setData(gemInfo.m_summary, GemModel::RoleSummary);
+        item->setData(gemInfo.m_directoryLink, GemModel::RoleDirectoryLink);
+        item->setData(gemInfo.m_documentationLink, GemModel::RoleDocLink);
+        item->setData(gemInfo.m_dependencies, GemModel::RoleDependingGems);
+        item->setData(gemInfo.m_version, GemModel::RoleVersion);
+        item->setData(gemInfo.m_lastUpdatedDate, GemModel::RoleLastUpdated);
+        item->setData(gemInfo.m_binarySizeInKB, GemModel::RoleBinarySize);
+        item->setData(gemInfo.m_features, GemModel::RoleFeatures);
+        item->setData(gemInfo.m_path, GemModel::RolePath);
+        item->setData(gemInfo.m_requirement, GemModel::RoleRequirement);
+        item->setData(gemInfo.m_downloadStatus, GemModel::RoleDownloadStatus);
+        item->setData(gemInfo.m_licenseText, GemModel::RoleLicenseText);
+        item->setData(gemInfo.m_licenseLink, GemModel::RoleLicenseLink);
+        item->setData(gemInfo.m_repoUri, GemModel::RoleRepoUri);
+        item->setData(gemInfo.IsEngineGem(), GemModel::RoleIsEngineGem);
+
+        if (!metaDataOnly)
         {
-            // do not add gems with duplicate names
-            // this can happen by mistake or when a gem repo has a gem with the same name as a local gem
-            AZ_TracePrintf("GemModel", "Ignoring duplicate gem: %s\n", gemInfo.m_name.toUtf8().constData());
-            return QModelIndex();
+            item->setData(false, GemModel::RoleWasPreviouslyAdded);
+            item->setData(gemInfo.m_isAdded, GemModel::RoleIsAdded);
+            item->setData("", GemModel::RoleNewVersion);
         }
+    }
 
-        QStandardItem* item = new QStandardItem();
+    void AddGemInfoVersion(QStandardItem* item, const GemInfo& gemInfo)
+    {
+        QList<QVariant> versionList;
+        auto variant = item->data(GemModel::RoleGemInfoVersions);
+        if (variant.isValid())
+        {
+            versionList = variant.value<QList<QVariant>>();
+        }
+        QVariant gemVariant;
+        gemVariant.setValue(gemInfo);
+        versionList.append(gemVariant);
+        item->setData(versionList, GemModel::RoleGemInfoVersions);
+    }
 
-        item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
+    QVector<QModelIndex> GemModel::AddGems(const QVector<GemInfo>& gemInfos)
+    {
+        QVector<QModelIndex> indexesChanged;
+        const int initialNumRows = rowCount();
+
+        // block dataChanged signal if we are adding a bunch of stuff
+        // to avoid sending a ton of signals that might cause large UI updates 
+        // and slows us down till we are done
+        blockSignals(true);
+
+        for (const auto& gemInfo : gemInfos)
+        {
+            // ${Name} is a special name used in templates and should not be shown 
+            // Though potentially it should be swapped out with the name of the Project being created
+            if (gemInfo.m_name == "${Name}")
+            {
+                continue;
+            }
+
+            auto modelIndex = FindIndexByNameString(gemInfo.m_name);
+            if (modelIndex.isValid())
+            {
+                auto gemItem = item(modelIndex.row(), modelIndex.column());
+                AZ_Assert(gemItem, "Failed to retrieve existing gem item from model index");
+
+                // if this is a greater version than the existing version, show it
+                if (ProjectUtils::VersionCompare(gemInfo.m_version, gemItem->data(RoleVersion).toString()) > 0)
+                {
+                    SetItemDataFromGemInfo(gemItem, gemInfo, /*metaDataOnly=*/ true);
+                }
+
+                AddGemInfoVersion(gemItem, gemInfo);
+
+                indexesChanged.append(modelIndex);
+            }
+            else
+            {
+                auto gemItem = new QStandardItem();
+                SetItemDataFromGemInfo(gemItem, gemInfo);
+                AddGemInfoVersion(gemItem, gemInfo);
+                appendRow(gemItem); 
 
-        item->setData(gemInfo.m_name, RoleName);
-        item->setData(gemInfo.m_displayName, RoleDisplayName);
-        item->setData(gemInfo.m_origin, RoleCreator);
-        item->setData(gemInfo.m_gemOrigin, RoleGemOrigin);
-        item->setData(aznumeric_cast<int>(gemInfo.m_platforms), RolePlatforms);
-        item->setData(aznumeric_cast<int>(gemInfo.m_types), RoleTypes);
-        item->setData(gemInfo.m_summary, RoleSummary);
-        item->setData(false, RoleWasPreviouslyAdded);
-        item->setData(gemInfo.m_isAdded, RoleIsAdded);
-        item->setData(gemInfo.m_directoryLink, RoleDirectoryLink);
-        item->setData(gemInfo.m_documentationLink, RoleDocLink);
-        item->setData(gemInfo.m_dependencies, RoleDependingGems);
-        item->setData(gemInfo.m_version, RoleVersion);
-        item->setData(gemInfo.m_lastUpdatedDate, RoleLastUpdated);
-        item->setData(gemInfo.m_binarySizeInKB, RoleBinarySize);
-        item->setData(gemInfo.m_features, RoleFeatures);
-        item->setData(gemInfo.m_path, RolePath);
-        item->setData(gemInfo.m_requirement, RoleRequirement);
-        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);
-
-        const QModelIndex modelIndex = index(rowCount()-1, 0);
-        m_nameToIndexMap[gemInfo.m_name] = modelIndex;
-        if (!gemInfo.m_path.isEmpty())
-        {
-            m_pathToIndexMap[gemInfo.m_path] = modelIndex;
-        }
-
-        return modelIndex;
+                modelIndex = index(rowCount() - 1, 0);
+                indexesChanged.append(modelIndex);
+
+                m_nameToIndexMap[gemInfo.m_name] = modelIndex;
+            }
+
+            if (modelIndex.isValid() && !!gemInfo.m_path.isEmpty())
+            {
+                m_pathToIndexMap[gemInfo.m_path] = modelIndex;
+            }
+        }
+
+        blockSignals(false);
+
+        // send a single dataChanged signal now that we've added everything
+        // this does not include rows that were changed and not added
+        const int startRow = AZStd::max(0, initialNumRows - 1);
+        const int endRow = AZStd::max(0, rowCount() - 1);
+        emit dataChanged(index(startRow, 0), index(endRow, 0));
+
+        return indexesChanged;
+    }
+
+    void GemModel::ActivateGems(const QHash<QString, QString>& enabledGemNames)
+    {
+        // block dataChanged signal if we are modifying a bunch of data 
+        // to avoid sending a many signals that might cause large UI updates 
+        // and slows us down till we are done
+        blockSignals(true);
+
+        for (auto itr = enabledGemNames.cbegin(); itr != enabledGemNames.cend(); itr++)
+        {
+            const QString& gemPath = itr.value();
+            const QString& gemNameWithSpecifier = itr.key();
+            AZ::Dependency<AZ::SemanticVersion::parts_count> dependency;
+            auto parseOutcome = dependency.ParseVersions({ gemNameWithSpecifier.toUtf8().constData() });
+            const QString& gemName = parseOutcome ? dependency.GetName().c_str() : gemNameWithSpecifier; 
+            if (gemName == "${Name}")
+            {
+                // ${Name} is a special name used in templates and is replaced with a real gem name later 
+                // in theory we could replace the name here with the project gem's name
+                continue;
+            }
+
+            if (auto nameFoundIter = m_nameToIndexMap.find(gemName); nameFoundIter != m_nameToIndexMap.end())
+            {
+                const QModelIndex modelIndex = nameFoundIter.value();
+                auto versionList = modelIndex.data(RoleGemInfoVersions).value<QList<QVariant>>();
+
+                if (versionList.count() > 1 && !gemPath.isEmpty())
+                {
+                    // make sure the gem item delegate displays the correct version info 
+                    for (auto versionVariant : versionList)
+                    {
+                        if (auto gemInfo = versionVariant.value<GemInfo>(); gemPath == gemInfo.m_path)
+                        {
+                            QStandardItem* gemItem = item(modelIndex.row(), modelIndex.column());
+                            AZ_Assert(gemItem, "Failed to retrieve enabled gem item from model index");
+                            SetItemDataFromGemInfo(gemItem, gemInfo);
+                            break;
+                        }
+                    }
+                }
+
+                // Set Added/PreviouslyAdded after potentially updating data above which might remove
+                // those settings
+                GemModel::SetWasPreviouslyAdded(*this, modelIndex, true);
+                GemModel::SetIsAdded(*this, modelIndex, true);
+
+                continue;
+            }
+
+            // This gem info is missing, but the project uses it so show it to the user
+            // so they can remove it if they want to
+            // In the future we want to let the user browse to this gem's location on disk, or
+            // let them download it
+            GemInfo gemInfo;
+            gemInfo.m_name = gemName;
+            gemInfo.m_displayName = gemName;
+            gemInfo.m_version = parseOutcome ? dependency.GetBounds().at(0).ToString().c_str() : "";
+            gemInfo.m_summary = QString("This project uses %1 but a compatible gem was not found, or has not been registered yet.").arg(gemNameWithSpecifier);
+            gemInfo.m_isAdded = true;
+
+            QStandardItem* gemItem = new QStandardItem();
+            SetItemDataFromGemInfo(gemItem, gemInfo);
+            appendRow(gemItem); 
+
+            const auto modelIndex = index(rowCount() - 1, 0);
+            GemModel::SetWasPreviouslyAdded(*this, modelIndex, true);
+            GemModel::SetIsAdded(*this, modelIndex, true);
+
+            m_nameToIndexMap[gemInfo.m_name] = modelIndex;
+
+            AZ_Warning("ProjectManager::GemCatalog", false,
+                "Cannot find entry for gem with name '%s'. The CMake target name probably does not match the specified name in the gem.json.",
+                gemName.toUtf8().constData());
+        }
+
+        blockSignals(false);
+
+        // send a single dataChanged signal now that we've added everything
+        emit dataChanged(index(0, 0), index(AZStd::max(0, rowCount() - 1), 0));
+    }
+
+    QModelIndex GemModel::AddGem(const GemInfo& gemInfo)
+    {
+        if (const auto& indexes = AddGems({gemInfo}); !indexes.isEmpty())
+        {
+            return indexes.at(0);
+        }
+
+        return index(rowCount()-1, 0);
     }
 
     void GemModel::RemoveGem(const QModelIndex& modelIndex)
@@ -129,32 +288,46 @@ namespace O3DE::ProjectManager
         }
     }
 
-    const GemInfo GemModel::GetGemInfo(const QModelIndex& modelIndex)
+    const GemInfo GemModel::GetGemInfo(const QModelIndex& modelIndex, const QString& version)
     {
-        GemInfo gemInfo;
-        gemInfo.m_name = modelIndex.data(RoleName).toString();
-        gemInfo.m_displayName = modelIndex.data(RoleDisplayName).toString();
-        gemInfo.m_origin = modelIndex.data(RoleCreator).toString();
-        gemInfo.m_gemOrigin = static_cast<GemInfo::GemOrigin>(modelIndex.data(RoleGemOrigin).toInt());
-        gemInfo.m_platforms = static_cast<GemInfo::Platforms>(modelIndex.data(RolePlatforms).toInt());
-        gemInfo.m_types = static_cast<GemInfo::Type>(modelIndex.data(RoleTypes).toInt());
-        gemInfo.m_summary = modelIndex.data(RoleSummary).toString();
-        gemInfo.m_isAdded = modelIndex.data(RoleIsAdded).toBool();
-        gemInfo.m_directoryLink = modelIndex.data(RoleDirectoryLink).toString();
-        gemInfo.m_documentationLink = modelIndex.data(RoleDocLink).toString();
-        gemInfo.m_dependencies = modelIndex.data(RoleDependingGems).toStringList();
-        gemInfo.m_version = modelIndex.data(RoleVersion).toString();
-        gemInfo.m_lastUpdatedDate = modelIndex.data(RoleLastUpdated).toString();
-        gemInfo.m_binarySizeInKB = modelIndex.data(RoleBinarySize).toInt();
-        gemInfo.m_features = modelIndex.data(RoleFeatures).toStringList();
-        gemInfo.m_path = modelIndex.data(RolePath).toString();
-        gemInfo.m_requirement = modelIndex.data(RoleRequirement).toString();
-        gemInfo.m_downloadStatus = static_cast<GemInfo::DownloadStatus>(modelIndex.data(RoleDownloadStatus).toInt());
-        gemInfo.m_licenseText = modelIndex.data(RoleLicenseText).toString();
-        gemInfo.m_licenseLink = modelIndex.data(RoleLicenseLink).toString();
-        gemInfo.m_repoUri = modelIndex.data(RoleRepoUri).toString();
+        const auto& versionList = modelIndex.data(RoleGemInfoVersions).value<QList<QVariant>>();
+        const QString& gemVersion = modelIndex.data(RoleVersion).toString();
+        if (versionList.isEmpty())
+        {
+            return {};
+        }
+        else if (gemVersion.isEmpty() && version.isEmpty())
+        {
+            // no version to look for so return the first GemInfo
+            return versionList.at(0).value<GemInfo>();
+        }
+
+        for (const auto& versionVariant : versionList)
+        {
+            const QString& variantVersion = versionVariant.value<GemInfo>().m_version;
 
-        return gemInfo;
+            // if a version is provided try to find an exact match
+            // if no version is provided, try to find the one that matches the current version
+            if (version == variantVersion || (version.isEmpty() && gemVersion == variantVersion))
+            {
+                // NOTE this gem info does not include any updates to m_isAdded or m_version
+                return versionVariant.value<GemInfo>();
+            }
+        }
+
+        // no gem info found for this version
+        return {};
+    }
+
+    const QStringList GemModel::GetGemVersions(const QModelIndex& modelIndex)
+    {
+        QStringList versionList;
+        const auto& versions = modelIndex.data(RoleGemInfoVersions).value<QList<QVariant>>();
+        for (const auto& version : versions)
+        {
+            versionList.append(version.value<GemInfo>().m_version);
+        }
+        return versionList;
     }
 
     QString GemModel::GetName(const QModelIndex& modelIndex)
@@ -201,6 +374,11 @@ namespace O3DE::ProjectManager
         return static_cast<GemInfo::DownloadStatus>(modelIndex.data(RoleDownloadStatus).toInt());
     }
 
+    bool GemModel::IsEngineGem(const QModelIndex& modelIndex)
+    {
+        return modelIndex.data(RoleIsEngineGem).toBool();
+    }
+
     QString GemModel::GetSummary(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleSummary).toString();
@@ -291,6 +469,11 @@ namespace O3DE::ProjectManager
         return modelIndex.data(RoleVersion).toString();
     }
 
+    QString GemModel::GetNewVersion(const QModelIndex& modelIndex)
+    {
+        return modelIndex.data(RoleNewVersion).toString();
+    }
+
     QString GemModel::GetLastUpdated(const QModelIndex& modelIndex)
     {
         return modelIndex.data(RoleLastUpdated).toString();
@@ -367,15 +550,26 @@ namespace O3DE::ProjectManager
         return modelIndex.data(RoleIsAddedDependency).toBool();
     }
 
-    void GemModel::SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded)
+    void GemModel::SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded, const QString& version)
     {
         // get the gemName first, because the modelIndex data change after adding because of filters
         QString gemName = modelIndex.data(RoleName).toString();
         model.setData(modelIndex, isAdded, RoleIsAdded);
 
+        if (!version.isEmpty())
+        {
+            QString gemVersion = modelIndex.data(RoleVersion).toString();
+            model.setData(modelIndex, version == gemVersion ? "" : version, RoleNewVersion);
+        }
+
         UpdateDependencies(model, gemName, isAdded);
     }
 
+    void GemModel::SetNewVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version)
+    {
+        model.setData(modelIndex, version, RoleNewVersion);
+    }
+
     bool GemModel::HasDependentGems(const QModelIndex& modelIndex) const
     {
         QVector<QModelIndex> dependentGems = GatherDependentGems(modelIndex);
@@ -447,6 +641,15 @@ namespace O3DE::ProjectManager
         gemModel->emit gemStatusChanged(gemName, numChangedDependencies);
     }
 
+    void GemModel::UpdateWithVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version)
+    {
+        GemModel* gemModel = GetSourceModel(&model);
+        AZ_Assert(gemModel, "Failed to obtain GemModel");
+        auto gemItem = gemModel->item(modelIndex.row(), modelIndex.column());
+        AZ_Assert(gemItem, "Failed to obtain gem model item");
+        SetItemDataFromGemInfo(gemItem, GetGemInfo(modelIndex, version), /*metaDataOnly*/ true);
+    }
+
     void GemModel::OnRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
     {
         bool selectedRowRemoved = false;
@@ -517,12 +720,13 @@ namespace O3DE::ProjectManager
     {
         bool previouslyAdded = modelIndex.data(RoleWasPreviouslyAdded).toBool();
         bool added = modelIndex.data(RoleIsAdded).toBool();
+        QString newVersion = modelIndex.data(RoleNewVersion).toString();
         if (includeDependencies)
         {
             previouslyAdded |= modelIndex.data(RoleWasPreviouslyAddedDependency).toBool();
             added |= modelIndex.data(RoleIsAddedDependency).toBool();
         }
-        return !previouslyAdded && added;
+        return (!previouslyAdded && added) || (added && !newVersion.isEmpty());
     }
 
     bool GemModel::NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies)

+ 15 - 4
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h

@@ -42,7 +42,8 @@ namespace O3DE::ProjectManager
             RoleDirectoryLink,
             RoleDocLink,
             RoleDependingGems,
-            RoleVersion,
+            RoleVersion,            // the current version
+            RoleNewVersion,         // the new version the user wants to use
             RoleLastUpdated,
             RoleBinarySize,
             RoleFeatures,
@@ -52,10 +53,14 @@ namespace O3DE::ProjectManager
             RoleDownloadStatus,
             RoleLicenseText,
             RoleLicenseLink,
-            RoleRepoUri
+            RoleRepoUri,
+            RoleGemInfoVersions,
+            RoleIsEngineGem
         };
 
         QModelIndex AddGem(const GemInfo& gemInfo);
+        QVector<QModelIndex> AddGems(const QVector<GemInfo>& gemInfos);
+        void ActivateGems(const QHash<QString, QString>& enabledGemNames);
         void RemoveGem(const QModelIndex& modelIndex);
         void RemoveGem(const QString& gemName);
         void Clear();
@@ -66,7 +71,8 @@ namespace O3DE::ProjectManager
         QVector<Tag> GetDependingGemTags(const QModelIndex& modelIndex);
         bool HasDependentGems(const QModelIndex& modelIndex) const;
 
-        static const GemInfo GetGemInfo(const QModelIndex& modelIndex);
+        static const GemInfo GetGemInfo(const QModelIndex& modelIndex, const QString& version = "");
+        static const QStringList GetGemVersions(const QModelIndex& modelIndex);
         static QString GetName(const QModelIndex& modelIndex);
         static QString GetDisplayName(const QModelIndex& modelIndex);
         static QString GetCreator(const QModelIndex& modelIndex);
@@ -78,6 +84,7 @@ namespace O3DE::ProjectManager
         static QString GetDirectoryLink(const QModelIndex& modelIndex);
         static QString GetDocLink(const QModelIndex& modelIndex);
         static QString GetVersion(const QModelIndex& modelIndex);
+        static QString GetNewVersion(const QModelIndex& modelIndex);
         static QString GetLastUpdated(const QModelIndex& modelIndex);
         static int GetBinarySizeInKB(const QModelIndex& modelIndex);
         static QStringList GetFeatures(const QModelIndex& modelIndex);
@@ -91,8 +98,10 @@ namespace O3DE::ProjectManager
 
         static bool IsAdded(const QModelIndex& modelIndex);
         static bool IsAddedDependency(const QModelIndex& modelIndex);
-        static void SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
+        static void SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded, const QString& version = "");
         static void SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded);
+        //! Set the version the user confirms they want to use
+        static void SetNewVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version);
         static void SetWasPreviouslyAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
         static bool WasPreviouslyAdded(const QModelIndex& modelIndex);
         static void SetWasPreviouslyAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool wasAdded);
@@ -101,8 +110,10 @@ namespace O3DE::ProjectManager
         static bool NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies = false);
         static bool HasRequirement(const QModelIndex& modelIndex);
         static void UpdateDependencies(QAbstractItemModel& model, const QString& gemName, bool isAdded);
+        static void UpdateWithVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version);
         static void DeactivateDependentGems(QAbstractItemModel& model, const QModelIndex& modelIndex);
         static void SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status);
+        static bool IsEngineGem(const QModelIndex& modelIndex);
 
         bool DoGemsToBeAddedHaveRequirements() const;
         bool HasDependentGemsToRemove() const;

+ 1 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemUninstallDialog.cpp

@@ -52,7 +52,7 @@ namespace O3DE::ProjectManager
         QPushButton* cancelButton = dialogButtons->addButton(tr("Cancel"), QDialogButtonBox::RejectRole);
         cancelButton->setProperty("secondary", true);
         QPushButton* uninstallButton = dialogButtons->addButton(tr("Uninstall Gem"), QDialogButtonBox::ApplyRole);
-        uninstallButton->setObjectName("gemCatalogUninstallGemButton");
+        uninstallButton->setProperty("class", "Danger");
 
         connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
         connect(uninstallButton, &QPushButton::clicked, this, &QDialog::accept);

+ 17 - 1
Code/Tools/ProjectManager/Source/ProjectGemCatalogScreen.cpp

@@ -82,7 +82,15 @@ namespace O3DE::ProjectManager
                 }
 
                 gemPaths.append(GemModel::GetPath(modelIndex));
-                gemNames.append(GemModel::GetName(modelIndex));
+
+                // use the version that was selected if available
+                auto gemInfo = GemModel::GetGemInfo(modelIndex);
+                if (auto gemVersion = GemModel::GetNewVersion(modelIndex); !gemVersion.isEmpty())
+                {
+                    gemInfo.m_version = gemVersion;
+                }
+
+                gemNames.append(gemInfo.GetNameWithVersionSpecifier());
             }
 
             // check compatibility of all gems
@@ -117,6 +125,14 @@ namespace O3DE::ProjectManager
                 for (const QModelIndex& modelIndex : toBeAdded)
                 {
                     GemModel::SetWasPreviouslyAdded(*m_gemModel, modelIndex, true);
+
+                    // if the user selected a new version then make sure to show that version
+                    const QString& newVersion = GemModel::GetNewVersion(modelIndex);
+                    if (!newVersion.isEmpty())
+                    {
+                        GemModel::UpdateWithVersion(*m_gemModel, modelIndex, newVersion);
+                        GemModel::SetNewVersion(*m_gemModel, modelIndex, "");
+                    }
                     const auto& gemPath = GemModel::GetPath(modelIndex);
 
                     // register external gems that were added with relative paths