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

WIP of the changes required for the remote project dialog and downloading projects

Signed-off-by: AMZN-Phil <[email protected]>
AMZN-Phil преди 3 години
родител
ревизия
8b66007135
променени са 27 файла, в които са добавени 651 реда и са изтрити 60 реда
  1. 60 5
      Code/Tools/ProjectManager/Source/AddRemoteProjectDialog.cpp
  2. 12 0
      Code/Tools/ProjectManager/Source/AddRemoteProjectDialog.h
  3. 75 16
      Code/Tools/ProjectManager/Source/DownloadController.cpp
  4. 27 6
      Code/Tools/ProjectManager/Source/DownloadController.h
  5. 34 6
      Code/Tools/ProjectManager/Source/DownloadWorker.cpp
  6. 6 1
      Code/Tools/ProjectManager/Source/DownloadWorker.h
  7. 7 4
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp
  8. 2 2
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp
  9. 1 1
      Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h
  10. 63 0
      Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp
  11. 13 1
      Code/Tools/ProjectManager/Source/ProjectButtonWidget.h
  12. 2 2
      Code/Tools/ProjectManager/Source/ProjectGemCatalogScreen.cpp
  13. 3 1
      Code/Tools/ProjectManager/Source/ProjectGemCatalogScreen.h
  14. 2 0
      Code/Tools/ProjectManager/Source/ProjectInfo.h
  15. 4 1
      Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp
  16. 5 0
      Code/Tools/ProjectManager/Source/ProjectManagerWindow.h
  17. 85 2
      Code/Tools/ProjectManager/Source/ProjectsScreen.cpp
  18. 8 2
      Code/Tools/ProjectManager/Source/ProjectsScreen.h
  19. 151 1
      Code/Tools/ProjectManager/Source/PythonBindings.cpp
  20. 6 0
      Code/Tools/ProjectManager/Source/PythonBindings.h
  21. 33 0
      Code/Tools/ProjectManager/Source/PythonBindingsInterface.h
  22. 5 4
      Code/Tools/ProjectManager/Source/ScreenFactory.cpp
  23. 3 2
      Code/Tools/ProjectManager/Source/ScreenFactory.h
  24. 4 2
      Code/Tools/ProjectManager/Source/ScreensCtrl.cpp
  25. 3 1
      Code/Tools/ProjectManager/Source/ScreensCtrl.h
  26. 9 0
      Code/Tools/ProjectManager/Source/ShowBuildLogScreen.cpp
  27. 28 0
      Code/Tools/ProjectManager/Source/ShowBuildLogScreen.h

+ 60 - 5
Code/Tools/ProjectManager/Source/AddRemoteProjectDialog.cpp

@@ -11,6 +11,7 @@
 #include <TextOverflowWidget.h>
 #include <AzQtComponents/Components/Widgets/CheckBox.h>
 #include <ProjectUtils.h>
+#include <PythonBindingsInterface.h>
 
 #include <QVBoxLayout>
 #include <QGridLayout>
@@ -20,6 +21,7 @@
 #include <QDialogButtonBox>
 #include <QPushButton>
 #include <QDir>
+#include <QTimer>
 
 namespace O3DE::ProjectManager
 {
@@ -143,7 +145,11 @@ namespace O3DE::ProjectManager
         m_applyButton = m_dialogButtons->addButton(tr("Download && Build"), QDialogButtonBox::ApplyRole);
 
         connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
-        connect(m_applyButton, &QPushButton::clicked, this, &QDialog::accept);
+        connect(m_applyButton, &QPushButton::clicked, this, &AddRemoteProjectDialog::DownloadObject);
+
+        m_inputTimer = new QTimer(this);
+        m_inputTimer->setSingleShot(true);
+        connect(m_inputTimer, &QTimer::timeout, this, &AddRemoteProjectDialog::ValidateURI);
 
         connect(
             m_autoBuild, &QCheckBox::clicked, [this](bool checked)
@@ -162,14 +168,56 @@ namespace O3DE::ProjectManager
         // Simulate repo being entered and UI enabling
         connect(
             m_repoPath->lineEdit(), &QLineEdit::textEdited,
-            [this](const QString& text)
+            [this](const QString& /*text*/)
             {
-                SetDialogReady(!text.isEmpty());
+                // wait for a second before attempting to validate so we're less likely to do it per keypress
+                m_inputTimer->start(1000);
+                
             });
 
         SetDialogReady(false);
     }
 
+    void AddRemoteProjectDialog::ValidateURI()
+    {
+        // validate URI, if it's a valid repository, get the project info and set the dialog as ready
+        bool validRepository = false;//!m_repoPath->lineEdit()->text().isEmpty();
+        // get project info
+        auto repoProjectsResult = PythonBindingsInterface::Get()->GetProjectsForRepo(m_repoPath->lineEdit()->text());
+        if (repoProjectsResult.IsSuccess())
+        {
+            auto repoProjects = repoProjectsResult.GetValue();
+            if (!repoProjects.isEmpty())
+            {
+                // only get the first one for now
+                ProjectInfo project = repoProjects.at(0);
+                SetCurrentProject(project);
+
+                validRepository = true;
+            }
+        }
+        SetDialogReady(validRepository);
+    }
+
+    void AddRemoteProjectDialog::DownloadObject()
+    {
+        // Add Repo:
+        const QString repoUri = m_repoPath->lineEdit()->text();
+        auto addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
+        if (addGemRepoResult.IsSuccess())
+        {
+            // Send download to project screen to initiate download
+            emit StartObjectDownload(m_currentProject.m_projectName);
+            emit QDialog::accept();
+        }
+        else
+        {
+            QString failureMessage = tr("Failed to add gem repo: %1.").arg(repoUri);
+            ProjectUtils::DisplayDetailedError(failureMessage, addGemRepoResult, this);
+            AZ_Error("Project Manager", false, failureMessage.toUtf8());
+        }
+    }
+
     QString AddRemoteProjectDialog::GetRepoPath()
     {
         return m_repoPath->lineEdit()->text();
@@ -187,12 +235,19 @@ namespace O3DE::ProjectManager
     {
         m_currentProject = projectInfo;
 
-        m_downloadProjectLabel->setText(projectInfo.m_displayName);
+        m_downloadProjectLabel->setText(tr("Download Project ") + projectInfo.m_displayName);
         m_installPath->lineEdit()->setText(QDir::toNativeSeparators(ProjectUtils::GetDefaultProjectPath() + "/" + projectInfo.m_projectName));
     }
 
     void AddRemoteProjectDialog::SetDialogReady(bool isReady)
     {
+        // Reset
+        if (!isReady)
+        {
+            m_downloadProjectLabel->setText(tr("Download Project..."));
+            m_installPath->setText("");
+        }
+
         m_downloadProjectLabel->setEnabled(isReady);
         m_installPath->setEnabled(isReady);
         m_autoBuild->setEnabled(isReady);
@@ -201,6 +256,6 @@ namespace O3DE::ProjectManager
         m_licensesTitleLabel->setEnabled(isReady);
         m_requirementsContentLabel->setEnabled(isReady);
         m_licensesContentLabel->setEnabled(isReady);
-        m_dialogButtons->setEnabled(isReady);
+        m_applyButton->setEnabled(isReady);
     }
 } // namespace O3DE::ProjectManager

+ 12 - 0
Code/Tools/ProjectManager/Source/AddRemoteProjectDialog.h

@@ -18,6 +18,7 @@ QT_FORWARD_DECLARE_CLASS(QLabel)
 QT_FORWARD_DECLARE_CLASS(QCheckBox)
 QT_FORWARD_DECLARE_CLASS(QDialogButtonBox)
 QT_FORWARD_DECLARE_CLASS(QPushButton)
+QT_FORWARD_DECLARE_CLASS(QTimer)
 
 namespace O3DE::ProjectManager
 {
@@ -26,6 +27,8 @@ namespace O3DE::ProjectManager
     class AddRemoteProjectDialog
         : public QDialog
     {
+        Q_OBJECT
+
     public:
         explicit AddRemoteProjectDialog(QWidget* parent = nullptr);
         ~AddRemoteProjectDialog() = default;
@@ -39,7 +42,14 @@ namespace O3DE::ProjectManager
     private:
         void SetDialogReady(bool isReady);
 
+    signals:
+        void StartObjectDownload(const QString& objectName);
+
+    private slots:
+        void ValidateURI();
+        void DownloadObject();
 
+    private:
         ProjectInfo m_currentProject;
 
         FormLineEditWidget* m_repoPath = nullptr;
@@ -59,5 +69,7 @@ namespace O3DE::ProjectManager
 
         QDialogButtonBox* m_dialogButtons = nullptr;
         QPushButton* m_applyButton = nullptr;
+
+        QTimer* m_inputTimer = nullptr;
     };
 } // namespace O3DE::ProjectManager

+ 75 - 16
Code/Tools/ProjectManager/Source/DownloadController.cpp

@@ -27,6 +27,8 @@ namespace O3DE::ProjectManager
         connect(m_worker, &DownloadWorker::Done, this, &DownloadController::HandleResults);
         connect(m_worker, &DownloadWorker::UpdateProgress, this, &DownloadController::UpdateUIProgress);
         connect(this, &DownloadController::StartGemDownload, m_worker, &DownloadWorker::SetGemToDownload);
+        connect(this, &DownloadController::StartProjectDownload, m_worker, &DownloadWorker::SetProjectToDownload);
+        connect(this, &DownloadController::StartTemplateDownload, m_worker, &DownloadWorker::SetTemplateToDownload);
     }
 
     DownloadController::~DownloadController()
