ProjectsScreen.cpp 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <ProjectsScreen.h>
  9. #include <ProjectManagerDefs.h>
  10. #include <ProjectButtonWidget.h>
  11. #include <PythonBindingsInterface.h>
  12. #include <PythonBindings.h>
  13. #include <ProjectUtils.h>
  14. #include <ProjectBuilderController.h>
  15. #include <ProjectExportController.h>
  16. #include <ScreensCtrl.h>
  17. #include <SettingsInterface.h>
  18. #include <AddRemoteProjectDialog.h>
  19. #include <AzCore/std/ranges/ranges_algorithm.h>
  20. #include <AzQtComponents/Components/FlowLayout.h>
  21. #include <AzCore/Platform.h>
  22. #include <AzCore/IO/SystemFile.h>
  23. #include <AzFramework/AzFramework_Traits_Platform.h>
  24. #include <AzFramework/Process/ProcessCommon.h>
  25. #include <AzFramework/Process/ProcessWatcher.h>
  26. #include <AzCore/Utils/Utils.h>
  27. #include <AzCore/std/sort.h>
  28. #include <AzCore/std/smart_ptr/unique_ptr.h>
  29. #include <QVBoxLayout>
  30. #include <QHBoxLayout>
  31. #include <QLabel>
  32. #include <QPushButton>
  33. #include <QFileDialog>
  34. #include <QMenu>
  35. #include <QListView>
  36. #include <QSpacerItem>
  37. #include <QListWidget>
  38. #include <QListWidgetItem>
  39. #include <QScrollArea>
  40. #include <QStackedWidget>
  41. #include <QFrame>
  42. #include <QIcon>
  43. #include <QPixmap>
  44. #include <QSettings>
  45. #include <QMessageBox>
  46. #include <QTimer>
  47. #include <QQueue>
  48. #include <QDir>
  49. #include <QGuiApplication>
  50. #include <QFileSystemWatcher>
  51. #include <QProcess>
  52. namespace O3DE::ProjectManager
  53. {
  54. ProjectsScreen::ProjectsScreen(DownloadController* downloadController, QWidget* parent)
  55. : ScreenWidget(parent)
  56. , m_downloadController(downloadController)
  57. {
  58. QVBoxLayout* vLayout = new QVBoxLayout();
  59. vLayout->setAlignment(Qt::AlignTop);
  60. vLayout->setContentsMargins(s_contentMargins, 0, s_contentMargins, 0);
  61. setLayout(vLayout);
  62. m_fileSystemWatcher = new QFileSystemWatcher(this);
  63. connect(m_fileSystemWatcher, &QFileSystemWatcher::fileChanged, this, &ProjectsScreen::HandleProjectFilePathChanged);
  64. m_stack = new QStackedWidget(this);
  65. m_firstTimeContent = CreateFirstTimeContent();
  66. m_stack->addWidget(m_firstTimeContent);
  67. m_projectsContent = CreateProjectsContent();
  68. m_stack->addWidget(m_projectsContent);
  69. vLayout->addWidget(m_stack);
  70. connect(static_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
  71. connect(m_downloadController, &DownloadController::Done, this, &ProjectsScreen::HandleDownloadResult);
  72. connect(m_downloadController, &DownloadController::ObjectDownloadProgress, this, &ProjectsScreen::HandleDownloadProgress);
  73. }
  74. ProjectsScreen::~ProjectsScreen() = default;
  75. QFrame* ProjectsScreen::CreateFirstTimeContent()
  76. {
  77. QFrame* frame = new QFrame(this);
  78. frame->setObjectName("firstTimeContent");
  79. {
  80. QVBoxLayout* layout = new QVBoxLayout();
  81. layout->setContentsMargins(0, 0, 0, 0);
  82. layout->setAlignment(Qt::AlignTop);
  83. frame->setLayout(layout);
  84. QLabel* titleLabel = new QLabel(tr("Ready? Set. Create!"), this);
  85. titleLabel->setObjectName("titleLabel");
  86. layout->addWidget(titleLabel);
  87. QLabel* introLabel = new QLabel(this);
  88. introLabel->setObjectName("introLabel");
  89. introLabel->setText(tr("Welcome to O3DE! Start something new by creating a project."));
  90. layout->addWidget(introLabel);
  91. QHBoxLayout* buttonLayout = new QHBoxLayout();
  92. buttonLayout->setAlignment(Qt::AlignLeft);
  93. buttonLayout->setSpacing(s_spacerSize);
  94. // use a newline to force the text up
  95. QPushButton* createProjectButton = new QPushButton(tr("Create a project\n"), this);
  96. createProjectButton->setObjectName("createProjectButton");
  97. buttonLayout->addWidget(createProjectButton);
  98. QPushButton* addProjectButton = new QPushButton(tr("Open a project\n"), this);
  99. addProjectButton->setObjectName("addProjectButton");
  100. buttonLayout->addWidget(addProjectButton);
  101. QPushButton* addRemoteProjectButton = new QPushButton(tr("Add a remote project\n"), this);
  102. addRemoteProjectButton->setObjectName("addRemoteProjectButton");
  103. buttonLayout->addWidget(addRemoteProjectButton);
  104. connect(createProjectButton, &QPushButton::clicked, this, &ProjectsScreen::HandleNewProjectButton);
  105. connect(addProjectButton, &QPushButton::clicked, this, &ProjectsScreen::HandleAddProjectButton);
  106. connect(addRemoteProjectButton, &QPushButton::clicked, this, &ProjectsScreen::HandleAddRemoteProjectButton);
  107. layout->addLayout(buttonLayout);
  108. }
  109. return frame;
  110. }
  111. QFrame* ProjectsScreen::CreateProjectsContent()
  112. {
  113. QFrame* frame = new QFrame(this);
  114. frame->setObjectName("projectsContent");
  115. {
  116. QVBoxLayout* layout = new QVBoxLayout();
  117. layout->setAlignment(Qt::AlignTop);
  118. layout->setContentsMargins(0, 0, 0, 0);
  119. frame->setLayout(layout);
  120. QFrame* header = new QFrame(frame);
  121. QHBoxLayout* headerLayout = new QHBoxLayout();
  122. {
  123. QLabel* titleLabel = new QLabel(tr("My Projects"), this);
  124. titleLabel->setObjectName("titleLabel");
  125. headerLayout->addWidget(titleLabel);
  126. QMenu* newProjectMenu = new QMenu(this);
  127. m_createNewProjectAction = newProjectMenu->addAction("Create New Project");
  128. m_addExistingProjectAction = newProjectMenu->addAction("Open Existing Project");
  129. m_addRemoteProjectAction = newProjectMenu->addAction("Add a Remote Project");
  130. connect(m_createNewProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleNewProjectButton);
  131. connect(m_addExistingProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleAddProjectButton);
  132. connect(m_addRemoteProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleAddRemoteProjectButton);
  133. QPushButton* newProjectMenuButton = new QPushButton(tr("New Project..."), this);
  134. newProjectMenuButton->setObjectName("newProjectButton");
  135. newProjectMenuButton->setMenu(newProjectMenu);
  136. newProjectMenuButton->setDefault(true);
  137. headerLayout->addWidget(newProjectMenuButton);
  138. }
  139. header->setLayout(headerLayout);
  140. layout->addWidget(header);
  141. QScrollArea* projectsScrollArea = new QScrollArea(this);
  142. QWidget* scrollWidget = new QWidget();
  143. m_projectsFlowLayout = new FlowLayout(0, s_spacerSize, s_spacerSize);
  144. scrollWidget->setLayout(m_projectsFlowLayout);
  145. projectsScrollArea->setWidget(scrollWidget);
  146. projectsScrollArea->setWidgetResizable(true);
  147. layout->addWidget(projectsScrollArea);
  148. }
  149. return frame;
  150. }
  151. ProjectButton* ProjectsScreen::CreateProjectButton(const ProjectInfo& project, const EngineInfo& engine)
  152. {
  153. ProjectButton* projectButton = new ProjectButton(project, engine, this);
  154. m_projectButtons.insert({ project.m_path.toUtf8().constData(), projectButton });
  155. m_projectsFlowLayout->addWidget(projectButton);
  156. connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
  157. connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
  158. connect(projectButton, &ProjectButton::EditProjectGems, this, &ProjectsScreen::HandleEditProjectGems);
  159. connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
  160. connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
  161. connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
  162. connect(projectButton, &ProjectButton::BuildProject, this, &ProjectsScreen::QueueBuildProject);
  163. connect(projectButton, &ProjectButton::ExportProject, this, &ProjectsScreen::QueueExportProject);
  164. connect(projectButton, &ProjectButton::OpenCMakeGUI, this,
  165. [this](const ProjectInfo& projectInfo)
  166. {
  167. AZ::Outcome result = ProjectUtils::OpenCMakeGUI(projectInfo.m_path);
  168. if (!result)
  169. {
  170. QMessageBox::critical(this, tr("Failed to open CMake GUI"), result.GetError(), QMessageBox::Ok);
  171. }
  172. });
  173. connect(projectButton, &ProjectButton::OpenAndroidProjectGenerator, this, &ProjectsScreen::HandleOpenAndroidProjectGenerator);
  174. connect(projectButton, &ProjectButton::OpenProjectExportSettings, this, &ProjectsScreen::HandleOpenProjectExportSettings);
  175. return projectButton;
  176. }
  177. void ProjectsScreen::RemoveProjectButtonsFromFlowLayout(const QVector<ProjectInfo>& projectsToKeep)
  178. {
  179. // If a project path is in this set then the button for it will be kept
  180. AZStd::unordered_set<AZ::IO::Path> keepProject;
  181. for (const ProjectInfo& project : projectsToKeep)
  182. {
  183. keepProject.insert(project.m_path.toUtf8().constData());
  184. }
  185. // Remove buttons from flow layout and delete buttons for removed projects
  186. auto projectButtonsIter = m_projectButtons.begin();
  187. while (projectButtonsIter != m_projectButtons.end())
  188. {
  189. const auto button = projectButtonsIter->second;
  190. m_projectsFlowLayout->removeWidget(button);
  191. if (!keepProject.contains(projectButtonsIter->first))
  192. {
  193. m_fileSystemWatcher->removePath(QDir::toNativeSeparators(button->GetProjectInfo().m_path + "/project.json"));
  194. button->deleteLater();
  195. projectButtonsIter = m_projectButtons.erase(projectButtonsIter);
  196. }
  197. else
  198. {
  199. ++projectButtonsIter;
  200. }
  201. }
  202. }
  203. void ProjectsScreen::UpdateIfCurrentScreen()
  204. {
  205. if (IsCurrentScreen())
  206. {
  207. UpdateWithProjects(GetAllProjects());
  208. }
  209. }
  210. void ProjectsScreen::UpdateWithProjects(const QVector<ProjectInfo>& projects)
  211. {
  212. PythonBindingsInterface::Get()->RemoveInvalidProjects();
  213. if (!projects.isEmpty())
  214. {
  215. // Remove all existing buttons before adding them back in the correct order
  216. RemoveProjectButtonsFromFlowLayout(/*projectsToKeep*/ projects);
  217. // It's more efficient to update the project engine by loading engine infos once
  218. // instead of loading them all each time we want to know what project an engine uses
  219. auto engineInfoResult = PythonBindingsInterface::Get()->GetAllEngineInfos();
  220. // Add all project buttons, restoring buttons to default state
  221. for (const ProjectInfo& project : projects)
  222. {
  223. ProjectButton* currentButton = nullptr;
  224. const AZ::IO::Path projectPath { project.m_path.toUtf8().constData() };
  225. auto projectButtonIter = m_projectButtons.find(projectPath);
  226. EngineInfo engine{};
  227. if (engineInfoResult && !project.m_enginePath.isEmpty())
  228. {
  229. AZ::IO::FixedMaxPath projectEnginePath{ project.m_enginePath.toUtf8().constData() };
  230. for (const EngineInfo& engineInfo : engineInfoResult.GetValue())
  231. {
  232. AZ::IO::FixedMaxPath enginePath{ engineInfo.m_path.toUtf8().constData() };
  233. if (enginePath == projectEnginePath)
  234. {
  235. engine = engineInfo;
  236. break;
  237. }
  238. }
  239. }
  240. if (projectButtonIter == m_projectButtons.end())
  241. {
  242. currentButton = CreateProjectButton(project, engine);
  243. m_projectButtons.insert({ projectPath, currentButton });
  244. m_fileSystemWatcher->addPath(QDir::toNativeSeparators(project.m_path + "/project.json"));
  245. }
  246. else
  247. {
  248. currentButton = projectButtonIter->second;
  249. currentButton->SetEngine(engine);
  250. currentButton->SetProject(project);
  251. currentButton->SetState(ProjectButtonState::ReadyToLaunch);
  252. }
  253. // Check whether project manager has successfully built the project
  254. AZ_Assert(currentButton, "Invalid ProjectButton");
  255. m_projectsFlowLayout->addWidget(currentButton);
  256. bool projectBuiltSuccessfully = false;
  257. SettingsInterface::Get()->GetProjectBuiltSuccessfully(projectBuiltSuccessfully, project);
  258. if (!projectBuiltSuccessfully)
  259. {
  260. currentButton->SetState(ProjectButtonState::NeedsToBuild);
  261. }
  262. if (project.m_remote)
  263. {
  264. currentButton->SetState(ProjectButtonState::NotDownloaded);
  265. currentButton->SetProjectButtonAction(
  266. tr("Download Project"),
  267. [this, currentButton, project]
  268. {
  269. m_downloadController->AddObjectDownload(project.m_projectName, "", DownloadController::DownloadObjectType::Project);
  270. currentButton->SetState(ProjectButtonState::Downloading);
  271. });
  272. }
  273. }
  274. if (m_currentBuilder)
  275. {
  276. AZ::IO::Path buildProjectPath = AZ::IO::Path(m_currentBuilder->GetProjectInfo().m_path.toUtf8().constData());
  277. if (!buildProjectPath.empty())
  278. {
  279. // Setup building button again
  280. auto buildProjectIter = m_projectButtons.find(buildProjectPath);
  281. if (buildProjectIter != m_projectButtons.end())
  282. {
  283. m_currentBuilder->SetProjectButton(buildProjectIter->second);
  284. }
  285. }
  286. }
  287. if (m_currentExporter)
  288. {
  289. AZ::IO::Path exportProjectPath = AZ::IO::Path(m_currentExporter->GetProjectInfo().m_path.toUtf8().constData());
  290. if (!exportProjectPath.empty())
  291. {
  292. //Setup export button
  293. if (auto exportProjectIter = m_projectButtons.find(exportProjectPath);
  294. exportProjectIter != m_projectButtons.end())
  295. {
  296. m_currentExporter->SetProjectButton(exportProjectIter->second);
  297. }
  298. }
  299. }
  300. // Let the user cancel builds for projects in the build queue and in the export queue
  301. for (const ProjectInfo& project : m_buildQueue)
  302. {
  303. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  304. if (projectIter != m_projectButtons.end())
  305. {
  306. projectIter->second->SetProjectButtonAction(
  307. tr("Cancel queued build"),
  308. [this, project]
  309. {
  310. UnqueueBuildProject(project);
  311. SuggestBuildProjectMsg(project, false);
  312. });
  313. }
  314. }
  315. for (const ProjectInfo& project : m_exportQueue)
  316. {
  317. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  318. if (projectIter != m_projectButtons.end())
  319. {
  320. projectIter->second->SetProjectButtonAction(
  321. tr("Cancel queued export"),
  322. [this, project]
  323. {
  324. UnqueueExportProject(project);
  325. });
  326. }
  327. }
  328. // Update the project build status if it requires building
  329. for (const ProjectInfo& project : m_requiresBuild)
  330. {
  331. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  332. if (projectIter != m_projectButtons.end())
  333. {
  334. // If project is not currently or about to build
  335. if (!m_currentBuilder || m_currentBuilder->GetProjectInfo() != project)
  336. {
  337. if (project.m_buildFailed)
  338. {
  339. projectIter->second->SetBuildLogsLink(project.m_logUrl);
  340. projectIter->second->SetState(ProjectButtonState::BuildFailed);
  341. }
  342. else
  343. {
  344. projectIter->second->SetState(ProjectButtonState::NeedsToBuild);
  345. }
  346. }
  347. }
  348. }
  349. }
  350. if (m_projectsContent)
  351. {
  352. m_stack->setCurrentWidget(m_projectsContent);
  353. }
  354. m_projectsFlowLayout->update();
  355. // Will focus whatever button it finds so the Project tab is not focused on start-up
  356. QTimer::singleShot(0, this, [this]
  357. {
  358. QPushButton* foundButton = m_stack->currentWidget()->findChild<QPushButton*>();
  359. if (foundButton)
  360. {
  361. foundButton->setFocus();
  362. }
  363. });
  364. }
  365. void ProjectsScreen::HandleProjectFilePathChanged(const QString& /*path*/)
  366. {
  367. // QFileWatcher automatically stops watching the path if it was removed so we will just refresh our view
  368. UpdateIfCurrentScreen();
  369. }
  370. ProjectManagerScreen ProjectsScreen::GetScreenEnum()
  371. {
  372. return ProjectManagerScreen::Projects;
  373. }
  374. bool ProjectsScreen::IsTab()
  375. {
  376. return true;
  377. }
  378. QString ProjectsScreen::GetTabText()
  379. {
  380. return tr("Projects");
  381. }
  382. void ProjectsScreen::paintEvent([[maybe_unused]] QPaintEvent* event)
  383. {
  384. // we paint the background here because qss does not support background cover scaling
  385. QPainter painter(this);
  386. const QSize winSize = size();
  387. const float pixmapRatio = (float)m_background.width() / m_background.height();
  388. const float windowRatio = (float)winSize.width() / winSize.height();
  389. QRect backgroundRect;
  390. if (pixmapRatio > windowRatio)
  391. {
  392. const int newWidth = (int)(winSize.height() * pixmapRatio);
  393. const int offset = (newWidth - winSize.width()) / -2;
  394. backgroundRect = QRect(offset, 0, newWidth, winSize.height());
  395. }
  396. else
  397. {
  398. const int newHeight = (int)(winSize.width() / pixmapRatio);
  399. backgroundRect = QRect(0, 0, winSize.width(), newHeight);
  400. }
  401. // Draw the background image.
  402. painter.drawPixmap(backgroundRect, m_background);
  403. // Draw a semi-transparent overlay to darken down the colors.
  404. // Use SourceOver, DestinationIn will make background transparent on Mac
  405. painter.setCompositionMode (QPainter::CompositionMode_SourceOver);
  406. const float overlayTransparency = 0.3f;
  407. painter.fillRect(backgroundRect, QColor(0, 0, 0, static_cast<int>(255.0f * overlayTransparency)));
  408. }
  409. void ProjectsScreen::HandleNewProjectButton()
  410. {
  411. emit ResetScreenRequest(ProjectManagerScreen::CreateProject);
  412. emit ChangeScreenRequest(ProjectManagerScreen::CreateProject);
  413. }
  414. void ProjectsScreen::HandleAddProjectButton()
  415. {
  416. QString title{ QObject::tr("Select Project File") };
  417. QString defaultPath;
  418. // get the default path to look for new projects in
  419. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  420. if (engineInfoResult.IsSuccess())
  421. {
  422. defaultPath = engineInfoResult.GetValue().m_defaultProjectsFolder;
  423. }
  424. QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, title, defaultPath, ProjectUtils::ProjectJsonFilename.data()));
  425. if (!path.isEmpty())
  426. {
  427. // RegisterProject will check compatibility and prompt user to continue if issues found
  428. // it will also handle detailed error messaging
  429. path.remove(ProjectUtils::ProjectJsonFilename.data());
  430. if (ProjectUtils::RegisterProject(path, this))
  431. {
  432. // notify the user the project was added successfully
  433. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  434. QMessageBox::information(this, "Project added", "Project added successfully");
  435. }
  436. }
  437. }
  438. void ProjectsScreen::HandleAddRemoteProjectButton()
  439. {
  440. AddRemoteProjectDialog* addRemoteProjectDialog = new AddRemoteProjectDialog(this);
  441. connect(addRemoteProjectDialog, &AddRemoteProjectDialog::StartObjectDownload, this, &ProjectsScreen::StartProjectDownload);
  442. if (addRemoteProjectDialog->exec() == QDialog::DialogCode::Accepted)
  443. {
  444. QString repoUri = addRemoteProjectDialog->GetRepoPath();
  445. if (repoUri.isEmpty())
  446. {
  447. QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
  448. return;
  449. }
  450. }
  451. }
  452. void ProjectsScreen::HandleOpenProject(const QString& projectPath)
  453. {
  454. if (!projectPath.isEmpty())
  455. {
  456. if (!WarnIfInBuildQueue(projectPath))
  457. {
  458. AZ::IO::FixedMaxPath fixedProjectPath = projectPath.toUtf8().constData();
  459. AZ::IO::FixedMaxPath editorExecutablePath = ProjectUtils::GetEditorExecutablePath(fixedProjectPath);
  460. if (editorExecutablePath.empty())
  461. {
  462. AZ_Error("ProjectManager", false, "Failed to locate editor");
  463. QMessageBox::critical(
  464. this, tr("Error"), tr("Failed to locate the Editor, please verify that it is built."));
  465. return;
  466. }
  467. AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
  468. processLaunchInfo.m_commandlineParameters = AZStd::vector<AZStd::string>{
  469. editorExecutablePath.String(),
  470. AZStd::string::format(R"(--regset="/Amazon/AzCore/Bootstrap/project_path=%s")", fixedProjectPath.c_str())
  471. };
  472. ;
  473. bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
  474. if (!launchSucceeded)
  475. {
  476. AZ_Error("ProjectManager", false, "Failed to launch editor");
  477. QMessageBox::critical(
  478. this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
  479. }
  480. else
  481. {
  482. // prevent the user from accidentally pressing the button while the editor is launching
  483. // and let them know what's happening
  484. ProjectButton* button = qobject_cast<ProjectButton*>(sender());
  485. if (button)
  486. {
  487. button->SetState(ProjectButtonState::Launching);
  488. }
  489. // enable the button after 3 seconds
  490. constexpr int waitTimeInMs = 3000;
  491. QTimer::singleShot(
  492. waitTimeInMs, this,
  493. [button]
  494. {
  495. if (button)
  496. {
  497. button->SetState(ProjectButtonState::ReadyToLaunch);
  498. }
  499. });
  500. }
  501. }
  502. }
  503. else
  504. {
  505. AZ_Error("ProjectManager", false, "Cannot open editor because an empty project path was provided");
  506. QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor because the project path is invalid."));
  507. }
  508. }
  509. void ProjectsScreen::HandleEditProject(const QString& projectPath)
  510. {
  511. if (!WarnIfInBuildQueue(projectPath))
  512. {
  513. emit NotifyCurrentProject(projectPath);
  514. emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
  515. }
  516. }
  517. void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
  518. {
  519. if (!WarnIfInBuildQueue(projectPath))
  520. {
  521. emit NotifyCurrentProject(projectPath);
  522. emit ChangeScreenRequest(ProjectManagerScreen::ProjectGemCatalog);
  523. }
  524. }
  525. void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
  526. {
  527. if (!WarnIfInBuildQueue(projectInfo.m_path))
  528. {
  529. ProjectInfo newProjectInfo(projectInfo);
  530. // Open file dialog and choose location for copied project then register copy with O3DE
  531. if (ProjectUtils::CopyProjectDialog(projectInfo.m_path, newProjectInfo, this))
  532. {
  533. emit NotifyBuildProject(newProjectInfo);
  534. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  535. }
  536. }
  537. }
  538. void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
  539. {
  540. if (!WarnIfInBuildQueue(projectPath))
  541. {
  542. // Unregister Project from O3DE and reload projects
  543. if (ProjectUtils::UnregisterProject(projectPath))
  544. {
  545. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  546. emit NotifyProjectRemoved(projectPath);
  547. }
  548. }
  549. }
  550. void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
  551. {
  552. if (!WarnIfInBuildQueue(projectPath))
  553. {
  554. QString projectName = tr("Project");
  555. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  556. if (getProjectResult)
  557. {
  558. projectName = getProjectResult.GetValue().m_displayName;
  559. }
  560. QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
  561. tr("Delete %1").arg(projectName),
  562. tr("%1 will be unregistered from O3DE and the project directory '%2' will be deleted from your disk.\n\nAre you sure you want to delete %1?").arg(projectName, projectPath),
  563. QMessageBox::No | QMessageBox::Yes);
  564. if (warningResult == QMessageBox::Yes)
  565. {
  566. QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
  567. // Remove project from O3DE and delete from disk
  568. HandleRemoveProject(projectPath);
  569. ProjectUtils::DeleteProjectFiles(projectPath);
  570. QGuiApplication::restoreOverrideCursor();
  571. emit NotifyProjectRemoved(projectPath);
  572. }
  573. }
  574. }
  575. void ProjectsScreen::HandleOpenAndroidProjectGenerator(const QString& projectPath)
  576. {
  577. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath);
  578. AZ::Outcome projectBuildPathResult = ProjectUtils::GetProjectBuildPath(projectPath);
  579. auto engineInfo = engineInfoResult.TakeValue();
  580. auto buildPath = projectBuildPathResult.TakeValue();
  581. QString projectName = tr("Project");
  582. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  583. if (getProjectResult)
  584. {
  585. projectName = getProjectResult.GetValue().m_displayName;
  586. }
  587. const QString pythonPath = ProjectUtils::GetPythonExecutablePath(engineInfo.m_path);
  588. const QString apgPath = QString("%1/Code/Tools/Android/ProjectGenerator/main.py").arg(engineInfo.m_path);
  589. AZ_Printf("ProjectManager", "APG Info:\nProject Name: %s\nProject Path: %s\nEngine Path: %s\n3rdParty Path: %s\nBuild Path: %s\nPython Path: %s\nAPG path: %s\n",
  590. projectName.toUtf8().constData(),
  591. projectPath.toUtf8().constData(),
  592. engineInfo.m_path.toUtf8().constData(),
  593. engineInfo.m_thirdPartyPath.toUtf8().constData(),
  594. buildPath.toUtf8().constData(),
  595. pythonPath.toUtf8().constData(),
  596. apgPath.toUtf8().constData());
  597. // Let's start the python script.
  598. QProcess process;
  599. process.setProgram(pythonPath);
  600. const QStringList commandArgs { apgPath,
  601. "--e", engineInfo.m_path,
  602. "--p", projectPath,
  603. "--b", buildPath,
  604. "--t", engineInfo.m_thirdPartyPath };
  605. process.setArguments(commandArgs);
  606. // It's important to dump the command details in the application log so the user
  607. // would know how to spawn the Android Project Generator from the command terminal
  608. // in case of errors and debugging is required.
  609. const QString commandArgsStr = QString("%1 %2").arg(pythonPath, commandArgs.join(" "));
  610. AZ_Printf("ProjectManager", "Will start the Android Project Generator with the following command:\n%s\n", commandArgsStr.toUtf8().constData());
  611. if (!process.startDetached())
  612. {
  613. QMessageBox::warning(
  614. this,
  615. tr("Tool Error"),
  616. tr("Failed to start Android Project Generator from path %1").arg(apgPath),
  617. QMessageBox::Ok);
  618. }
  619. }
  620. void ProjectsScreen::HandleOpenProjectExportSettings(const QString& projectPath)
  621. {
  622. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath);
  623. AZ::Outcome projectBuildPathResult = ProjectUtils::GetProjectBuildPath(projectPath);
  624. auto engineInfo = engineInfoResult.TakeValue();
  625. auto buildPath = projectBuildPathResult.TakeValue();
  626. QString projectName = tr("Project");
  627. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  628. if (getProjectResult)
  629. {
  630. projectName = getProjectResult.GetValue().m_displayName;
  631. }
  632. else
  633. {
  634. QMessageBox::critical(this, tr("Tool Error"), tr("Failed to retrieve project information."), QMessageBox::Ok);
  635. return;
  636. }
  637. const QString pythonPath = ProjectUtils::GetPythonExecutablePath(engineInfo.m_path);
  638. const QString o3dePath = QString("%1/scripts/o3de.py").arg(engineInfo.m_path);
  639. // Let's start the python script.
  640. QProcess process;
  641. process.setProgram(pythonPath);
  642. const QStringList commandArgs{ o3dePath, "export-project", "-pp", projectPath, "--configure" };
  643. process.setArguments(commandArgs);
  644. // It's important to dump the command details in the application log so the user
  645. // would know how to spawn the Export Configuration Panel from the command terminal
  646. // in case of errors and debugging is required.
  647. const QString commandArgsStr = QString("%1 %2").arg(pythonPath, commandArgs.join(" "));
  648. AZ_Printf(
  649. "ProjectManager",
  650. "Will start the Export Configuration Panel with the following command:\n%s\n",
  651. commandArgsStr.toUtf8().constData());
  652. if (!process.startDetached())
  653. {
  654. QMessageBox::critical(this, tr("Tool Error"), tr("Failed to start o3de.py from path %1").arg(o3dePath), QMessageBox::Ok);
  655. }
  656. }
  657. void ProjectsScreen::SuggestBuildProjectMsg(const ProjectInfo& projectInfo, bool showMessage)
  658. {
  659. if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end() || projectInfo.m_buildFailed)
  660. {
  661. m_requiresBuild.append(projectInfo);
  662. }
  663. UpdateIfCurrentScreen();
  664. if (showMessage)
  665. {
  666. QMessageBox::information(this,
  667. tr("Project should be rebuilt."),
  668. projectInfo.GetProjectDisplayName() + tr(" project likely needs to be rebuilt."));
  669. }
  670. }
  671. void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
  672. {
  673. SuggestBuildProjectMsg(projectInfo, true);
  674. }
  675. void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo, bool skipDialogBox)
  676. {
  677. auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
  678. if (requiredIter != m_requiresBuild.end())
  679. {
  680. m_requiresBuild.erase(requiredIter);
  681. }
  682. if (!BuildQueueContainsProject(projectInfo.m_path))
  683. {
  684. if (m_buildQueue.empty() && !m_currentBuilder)
  685. {
  686. StartProjectBuild(projectInfo, skipDialogBox);
  687. // Projects Content is already reset in function
  688. }
  689. else
  690. {
  691. m_buildQueue.append(projectInfo);
  692. UpdateIfCurrentScreen();
  693. }
  694. }
  695. }
  696. void ProjectsScreen::UnqueueBuildProject(const ProjectInfo& projectInfo)
  697. {
  698. m_buildQueue.removeAll(projectInfo);
  699. UpdateIfCurrentScreen();
  700. }
  701. void ProjectsScreen::QueueExportProject(const ProjectInfo& projectInfo, const QString& exportScript, bool skipDialogBox)
  702. {
  703. if (!ExportQueueContainsProject(projectInfo.m_path))
  704. {
  705. if (m_exportQueue.empty() && !m_currentExporter)
  706. {
  707. ProjectInfo info = projectInfo;
  708. info.m_currentExportScript = exportScript;
  709. StartProjectExport(info, skipDialogBox);
  710. //Projects Content should be reset in function
  711. }
  712. else
  713. {
  714. m_exportQueue.append(projectInfo);
  715. UpdateIfCurrentScreen();
  716. }
  717. }
  718. }
  719. void ProjectsScreen::UnqueueExportProject(const ProjectInfo& projectInfo)
  720. {
  721. m_exportQueue.removeAll(projectInfo);
  722. UpdateIfCurrentScreen();
  723. }
  724. void ProjectsScreen::StartProjectDownload(const QString& projectName, const QString& destinationPath, bool queueBuild)
  725. {
  726. m_downloadController->AddObjectDownload(projectName, destinationPath, DownloadController::DownloadObjectType::Project);
  727. UpdateIfCurrentScreen();
  728. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  729. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  730. {
  731. return (value.second->GetProjectInfo().m_projectName == projectName);
  732. });
  733. if (foundButton != m_projectButtons.end())
  734. {
  735. (*foundButton).second->SetState(queueBuild ? ProjectButtonState::DownloadingBuildQueued : ProjectButtonState::Downloading);
  736. }
  737. }
  738. void ProjectsScreen::HandleDownloadResult(const QString& projectName, bool succeeded)
  739. {
  740. auto foundButton = AZStd::ranges::find_if(
  741. m_projectButtons,
  742. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  743. {
  744. return (value.second->GetProjectInfo().m_projectName == projectName);
  745. });
  746. if (foundButton != m_projectButtons.end())
  747. {
  748. if (succeeded)
  749. {
  750. // Find the project info since it should now be local
  751. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  752. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  753. {
  754. for (const ProjectInfo& projectInfo : projectsResult.GetValue())
  755. {
  756. if (projectInfo.m_projectName == projectName)
  757. {
  758. (*foundButton).second->SetProject(projectInfo);
  759. if ((*foundButton).second->GetState() == ProjectButtonState::DownloadingBuildQueued)
  760. {
  761. QueueBuildProject(projectInfo, true);
  762. }
  763. else
  764. {
  765. (*foundButton).second->SetState(ProjectButtonState::NeedsToBuild);
  766. }
  767. }
  768. }
  769. }
  770. }
  771. else
  772. {
  773. (*foundButton).second->SetState(ProjectButtonState::NotDownloaded);
  774. }
  775. }
  776. else
  777. {
  778. UpdateIfCurrentScreen();
  779. }
  780. }
  781. void ProjectsScreen::HandleDownloadProgress(const QString& projectName, DownloadController::DownloadObjectType objectType, int bytesDownloaded, int totalBytes)
  782. {
  783. if (objectType != DownloadController::DownloadObjectType::Project)
  784. {
  785. return;
  786. }
  787. //Find button for project name
  788. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  789. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  790. {
  791. return (value.second->GetProjectInfo().m_projectName == projectName);
  792. });
  793. if (foundButton != m_projectButtons.end())
  794. {
  795. float percentage = static_cast<float>(bytesDownloaded) / totalBytes;
  796. (*foundButton).second->SetProgressBarPercentage(percentage);
  797. }
  798. }
  799. QVector<ProjectInfo> ProjectsScreen::GetAllProjects()
  800. {
  801. QVector<ProjectInfo> projects;
  802. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  803. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  804. {
  805. projects.append(projectsResult.GetValue());
  806. }
  807. auto remoteProjectsResult = PythonBindingsInterface::Get()->GetProjectsForAllRepos();
  808. if (remoteProjectsResult.IsSuccess() && !remoteProjectsResult.GetValue().isEmpty())
  809. {
  810. for (const ProjectInfo& remoteProject : remoteProjectsResult.TakeValue())
  811. {
  812. auto foundProject = AZStd::ranges::find_if( projects,
  813. [&remoteProject](const ProjectInfo& value)
  814. {
  815. return remoteProject.m_id == value.m_id;
  816. });
  817. if (foundProject == projects.end())
  818. {
  819. projects.append(remoteProject);
  820. }
  821. }
  822. }
  823. AZ::IO::Path buildProjectPath;
  824. if (m_currentBuilder)
  825. {
  826. buildProjectPath = AZ::IO::Path(m_currentBuilder->GetProjectInfo().m_path.toUtf8().constData());
  827. }
  828. // Sort the projects, putting currently building project in front, then queued projects, then sorts alphabetically
  829. AZStd::sort(projects.begin(), projects.end(), [buildProjectPath, this](const ProjectInfo& arg1, const ProjectInfo& arg2)
  830. {
  831. if (!buildProjectPath.empty())
  832. {
  833. if (AZ::IO::Path(arg1.m_path.toUtf8().constData()) == buildProjectPath)
  834. {
  835. return true;
  836. }
  837. else if (AZ::IO::Path(arg2.m_path.toUtf8().constData()) == buildProjectPath)
  838. {
  839. return false;
  840. }
  841. }
  842. bool arg1InBuildQueue = BuildQueueContainsProject(arg1.m_path);
  843. bool arg2InBuildQueue = BuildQueueContainsProject(arg2.m_path);
  844. if (arg1InBuildQueue && !arg2InBuildQueue)
  845. {
  846. return true;
  847. }
  848. else if (!arg1InBuildQueue && arg2InBuildQueue)
  849. {
  850. return false;
  851. }
  852. else if (arg1.m_displayName.compare(arg2.m_displayName, Qt::CaseInsensitive) == 0)
  853. {
  854. // handle case where names are the same
  855. return arg1.m_path.toLower() < arg2.m_path.toLower();
  856. }
  857. else
  858. {
  859. return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
  860. }
  861. });
  862. return projects;
  863. }
  864. void ProjectsScreen::NotifyCurrentScreen()
  865. {
  866. const QVector<ProjectInfo>& projects = GetAllProjects();
  867. const bool projectsFound = !projects.isEmpty();
  868. if (ShouldDisplayFirstTimeContent(projectsFound))
  869. {
  870. m_background.load(":/Backgrounds/FtueBackground.jpg");
  871. m_stack->setCurrentWidget(m_firstTimeContent);
  872. }
  873. else
  874. {
  875. m_background.load(":/Backgrounds/DefaultBackground.jpg");
  876. UpdateWithProjects(projects);
  877. }
  878. }
  879. bool ProjectsScreen::ShouldDisplayFirstTimeContent(bool projectsFound)
  880. {
  881. if (projectsFound)
  882. {
  883. return false;
  884. }
  885. // only show this screen once
  886. QSettings settings;
  887. bool displayFirstTimeContent = settings.value("displayFirstTimeContent", true).toBool();
  888. if (displayFirstTimeContent)
  889. {
  890. settings.setValue("displayFirstTimeContent", false);
  891. }
  892. return displayFirstTimeContent;
  893. }
  894. bool ProjectsScreen::StartProjectExport(const ProjectInfo& projectInfo, bool skipDialogBox)
  895. {
  896. bool proceedToExport = skipDialogBox;
  897. if (!proceedToExport)
  898. {
  899. QMessageBox::StandardButton buildProject = QMessageBox::information(
  900. this,
  901. tr("Exporting \"%1\"").arg(projectInfo.GetProjectDisplayName()),
  902. tr("Ready to export \"%1\"? Please ensure you have configured the export settings before proceeding.").arg(projectInfo.GetProjectDisplayName()),
  903. QMessageBox::No | QMessageBox::Yes);
  904. proceedToExport = buildProject == QMessageBox::Yes;
  905. }
  906. if (proceedToExport)
  907. {
  908. m_currentExporter = AZStd::make_unique<ProjectExportController>(projectInfo, nullptr, this);
  909. UpdateWithProjects(GetAllProjects());
  910. connect(m_currentExporter.get(), &ProjectExportController::Done, this, &ProjectsScreen::ProjectExportDone);
  911. m_currentExporter->Start();
  912. }
  913. else
  914. {
  915. return false;
  916. }
  917. return true;
  918. }
  919. bool ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo, bool skipDialogBox)
  920. {
  921. if (ProjectUtils::FindSupportedCompiler(projectInfo, this))
  922. {
  923. bool proceedToBuild = skipDialogBox;
  924. if (!proceedToBuild)
  925. {
  926. QMessageBox::StandardButton buildProject = QMessageBox::information(
  927. this,
  928. tr("Building \"%1\"").arg(projectInfo.GetProjectDisplayName()),
  929. tr("Ready to build \"%1\"?").arg(projectInfo.GetProjectDisplayName()),
  930. QMessageBox::No | QMessageBox::Yes);
  931. proceedToBuild = buildProject == QMessageBox::Yes;
  932. }
  933. if (proceedToBuild)
  934. {
  935. m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
  936. UpdateWithProjects(GetAllProjects());
  937. connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
  938. connect(m_currentBuilder, &ProjectBuilderController::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
  939. m_currentBuilder->Start();
  940. }
  941. else
  942. {
  943. SuggestBuildProjectMsg(projectInfo, false);
  944. return false;
  945. }
  946. return true;
  947. }
  948. return false;
  949. }
  950. void ProjectsScreen::ProjectBuildDone(bool success)
  951. {
  952. ProjectInfo currentBuilderProject;
  953. if (!success)
  954. {
  955. currentBuilderProject = m_currentBuilder->GetProjectInfo();
  956. }
  957. delete m_currentBuilder;
  958. m_currentBuilder = nullptr;
  959. if (!success)
  960. {
  961. SuggestBuildProjectMsg(currentBuilderProject, false);
  962. }
  963. if (!m_buildQueue.empty())
  964. {
  965. while (!StartProjectBuild(m_buildQueue.front()) && m_buildQueue.size() > 1)
  966. {
  967. m_buildQueue.pop_front();
  968. }
  969. m_buildQueue.pop_front();
  970. }
  971. UpdateIfCurrentScreen();
  972. }
  973. void ProjectsScreen::ProjectExportDone(bool success)
  974. {
  975. ProjectInfo currentExportProject;
  976. if (!success)
  977. {
  978. currentExportProject = m_currentExporter->GetProjectInfo();
  979. }
  980. m_currentExporter.reset();
  981. if (!m_exportQueue.empty())
  982. {
  983. while (!StartProjectExport(m_exportQueue.front()) && m_exportQueue.size() > 1)
  984. {
  985. m_exportQueue.pop_front();
  986. }
  987. m_exportQueue.pop_front();
  988. }
  989. UpdateIfCurrentScreen();
  990. }
  991. QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
  992. {
  993. QString nativeProjPath(QDir::toNativeSeparators(projectPath));
  994. auto projectIter = m_requiresBuild.begin();
  995. for (; projectIter != m_requiresBuild.end(); ++projectIter)
  996. {
  997. if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
  998. {
  999. break;
  1000. }
  1001. }
  1002. return projectIter;
  1003. }
  1004. bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
  1005. {
  1006. const AZ::IO::PathView path { projectPath.toUtf8().constData() };
  1007. for (const ProjectInfo& project : m_buildQueue)
  1008. {
  1009. if (AZ::IO::PathView(project.m_path.toUtf8().constData()) == path)
  1010. {
  1011. return true;
  1012. }
  1013. }
  1014. return false;
  1015. }
  1016. bool ProjectsScreen::ExportQueueContainsProject(const QString& projectPath)
  1017. {
  1018. const AZ::IO::PathView path { projectPath.toUtf8().constData() };
  1019. for (const ProjectInfo& project : m_exportQueue)
  1020. {
  1021. if (AZ::IO::PathView(project.m_path.toUtf8().constData()) == path)
  1022. {
  1023. return true;
  1024. }
  1025. }
  1026. return false;
  1027. }
  1028. bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
  1029. {
  1030. if (BuildQueueContainsProject(projectPath))
  1031. {
  1032. QMessageBox::warning(
  1033. this,
  1034. tr("Action Temporarily Disabled!"),
  1035. tr("Action not allowed on projects in build queue."));
  1036. return true;
  1037. }
  1038. return false;
  1039. }
  1040. } // namespace O3DE::ProjectManager