Преглед на файлове

fix modelindex invalid after insert/remove

add ability to update gem info version optionally by path incase there are multiple copies of a version on disk
add ability to remove gem info version

Signed-off-by: Alex Peterson <[email protected]>
Alex Peterson преди 2 години
родител
ревизия
3ccde6ce23

+ 2 - 2
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -359,10 +359,10 @@ namespace O3DE::ProjectManager
                 QString version =  GemModel::GetNewVersion(modelIndex);
                 if (version.isEmpty())
                 {
-                    version =  GemModel::GetVersion(modelIndex);
+                    version =  gemInfo.m_version;
                 }
 
-                if (version.isEmpty() || version.contains("Unknown", Qt::CaseInsensitive))
+                if (version.isEmpty() || version.contains("Unknown", Qt::CaseInsensitive) || gemInfo.m_displayName.contains(version))
                 {
                     tags.push_back({ gemInfo.m_displayName, gemInfo.m_name });
                 }

+ 99 - 97
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -36,6 +36,7 @@
 #include <QFileDialog>
 #include <QMessageBox>
 #include <QHash>
+#include <QSet>
 #include <QStackedWidget>
 
 namespace O3DE::ProjectManager
@@ -98,6 +99,7 @@ namespace O3DE::ProjectManager
         connect(m_gemInspector, &GemInspector::UpdateGem, this, &GemCatalogScreen::UpdateGem);
         connect(m_gemInspector, &GemInspector::UninstallGem, this, &GemCatalogScreen::UninstallGem);
         connect(m_gemInspector, &GemInspector::EditGem, this, &GemCatalogScreen::HandleEditGem);
+        connect(m_gemInspector, &GemInspector::DownloadGem, this, &GemCatalogScreen::DownloadGem);
 
         QWidget* filterWidget = new QWidget(this);
         filterWidget->setFixedWidth(sidePanelWidth);
@@ -106,6 +108,9 @@ namespace O3DE::ProjectManager
         m_filterWidgetLayout->setSpacing(0);
         filterWidget->setLayout(m_filterWidgetLayout);
 
+        m_filterWidget = new GemFilterWidget(m_proxyModel);
+        m_filterWidgetLayout->addWidget(m_filterWidget);
+
         GemListHeaderWidget* catalogHeaderWidget = new GemListHeaderWidget(m_proxyModel);
         connect(catalogHeaderWidget, &GemListHeaderWidget::OnRefresh, this, &GemCatalogScreen::Refresh);
 
@@ -197,42 +202,22 @@ namespace O3DE::ProjectManager
         }
 
         m_projectPath = projectPath;
+
         m_gemModel->Clear();
         m_gemsToRegisterWithProject.clear();
 
-        if (m_filterWidget)
-        {
-            // disconnect so we don't update the status filter for every gem we add
-            disconnect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter);
-            disconnect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetUpdatesFilter);
-        }
-
         FillModel(projectPath);
 
-        m_proxyModel->ResetFilters(false);
+        m_gemModel->UpdateGemDependencies();
         m_proxyModel->sort(/*column=*/0);
-
-        if (m_filterWidget)
-        {
-            m_filterWidget->ResetAllFilters();
-        }
-        else
-        {
-            m_filterWidget = new GemFilterWidget(m_proxyModel);
-            m_filterWidgetLayout->addWidget(m_filterWidget);
-        }
+        m_proxyModel->ResetFilters();
+        m_filterWidget->UpdateAllFilters(/*clearAllCheckboxes=*/true);
 
         m_headerWidget->ReinitForProject();
 
-        connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter);
-        connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetUpdatesFilter);
-
-        // Select the first entry after everything got correctly sized
-        QTimer::singleShot(200, [=]{
-            QModelIndex firstModelIndex = m_gemModel->index(0, 0);
-            QModelIndex proxyIndex = m_proxyModel->mapFromSource(firstModelIndex);
-            m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
-        });
+        QModelIndex firstProxyIndex = m_proxyModel->index(0, 0);
+        m_proxyModel->GetSelectionModel()->setCurrentIndex(firstProxyIndex, QItemSelectionModel::ClearAndSelect);
+        m_gemInspector->Update(firstProxyIndex);
     }
 
     void GemCatalogScreen::OnAddGemClicked()
@@ -272,6 +257,7 @@ namespace O3DE::ProjectManager
                     GemInfo& addedGemInfo = gemInfoResult.GetValue<GemInfo>();
                     addedGemInfo.m_downloadStatus = GemInfo::DownloadStatus::Downloaded;
                     AddToGemModel(addedGemInfo);