@@ -39,38 +41,88 @@ namespace O3DE::ProjectManager
 
     void DownloadController::AddGemDownload(const QString& gemName)
     {
-        m_gemNames.push_back(gemName);
+        m_objects.push_back({ gemName, DownloadObjectType::Gem });
         emit GemDownloadAdded(gemName);
 
-        if (m_gemNames.size() == 1)
+        if (m_objects.size() == 1)
         {
-            m_worker->SetGemToDownload(m_gemNames.front(), false);
+            m_worker->SetGemToDownload(m_objects.front().m_objectName, false);
             m_workerThread.start();
         }
     }
 
     void DownloadController::CancelGemDownload(const QString& gemName)
     {
-        auto findResult = AZStd::find(m_gemNames.begin(), m_gemNames.end(), gemName);
+        auto findResult = AZStd::find_if(
+            m_objects.begin(),
+            m_objects.end(),
+            [gemName](const DownloadableObject& object)
+            {
+                return (object.m_objectType == DownloadObjectType::Gem && object.m_objectName == gemName);
+            });
 
-        if (findResult != m_gemNames.end())
+        if (findResult != m_objects.end())
         {
-            if (findResult == m_gemNames.begin())
+            if (findResult == m_objects.begin())
             {
                 // HandleResults will remove the gem upon cancelling
                 PythonBindingsInterface::Get()->CancelDownload();
             }
             else
             {
-                m_gemNames.erase(findResult);
+                m_objects.erase(findResult);
                 emit GemDownloadRemoved(gemName);
             }
         }
     }
 
+    void DownloadController::AddProjectDownload(const QString& projectName)
+    {
+        m_objects.push_back({ projectName, DownloadObjectType::Project });
+        emit GemDownloadAdded(projectName);
+
+        if (m_objects.size() == 1)
+        {
+            m_worker->SetProjectToDownload(m_objects.front().m_objectName, false);
+            m_workerThread.start();
+        }
+    }
+
+    void DownloadController::CancelProjectDownload(const QString& projectName)
+    {
+        auto findResult = AZStd::find_if(
+            m_objects.begin(),
+            m_objects.end(),
+            [projectName](const DownloadableObject& object)
+            {
+                return (object.m_objectType == DownloadObjectType::Project && object.m_objectName == projectName);
+            });
+
+        if (findResult != m_objects.end())
+        {
+            if (findResult == m_objects.begin())
+            {
+                // HandleResults will remove the gem upon cancelling
+                PythonBindingsInterface::Get()->CancelDownload();
+            }
+            else
+            {
+                m_objects.erase(findResult);
+                emit ProjectDownloadRemoved(projectName);
+            }
+        }
+    }
+
     void DownloadController::UpdateUIProgress(int bytesDownloaded, int totalBytes)
     {
-        emit GemDownloadProgress(m_gemNames.front(), bytesDownloaded, totalBytes);
+        if (m_objects.front().m_objectType == DownloadObjectType::Gem)
+        {
+            emit GemDownloadProgress(m_objects.front().m_objectName, bytesDownloaded, totalBytes);
+        }
+        else if (m_objects.front().m_objectType == DownloadObjectType::Project)
+        {
+            emit ProjectDownloadProgress(m_objects.front().m_objectName, bytesDownloaded, totalBytes);
+        }
     }
 
     void DownloadController::HandleResults(const QString& result, const QString& detailedError)
@@ -83,26 +135,33 @@ namespace O3DE::ProjectManager
             {
                 QMessageBox gemDownloadError;
                 gemDownloadError.setIcon(QMessageBox::Critical);
-                gemDownloadError.setWindowTitle(tr("Gem download"));
+                gemDownloadError.setWindowTitle(tr("Object download"));
                 gemDownloadError.setText(result);
                 gemDownloadError.setDetailedText(detailedError);
                 gemDownloadError.exec();
             }
             else
             {
-                QMessageBox::critical(nullptr, tr("Gem download"), result);
+                QMessageBox::critical(nullptr, tr("Object download"), result);
             }
             succeeded = false;
         }
 
-        QString gemName = m_gemNames.front();
-        m_gemNames.erase(m_gemNames.begin());
-        emit Done(gemName, succeeded);
-        emit GemDownloadRemoved(gemName);
+        DownloadableObject downloadableObject = m_objects.front();
+        m_objects.erase(m_objects.begin());
+        emit Done(downloadableObject.m_objectName, succeeded);
+        emit GemDownloadRemoved(downloadableObject.m_objectName);
 
