/* * Copyright (c) Contributors to the Open 3D Engine Project. * For complete copyright and license terms please see the LICENSE at the root of this distribution. * * SPDX-License-Identifier: Apache-2.0 OR MIT * */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace O3DE::ProjectManager { GemCatalogScreen::GemCatalogScreen(QWidget* parent) : ScreenWidget(parent) { m_gemModel = new GemModel(this); m_proxyModel = new GemSortFilterProxyModel(m_gemModel, this); // default to sort by gem name m_proxyModel->setSortRole(GemModel::RoleName); QVBoxLayout* vLayout = new QVBoxLayout(); vLayout->setMargin(0); vLayout->setSpacing(0); setLayout(vLayout); m_downloadController = new DownloadController(); m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxyModel, m_downloadController); vLayout->addWidget(m_headerWidget); connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); connect(m_gemModel->GetSelectionModel(), &QItemSelectionModel::selectionChanged, this, [this]{ ShowInspector(); }); connect(m_headerWidget, &GemCatalogHeaderWidget::RefreshGems, this, &GemCatalogScreen::Refresh); connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo); connect(m_headerWidget, &GemCatalogHeaderWidget::AddGem, this, &GemCatalogScreen::OnAddGemClicked); connect(m_headerWidget, &GemCatalogHeaderWidget::UpdateGemCart, this, &GemCatalogScreen::UpdateAndShowGemCart); connect(m_downloadController, &DownloadController::Done, this, &GemCatalogScreen::OnGemDownloadResult); QHBoxLayout* hLayout = new QHBoxLayout(); hLayout->setMargin(0); vLayout->addLayout(hLayout); m_gemListView = new GemListView(m_proxyModel, m_proxyModel->GetSelectionModel(), this); m_rightPanelStack = new QStackedWidget(this); m_rightPanelStack->setFixedWidth(240); m_gemInspector = new GemInspector(m_gemModel, this); connect(m_gemInspector, &GemInspector::TagClicked, [=](const Tag& tag) { SelectGem(tag.id); }); connect(m_gemInspector, &GemInspector::UpdateGem, this, &GemCatalogScreen::UpdateGem); connect(m_gemInspector, &GemInspector::UninstallGem, this, &GemCatalogScreen::UninstallGem); QWidget* filterWidget = new QWidget(this); filterWidget->setFixedWidth(240); m_filterWidgetLayout = new QVBoxLayout(); m_filterWidgetLayout->setMargin(0); m_filterWidgetLayout->setSpacing(0); filterWidget->setLayout(m_filterWidgetLayout); GemListHeaderWidget* listHeaderWidget = new GemListHeaderWidget(m_proxyModel); QVBoxLayout* middleVLayout = new QVBoxLayout(); middleVLayout->setMargin(0); middleVLayout->setSpacing(0); middleVLayout->addWidget(listHeaderWidget); middleVLayout->addWidget(m_gemListView); hLayout->addWidget(filterWidget); hLayout->addLayout(middleVLayout); hLayout->addWidget(m_rightPanelStack); m_rightPanelStack->addWidget(m_gemInspector); m_notificationsView = AZStd::make_unique(this, AZ_CRC("GemCatalogNotificationsView")); m_notificationsView->SetOffset(QPoint(10, 70)); m_notificationsView->SetMaxQueuedNotifications(1); } void GemCatalogScreen::ReinitForProject(const QString& projectPath) { 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); } FillModel(projectPath); m_proxyModel->ResetFilters(false); m_proxyModel->sort(/*column=*/0); if (m_filterWidget) { m_filterWidget->ResetAllFilters(); } else { m_filterWidget = new GemFilterWidget(m_proxyModel); m_filterWidgetLayout->addWidget(m_filterWidget); } m_headerWidget->ReinitForProject(); connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); // 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); }); } void GemCatalogScreen::OnAddGemClicked() { EngineInfo engineInfo; QString defaultPath; AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); if (engineInfoResult.IsSuccess()) { engineInfo = engineInfoResult.GetValue(); defaultPath = engineInfo.m_defaultGemsFolder; } if (defaultPath.isEmpty()) { defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); } QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath)); if (!directory.isEmpty()) { // register the gem to the o3de_manifest.json and to the project after the user confirms // project creation/update auto registerResult = PythonBindingsInterface::Get()->RegisterGem(directory); if(!registerResult) { QMessageBox::critical(this, tr("Failed to add gem"), registerResult.GetError().c_str()); } else { m_gemsToRegisterWithProject.insert(directory); AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(directory); if (gemInfoResult) { m_gemModel->AddGem(gemInfoResult.GetValue()); m_gemModel->UpdateGemDependencies(); m_proxyModel->sort(/*column=*/0); } } } } void GemCatalogScreen::Refresh() { QHash gemInfoHash; // create a hash with the gem name as key const AZ::Outcome, AZStd::string>& allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(m_projectPath); if (allGemInfosResult.IsSuccess()) { const QVector& gemInfos = allGemInfosResult.GetValue(); for (const GemInfo& gemInfo : gemInfos) { gemInfoHash.insert(gemInfo.m_name, gemInfo); } } // add all the gem repos into the hash const AZ::Outcome, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos(); if (allRepoGemInfosResult.IsSuccess()) { const QVector& allRepoGemInfos = allRepoGemInfosResult.GetValue(); for (const GemInfo& gemInfo : allRepoGemInfos) { if (!gemInfoHash.contains(gemInfo.m_name)) { gemInfoHash.insert(gemInfo.m_name, gemInfo); } } } // remove gems from the model that no longer exist in the hash and are not project dependencies int i = 0; while (i < m_gemModel->rowCount()) { QModelIndex index = m_gemModel->index(i,0); QString gemName = m_gemModel->GetName(index); const bool gemFound = gemInfoHash.contains(gemName); if (!gemFound && !m_gemModel->IsAdded(index) && !m_gemModel->IsAddedDependency(index)) { m_gemModel->RemoveGem(index); } else { if (!gemFound && (m_gemModel->IsAdded(index) || m_gemModel->IsAddedDependency(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); // 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); } void GemCatalogScreen::OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies) { if (m_notificationsEnabled) { QModelIndex modelIndex = m_gemModel->FindIndexByNameString(gemName); bool added = GemModel::IsAdded(modelIndex); bool dependency = GemModel::IsAddedDependency(modelIndex); bool gemStateChanged = (added && !dependency) || (!added && !dependency); if (!gemStateChanged && !numChangedDependencies) { // no actual changes made return; } QString notification; if (gemStateChanged) { notification = GemModel::GetDisplayName(modelIndex); if (numChangedDependencies > 0) { notification += tr(" and "); } if (added && GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded) { m_downloadController->AddGemDownload(GemModel::GetName(modelIndex)); GemModel::SetDownloadStatus(*m_gemModel, modelIndex, GemInfo::DownloadStatus::Downloading); } } if (numChangedDependencies == 1) { notification += tr("1 Gem dependency"); } else if (numChangedDependencies > 1) { notification += tr("%1 Gem %2").arg(QString(numChangedDependencies), tr("dependencies")); } notification += (added ? tr(" activated") : tr(" deactivated")); AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); toastConfiguration.m_customIconImage = ":/gem.svg"; toastConfiguration.m_borderRadius = 4; toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000); m_notificationsView->ShowToastNotification(toastConfiguration); } } void GemCatalogScreen::SelectGem(const QString& gemName) { QModelIndex modelIndex = m_gemModel->FindIndexByNameString(gemName); if (!m_proxyModel->filterAcceptsRow(modelIndex.row(), QModelIndex())) { m_proxyModel->ResetFilters(); m_filterWidget->ResetAllFilters(); } QModelIndex proxyIndex = m_proxyModel->mapFromSource(modelIndex); m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect); m_gemListView->scrollTo(proxyIndex); ShowInspector(); } void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex) { const QString selectedGemName = m_gemModel->GetName(modelIndex); const QString selectedGemLastUpdate = m_gemModel->GetLastUpdated(modelIndex); const QString selectedDisplayGemName = m_gemModel->GetDisplayName(modelIndex); const QString selectedGemRepoUri = m_gemModel->GetRepoUri(modelIndex); // Refresh gem repo if (!selectedGemRepoUri.isEmpty()) { AZ::Outcome refreshResult = PythonBindingsInterface::Get()->RefreshGemRepo(selectedGemRepoUri); if (refreshResult.IsSuccess()) { Refresh(); } else { QMessageBox::critical( this, tr("Operation failed"), tr("Failed to refresh gem repository %1
Error:
%2").arg(selectedGemRepoUri, refreshResult.GetError().c_str())); } } // If repo uri isn't specified warn user that repo might not be refreshed else { int result = QMessageBox::warning( this, tr("Gem Repository Unspecified"), tr("The repo for %1 is unspecfied. Repository cannot be automatically refreshed. " "Please ensure this gem's repo is refreshed before attempting to update.") .arg(selectedDisplayGemName), QMessageBox::Cancel, QMessageBox::Ok); // Allow user to cancel update to manually refresh repo if (result != QMessageBox::Ok) { return; } } // Check if there is an update avaliable now that repo is refreshed bool updateAvaliable = PythonBindingsInterface::Get()->IsGemUpdateAvaliable(selectedGemName, selectedGemLastUpdate); GemUpdateDialog* confirmUpdateDialog = new GemUpdateDialog(selectedGemName, updateAvaliable, this); if (confirmUpdateDialog->exec() == QDialog::Accepted) { m_downloadController->AddGemDownload(selectedGemName); } } void GemCatalogScreen::UninstallGem(const QModelIndex& modelIndex) { const QString selectedDisplayGemName = m_gemModel->GetDisplayName(modelIndex); GemUninstallDialog* confirmUninstallDialog = new GemUninstallDialog(selectedDisplayGemName, this); if (confirmUninstallDialog->exec() == QDialog::Accepted) { const QString selectedGemPath = m_gemModel->GetPath(modelIndex); const bool wasAdded = GemModel::WasPreviouslyAdded(modelIndex); const bool wasAddedDependency = GemModel::WasPreviouslyAddedDependency(modelIndex); // Remove gem from gems to be added to update any dependencies GemModel::SetIsAdded(*m_gemModel, modelIndex, false); // Unregister the gem auto unregisterResult = PythonBindingsInterface::Get()->UnregisterGem(selectedGemPath); if (!unregisterResult) { QMessageBox::critical(this, tr("Failed to unregister gem"), unregisterResult.GetError().c_str()); } else { const QString selectedGemName = m_gemModel->GetName(modelIndex); // Remove gem from model m_gemModel->RemoveGem(modelIndex); // Delete uninstalled gem directory if (!ProjectUtils::DeleteProjectFiles(selectedGemPath, /*force*/true)) { QMessageBox::critical( this, tr("Failed to remove gem directory"), tr("Could not delete gem directory at:
%1").arg(selectedGemPath)); } // Show undownloaded remote gem again Refresh(); // 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); } } } void GemCatalogScreen::hideEvent(QHideEvent* event) { ScreenWidget::hideEvent(event); m_notificationsView->OnHide(); } void GemCatalogScreen::showEvent(QShowEvent* event) { ScreenWidget::showEvent(event); m_notificationsView->OnShow(); } void GemCatalogScreen::resizeEvent(QResizeEvent* event) { ScreenWidget::resizeEvent(event); m_notificationsView->UpdateToastPosition(); } void GemCatalogScreen::moveEvent(QMoveEvent* event) { ScreenWidget::moveEvent(event); m_notificationsView->UpdateToastPosition(); } void GemCatalogScreen::FillModel(const QString& projectPath) { m_projectPath = projectPath; const AZ::Outcome, AZStd::string>& allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); if (allGemInfosResult.IsSuccess()) { // Add all available gems to the model. const QVector& allGemInfos = allGemInfosResult.GetValue(); for (const GemInfo& gemInfo : allGemInfos) { m_gemModel->AddGem(gemInfo); } const AZ::Outcome, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos(); if (allRepoGemInfosResult.IsSuccess()) { const QVector& 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); } } } else { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems from repos.

Error:
%1").arg(allRepoGemInfosResult.GetError().c_str())); } m_gemModel->UpdateGemDependencies(); m_notificationsEnabled = false; // Gather enabled gems for the given project. const auto& enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); if (enabledGemNamesResult.IsSuccess()) { const QVector& enabledGemNames = enabledGemNamesResult.GetValue(); for (const AZStd::string& enabledGemName : enabledGemNames) { const QModelIndex modelIndex = m_gemModel->FindIndexByNameString(enabledGemName.c_str()); 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 (enabledGemName != "${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.", enabledGemName.c_str()); } } } else { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.

Error:
%2").arg(projectPath, enabledGemNamesResult.GetError().c_str())); } m_notificationsEnabled = true; } else { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems for %1.

Error:
%2").arg(projectPath, allGemInfosResult.GetError().c_str())); } } void GemCatalogScreen::ShowInspector() { m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Inspector); m_headerWidget->GemCartShown(); } GemCatalogScreen::EnableDisableGemsResult GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath) { IPythonBindings* pythonBindings = PythonBindingsInterface::Get(); QVector toBeAdded = m_gemModel->GatherGemsToBeAdded(); QVector toBeRemoved = m_gemModel->GatherGemsToBeRemoved(); if (m_gemModel->DoGemsToBeAddedHaveRequirements()) { GemRequirementDialog* confirmRequirementsDialog = new GemRequirementDialog(m_gemModel, this); if(confirmRequirementsDialog->exec() == QDialog::Rejected) { return EnableDisableGemsResult::Cancel; } } if (m_gemModel->HasDependentGemsToRemove()) { GemDependenciesDialog* dependenciesDialog = new GemDependenciesDialog(m_gemModel, this); if(dependenciesDialog->exec() == QDialog::Rejected) { return EnableDisableGemsResult::Cancel; } toBeAdded = m_gemModel->GatherGemsToBeAdded(); toBeRemoved = m_gemModel->GatherGemsToBeRemoved(); } for (const QModelIndex& modelIndex : toBeAdded) { const QString& gemPath = GemModel::GetPath(modelIndex); // make sure any remote gems we added were downloaded successfully const GemInfo::DownloadStatus status = GemModel::GetDownloadStatus(modelIndex); if (GemModel::GetGemOrigin(modelIndex) == GemInfo::Remote && !(status == GemInfo::Downloaded || status == GemInfo::DownloadSuccessful)) { QMessageBox::critical( nullptr, "Cannot add gem that isn't downloaded", tr("Cannot add gem %1 to project because it isn't downloaded yet or failed to download.") .arg(GemModel::GetDisplayName(modelIndex))); return EnableDisableGemsResult::Failed; } const AZ::Outcome result = pythonBindings->AddGemToProject(gemPath, projectPath); if (!result.IsSuccess()) { QMessageBox::critical(nullptr, "Failed to add gem to project", tr("Cannot add gem %1 to project.

Error:
%2").arg(GemModel::GetDisplayName(modelIndex), result.GetError().c_str())); return EnableDisableGemsResult::Failed; } // register external gems that were added with relative paths if (m_gemsToRegisterWithProject.contains(gemPath)) { pythonBindings->RegisterGem(QDir(projectPath).relativeFilePath(gemPath), projectPath); } } for (const QModelIndex& modelIndex : toBeRemoved) { const QString gemPath = GemModel::GetPath(modelIndex); const AZ::Outcome result = pythonBindings->RemoveGemFromProject(gemPath, projectPath); if (!result.IsSuccess()) { QMessageBox::critical(nullptr, "Failed to remove gem from project", tr("Cannot remove gem %1 from project.

Error:
%2").arg(GemModel::GetDisplayName(modelIndex), result.GetError().c_str())); return EnableDisableGemsResult::Failed; } } return EnableDisableGemsResult::Success; } void GemCatalogScreen::HandleOpenGemRepo() { emit ChangeScreenRequest(ProjectManagerScreen::GemRepos); } void GemCatalogScreen::UpdateAndShowGemCart(QWidget* cartWidget) { QWidget* previousCart = m_rightPanelStack->widget(RightPanelWidgetOrder::Cart); if (previousCart) { m_rightPanelStack->removeWidget(previousCart); } m_rightPanelStack->insertWidget(RightPanelWidgetOrder::Cart, cartWidget); m_rightPanelStack->setCurrentIndex(RightPanelWidgetOrder::Cart); } void GemCatalogScreen::OnGemDownloadResult(const QString& gemName, bool succeeded) { if (succeeded) { // refresh the information for downloaded gems const AZ::Outcome, AZStd::string>& allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(m_projectPath); if (allGemInfosResult.IsSuccess()) { // we should find the gem name now in all gem infos for (const GemInfo& gemInfo : allGemInfosResult.GetValue()) { if (gemInfo.m_name == gemName) { QModelIndex oldIndex = m_gemModel->FindIndexByNameString(gemName); if (oldIndex.isValid()) { // Check if old gem is selected bool oldGemSelected = false; if (m_gemModel->GetSelectionModel()->currentIndex() == oldIndex) { oldGemSelected = true; } // Remove old remote gem m_gemModel->RemoveGem(oldIndex); // Add new downloaded version of gem QModelIndex newIndex = m_gemModel->AddGem(gemInfo); GemModel::SetDownloadStatus(*m_gemModel, newIndex, GemInfo::DownloadSuccessful); GemModel::SetIsAdded(*m_gemModel, newIndex, true); // Select new version of gem if it was previously selected if (oldGemSelected) { QModelIndex proxyIndex = m_proxyModel->mapFromSource(newIndex); m_proxyModel->GetSelectionModel()->setCurrentIndex(proxyIndex, QItemSelectionModel::ClearAndSelect); } } break; } } } } else { QModelIndex index = m_gemModel->FindIndexByNameString(gemName); if (index.isValid()) { GemModel::SetDownloadStatus(*m_gemModel, index, GemInfo::DownloadFailed); } } } ProjectManagerScreen GemCatalogScreen::GetScreenEnum() { return ProjectManagerScreen::GemCatalog; } } // namespace O3DE::ProjectManager