You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

640 lines
26 KiB
C++

/*
* 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 <GemCatalog/GemCatalogScreen.h>
#include <PythonBindingsInterface.h>
#include <GemCatalog/GemListHeaderWidget.h>
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemRequirementDialog.h>
#include <GemCatalog/GemDependenciesDialog.h>
#include <GemCatalog/GemUpdateDialog.h>
#include <GemCatalog/GemUninstallDialog.h>
#include <DownloadController.h>
#include <ProjectUtils.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTimer>
#include <PythonBindingsInterface.h>
#include <QMessageBox>
#include <QDir>
#include <QStandardPaths>
#include <QFileDialog>
#include <QMessageBox>
#include <QHash>
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_headerWidget, &GemCatalogHeaderWidget::RefreshGems, this, &GemCatalogScreen::Refresh);
connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo);
connect(m_headerWidget, &GemCatalogHeaderWidget::AddGem, this, &GemCatalogScreen::OnAddGemClicked);
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_gemInspector = new GemInspector(m_gemModel, this);
m_gemInspector->setFixedWidth(240);
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_gemInspector);
m_notificationsView = AZStd::make_unique<AzToolsFramework::ToastNotificationsView>(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<EngineInfo> 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<GemInfo, void> gemInfoResult = PythonBindingsInterface::Get()->GetGemInfo(directory);
if (gemInfoResult)
{
m_gemModel->AddGem(gemInfoResult.GetValue<GemInfo>());
m_gemModel->UpdateGemDependencies();
m_proxyModel->sort(/*column=*/0);
}
}
}
}
void GemCatalogScreen::Refresh()
{
QHash<QString, GemInfo> gemInfoHash;
// 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())
{
const QVector<GemInfo>& 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<QVector<GemInfo>, AZStd::string>& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
if (allRepoGemInfosResult.IsSuccess())
{
const QVector<GemInfo>& 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);
}
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<void, AZStd::string> refreshResult = PythonBindingsInterface::Get()->RefreshGemRepo(selectedGemRepoUri);
if (refreshResult.IsSuccess())
{
Refresh();
}
else
{
QMessageBox::critical(
this, tr("Operation failed"),
tr("Failed to refresh gem repository %1<br>Error:<br>%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:<br>%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<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)
{
m_gemModel->AddGem(gemInfo);
}
const AZ::Outcome<QVector<GemInfo>, AZStd::string>& 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);
}
}
}
else
{
QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems from repos.<br><br>Error:<br>%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<AZStd::string>& 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.<br><br>Error:<br>%2").arg(projectPath, enabledGemNamesResult.GetError().c_str()));
}
m_notificationsEnabled = true;
}
else
{
QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve gems for %1.<br><br>Error:<br>%2").arg(projectPath, allGemInfosResult.GetError().c_str()));
}
}
GemCatalogScreen::EnableDisableGemsResult GemCatalogScreen::EnableDisableGemsForProject(const QString& projectPath)
{
IPythonBindings* pythonBindings = PythonBindingsInterface::Get();
QVector<QModelIndex> toBeAdded = m_gemModel->GatherGemsToBeAdded();
QVector<QModelIndex> 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<void, AZStd::string> result = pythonBindings->AddGemToProject(gemPath, projectPath);
if (!result.IsSuccess())
{
QMessageBox::critical(nullptr, "Failed to add gem to project",
tr("Cannot add gem %1 to project.<br><br>Error:<br>%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<void, AZStd::string> result = pythonBindings->RemoveGemFromProject(gemPath, projectPath);
if (!result.IsSuccess())
{
QMessageBox::critical(nullptr, "Failed to remove gem from project",
tr("Cannot remove gem %1 from project.<br><br>Error:<br>%2").arg(GemModel::GetDisplayName(modelIndex), result.GetError().c_str()));
return EnableDisableGemsResult::Failed;
}
}
return EnableDisableGemsResult::Success;
}
void GemCatalogScreen::HandleOpenGemRepo()
{
emit ChangeScreenRequest(ProjectManagerScreen::GemRepos);
}
void GemCatalogScreen::OnGemDownloadResult(const QString& gemName, bool succeeded)
{
if (succeeded)
{
// refresh the information for downloaded gems
const AZ::Outcome<QVector<GemInfo>, 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