-        if (!m_gemNames.empty())
+        if (!m_objects.empty())
         {
-            emit StartGemDownload(m_gemNames.front(), true);
+            if (m_objects.front().m_objectType == DownloadObjectType::Gem)
+            {
+                emit StartGemDownload(m_objects.front().m_objectName, true);
+            }
+            else if (m_objects.front().m_objectType == DownloadObjectType::Project)
+            {
+                emit StartProjectDownload(m_objects.front().m_objectName, true);
+            }
         }
         else
         {

+ 27 - 6
Code/Tools/ProjectManager/Source/DownloadController.h

@@ -24,27 +24,43 @@ namespace O3DE::ProjectManager
         Q_OBJECT
 
     public:
+        enum DownloadObjectType
+        {
+            Gem = 1 << 0,
+            Project = 1 << 1,
+            Template = 1 << 2
+        };
+
+        struct DownloadableObject
+        {
+            QString m_objectName;
+            DownloadObjectType m_objectType;
+        };
+
         explicit DownloadController(QWidget* parent = nullptr);
         ~DownloadController();
 
         void AddGemDownload(const QString& gemName);
         void CancelGemDownload(const QString& gemName);
 
+        void AddProjectDownload(const QString& projectName);
+        void CancelProjectDownload(const QString& projectName);
+
         bool IsDownloadQueueEmpty()
         {
-            return m_gemNames.empty();
+            return m_objects.empty();
         }
 
-        const AZStd::vector<QString>& GetDownloadQueue() const
+        const AZStd::vector<DownloadableObject>& GetDownloadQueue() const
         {
-            return m_gemNames;
+            return m_objects;
         }
 
         const QString& GetCurrentDownloadingGem() const
         {
-            if (!m_gemNames.empty())
+            if (!m_objects.empty())
             {
-                return m_gemNames[0];
+                return m_objects[0].m_objectName;
             }
             else
             {
@@ -58,15 +74,20 @@ namespace O3DE::ProjectManager
 
     signals:
         void StartGemDownload(const QString& gemName, bool downloadNow);
+        void StartProjectDownload(const QString& projectName, bool downloadNow);
+        void StartTemplateDownload(const QString& templateName, bool downloadNow);
         void Done(const QString& gemName, bool success = true);
         void GemDownloadAdded(const QString& gemName);
         void GemDownloadRemoved(const QString& gemName);
         void GemDownloadProgress(const QString& gemName, int bytesDownloaded, int totalBytes);
+        void ProjectDownloadAdded(const QString& projectName);
+        void ProjectDownloadRemoved(const QString& projectName);
+        void ProjectDownloadProgress(const QString& projectName, int bytesDownloaded, int totalBytes);
 
     private:
         DownloadWorker* m_worker;
         QThread m_workerThread;
         QWidget* m_parent;
-        AZStd::vector<QString> m_gemNames;
+        AZStd::vector<DownloadableObject> m_objects;
     };
 } // namespace O3DE::ProjectManager

+ 34 - 6
Code/Tools/ProjectManager/Source/DownloadWorker.cpp

@@ -20,26 +20,54 @@ namespace O3DE::ProjectManager
 
     void DownloadWorker::StartDownload()
     {
-        auto gemDownloadProgress = [=](int bytesDownloaded, int totalBytes)
+        auto objectDownloadProgress = [=](int bytesDownloaded, int totalBytes)
         {
             emit UpdateProgress(bytesDownloaded, totalBytes);
         };
-        AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> gemInfoResult =
-            PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress, /*force*/true);
+        AZ::Outcome<void, AZStd::pair<AZStd::string, AZStd::string>> objectInfoResult;
+        if (m_downloadType == DownloadController::DownloadObjectType::Gem)
+        {
+            objectInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_objectName, objectDownloadProgress, /*force*/ true);
+        }
+        else if (m_downloadType == DownloadController::DownloadObjectType::Project)
+        {
+            objectInfoResult = PythonBindingsInterface::Get()->DownloadProject(m_objectName, objectDownloadProgress, /*force*/ true);
+        }
 
-        if (gemInfoResult.IsSuccess())
+        if (objectInfoResult.IsSuccess())
         {
             emit Done("", "");
         }
         else
         {
-            emit Done(gemInfoResult.GetError().first.c_str(), gemInfoResult.GetError().second.c_str());
+            emit Done(objectInfoResult.GetError().first.c_str(), objectInfoResult.GetError().second.c_str());
         }
     }
 
     void DownloadWorker::SetGemToDownload(const QString& gemName, bool downloadNow)
     {
-        m_gemName = gemName;
+        m_objectName = gemName;
+        m_downloadType = DownloadController::DownloadObjectType::Gem;
+        if (downloadNow)
+        {
+            StartDownload();
+        }
+    }
+
+    void DownloadWorker::SetProjectToDownload(const QString& projectName, bool downloadNow)
+    {
+        m_objectName = projectName;
+        m_downloadType = DownloadController::DownloadObjectType::Project;
+        if (downloadNow)
+        {
+            StartDownload();
+        }
+    }
+
+    void DownloadWorker::SetTemplateToDownload(const QString& templateName, bool downloadNow)
+    {
+        m_objectName = templateName;
+        m_downloadType = DownloadController::DownloadObjectType::Template;
         if (downloadNow)
         {
             StartDownload();

+ 6 - 1
Code/Tools/ProjectManager/Source/DownloadWorker.h

@@ -11,6 +11,8 @@
 #include <AzCore/Outcome/Outcome.h>
 #endif
 
+#include <DownloadController.h>
+
 QT_FORWARD_DECLARE_CLASS(QProcess)
 
 namespace O3DE::ProjectManager
@@ -29,6 +31,8 @@ namespace O3DE::ProjectManager
     public slots:
         void StartDownload();
         void SetGemToDownload(const QString& gemName, bool downloadNow = true);
+        void SetProjectToDownload(const QString& projectName, bool downloadNow = true);
+        void SetTemplateToDownload(const QString& templateName, bool downloadNow = true);
 
     signals:
         void UpdateProgress(int bytesDownloaded, int totalBytes);
@@ -36,6 +40,7 @@ namespace O3DE::ProjectManager
 
     private:
 
-        QString m_gemName;
+        QString m_objectName;
+        DownloadController::DownloadObjectType m_downloadType;
     };
 } // namespace O3DE::ProjectManager

+ 7 - 4
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp

@@ -219,11 +219,14 @@ namespace O3DE::ProjectManager
         else
         {
             // Setup gem download rows for gems that are already in the queue
-            const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
+            const AZStd::vector<DownloadController::DownloadableObject>& downloadQueue = m_downloadController->GetDownloadQueue();
 
-            for (const QString& gemName : downloadQueue)
+            for (const DownloadController::DownloadableObject& o3deObject : downloadQueue)
             {
-                GemDownloadAdded(gemName);
+                if (o3deObject.m_objectType == DownloadController::DownloadObjectType::Gem)
+                {
+                    GemDownloadAdded(o3deObject.m_objectName);
+                }
             }
         }
 