+                    ShowStandardToastNotification(tr("%1 added").arg(addedGemInfo.m_displayName));
                 }
             }
         }
@@ -279,43 +265,33 @@ namespace O3DE::ProjectManager
 
     void GemCatalogScreen::AddToGemModel(const GemInfo& gemInfo)
     {
-        m_gemModel->AddGem(gemInfo);
+        const auto modelIndex = m_gemModel->AddGem(gemInfo);
         m_gemModel->UpdateGemDependencies();
         m_proxyModel->sort(/*column=*/0);
+        m_proxyModel->InvalidateFilter();
+        m_filterWidget->UpdateAllFilters(/*clearAllCheckboxes=*/false);
+
+        // attempt to select the newly added gem
+        if(auto proxyIndex = m_proxyModel->mapFromSource(modelIndex); proxyIndex.isValid())
+        {
+            m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
+        }
     }
 
     void GemCatalogScreen::Refresh()
     {
-        QHash<QString, GemInfo> gemInfoHash;
+        QSet<QPersistentModelIndex> validIndexes;
 
-        // create a hash with the gem name as key
-        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(m_projectPath);
-        if (allGemInfosResult.IsSuccess())
+        if (const auto& outcome = PythonBindingsInterface::Get()->GetAllGemInfos(m_projectPath); outcome.IsSuccess())
         {
-            const QVector<GemInfo>& gemInfos = allGemInfosResult.GetValue();
-            for (const GemInfo& gemInfo : gemInfos)
-            {
-                // ${Name} is a special name used for templates and should be ignored
-                // eventually we should handle this in Python instead of here
-                if (gemInfo.m_name != "${Name}")
-                {
-                    gemInfoHash.insert(gemInfo.m_name, gemInfo);
-                }
-            }
+            const auto& indexes = m_gemModel->AddGems(outcome.GetValue(), /*updateExisting=*/true);
+            validIndexes = QSet(indexes.cbegin(), indexes.cend());
         }
 
-        // add all the gem repos into the hash
-        const AZ::Outcome<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
-        if (allRepoGemInfosResult.IsSuccess())
+        if (const auto& outcome = PythonBindingsInterface::Get()->GetGemInfosForAllRepos(); outcome.IsSuccess())
         {
-            const QVector<GemInfo>& allRepoGemInfos = allRepoGemInfosResult.GetValue();
-            for (const GemInfo& gemInfo : allRepoGemInfos)
-            {
-                if (!gemInfoHash.contains(gemInfo.m_name))
-                {
-                    gemInfoHash.insert(gemInfo.m_name, gemInfo);
-                }
-            }
+            const auto& indexes = m_gemModel->AddGems(outcome.GetValue(), /*updateExisting=*/true);
+            validIndexes.unite(QSet(indexes.cbegin(), indexes.cend()));
         }
 
         // remove gems from the model that no longer exist in the hash and are not project dependencies
@@ -323,8 +299,7 @@ namespace O3DE::ProjectManager
         while (i < m_gemModel->rowCount())
         {
             QModelIndex index = m_gemModel->index(i,0);
-            QString gemName = m_gemModel->GetName(index);
-            const bool gemFound = gemInfoHash.contains(gemName);
+            const bool gemFound = validIndexes.contains(index);
             if (!gemFound && !m_gemModel->IsAdded(index) && !m_gemModel->IsAddedDependency(index))
             {
                 m_gemModel->RemoveGem(index);
@@ -333,31 +308,22 @@ namespace O3DE::ProjectManager
             {
                 if (!gemFound && (m_gemModel->IsAdded(index) || m_gemModel->IsAddedDependency(index)))
                 {
+                    QString gemName = m_gemModel->GetName(index);
                     const QString error = tr("Gem %1 was removed or unregistered, but is still used by the project.").arg(gemName);
                     AZ_Warning("Project Manager", false, error.toUtf8().constData());
                     QMessageBox::warning(this, tr("Gem not found"), error.toUtf8().constData());
                 }
 
-                gemInfoHash.remove(gemName);
                 i++;
             }
         }
 
-        // add all gems remaining in the hash that were not removed
-        for(auto iter = gemInfoHash.begin(); iter != gemInfoHash.end(); ++iter)
-        {
-            m_gemModel->AddGem(iter.value());
-        }
-
         m_gemModel->UpdateGemDependencies();
         m_proxyModel->sort(/*column=*/0);
+        m_proxyModel->InvalidateFilter();
+        m_filterWidget->UpdateAllFilters(/*clearAllCheckboxes=*/false);
 
-        // temporary, until we can refresh filter counts 
-        m_proxyModel->ResetFilters(false);
-        m_filterWidget->ResetAllFilters();
-
-        // Reselect the same selection to proc UI updates
-        m_proxyModel->GetSelectionModel()->setCurrentIndex(m_proxyModel->GetSelectionModel()->currentIndex(), QItemSelectionModel::Select);
+        m_gemInspector->Update(m_proxyModel->GetSelectionModel()->currentIndex());
     }
 
     void GemCatalogScreen::OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies) 
