Merge pull request #4807 from aws-lumberyard-dev/GemUIDownload

First part of UI feedback for downloading gems
This commit is contained in:
AMZN-Phil
2021-10-21 15:11:16 -07:00
committed by GitHub
14 changed files with 425 additions and 9 deletions
@@ -481,6 +481,18 @@ QProgressBar::chunk {
font-weight: 600;
}
#GemCatalogCartOverlayGemDownloadHeader {
margin:0;
padding: 0px;
background-color: #333333;
}
#GemCatalogCartOverlayGemDownloadBG {
margin:0;
padding: 0px;
background-color: #444444;
}
#GemCatalogHeaderLabel {
font-size: 12px;
color: #FFFFFF;
@@ -0,0 +1,84 @@
/*
* 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 <DownloadController.h>
#include <DownloadWorker.h>
#include <QMessageBox>
namespace O3DE::ProjectManager
{
DownloadController::DownloadController(QWidget* parent)
: QObject()
, m_lastProgress(0)
, m_parent(parent)
{
m_worker = new DownloadWorker();
m_worker->moveToThread(&m_workerThread);
connect(&m_workerThread, &QThread::started, m_worker, &DownloadWorker::StartDownload);
connect(m_worker, &DownloadWorker::Done, this, &DownloadController::HandleResults);
connect(m_worker, &DownloadWorker::UpdateProgress, this, &DownloadController::UpdateUIProgress);
connect(this, &DownloadController::StartGemDownload, m_worker, &DownloadWorker::StartDownload);
}
DownloadController::~DownloadController()
{
connect(&m_workerThread, &QThread::finished, m_worker, &DownloadController::deleteLater);
m_workerThread.requestInterruption();
m_workerThread.quit();
m_workerThread.wait();
}
void DownloadController::AddGemDownload(const QString& gemName)
{
m_gemNames.push_back(gemName);
if (m_gemNames.size() == 1)
{
m_worker->SetGemToDownload(m_gemNames[0], false);
m_workerThread.start();
}
}
void DownloadController::UpdateUIProgress(int progress)
{
m_lastProgress = progress;
emit GemDownloadProgress(progress);
}
void DownloadController::HandleResults(const QString& result)
{
bool succeeded = true;
if (!result.isEmpty())
{
QMessageBox::critical(nullptr, tr("Gem download"), result);
succeeded = false;
}
m_gemNames.erase(m_gemNames.begin());
emit Done(succeeded);
if (!m_gemNames.empty())
{
emit StartGemDownload(m_gemNames[0]);
}
else
{
m_workerThread.quit();
m_workerThread.wait();
}
}
void DownloadController::HandleCancel()
{
m_workerThread.quit();
emit Done(false);
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,72 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <QString>
#include <QThread>
#include <AzCore/std/containers/vector.h>
#endif
QT_FORWARD_DECLARE_CLASS(QProcess)
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(DownloadWorker)
class DownloadController : public QObject
{
Q_OBJECT
public:
explicit DownloadController(QWidget* parent = nullptr);
~DownloadController();
void AddGemDownload(const QString& m_gemName);
bool IsDownloadQueueEmpty()
{
return m_gemNames.empty();
}
const AZStd::vector<QString>& GetDownloadQueue() const
{
return m_gemNames;
}
const QString& GetCurrentDownloadingGem() const
{
if (!m_gemNames.empty())
{
return m_gemNames[0];
}
else
{
static const QString emptyString;
return emptyString;
}
}
public slots:
void UpdateUIProgress(int progress);
void HandleResults(const QString& result);
void HandleCancel();
signals:
void StartGemDownload(const QString& gemName);
void Done(bool success = true);
void GemDownloadProgress(int percentage);
private:
DownloadWorker* m_worker;
QThread m_workerThread;
QWidget* m_parent;
AZStd::vector<QString> m_gemNames;
int m_lastProgress;
};
} // namespace O3DE::ProjectManager
@@ -0,0 +1,48 @@
/*
* 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 <DownloadController.h>
#include <DownloadWorker.h>
#include <PythonBindings.h>
namespace O3DE::ProjectManager
{
DownloadWorker::DownloadWorker()
: QObject()
{
}
void DownloadWorker::StartDownload()
{
auto gemDownloadProgress = [=](int downloadProgress)
{
m_downloadProgress = downloadProgress;
emit UpdateProgress(downloadProgress);
};
AZ::Outcome<void, AZStd::string> gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress);
if (gemInfoResult.IsSuccess())
{
emit Done("");
}
else
{
emit Done(tr("Gem download failed"));
}
}
void DownloadWorker::SetGemToDownload(const QString& gemName, bool downloadNow)
{
m_gemName = gemName;
if (downloadNow)
{
StartDownload();
}
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,42 @@
/*
* 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
*
*/
#pragma once
#if !defined(Q_MOC_RUN)
#include <AzCore/Outcome/Outcome.h>
#endif
QT_FORWARD_DECLARE_CLASS(QProcess)
namespace O3DE::ProjectManager
{
class DownloadWorker : public QObject
{
// Download was cancelled
inline static const QString DownloadCancelled = QObject::tr("Download Cancelled.");
Q_OBJECT
public:
explicit DownloadWorker();
~DownloadWorker() = default;
public slots:
void StartDownload();
void SetGemToDownload(const QString& gemName, bool downloadNow = true);
signals:
void UpdateProgress(int progress);
void Done(QString result = "");
private:
QString m_gemName;
int m_downloadProgress;
};
} // namespace O3DE::ProjectManager
@@ -12,13 +12,15 @@
#include <QMouseEvent>
#include <QLabel>
#include <QPushButton>
#include <QProgressBar>
#include <TagWidget.h>
namespace O3DE::ProjectManager
{
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent)
CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
, m_downloadController(downloadController)
{
setObjectName("GemCatalogCart");
@@ -42,6 +44,9 @@ namespace O3DE::ProjectManager
hLayout->addWidget(closeButton);
m_layout->addLayout(hLayout);
// downloading gems
CreateDownloadSection();
// added
CreateGemSection( tr("Gem to be activated"), tr("Gems to be activated"), [=]
{
@@ -149,6 +154,109 @@ namespace O3DE::ProjectManager
update();
}
void CartOverlayWidget::CreateDownloadSection()
{
QWidget* widget = new QWidget();
widget->setFixedWidth(s_width);
m_layout->addWidget(widget);
QVBoxLayout* layout = new QVBoxLayout();
layout->setAlignment(Qt::AlignTop);
widget->setLayout(layout);
QLabel* titleLabel = new QLabel();
titleLabel->setObjectName("GemCatalogCartOverlaySectionLabel");
layout->addWidget(titleLabel);
titleLabel->setText(tr("Gems to be installed"));
// Create header section
QWidget* downloadingGemsWidget = new QWidget();
downloadingGemsWidget->setObjectName("GemCatalogCartOverlayGemDownloadHeader");
layout->addWidget(downloadingGemsWidget);
QVBoxLayout* gemDownloadLayout = new QVBoxLayout();
gemDownloadLayout->setMargin(0);
gemDownloadLayout->setAlignment(Qt::AlignTop);
downloadingGemsWidget->setLayout(gemDownloadLayout);
QLabel* processingQueueLabel = new QLabel("Processing Queue");
gemDownloadLayout->addWidget(processingQueueLabel);
QWidget* downloadingItemWidget = new QWidget();
downloadingItemWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG");
gemDownloadLayout->addWidget(downloadingItemWidget);
QVBoxLayout* downloadingItemLayout = new QVBoxLayout();
downloadingItemLayout->setAlignment(Qt::AlignTop);
downloadingItemWidget->setLayout(downloadingItemLayout);
auto update = [=](int downloadProgress)
{
if (m_downloadController->IsDownloadQueueEmpty())
{
widget->hide();
}
else
{
widget->setUpdatesEnabled(false);
// remove items
QLayoutItem* layoutItem = nullptr;
while ((layoutItem = downloadingItemLayout->takeAt(0)) != nullptr)
{
if (layoutItem->layout())
{
// Gem info row
QLayoutItem* rowLayoutItem = nullptr;
while ((rowLayoutItem = layoutItem->layout()->takeAt(0)) != nullptr)
{
rowLayoutItem->widget()->deleteLater();
}
layoutItem->layout()->deleteLater();
}
if (layoutItem->widget())
{
layoutItem->widget()->deleteLater();
}
}
// Setup gem download rows
const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
QLabel* downloadsInProgessLabel = new QLabel("");
downloadsInProgessLabel->setText(
QString("%1 %2").arg(downloadQueue.size()).arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress...")));
downloadingItemLayout->addWidget(downloadsInProgessLabel);
for (int downloadingGemNumber = 0; downloadingGemNumber < downloadQueue.size(); ++downloadingGemNumber)
{
QHBoxLayout* nameProgressLayout = new QHBoxLayout();
TagWidget* newTag = new TagWidget(downloadQueue[downloadingGemNumber]);
nameProgressLayout->addWidget(newTag);
QLabel* progress = new QLabel(downloadingGemNumber == 0? QString("%1%").arg(downloadProgress) : tr("Queued"));
nameProgressLayout->addWidget(progress);
QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
nameProgressLayout->addSpacerItem(spacer);
QLabel* cancelText = new QLabel(tr("Cancel"));
nameProgressLayout->addWidget(cancelText);
downloadingItemLayout->addLayout(nameProgressLayout);
QProgressBar* downloadProgessBar = new QProgressBar();
downloadingItemLayout->addWidget(downloadProgessBar);
downloadProgessBar->setValue(downloadingGemNumber == 0 ? downloadProgress : 0);
}
widget->setUpdatesEnabled(true);
widget->show();
}
};
auto downloadEnded = [=](bool /*success*/)
{
update(0); // update the list to remove the gem that has finished
};
// connect to download controller data changed
connect(m_downloadController, &DownloadController::GemDownloadProgress, this, update);
connect(m_downloadController, &DownloadController::Done, this, downloadEnded);
update(0);
}
QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector<QModelIndex>& gems) const
{
QStringList gemNames;
@@ -160,9 +268,10 @@ namespace O3DE::ProjectManager
return gemNames;
}
CartButton::CartButton(GemModel* gemModel, QWidget* parent)
CartButton::CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent)
: QWidget(parent)
, m_gemModel(gemModel)
, m_downloadController(downloadController)
{
m_layout = new QHBoxLayout();
m_layout->setMargin(0);
@@ -239,7 +348,7 @@ namespace O3DE::ProjectManager
delete m_cartOverlay;
}
m_cartOverlay = new CartOverlayWidget(m_gemModel, this);
m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this);
connect(m_cartOverlay, &QWidget::destroyed, this, [=]
{
// Reset the overlay pointer on destruction to prevent dangling pointers.
@@ -265,7 +374,7 @@ namespace O3DE::ProjectManager
}
}
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent)
GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent)
: QFrame(parent)
{
QHBoxLayout* hLayout = new QHBoxLayout();
@@ -293,7 +402,7 @@ namespace O3DE::ProjectManager
hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding));
hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed));
CartButton* cartButton = new CartButton(gemModel);
CartButton* cartButton = new CartButton(gemModel, downloadController);
hLayout->addWidget(cartButton);
}
@@ -21,6 +21,7 @@
#include <QMoveEvent>
#include <QHideEvent>
#include <QVBoxLayout>
#include <DownloadController.h>
#endif
namespace O3DE::ProjectManager
@@ -31,16 +32,18 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr);
CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
private:
QStringList ConvertFromModelIndices(const QVector<QModelIndex>& gems) const;
using GetTagIndicesCallback = AZStd::function<QVector<QModelIndex>()>;
void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices);
void CreateDownloadSection();
QVBoxLayout* m_layout = nullptr;
GemModel* m_gemModel = nullptr;
DownloadController* m_downloadController = nullptr;
inline constexpr static int s_width = 240;
};
@@ -51,7 +54,7 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
CartButton(GemModel* gemModel, QWidget* parent = nullptr);
CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr);
~CartButton();
void ShowOverlay();
@@ -64,6 +67,7 @@ namespace O3DE::ProjectManager
QLabel* m_countLabel = nullptr;
QPushButton* m_dropDownButton = nullptr;
CartOverlayWidget* m_cartOverlay = nullptr;
DownloadController* m_downloadController = nullptr;
inline constexpr static int s_iconSize = 24;
inline constexpr static int s_arrowDownIconSize = 8;
@@ -75,7 +79,7 @@ namespace O3DE::ProjectManager
Q_OBJECT // AUTOMOC
public:
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr);
explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent = nullptr);
~GemCatalogHeaderWidget() = default;
void ReinitForProject();
@@ -12,6 +12,7 @@
#include <GemCatalog/GemSortFilterProxyModel.h>
#include <GemCatalog/GemRequirementDialog.h>
#include <GemCatalog/GemDependenciesDialog.h>
#include <DownloadController.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
@@ -32,7 +33,9 @@ namespace O3DE::ProjectManager
vLayout->setSpacing(0);
setLayout(vLayout);
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel);
m_downloadController = new DownloadController();
m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel, m_downloadController);
vLayout->addWidget(m_headerWidget);
QHBoxLayout* hLayout = new QHBoxLayout();
@@ -39,6 +39,7 @@ namespace O3DE::ProjectManager
EnableDisableGemsResult EnableDisableGemsForProject(const QString& projectPath);
GemModel* GetGemModel() const { return m_gemModel; }
DownloadController* GetDownloadController() const { return m_downloadController; }
private:
void FillModel(const QString& projectPath);
@@ -50,5 +51,6 @@ namespace O3DE::ProjectManager
GemSortFilterProxyModel* m_proxModel = nullptr;
QVBoxLayout* m_filterWidgetLayout = nullptr;
GemFilterWidget* m_filterWidget = nullptr;
DownloadController* m_downloadController = nullptr;
};
} // namespace O3DE::ProjectManager
@@ -301,6 +301,7 @@ namespace O3DE::ProjectManager
m_enableGemProject = pybind11::module::import("o3de.enable_gem");
m_disableGemProject = pybind11::module::import("o3de.disable_gem");
m_editProjectProperties = pybind11::module::import("o3de.project_properties");
m_download = pybind11::module::import("o3de.download");
m_pathlib = pybind11::module::import("pathlib");
// make sure the engine is registered
@@ -1075,4 +1076,30 @@ namespace O3DE::ProjectManager
std::sort(gemRepos.begin(), gemRepos.end());
return AZ::Success(AZStd::move(gemRepos));
}
AZ::Outcome<void, AZStd::string> PythonBindings::DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback)
{
bool downloadSucceeded = false;
auto result = ExecuteWithLockErrorHandling(
[&]
{
auto downloadResult = m_download.attr("download_gem")(
QString_To_Py_String(gemName), // gem name
pybind11::none(), // destination path
false// skip auto register
);
downloadSucceeded = (downloadResult.cast<int>() == 0);
});
if (!result.IsSuccess())
{
return result;
}
else if (!downloadSucceeded)
{
return AZ::Failure<AZStd::string>("Failed to download gem.");
}
return AZ::Success();
}
}
@@ -61,6 +61,7 @@ namespace O3DE::ProjectManager
bool AddGemRepo(const QString& repoUri) override;
bool RemoveGemRepo(const QString& repoUri) override;
AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) override;
private:
AZ_DISABLE_COPY_MOVE(PythonBindings);
@@ -87,6 +88,7 @@ namespace O3DE::ProjectManager
pybind11::handle m_enableGemProject;
pybind11::handle m_disableGemProject;
pybind11::handle m_editProjectProperties;
pybind11::handle m_download;
pybind11::handle m_pathlib;
};
}
@@ -187,6 +187,8 @@ namespace O3DE::ProjectManager
* @return A list of gem repo infos.
*/
virtual AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() = 0;
virtual AZ::Outcome<void, AZStd::string> DownloadGem(const QString& gemName, std::function<void(int)> gemProgressCallback) = 0;
};
using PythonBindingsInterface = AZ::Interface<IPythonBindings>;
@@ -136,6 +136,11 @@ namespace O3DE::ProjectManager
}
else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen)
{
if (!m_gemCatalogScreen->GetDownloadController()->IsDownloadQueueEmpty())
{
QMessageBox::critical(this, tr("Gems downloading"), tr("You must wait for gems to finish downloading before continuing."));
return;
}
// Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project.
const GemCatalogScreen::EnableDisableGemsResult result = m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path);
if (result == GemCatalogScreen::EnableDisableGemsResult::Failed)
@@ -29,6 +29,10 @@ set(FILES
Source/FormImageBrowseEditWidget.cpp
Source/GemsSubWidget.h
Source/GemsSubWidget.cpp
Source/DownloadController.h
Source/DownloadController.cpp
Source/DownloadWorker.h
Source/DownloadWorker.cpp
Source/PathValidator.h
Source/PathValidator.cpp
Source/ProjectManagerWindow.h