@@ -263,7 +266,7 @@ namespace O3DE::ProjectManager
 
         m_downloadingListWidget->layout()->addWidget(newGemDownloadWidget);
 
-        const AZStd::vector<QString>& downloadQueue = m_downloadController->GetDownloadQueue();
+        const AZStd::vector<DownloadController::DownloadableObject>& downloadQueue = m_downloadController->GetDownloadQueue();
         QLabel* numDownloads = m_downloadingListWidget->findChild<QLabel*>("NumDownloadsInProgressLabel");
         numDownloads->setText(QString("%1 %2")
                                   .arg(downloadQueue.size())

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

@@ -36,7 +36,7 @@
 
 namespace O3DE::ProjectManager
 {
-    GemCatalogScreen::GemCatalogScreen(bool readOnly, QWidget* parent)
+    GemCatalogScreen::GemCatalogScreen(bool readOnly, QWidget* parent, DownloadController* downloadController)
         : ScreenWidget(parent)
         , m_readOnly(readOnly)
     {
@@ -58,7 +58,7 @@ namespace O3DE::ProjectManager
         vLayout->setSpacing(0);
         setLayout(vLayout);
 
-        m_downloadController = new DownloadController();
+        m_downloadController = downloadController;
 
         m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxyModel, m_downloadController);
         vLayout->addWidget(m_headerWidget);

+ 1 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h

@@ -34,7 +34,7 @@ namespace O3DE::ProjectManager
         : public ScreenWidget
     {
     public:
-        explicit GemCatalogScreen(bool readOnly = false, QWidget* parent = nullptr);
+        explicit GemCatalogScreen(bool readOnly = false, QWidget* parent = nullptr, DownloadController* downloadController = nullptr);
         ~GemCatalogScreen() = default;
         ProjectManagerScreen GetScreenEnum() override;
 

+ 63 - 0
Code/Tools/ProjectManager/Source/ProjectButtonWidget.cpp

@@ -89,6 +89,13 @@ namespace O3DE::ProjectManager
         m_warningIcon->setVisible(false);
         horizontalWarningMessageLayout->addWidget(m_warningIcon);
 
+        m_cloudIcon = new QLabel(this);
+        m_cloudIcon->setObjectName("projectCloudIconOverlay");
+        m_cloudIcon->setPixmap(QIcon(":/Cloud.svg").pixmap(30, 70));
+        m_cloudIcon->setAlignment(Qt::AlignCenter);
+        m_cloudIcon->setVisible(false);
+        horizontalWarningMessageLayout->addWidget(m_cloudIcon);
+
         horizontalWarningMessageLayout->addSpacing(15);
 
         verticalMessageLayout->addLayout(horizontalWarningMessageLayout);
@@ -125,6 +132,11 @@ namespace O3DE::ProjectManager
         m_buildingAnimation->movie()->start();
         verticalCenterLayout->addWidget(m_buildingAnimation);
 
+        m_downloadProgessBar = new QProgressBar(this);
+        m_downloadProgessBar->setObjectName("DownloadProgressBar");
+        m_downloadProgessBar->setValue(0);
+        verticalCenterLayout->addWidget(m_buildingAnimation);
+
         m_projectOverlayLayout->addWidget(middleWidget);
 
         QWidget* bottomWidget = new QWidget();
@@ -186,6 +198,11 @@ namespace O3DE::ProjectManager
         return m_warningIcon;
     }
 
+    QLabel* LabelButton::GetCloudIcon()
+    {
+        return m_cloudIcon;
+    }
+
     QSpacerItem* LabelButton::GetWarningSpacer()
     {
         return m_warningSpacer;
@@ -221,6 +238,10 @@ namespace O3DE::ProjectManager
         return m_darkenOverlay;
     }
 
+    QProgressBar* LabelButton::GetProgressBar()
+    {
+        return m_downloadProgessBar;
+    }
 
     ProjectButton::ProjectButton(const ProjectInfo& projectInfo, QWidget* parent)
         : QFrame(parent)
@@ -353,6 +374,12 @@ namespace O3DE::ProjectManager
         case ProjectButtonState::BuildFailed:
             ShowBuildFailedState();
             break;
+        case ProjectButtonState::NotDownloaded:
+            ShowNotDownloadedState();
+            break;
+        case ProjectButtonState::Downloading:
+            ShowDownloadingState();
+            break;
         }
     }
 
@@ -415,6 +442,37 @@ namespace O3DE::ProjectManager
         ShowWarning(tr("Failed to build"));
     }
 
+    void ProjectButton::ShowNotDownloadedState()
+    {
+        HideContextualLabelButtonWidgets();
+        SetLaunchingEnabled(false);
+        SetProjectBuilding(false);
+
+        m_projectImageLabel->GetCloudIcon()->setVisible(true);
+        m_projectImageLabel->GetWarningSpacer()->changeSize(0, 0, QSizePolicy::Expanding, QSizePolicy::Fixed);
+        QPushButton* projectActionButton = m_projectImageLabel->GetActionButton();
+        projectActionButton->setVisible(true);
+        projectActionButton->setText(tr("Download Project"));
+        projectActionButton->setMenu(nullptr);
+    }
+
+    void ProjectButton::ShowDownloadingState()
+    {
+        HideContextualLabelButtonWidgets();
+        SetLaunchingEnabled(false);
+        SetProjectBuilding(false);
+        m_projectImageLabel->GetCloudIcon()->setVisible(true);
+        m_projectImageLabel->GetWarningSpacer()->changeSize(0, 0, QSizePolicy::Expanding, QSizePolicy::Fixed);
+        QPushButton* projectActionButton = m_projectImageLabel->GetActionButton();
+        projectActionButton->setVisible(true);
+        projectActionButton->setText(tr("Cancel Download"));
+        projectActionButton->setMenu(nullptr);
+
+        // Show progress bar
+        m_projectImageLabel->GetProgressBar()->setVisible(true);
+        m_projectImageLabel->GetProgressBar()->setValue(30);
+    }
+
     void ProjectButton::SetProjectButtonAction(const QString& text, AZStd::function<void()> lambda)
     {
         QPushButton* projectActionButton;
@@ -450,6 +508,11 @@ namespace O3DE::ProjectManager
         m_projectInfo.m_logUrl = logUrl;
     }
 