@@ -379,15 +345,17 @@ namespace O3DE::ProjectManager
             if (gemStateChanged)
             {
                 const GemInfo& gemInfo = GemModel::GetGemInfo(modelIndex);
-                const QString& displayName = GemModel::GetDisplayName(modelIndex);
                 const QString& newVersion = GemModel::GetNewVersion(modelIndex);
-                if (gemInfo.m_isEngineGem || (gemInfo.m_version.isEmpty() && newVersion.isEmpty()))
+                const QString& version = newVersion.isEmpty() ? gemInfo.m_version : newVersion;
+
+                // avoid showing the version twice if it's already in the display name
+                if (gemInfo.m_isEngineGem || (version.isEmpty() || gemInfo.m_displayName.contains(version) || version.contains("Unknown", Qt::CaseInsensitive)))
                 {
-                    notification = displayName;
+                    notification = gemInfo.m_displayName;
                 } 
                 else
                 {
-                    notification = QString("%1 %2").arg(displayName, newVersion.isEmpty() ? gemInfo.m_version : newVersion);
+                    notification = QString("%1 %2").arg(gemInfo.m_displayName, version);
                 }
 
                 if (numChangedDependencies > 0)
@@ -444,12 +412,14 @@ namespace O3DE::ProjectManager
         if (!m_proxyModel->filterAcceptsRow(modelIndex.row(), QModelIndex()))
         {
             m_proxyModel->ResetFilters();
-            m_filterWidget->ResetAllFilters();
+            m_filterWidget->UpdateAllFilters(/*clearAllCheckboxes=*/true);
         }
 
-        QModelIndex proxyIndex = m_proxyModel->mapFromSource(modelIndex);
-        m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
-        m_gemListView->scrollTo(proxyIndex);
+        if (QModelIndex proxyIndex = m_proxyModel->mapFromSource(modelIndex); proxyIndex.isValid())
+        {
+            m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
+            m_gemListView->scrollTo(proxyIndex);
+        }
 
         ShowInspector();
     }