+    void ProjectButton::SetProgressBarPercentage(const float percent)
+    {
+        m_projectImageLabel->GetProgressBar()->setValue(percent*100);
+    }
+
     void ProjectButton::SetContextualText(const QString& text)
     {
         if (m_currentState == ProjectButtonState::Building)

+ 13 - 1
Code/Tools/ProjectManager/Source/ProjectButtonWidget.h

@@ -40,6 +40,7 @@ namespace O3DE::ProjectManager
         QLabel* GetMessageLabel();
         QLabel* GetSubMessageLabel();
         QLabel* GetWarningIcon();
+        QLabel* GetCloudIcon();
         QSpacerItem* GetWarningSpacer();
         QLabel* GetBuildingAnimationLabel();
         QPushButton* GetOpenEditorButton();
@@ -47,6 +48,7 @@ namespace O3DE::ProjectManager
         QPushButton* GetActionCancelButton();
         QPushButton* GetShowLogsButton();
         QLabel* GetDarkenOverlay();
+        QProgressBar* GetProgressBar();
 
     public slots:
         void mousePressEvent(QMouseEvent* event) override;
@@ -65,7 +67,10 @@ namespace O3DE::ProjectManager
         QLabel* m_warningIcon = nullptr;
         QSpacerItem* m_warningSpacer = nullptr;
 
+        QLabel* m_cloudIcon = nullptr;
+
         QLabel* m_buildingAnimation = nullptr;
+        QProgressBar* m_downloadProgessBar = nullptr;
 
         QPushButton* m_openEditorButton = nullptr;
         QPushButton* m_actionButton = nullptr;
@@ -79,7 +84,10 @@ namespace O3DE::ProjectManager
         Launching,
         NeedsToBuild,
         Building,
-        BuildFailed
+        BuildFailed,
+        NotDownloaded,
+        Downloading,
+        DownloadFailed
     };
 
     class ProjectButton
@@ -98,6 +106,8 @@ namespace O3DE::ProjectManager
         void SetProjectButtonAction(const QString& text, AZStd::function<void()> lambda);
         void SetBuildLogsLink(const QUrl& logUrl);
 
+        void SetProgressBarPercentage(const float percent);
+
         void SetContextualText(const QString& text);
 
         LabelButton* GetLabelButton();
@@ -124,6 +134,8 @@ namespace O3DE::ProjectManager
         void ShowBuildRequiredState();
         void ShowBuildingState();
         void ShowBuildFailedState();
+        void ShowNotDownloadedState();
+        void ShowDownloadingState();
         void ShowMessage(const QString& message = {}, const QString& submessage = {});
         void ShowWarning(const QString& warning = {});
         void ShowBuildButton();

+ 2 - 2
Code/Tools/ProjectManager/Source/ProjectGemCatalogScreen.cpp

@@ -20,8 +20,8 @@
 
 namespace O3DE::ProjectManager
 {
-    ProjectGemCatalogScreen::ProjectGemCatalogScreen(QWidget* parent)
-        : GemCatalogScreen(/*readOnly = */ false, parent)
+    ProjectGemCatalogScreen::ProjectGemCatalogScreen(QWidget* parent, DownloadController* downloadController)
+        : GemCatalogScreen(/*readOnly = */ false, parent, downloadController)
     {
 
     }

+ 3 - 1
Code/Tools/ProjectManager/Source/ProjectGemCatalogScreen.h

@@ -13,12 +13,14 @@
 
 namespace O3DE::ProjectManager
 {
+    QT_FORWARD_DECLARE_CLASS(DownloadController);
+
     //! A wrapper for a GemCatalogScreen that shows what gems are active in a project 
     class ProjectGemCatalogScreen
         : public GemCatalogScreen
     {
     public:
-        explicit ProjectGemCatalogScreen(QWidget* parent = nullptr);
+        explicit ProjectGemCatalogScreen(QWidget* parent = nullptr, DownloadController* downloadController = nullptr);
         ~ProjectGemCatalogScreen() = default;
 
         ProjectManagerScreen GetScreenEnum() override;

+ 2 - 0
Code/Tools/ProjectManager/Source/ProjectInfo.h

@@ -55,6 +55,8 @@ namespace O3DE::ProjectManager
         QString m_newPreviewImagePath;
         QString m_newBackgroundImagePath;
 
+        bool m_remote = false;
+
         // Used in project creation
         bool m_needsBuild = false; //! Does this project need to be built
         bool m_buildFailed = false;

+ 4 - 1
Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp

@@ -8,6 +8,7 @@
 
 #include <ProjectManagerWindow.h>
 #include <ScreensCtrl.h>
+#include <DownloadController.h>
 
 namespace O3DE::ProjectManager
 {
@@ -16,7 +17,9 @@ namespace O3DE::ProjectManager
     {
         setWindowTitle(tr("O3DE Project Manager"));
 
-        ScreensCtrl* screensCtrl = new ScreensCtrl();
+        m_downloadController = new DownloadController();
+
+        ScreensCtrl* screensCtrl = new ScreensCtrl(nullptr, m_downloadController);
 
         // currently the tab order on the home page is based on the order of this list
         QVector<ProjectManagerScreen> screenEnums =

+ 5 - 0
Code/Tools/ProjectManager/Source/ProjectManagerWindow.h

@@ -15,6 +15,8 @@
 
 namespace O3DE::ProjectManager
 {
+    QT_FORWARD_DECLARE_CLASS(DownloadController);
+
     class ProjectManagerWindow
         : public QMainWindow
     {
@@ -23,6 +25,9 @@ namespace O3DE::ProjectManager
     public:
         explicit ProjectManagerWindow(QWidget* parent, const AZ::IO::PathView& projectPath,
             ProjectManagerScreen startScreen = ProjectManagerScreen::Projects);
+
+    private:
+        DownloadController* m_downloadController = nullptr;
     };
 
 } // namespace O3DE::ProjectManager

+ 85 - 2
Code/Tools/ProjectManager/Source/ProjectsScreen.cpp

@@ -16,7 +16,9 @@
 #include <ScreensCtrl.h>
 #include <SettingsInterface.h>
 #include <AddRemoteProjectDialog.h>
+#include <DownloadController.h>
 
+#include <AzCore/std/algorithm.h>
 #include <AzQtComponents/Components/FlowLayout.h>
 #include <AzCore/Platform.h>
 #include <AzCore/IO/SystemFile.h>
@@ -49,7 +51,7 @@
 
 namespace O3DE::ProjectManager
 {
-    ProjectsScreen::ProjectsScreen(QWidget* parent)
+    ProjectsScreen::ProjectsScreen(QWidget* parent, DownloadController* downloadController)
         : ScreenWidget(parent)
     {
         QVBoxLayout* vLayout = new QVBoxLayout();
@@ -81,6 +83,10 @@ namespace O3DE::ProjectManager
                     foundButton->setFocus();
                 }
             });
+
+        m_downloadController = downloadController;
+        connect(m_downloadController, &DownloadController::Done, this, &ProjectsScreen::HandleDownloadResult);
+        connect(m_downloadController, &DownloadController::ProjectDownloadProgress, this, &ProjectsScreen::HandleDownloadProgress);
     }
 
     ProjectsScreen::~ProjectsScreen() = default;
@@ -228,6 +234,29 @@ namespace O3DE::ProjectManager
         if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
         {
             QVector<ProjectInfo> projectsVector = projectsResult.GetValue();
+
+            // additional
+            auto remoteProjectsResult = PythonBindingsInterface::Get()->GetProjectsForAllRepos();
+            if (remoteProjectsResult.IsSuccess() && !remoteProjectsResult.GetValue().isEmpty())
+            {
+                QVector<ProjectInfo>& remoteProjects = remoteProjectsResult.GetValue();
+                for (ProjectInfo& remoteProject : remoteProjects)
+                {
+                    auto rem = AZStd::find_if(
+                        projectsVector.begin(),
+                        projectsVector.end(),
+                        [remoteProject](const ProjectInfo& value)
+                        {
+                            return remoteProject.m_id == value.m_id;
+                        });
+                    if (rem == projectsVector.end())
+                    {
+                        projectsVector.append(remoteProject);
+                    }
+                }
+                
+            }
+
             // If a project path is in this set then the button for it will be kept
             QSet<QString> keepProject;
             for (const ProjectInfo& project : projectsVector)
@@ -319,6 +348,17 @@ namespace O3DE::ProjectManager
                     {
                         currentButton->SetState(ProjectButtonState::NeedsToBuild);
                     }
+
+                    if (project.m_remote)
+                    {
+                        currentButton->SetState(ProjectButtonState::NotDownloaded);
+                        currentButton->SetProjectButtonAction(
+                            tr("Download Project"),
+                            [this, project]
+                            {
+                                m_downloadController->AddProjectDownload(project.m_projectName);
+                            });
+                    }
                 }
             }
 
@@ -444,7 +484,7 @@ namespace O3DE::ProjectManager
     void ProjectsScreen::HandleAddRemoteProjectButton()
     {
         AddRemoteProjectDialog* addRemoteProjectDialog = new AddRemoteProjectDialog(this);
-
+        connect(addRemoteProjectDialog, &AddRemoteProjectDialog::StartObjectDownload, this, &ProjectsScreen::StartProjectDownload);
         if (addRemoteProjectDialog->exec() == QDialog::DialogCode::Accepted)
         {
             QString repoUri = addRemoteProjectDialog->GetRepoPath();
@@ -631,6 +671,49 @@ namespace O3DE::ProjectManager
         ResetProjectsContent();
     }
 
+    void ProjectsScreen::StartProjectDownload(const QString& projectName)
+    {
+        m_downloadController->AddProjectDownload(projectName);
+
+        const auto valueList = m_projectButtons.values();
+        auto foundButton = AZStd::find_if(
+            valueList.begin(),
+            valueList.end(),
+            [projectName](const ProjectButton* projectButton)
+            {
+                return (projectButton->GetProjectInfo().m_projectName == projectName);
+            });
+
+        if (foundButton != valueList.end())
+        {
+            (*foundButton)->SetState(ProjectButtonState::Downloading);
+        }
+    }
+
+    void ProjectsScreen::HandleDownloadResult(const QString& /*projectName*/, bool /*succeeded*/)
+    {
+        ResetProjectsContent();
+    }
+
+    void ProjectsScreen::HandleDownloadProgress(const QString& projectName, int bytesDownloaded, int totalBytes)
+    {
+        //Find button for project name
+        const auto valueList = m_projectButtons.values();
+        auto foundButton = AZStd::find_if(
+            valueList.begin(),
+            valueList.end(),
+            [projectName](const ProjectButton* projectButton)
+            {
+                return (projectButton->GetProjectInfo().m_projectName == projectName);
+            });
+
+        if (foundButton != valueList.end())
+        {
+            float percentage = static_cast<float>(bytesDownloaded) / totalBytes;
+            (*foundButton)->SetProgressBarPercentage(percentage);
+        }
+    }
+
     void ProjectsScreen::NotifyCurrentScreen()
     {
         if (ShouldDisplayFirstTimeContent())

+ 8 - 2
Code/Tools/ProjectManager/Source/ProjectsScreen.h

@@ -14,7 +14,7 @@
 #include <QQueue>
 #endif
 
-// #define ADD_REMOTE_PROJECT_ENABLED
+#define ADD_REMOTE_PROJECT_ENABLED
 
 QT_FORWARD_DECLARE_CLASS(QPaintEvent)
 QT_FORWARD_DECLARE_CLASS(QFrame)
@@ -27,13 +27,14 @@ namespace O3DE::ProjectManager
 {
     QT_FORWARD_DECLARE_CLASS(ProjectBuilderController);
     QT_FORWARD_DECLARE_CLASS(ProjectButton);
+    QT_FORWARD_DECLARE_CLASS(DownloadController);
 
     class ProjectsScreen
         : public ScreenWidget
     {
 
     public:
-        explicit ProjectsScreen(QWidget* parent = nullptr);
+        explicit ProjectsScreen(QWidget* parent = nullptr, DownloadController* downloadController = nullptr);
         ~ProjectsScreen();
 
         ProjectManagerScreen GetScreenEnum() override;
@@ -59,6 +60,10 @@ namespace O3DE::ProjectManager
         void QueueBuildProject(const ProjectInfo& projectInfo);
         void UnqueueBuildProject(const ProjectInfo& projectInfo);
 
+        void StartProjectDownload(const QString& projectName);
+        void HandleDownloadProgress(const QString& projectName, int bytesDownloaded, int totalBytes);
+        void HandleDownloadResult(const QString& projectName, bool succeeded);
+
         void ProjectBuildDone(bool success = true);
 
         void paintEvent(QPaintEvent* event) override;
@@ -93,6 +98,7 @@ namespace O3DE::ProjectManager
         QList<ProjectInfo> m_requiresBuild;
         QQueue<ProjectInfo> m_buildQueue;
         ProjectBuilderController* m_currentBuilder = nullptr;
+        DownloadController* m_downloadController = nullptr;
 
         inline constexpr static int s_contentMargins = 80;
         inline constexpr static int s_spacerSize = 20;

+ 151 - 1
Code/Tools/ProjectManager/Source/PythonBindings.cpp

@@ -920,6 +920,78 @@ namespace O3DE::ProjectManager
         }
     }
 
+    AZ::Outcome<QVector<ProjectInfo>, AZStd::string> PythonBindings::GetProjectsForRepo(const QString& repoUri)
+    {
+        QVector<ProjectInfo> projects;
+
+        AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
+            [&]
+            {
+                auto pyUri = QString_To_Py_String(repoUri);
+                auto projectPaths = m_repo.attr("get_project_json_paths_from_cached_repo")(pyUri);
+
+                if (pybind11::isinstance<pybind11::set>(projectPaths))
+                {
+                    for (auto path : projectPaths)
+                    {
+                        //auto cpath = Py_To_String(path);
+                        //AZ::Outcome<ProjectInfo> projectOutcome = GetProject(QString(cpath));
+                        //if (projectOutcome.IsSuccess())
+                        {
+                            //ProjectInfo projectInfo = projectOutcome.GetValue();
+                            ProjectInfo projectInfo = ProjectInfoFromPath(path);
+                            projectInfo.m_remote = true;
+                            // projectInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
+                            projects.push_back(projectInfo);
+                        }
+                    }
+                }
+            });
+
+        if (!result.IsSuccess())
+        {
+            return AZ::Failure(result.GetError());
+        }
+
+        return AZ::Success(AZStd::move(projects));
+    }
+
+    AZ::Outcome<QVector<ProjectInfo>, AZStd::string> PythonBindings::GetProjectsForAllRepos()
+    {
+        QVector<ProjectInfo> projectInfos;
+        AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
+            [&]
+            {
+                auto projectPaths = m_repo.attr("get_project_json_paths_from_all_cached_repos")();
+
+                if (pybind11::isinstance<pybind11::set>(projectPaths))
+                {
+                    for (auto path : projectPaths)
+                    {
+                        //AZ::Outcome<ProjectInfo> projectOutcome = GetProject(QString(Py_To_String(path)));
+                        //if (projectOutcome.IsSuccess())
+                        {
+                           // ProjectInfo projectInfo = projectOutcome.GetValue();
+                            ProjectInfo projectInfo = ProjectInfoFromPath(path);
+                           projectInfo.m_remote = true;
+                            // projectInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
+                            projectInfos.push_back(projectInfo);
+                        }
+                        //GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
+                        //gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
+                        //projectInfos.push_back(gemInfo);
+                    }
+                }
+            });
+
+        if (!result.IsSuccess())
+        {
+            return AZ::Failure(result.GetError());
+        }
+
+        return AZ::Success(AZStd::move(projectInfos));
+    }
+
     AZ::Outcome<void, AZStd::string> PythonBindings::AddGemToProject(const QString& gemPath, const QString& projectPath)
     {
         return ExecuteWithLockErrorHandling([&]
@@ -1282,7 +1354,7 @@ namespace O3DE::ProjectManager
                 {
                     for (auto path : gemPaths)
                     {
-                        GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
+                        GemInfo gemInfo = GemInfoFromPath(path, pybind11::none()); 
                         gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
                         gemInfos.push_back(gemInfo);
                     }
@@ -1337,6 +1409,84 @@ namespace O3DE::ProjectManager
         return AZ::Success();
     }
 
+    IPythonBindings::DetailedOutcome PythonBindings::DownloadProject(
+        const QString& projectName, std::function<void(int, int)> projectProgressCallback, bool force)
+    {
+        // This process is currently limited to download a single gem at a time.
+        bool downloadSucceeded = false;
+
+        m_requestCancelDownload = false;
+        auto result = ExecuteWithLockErrorHandling(
+            [&]
+            {
+                auto downloadResult = m_download.attr("download_project")(
+                    QString_To_Py_String(projectName), // gem name
+                    pybind11::none(), // destination path
+                    false, // skip auto register
+                    force, // force overwrite
+                    pybind11::cpp_function(
+                        [this, projectProgressCallback](int bytesDownloaded, int totalBytes)
+                        {
+                            projectProgressCallback(bytesDownloaded, totalBytes);
+
+                            return m_requestCancelDownload;
+                        }) // Callback for download progress and cancelling
+                );
+                downloadSucceeded = (downloadResult.cast<int>() == 0);
+            });
+
+        if (!result.IsSuccess())
+        {
+            IPythonBindings::ErrorPair pythonRunError(result.GetError(), result.GetError());
+            return AZ::Failure<IPythonBindings::ErrorPair>(AZStd::move(pythonRunError));
+        }
+        else if (!downloadSucceeded)
+        {
+            return AZ::Failure<IPythonBindings::ErrorPair>(GetErrorPair());
+        }
+
+        return AZ::Success();
+    }
+
+    IPythonBindings::DetailedOutcome PythonBindings::DownloadTemplate(
+        const QString& templateName, std::function<void(int, int)> templateProgressCallback, bool force)
+    {
+        // This process is currently limited to download a single gem at a time.
+        bool downloadSucceeded = false;
+
+        m_requestCancelDownload = false;
+        auto result = ExecuteWithLockErrorHandling(
+            [&]
+            {
+                auto downloadResult = m_download.attr("download_template")(
+                    QString_To_Py_String(templateName), // gem name
+                    pybind11::none(), // destination path
+                    false, // skip auto register
+                    force, // force overwrite
+                    pybind11::cpp_function(
+                        [this, templateProgressCallback](int bytesDownloaded, int totalBytes)
+                        {
+                            templateProgressCallback(bytesDownloaded, totalBytes);
+
+                            return m_requestCancelDownload;
+                        }) // Callback for download progress and cancelling
+                );
+                downloadSucceeded = (downloadResult.cast<int>() == 0);
+            });
+
+        if (!result.IsSuccess())
+        {
+            IPythonBindings::ErrorPair pythonRunError(result.GetError(), result.GetError());
+            return AZ::Failure<IPythonBindings::ErrorPair>(AZStd::move(pythonRunError));
+        }
+        else if (!downloadSucceeded)
+        {
+            return AZ::Failure<IPythonBindings::ErrorPair>(GetErrorPair());
+        }
+
+        return AZ::Success();
+    }
+
     void PythonBindings::CancelDownload()
     {
         m_requestCancelDownload = true;

+ 6 - 0
Code/Tools/ProjectManager/Source/PythonBindings.h

@@ -50,6 +50,8 @@ namespace O3DE::ProjectManager
         AZ::Outcome<ProjectInfo> CreateProject(const QString& projectTemplatePath, const ProjectInfo& projectInfo, bool registerProject = true) override;
         AZ::Outcome<ProjectInfo> GetProject(const QString& path) override;
         AZ::Outcome<QVector<ProjectInfo>> GetProjects() override;
+        AZ::Outcome<QVector<ProjectInfo>, AZStd::string> GetProjectsForRepo(const QString& repoUri) override;
+        AZ::Outcome<QVector<ProjectInfo>, AZStd::string> GetProjectsForAllRepos() override;
         bool AddProject(const QString& path) override;
         bool RemoveProject(const QString& path) override;
         AZ::Outcome<void, AZStd::string> UpdateProject(const ProjectInfo& projectInfo) override;
@@ -70,6 +72,10 @@ namespace O3DE::ProjectManager
         AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos() override;
         DetailedOutcome DownloadGem(
             const QString& gemName, std::function<void(int, int)> gemProgressCallback, bool force = false) override;
+        DetailedOutcome DownloadProject(
+            const QString& projectName, std::function<void(int, int)> projectProgressCallback, bool force = false) override;
+        DetailedOutcome DownloadTemplate(
+            const QString& templateName, std::function<void(int, int)> templateProgressCallback, bool force = false) override;
         void CancelDownload() override;
         bool IsGemUpdateAvaliable(const QString& gemName, const QString& lastUpdated) override;
 

+ 33 - 0
Code/Tools/ProjectManager/Source/PythonBindingsInterface.h

@@ -143,6 +143,19 @@ namespace O3DE::ProjectManager
          * @return an outcome with ProjectInfos on success 
          */
         virtual AZ::Outcome<QVector<ProjectInfo>> GetProjects() = 0;
+
+        /**
+         * Gathers all projects from the provided repo
+         * @param repoUri the absolute filesystem path or url to the gem repo.
+         * @return A list of project infos or an error string on failure.
+         */
+        virtual AZ::Outcome<QVector<ProjectInfo>, AZStd::string> GetProjectsForRepo(const QString& repoUri) = 0;
+
+        /**
+         * Gathers all projects from all registered repos
+         * @return A list of project infos or an error string on failure.
+         */
+        virtual AZ::Outcome<QVector<ProjectInfo>, AZStd::string> GetProjectsForAllRepos() = 0;
         
         /**
          * Adds existing project on disk
@@ -253,6 +266,26 @@ namespace O3DE::ProjectManager
         virtual DetailedOutcome DownloadGem(
             const QString& gemName, std::function<void(int, int)> gemProgressCallback, bool force = false) = 0;
 
+        /**
+         * Downloads and registers a Gem.
+         * @param gemName the name of the Gem to download.
+         * @param gemProgressCallback a callback function that is called with an int percentage download value.
+         * @param force should we forcibly overwrite the old version of the gem.
+         * @return an outcome with a pair of string error and detailed messages on failure.
+         */
+        virtual DetailedOutcome DownloadProject(
+            const QString& projectName, std::function<void(int, int)> projectProgressCallback, bool force = false) = 0;
+
+        /**
+         * Downloads and registers a Gem.
+         * @param gemName the name of the Gem to download.
+         * @param gemProgressCallback a callback function that is called with an int percentage download value.
+         * @param force should we forcibly overwrite the old version of the gem.
+         * @return an outcome with a pair of string error and detailed messages on failure.
+         */
+        virtual DetailedOutcome DownloadTemplate(
+            const QString& templateName, std::function<void(int, int)> templateProgressCallback, bool force = false) = 0;
+
         /**
          * Cancels the current download.
          */

+ 5 - 4
Code/Tools/ProjectManager/Source/ScreenFactory.cpp

@@ -18,10 +18,11 @@
 #include <EngineScreenCtrl.h>
 #include <EngineSettingsScreen.h>
 #include <GemRepo/GemRepoScreen.h>
+#include <DownloadController.h>
 
 namespace O3DE::ProjectManager
 {
-    ScreenWidget* BuildScreen(QWidget* parent, ProjectManagerScreen screen)
+    ScreenWidget* BuildScreen(QWidget* parent, ProjectManagerScreen screen, DownloadController* downloadController)
     {
         ScreenWidget* newScreen;
 
@@ -34,13 +35,13 @@ namespace O3DE::ProjectManager
             newScreen = new NewProjectSettingsScreen(parent);
             break;
         case (ProjectManagerScreen::GemCatalog):
-            newScreen = new GemCatalogScreen(parent);
+            newScreen = new GemCatalogScreen(true, parent, downloadController);
             break;
         case (ProjectManagerScreen::ProjectGemCatalog):
-            newScreen = new ProjectGemCatalogScreen(parent);
+            newScreen = new ProjectGemCatalogScreen(parent, downloadController);
             break;
         case (ProjectManagerScreen::Projects):
-            newScreen = new ProjectsScreen(parent);
+            newScreen = new ProjectsScreen(parent, downloadController);
             break;
         case (ProjectManagerScreen::UpdateProject):
             newScreen = new UpdateProjectCtrl(parent);

+ 3 - 2
Code/Tools/ProjectManager/Source/ScreenFactory.h

@@ -13,7 +13,8 @@
 
 namespace O3DE::ProjectManager
 {
-    class ScreenWidget;
+    QT_FORWARD_DECLARE_CLASS(ScreenWidget);
+    QT_FORWARD_DECLARE_CLASS(DownloadController);
 
-    ScreenWidget* BuildScreen(QWidget* parent = nullptr, ProjectManagerScreen screen = ProjectManagerScreen::Empty);
+    ScreenWidget* BuildScreen(QWidget* parent = nullptr, ProjectManagerScreen screen = ProjectManagerScreen::Empty, DownloadController* downloadController = nullptr);
 } // namespace O3DE::ProjectManager

+ 4 - 2
Code/Tools/ProjectManager/Source/ScreensCtrl.cpp

@@ -16,7 +16,7 @@
 
 namespace O3DE::ProjectManager
 {
-    ScreensCtrl::ScreensCtrl(QWidget* parent)
+    ScreensCtrl::ScreensCtrl(QWidget* parent, DownloadController* downloadController)
         : QWidget(parent)
     {
         setObjectName("ScreensCtrl");
@@ -33,6 +33,8 @@ namespace O3DE::ProjectManager
         m_tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus);
         m_screenStack->addWidget(m_tabWidget);
         connect(m_tabWidget, &QTabWidget::currentChanged, this, &ScreensCtrl::TabChanged);
+
+        m_downloadController = downloadController;
     }
 
     void ScreensCtrl::BuildScreens(QVector<ProjectManagerScreen> screens)
@@ -167,7 +169,7 @@ namespace O3DE::ProjectManager
         DeleteScreen(screen);
 
         // Add new screen
-        ScreenWidget* newScreen = BuildScreen(this, screen);
+        ScreenWidget* newScreen = BuildScreen(this, screen, m_downloadController);
         if (newScreen->IsTab())
         {
             if (tabIndex > -1)

+ 3 - 1
Code/Tools/ProjectManager/Source/ScreensCtrl.h

@@ -20,6 +20,7 @@ QT_FORWARD_DECLARE_CLASS(QTabWidget)
 namespace O3DE::ProjectManager
 {
     QT_FORWARD_DECLARE_CLASS(ScreenWidget);
+    QT_FORWARD_DECLARE_CLASS(DownloadController);
 
     class ScreensCtrl
         : public QWidget
@@ -27,7 +28,7 @@ namespace O3DE::ProjectManager
         Q_OBJECT
 
     public:
-        explicit ScreensCtrl(QWidget* parent = nullptr);
+        explicit ScreensCtrl(QWidget* parent = nullptr, DownloadController* downloadController = nullptr);
         ~ScreensCtrl() = default;
 
         void BuildScreens(QVector<ProjectManagerScreen> screens);
@@ -55,6 +56,7 @@ namespace O3DE::ProjectManager
         QHash<ProjectManagerScreen, ScreenWidget*> m_screenMap;
         QStack<ProjectManagerScreen> m_screenVisitOrder;
         QTabWidget* m_tabWidget;
+        DownloadController* m_downloadController;
     };
 
 } // namespace O3DE::ProjectManager

+ 9 - 0
Code/Tools/ProjectManager/Source/ShowBuildLogScreen.cpp

@@ -0,0 +1,9 @@
+/*
+ * 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
+

+ 28 - 0
Code/Tools/ProjectManager/Source/ShowBuildLogScreen.h

@@ -0,0 +1,28 @@
+/*
+ * 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 <QDialog>
+#endif
+
+namespace O3DE::ProjectManager
+{
+    QT_FORWARD_DECLARE_CLASS(FormLineEditWidget)
+
+    class ShowBuildLogScreen : public QDialog
+    {
+    public:
+        explicit ShowBuildLogScreen(QWidget* parent = nullptr);
+        ~ShowBuildLogScreen() = default;
+
+    private:
+        FormLineEditWidget* m_repoPath = nullptr;
+    };
+} // namespace O3DE::ProjectManager