@@ -458,7 +428,6 @@ namespace O3DE::ProjectManager
     {
         const GemInfo& gemInfo = m_gemModel->GetGemInfo(modelIndex);
 
-        // Refresh gem repo
         if (!gemInfo.m_repoUri.isEmpty())
         {
             AZ::Outcome<void, AZStd::string> refreshResult = PythonBindingsInterface::Get()->RefreshGemRepo(gemInfo.m_repoUri);
@@ -535,34 +504,67 @@ namespace O3DE::ProjectManager
             GemModel::DeactivateDependentGems(*m_gemModel, modelIndex);
 
             // Unregister the gem
-            auto unregisterResult = PythonBindingsInterface::Get()->UnregisterGem(selectedGemPath);
+            auto unregisterResult = PythonBindingsInterface::Get()->UnregisterGem(path);
             if (!unregisterResult)
             {
-                QMessageBox::critical(this, tr("Failed to unregister gem"), unregisterResult.GetError().c_str());
+                QMessageBox::critical(this, tr("Failed to unregister %1").arg(gemDisplayName), unregisterResult.GetError().c_str());
             }
             else
             {
-                const QString selectedGemName = m_gemModel->GetName(modelIndex);
-
-                // Remove gem from model
-                m_gemModel->RemoveGem(modelIndex);
+                const QString gemName = m_gemModel->GetName(modelIndex);
+                m_gemModel->RemoveGem(gemName, /*version=*/"", path);
 
-                // Delete uninstalled gem directory
-                if (!ProjectUtils::DeleteProjectFiles(selectedGemPath, /*force*/true))
+                bool filesDeleted = false;
+                if (gemInfo.m_gemOrigin == GemInfo::Remote)
                 {
-                    QMessageBox::critical(
-                        this, tr("Failed to remove gem directory"), tr("Could not delete gem directory at:<br>%1").arg(selectedGemPath));
+                    // Remote gems will have their files deleted 
+                    filesDeleted = ProjectUtils::DeleteProjectFiles(path, /*force*/ true);
+                    if (!filesDeleted)
+                    {
+                        QMessageBox::critical(
+                            this, tr("Failed to remove gem directory"), tr("Could not delete gem directory at:<br>%1").arg(path));
+                    }
+                    else
+                    {
+                        ShowStandardToastNotification(tr("%1 uninstalled").arg(gemDisplayName));
+                    }
+                }
+                else
+                {
+                        ShowStandardToastNotification(tr("%1 removed").arg(gemDisplayName));
                 }
 
-                // Show undownloaded remote gem again
                 Refresh();
 
-                // Select remote gem
-                QModelIndex remoteGemIndex = m_gemModel->FindIndexByNameString(selectedGemName);
-                GemModel::SetWasPreviouslyAdded(*m_gemModel, remoteGemIndex, wasAdded);
-                GemModel::SetWasPreviouslyAddedDependency(*m_gemModel, remoteGemIndex, wasAddedDependency);
-                QModelIndex proxyIndex = m_proxyModel->mapFromSource(remoteGemIndex);
-                m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
+                const QModelIndex gemIndex = m_gemModel->FindIndexByNameString(gemName);
+                QModelIndex proxyIndex;
+                if (gemIndex.isValid())
+                {
+                    GemModel::SetWasPreviouslyAdded(*m_gemModel, gemIndex, wasAdded);
+                    GemModel::SetWasPreviouslyAddedDependency(*m_gemModel, gemIndex, wasAddedDependency);
+
+                    if (filesDeleted)
+                    {
+                        GemModel::SetDownloadStatus(*m_gemModel, gemIndex, GemInfo::DownloadStatus::NotDownloaded);
+                    }
+
+                    proxyIndex = m_proxyModel->mapFromSource(gemIndex);
+                }
+
+                if (!proxyIndex.isValid())
+                {
+                    // if the gem was removed then we need to pick a new entry
+                    proxyIndex = m_proxyModel->mapFromSource(m_gemModel->index(0, 0));
+                }
+
+                if (proxyIndex.isValid())
+                {
+                    m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect);
+                }
+                else
+                {
+                    m_proxyModel->GetSelectionModel()->setCurrentIndex(m_proxyModel->index(0,0), QItemSelectionModel::ClearAndSelect);
+                }
             }
         }
     }
@@ -625,10 +627,10 @@ namespace O3DE::ProjectManager
                 {
                     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()));
                 }
-
-                // sort after activating gems in case the display name for a gem is different for the active version 
-                m_proxyModel->sort(/*column=*/0);
             }
+
+            // sort after activating gems in case the display name for a gem is different for the active version 
+            m_proxyModel->sort(/*column=*/0);
         }
         else
         {

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

@@ -268,7 +268,7 @@ namespace O3DE::ProjectManager
         return QStyledItemDelegate::editorEvent(event, model, option, modelIndex);
     }
 
-    QString GetGemNameList(const QVector<QModelIndex> modelIndices)
+    QString GetGemNameList(const QVector<QPersistentModelIndex> modelIndices)
     {
         QString gemNameList;
         for (int i = 0; i < modelIndices.size(); ++i)
@@ -310,7 +310,7 @@ namespace O3DE::ProjectManager
                         // 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);
+                        auto dependents = gemModel->GatherDependentGems(index, addedOnly);
                         QString nameList = GetGemNameList(dependents);
                         if (!nameList.isEmpty())
                         {

+ 105 - 42
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp

@@ -49,7 +49,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    void AddGemInfoVersion(QStandardItem* item, const GemInfo& gemInfo)
+    void AddGemInfoVersion(QStandardItem* item, const GemInfo& gemInfo, bool update)
     {
         QList<QVariant> versionList;
         auto variant = item->data(GemModel::RoleGemInfoVersions);
@@ -68,7 +68,7 @@ namespace O3DE::ProjectManager
             const GemInfo& existingGemInfo = existingGemVariant.value<GemInfo>();
             if (QDir(existingGemInfo.m_path) == QDir(gemInfo.m_path))
             {
-                if (existingGemInfo.m_version == gemInfo.m_version)
+                if (existingGemInfo.m_version == gemInfo.m_version && !update)
                 {
                     AZ_Info("ProjectManager", "Not adding GemInfo because a GemInfo with path (%s) and version (%s) already exists.",
                         gemInfo.m_path.toUtf8().constData(),
@@ -81,6 +81,24 @@ namespace O3DE::ProjectManager
                 versionToReplaceIndex = i;
                 break;
             }
+            else if (existingGemInfo.m_version == gemInfo.m_version &&
+                existingGemInfo.m_downloadStatus == GemInfo::NotDownloaded &&
+                gemInfo.m_downloadStatus == GemInfo::Downloaded)
+            {
+                // we are adding  a gem version for a gem that has been downloaded
+                // so replace the content for remote gem
+                versionToReplaceIndex = i;
+                break;
+            }
+
+            if (existingGemInfo.m_version == gemInfo.m_version &&
+                existingGemInfo.m_downloadStatus == GemInfo::Downloaded &&
+                gemInfo.m_downloadStatus == GemInfo::NotDownloaded)
+            {
+                // do not add the not downloaded remote version of
+                // something we have downloaded
+                return;
+            }
         }
 
         if(versionToReplaceIndex != -1)
@@ -94,9 +112,39 @@ namespace O3DE::ProjectManager
         item->setData(versionList, GemModel::RoleGemInfoVersions);
     }
 
-    QVector<QModelIndex> GemModel::AddGems(const QVector<GemInfo>& gemInfos)
+    bool RemoveGemInfoVersion(QStandardItem* item, const QString& version, const QString& path)
     {
-        QVector<QModelIndex> indexesChanged;
+        QVariant variant = item->data(GemModel::RoleGemInfoVersions);
+        auto versionList = variant.isValid() ? variant.value<QList<QVariant>>() : QList<QVariant>();
+        const bool removeByPath = !path.isEmpty();
+
+        QDir dir{ path };
+        for (int i = 0; i < versionList.size(); ++i)
+        {
+            const QVariant& existingGemVariant = versionList.at(i);
+            const GemInfo& existingGemInfo = existingGemVariant.value<GemInfo>();
+            if (removeByPath)
+            {
+                if (QDir(existingGemInfo.m_path) == dir)
+                {
+                    versionList.removeAt(i);
+                    break;
+                }
+            }
+            else if (existingGemInfo.m_version == version)
+            {
+                // there could be multiple instances of the same version
+                versionList.removeAt(i);
+            }
+        }
+
+        item->setData(versionList, GemModel::RoleGemInfoVersions);
+        return versionList.isEmpty();
+    }
+
+    QVector<QPersistentModelIndex> GemModel::AddGems(const QVector<GemInfo>& gemInfos, bool updateExisting)
+    {
+        QVector<QPersistentModelIndex> indexesChanged;
         const int initialNumRows = rowCount();
 
         // block dataChanged signal if we are adding a bunch of stuff
@@ -119,13 +167,15 @@ namespace O3DE::ProjectManager
                 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)
+                // if this is a greater version than the existing version
+                // or we are updating the existing version, update
+                int versionResult = ProjectUtils::VersionCompare(gemInfo.m_version, gemItem->data(RoleVersion).toString());
+                if (versionResult > 0 || (versionResult == 0 && updateExisting))
                 {
                     SetItemDataFromGemInfo(gemItem, gemInfo, /*metaDataOnly=*/ true);
                 }
 
-                AddGemInfoVersion(gemItem, gemInfo);
+                AddGemInfoVersion(gemItem, gemInfo, updateExisting);
 
                 indexesChanged.append(modelIndex);
             }
@@ -133,7 +183,7 @@ namespace O3DE::ProjectManager
             {
                 auto gemItem = new QStandardItem();
                 SetItemDataFromGemInfo(gemItem, gemInfo);
-                AddGemInfoVersion(gemItem, gemInfo);
+                AddGemInfoVersion(gemItem, gemInfo, updateExisting);
                 appendRow(gemItem); 
 
                 modelIndex = index(rowCount() - 1, 0);
@@ -216,7 +266,7 @@ namespace O3DE::ProjectManager
 
             QStandardItem* gemItem = new QStandardItem();
             SetItemDataFromGemInfo(gemItem, gemInfo);
-            AddGemInfoVersion(gemItem, gemInfo);
+            AddGemInfoVersion(gemItem, gemInfo, /*updateExisting=*/false);
             appendRow(gemItem); 
 
             const auto modelIndex = index(rowCount() - 1, 0);
@@ -236,7 +286,7 @@ namespace O3DE::ProjectManager
         emit dataChanged(index(0, 0), index(AZStd::max(0, rowCount() - 1), 0));
     }
 
-    QModelIndex GemModel::AddGem(const GemInfo& gemInfo)
+    QPersistentModelIndex GemModel::AddGem(const GemInfo& gemInfo)
     {
         if (const auto& indexes = AddGems({gemInfo}); !indexes.isEmpty())
         {
@@ -251,12 +301,23 @@ namespace O3DE::ProjectManager
         removeRow(modelIndex.row());
     }
 
-    void GemModel::RemoveGem(const QString& gemName)
+    void GemModel::RemoveGem(const QString& gemName, const QString& version, const QString& path)
     {
         auto nameFind = m_nameToIndexMap.find(gemName);
         if (nameFind != m_nameToIndexMap.end())
         {
-            removeRow(nameFind->row());
+            if (!version.isEmpty() || !path.isEmpty())
+            {
+                const bool removedAllVersions = RemoveGemInfoVersion(item(nameFind->row(), nameFind->column()), version, path);
+                if (removedAllVersions)
+                {
+                    removeRow(nameFind->row());
+                }
+            }
+            else
+            {
+                removeRow(nameFind->row());
+            }
         }
     }
 
@@ -275,7 +336,7 @@ namespace O3DE::ProjectManager
         {
             const QString& key = iter.key();
             const QModelIndex modelIndex = iter.value();
-            QSet<QModelIndex> dependencies;
+            QSet<QPersistentModelIndex> dependencies;
             GetAllDependingGems(modelIndex, dependencies);
             if (!dependencies.isEmpty())
             {
@@ -291,7 +352,7 @@ namespace O3DE::ProjectManager
                 const QString& dependencyName = dependency.data(RoleName).toString();
                 if (!m_gemReverseDependencyMap.contains(dependencyName))
                 {
-                    m_gemReverseDependencyMap.insert(dependencyName, QSet<QModelIndex>());
+                    m_gemReverseDependencyMap.insert(dependencyName, QSet<QPersistentModelIndex>());
                 }
 
                 m_gemReverseDependencyMap[dependencyName].insert(m_nameToIndexMap[dependant]);
@@ -299,7 +360,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    const GemInfo GemModel::GetGemInfo(const QModelIndex& modelIndex, const QString& version)
+    const GemInfo GemModel::GetGemInfo(const QModelIndex& modelIndex, const QString& version, const QString& path)
     {
         const auto& versionList = modelIndex.data(RoleGemInfoVersions).value<QList<QVariant>>();
         const QString& gemVersion = modelIndex.data(RoleVersion).toString();
@@ -313,13 +374,21 @@ namespace O3DE::ProjectManager
             return versionList.at(0).value<GemInfo>();
         }
 
+        bool usePath = !path.isEmpty();
+        bool useVersion = !version.isEmpty();
+        bool useCurrentVersion = !useVersion && !usePath;
         for (const auto& versionVariant : versionList)
         {
+            // there may be multiple instances of the same gem with the same version
+            // at different paths
             const QString& variantVersion = versionVariant.value<GemInfo>().m_version;
+            const QString& variantPath = versionVariant.value<GemInfo>().m_path;
 
-            // 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))
+            // if a path and/or version is provided try to find an exact match
+            if ((useCurrentVersion && gemVersion == variantVersion) ||
+                (usePath && variantPath == path) ||
+                (!usePath && useVersion && variantVersion == version))
             {
                 return versionVariant.value<GemInfo>();
             }
@@ -329,15 +398,9 @@ namespace O3DE::ProjectManager
         return {};
     }
 
-    const QStringList GemModel::GetGemVersions(const QModelIndex& modelIndex)
+    const QList<QVariant> 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;
+        return modelIndex.data(RoleGemInfoVersions).value<QList<QVariant>>();
     }
 
     QString GemModel::GetName(const QModelIndex& modelIndex)
@@ -364,7 +427,7 @@ namespace O3DE::ProjectManager
         return static_cast<GemInfo::DownloadStatus>(modelIndex.data(RoleDownloadStatus).toInt());
     }
 
-    QModelIndex GemModel::FindIndexByNameString(const QString& nameString) const
+    QPersistentModelIndex GemModel::FindIndexByNameString(const QString& nameString) const
     {
         const auto iterator = m_nameToIndexMap.find(nameString);
         if (iterator != m_nameToIndexMap.end())
@@ -390,7 +453,7 @@ namespace O3DE::ProjectManager
         }
     }
 
-    void GemModel::GetAllDependingGems(const QModelIndex& modelIndex, QSet<QModelIndex>& inOutGems)
+    void GemModel::GetAllDependingGems(const QModelIndex& modelIndex, QSet<QPersistentModelIndex>& inOutGems)
     {
         QStringList dependencies = GetDependingGems(modelIndex);
         for (const QString& dependency : dependencies)
@@ -491,7 +554,7 @@ namespace O3DE::ProjectManager
 
     bool GemModel::HasDependentGems(const QModelIndex& modelIndex) const
     {
-        QVector<QModelIndex> dependentGems = GatherDependentGems(modelIndex);
+        auto dependentGems = GatherDependentGems(modelIndex);
         for (const QModelIndex& dependency : dependentGems)
         {
             if (IsAdded(dependency))
@@ -507,9 +570,9 @@ namespace O3DE::ProjectManager
         GemModel* gemModel = GetSourceModel(&model);
         AZ_Assert(gemModel, "Failed to obtain GemModel");
 
-        QModelIndex modelIndex = gemModel->FindIndexByNameString(gemName);
+        auto modelIndex = gemModel->FindIndexByNameString(gemName);
 
-        QVector<QModelIndex> dependencies = gemModel->GatherGemDependencies(modelIndex);
+        auto dependencies = gemModel->GatherGemDependencies(modelIndex);
         uint32_t numChangedDependencies = 0;
 
         if (isAdded)
@@ -539,7 +602,7 @@ namespace O3DE::ProjectManager
                 SetIsAddedDependency(*gemModel, modelIndex, hasDependentGems);
             }
 
-            for (const QModelIndex& dependency : dependencies)
+            for (const auto& dependency : dependencies)
             {
                 hasDependentGems = gemModel->HasDependentGems(dependency);
                 if (IsAddedDependency(dependency) != hasDependentGems)
@@ -560,13 +623,13 @@ namespace O3DE::ProjectManager
         gemModel->emit gemStatusChanged(gemName, numChangedDependencies);
     }
 
-    void GemModel::UpdateWithVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version)
+    void GemModel::UpdateWithVersion(QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version, const QString& path)
     {
         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);
+        SetItemDataFromGemInfo(gemItem, GetGemInfo(modelIndex, version, path), /*metaDataOnly*/ true);
     }
 
     void GemModel::OnRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last)
@@ -612,7 +675,7 @@ namespace O3DE::ProjectManager
             // update all dependencies
             GemModel* gemModel = GetSourceModel(&model);
             AZ_Assert(gemModel, "Failed to obtain GemModel");
-            QVector<QModelIndex> dependencies = gemModel->GatherGemDependencies(modelIndex);
+            auto dependencies = gemModel->GatherGemDependencies(modelIndex);
             for (const QModelIndex& dependency : dependencies)
             {
                 SetWasPreviouslyAddedDependency(*gemModel, dependency, true);
@@ -665,7 +728,7 @@ namespace O3DE::ProjectManager
         GemModel* gemModel = GetSourceModel(&model);
         AZ_Assert(gemModel, "Failed to obtain GemModel");
 
-        QVector<QModelIndex> dependentGems = gemModel->GatherDependentGems(modelIndex);
+        auto dependentGems = gemModel->GatherDependentGems(modelIndex);
         if (!dependentGems.isEmpty())
         {
             // we need to deactivate all gems that depend on this one
@@ -713,7 +776,7 @@ namespace O3DE::ProjectManager
             auto currentVersion = modelIndex.data(RoleVersion).toString();
 
             // gem versions are sorted so we can just compare if we're using the latest version
-            return currentVersion != versions.at(0);
+            return currentVersion != versions.at(0).value<GemInfo>().m_version;
         }
 
         return false;
@@ -746,13 +809,13 @@ namespace O3DE::ProjectManager
         return false;
     }
 
-    QVector<QModelIndex> GemModel::GatherGemDependencies(const QModelIndex& modelIndex) const 
+    QVector<QPersistentModelIndex> GemModel::GatherGemDependencies(const QPersistentModelIndex& modelIndex) const 
     {
-        QVector<QModelIndex> result;
+        QVector<QPersistentModelIndex> result;
         const QString& gemName = modelIndex.data(RoleName).toString();
         if (m_gemDependencyMap.contains(gemName))
         {
-            for (const QModelIndex& dependency : m_gemDependencyMap[gemName])
+            for (const auto& dependency : m_gemDependencyMap[gemName])
             {
                 result.push_back(dependency);
             }
@@ -760,13 +823,13 @@ namespace O3DE::ProjectManager
         return result;
     }
 
-    QVector<QModelIndex> GemModel::GatherDependentGems(const QModelIndex& modelIndex, bool addedOnly) const
+    QVector<QPersistentModelIndex> GemModel::GatherDependentGems(const QPersistentModelIndex& modelIndex, bool addedOnly) const
     {
-        QVector<QModelIndex> result;
+        QVector<QPersistentModelIndex> result;
         const QString& gemName = modelIndex.data(RoleName).toString();
         if (m_gemReverseDependencyMap.contains(gemName))
         {
-            for (const QModelIndex& dependency : m_gemReverseDependencyMap[gemName])
+            for (const auto& dependency : m_gemReverseDependencyMap[gemName])
             {
                 if (!addedOnly || GemModel::IsAdded(dependency))
                 {

+ 14 - 13
Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h

@@ -42,20 +42,20 @@ namespace O3DE::ProjectManager
             RoleGemInfoVersions
         };
 
-        QModelIndex AddGem(const GemInfo& gemInfo);
-        QVector<QModelIndex> AddGems(const QVector<GemInfo>& gemInfos);
+        QPersistentModelIndex AddGem(const GemInfo& gemInfo);
+        QVector<QPersistentModelIndex> AddGems(const QVector<GemInfo>& gemInfos, bool updateExisting = false);
         void ActivateGems(const QHash<QString, QString>& enabledGemNames);
         void RemoveGem(const QModelIndex& modelIndex);
-        void RemoveGem(const QString& gemName);
+        void RemoveGem(const QString& gemName, const QString& version = "", const QString& path = "");
         void Clear();
         void UpdateGemDependencies();
 
-        QModelIndex FindIndexByNameString(const QString& nameString) const;
+        QPersistentModelIndex FindIndexByNameString(const QString& nameString) const;
         QVector<Tag> GetDependingGemTags(const QModelIndex& modelIndex);
         bool HasDependentGems(const QModelIndex& modelIndex) const;
 
-        static const GemInfo GetGemInfo(const QModelIndex& modelIndex, const QString& version = "");
-        static const QStringList GetGemVersions(const QModelIndex& modelIndex);
+        static const GemInfo GetGemInfo(const QModelIndex& modelIndex, const QString& version = "", const QString& path = "");
+        static const QList<QVariant> GetGemVersions(const QModelIndex& modelIndex);
         static QString GetName(const QModelIndex& modelIndex);
         static QString GetDisplayName(const QModelIndex& modelIndex);
         static GemInfo::DownloadStatus GetDownloadStatus(const QModelIndex& modelIndex);
@@ -80,15 +80,16 @@ namespace O3DE::ProjectManager
         static bool HasRequirement(const QModelIndex& modelIndex);
         static bool HasUpdates(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 UpdateWithVersion(
+            QAbstractItemModel& model, const QModelIndex& modelIndex, const QString& version, const QString& path = "");
         static void DeactivateDependentGems(QAbstractItemModel& model, const QModelIndex& modelIndex);
         static void SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status);
 
         bool DoGemsToBeAddedHaveRequirements() const;
         bool HasDependentGemsToRemove() const;
 
-        QVector<QModelIndex> GatherGemDependencies(const QModelIndex& modelIndex) const;
-        QVector<QModelIndex> GatherDependentGems(const QModelIndex& modelIndex, bool addedOnly = false) const;
+        QVector<QPersistentModelIndex> GatherGemDependencies(const QPersistentModelIndex& modelIndex) const;
+        QVector<QPersistentModelIndex> GatherDependentGems(const QPersistentModelIndex& modelIndex, bool addedOnly = false) const;
         QVector<QModelIndex> GatherGemsToBeAdded(bool includeDependencies = false) const;
         QVector<QModelIndex> GatherGemsToBeRemoved(bool includeDependencies = false) const;
 
@@ -103,12 +104,12 @@ namespace O3DE::ProjectManager
         void OnRowsRemoved(const QModelIndex& parent, int first, int last);
 
     private:
-        void GetAllDependingGems(const QModelIndex& modelIndex, QSet<QModelIndex>& inOutGems);
+        void GetAllDependingGems(const QModelIndex& modelIndex, QSet<QPersistentModelIndex>& inOutGems);
         QStringList GetDependingGems(const QModelIndex& modelIndex);
 
-        QHash<QString, QModelIndex> m_nameToIndexMap;
+        QHash<QString, QPersistentModelIndex> m_nameToIndexMap;
         QItemSelectionModel* m_selectionModel = nullptr;
-        QHash<QString, QSet<QModelIndex>> m_gemDependencyMap;
-        QHash<QString, QSet<QModelIndex>> m_gemReverseDependencyMap;
+        QHash<QString, QSet<QPersistentModelIndex>> m_gemDependencyMap;
+        QHash<QString, QSet<QPersistentModelIndex>> m_gemReverseDependencyMap;
     };
 } // namespace O3DE::ProjectManager