2
0

ProjectsScreen.cpp 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209
  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() && !m_projectButtons.empty())
  214. {
  215. RemoveProjectButtonsFromFlowLayout(projects);
  216. }
  217. if (!projects.isEmpty())
  218. {
  219. // Remove all existing buttons before adding them back in the correct order
  220. RemoveProjectButtonsFromFlowLayout(/*projectsToKeep*/ projects);
  221. // It's more efficient to update the project engine by loading engine infos once
  222. // instead of loading them all each time we want to know what project an engine uses
  223. auto engineInfoResult = PythonBindingsInterface::Get()->GetAllEngineInfos();
  224. // Add all project buttons, restoring buttons to default state
  225. for (const ProjectInfo& project : projects)
  226. {
  227. ProjectButton* currentButton = nullptr;
  228. const AZ::IO::Path projectPath { project.m_path.toUtf8().constData() };
  229. auto projectButtonIter = m_projectButtons.find(projectPath);
  230. EngineInfo engine{};
  231. if (engineInfoResult && !project.m_enginePath.isEmpty())
  232. {
  233. AZ::IO::FixedMaxPath projectEnginePath{ project.m_enginePath.toUtf8().constData() };
  234. for (const EngineInfo& engineInfo : engineInfoResult.GetValue())
  235. {
  236. AZ::IO::FixedMaxPath enginePath{ engineInfo.m_path.toUtf8().constData() };
  237. if (enginePath == projectEnginePath)
  238. {
  239. engine = engineInfo;
  240. break;
  241. }
  242. }
  243. }
  244. if (projectButtonIter == m_projectButtons.end())
  245. {
  246. currentButton = CreateProjectButton(project, engine);
  247. m_projectButtons.insert({ projectPath, currentButton });
  248. m_fileSystemWatcher->addPath(QDir::toNativeSeparators(project.m_path + "/project.json"));
  249. }
  250. else
  251. {
  252. currentButton = projectButtonIter->second;
  253. currentButton->SetEngine(engine);
  254. currentButton->SetProject(project);
  255. currentButton->SetState(ProjectButtonState::ReadyToLaunch);
  256. }
  257. // Check whether project manager has successfully built the project
  258. AZ_Assert(currentButton, "Invalid ProjectButton");
  259. m_projectsFlowLayout->addWidget(currentButton);
  260. bool projectBuiltSuccessfully = false;
  261. SettingsInterface::Get()->GetProjectBuiltSuccessfully(projectBuiltSuccessfully, project);
  262. if (!projectBuiltSuccessfully)
  263. {
  264. currentButton->SetState(ProjectButtonState::NeedsToBuild);
  265. }
  266. if (project.m_remote)
  267. {
  268. currentButton->SetState(ProjectButtonState::NotDownloaded);
  269. currentButton->SetProjectButtonAction(
  270. tr("Download Project"),
  271. [this, currentButton, project]
  272. {
  273. m_downloadController->AddObjectDownload(project.m_projectName, "", DownloadController::DownloadObjectType::Project);
  274. currentButton->SetState(ProjectButtonState::Downloading);
  275. });
  276. }
  277. }
  278. if (m_currentBuilder)
  279. {
  280. AZ::IO::Path buildProjectPath = AZ::IO::Path(m_currentBuilder->GetProjectInfo().m_path.toUtf8().constData());
  281. if (!buildProjectPath.empty())
  282. {
  283. // Setup building button again
  284. auto buildProjectIter = m_projectButtons.find(buildProjectPath);
  285. if (buildProjectIter != m_projectButtons.end())
  286. {
  287. m_currentBuilder->SetProjectButton(buildProjectIter->second);
  288. }
  289. }
  290. }
  291. if (m_currentExporter)
  292. {
  293. AZ::IO::Path exportProjectPath = AZ::IO::Path(m_currentExporter->GetProjectInfo().m_path.toUtf8().constData());
  294. if (!exportProjectPath.empty())
  295. {
  296. //Setup export button
  297. if (auto exportProjectIter = m_projectButtons.find(exportProjectPath);
  298. exportProjectIter != m_projectButtons.end())
  299. {
  300. m_currentExporter->SetProjectButton(exportProjectIter->second);
  301. }
  302. }
  303. }
  304. // Let the user cancel builds for projects in the build queue and in the export queue
  305. for (const ProjectInfo& project : m_buildQueue)
  306. {
  307. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  308. if (projectIter != m_projectButtons.end())
  309. {
  310. projectIter->second->SetProjectButtonAction(
  311. tr("Cancel queued build"),
  312. [this, project]
  313. {
  314. UnqueueBuildProject(project);
  315. SuggestBuildProjectMsg(project, false);
  316. });
  317. }
  318. }
  319. for (const ProjectInfo& project : m_exportQueue)
  320. {
  321. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  322. if (projectIter != m_projectButtons.end())
  323. {
  324. projectIter->second->SetProjectButtonAction(
  325. tr("Cancel queued export"),
  326. [this, project]
  327. {
  328. UnqueueExportProject(project);
  329. });
  330. }
  331. }
  332. // Update the project build status if it requires building
  333. for (const ProjectInfo& project : m_requiresBuild)
  334. {
  335. auto projectIter = m_projectButtons.find(project.m_path.toUtf8().constData());
  336. if (projectIter != m_projectButtons.end())
  337. {
  338. // If project is not currently or about to build
  339. if (!m_currentBuilder || m_currentBuilder->GetProjectInfo() != project)
  340. {
  341. if (project.m_buildFailed)
  342. {
  343. projectIter->second->SetBuildLogsLink(project.m_logUrl);
  344. projectIter->second->SetState(ProjectButtonState::BuildFailed);
  345. }
  346. else
  347. {
  348. projectIter->second->SetState(ProjectButtonState::NeedsToBuild);
  349. }
  350. }
  351. }
  352. }
  353. }
  354. if (m_projectsContent)
  355. {
  356. m_stack->setCurrentWidget(m_projectsContent);
  357. }
  358. m_projectsFlowLayout->update();
  359. // Will focus whatever button it finds so the Project tab is not focused on start-up
  360. QTimer::singleShot(0, this, [this]
  361. {
  362. QPushButton* foundButton = m_stack->currentWidget()->findChild<QPushButton*>();
  363. if (foundButton)
  364. {
  365. foundButton->setFocus();
  366. }
  367. });
  368. }
  369. void ProjectsScreen::HandleProjectFilePathChanged(const QString& /*path*/)
  370. {
  371. // QFileWatcher automatically stops watching the path if it was removed so we will just refresh our view
  372. UpdateIfCurrentScreen();
  373. }
  374. ProjectManagerScreen ProjectsScreen::GetScreenEnum()
  375. {
  376. return ProjectManagerScreen::Projects;
  377. }
  378. bool ProjectsScreen::IsTab()
  379. {
  380. return true;
  381. }
  382. QString ProjectsScreen::GetTabText()
  383. {
  384. return tr("Projects");
  385. }
  386. void ProjectsScreen::paintEvent([[maybe_unused]] QPaintEvent* event)
  387. {
  388. // we paint the background here because qss does not support background cover scaling
  389. QPainter painter(this);
  390. const QSize winSize = size();
  391. const float pixmapRatio = (float)m_background.width() / m_background.height();
  392. const float windowRatio = (float)winSize.width() / winSize.height();
  393. QRect backgroundRect;
  394. if (pixmapRatio > windowRatio)
  395. {
  396. const int newWidth = (int)(winSize.height() * pixmapRatio);
  397. const int offset = (newWidth - winSize.width()) / -2;
  398. backgroundRect = QRect(offset, 0, newWidth, winSize.height());
  399. }
  400. else
  401. {
  402. const int newHeight = (int)(winSize.width() / pixmapRatio);
  403. backgroundRect = QRect(0, 0, winSize.width(), newHeight);
  404. }
  405. // Draw the background image.
  406. painter.drawPixmap(backgroundRect, m_background);
  407. // Draw a semi-transparent overlay to darken down the colors.
  408. // Use SourceOver, DestinationIn will make background transparent on Mac
  409. painter.setCompositionMode (QPainter::CompositionMode_SourceOver);
  410. const float overlayTransparency = 0.3f;
  411. painter.fillRect(backgroundRect, QColor(0, 0, 0, static_cast<int>(255.0f * overlayTransparency)));
  412. }
  413. void ProjectsScreen::HandleNewProjectButton()
  414. {
  415. emit ResetScreenRequest(ProjectManagerScreen::CreateProject);
  416. emit ChangeScreenRequest(ProjectManagerScreen::CreateProject);
  417. }
  418. void ProjectsScreen::HandleAddProjectButton()
  419. {
  420. QString title{ QObject::tr("Select Project File") };
  421. QString defaultPath;
  422. // get the default path to look for new projects in
  423. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
  424. if (engineInfoResult.IsSuccess())
  425. {
  426. defaultPath = engineInfoResult.GetValue().m_defaultProjectsFolder;
  427. }
  428. QString path = QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, title, defaultPath, ProjectUtils::ProjectJsonFilename.data()));
  429. if (!path.isEmpty())
  430. {
  431. // RegisterProject will check compatibility and prompt user to continue if issues found
  432. // it will also handle detailed error messaging
  433. path.remove(ProjectUtils::ProjectJsonFilename.data());
  434. if (ProjectUtils::RegisterProject(path, this))
  435. {
  436. // notify the user the project was added successfully
  437. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  438. QMessageBox::information(this, "Project added", "Project added successfully");
  439. }
  440. }
  441. }
  442. void ProjectsScreen::HandleAddRemoteProjectButton()
  443. {
  444. AddRemoteProjectDialog* addRemoteProjectDialog = new AddRemoteProjectDialog(this);
  445. connect(addRemoteProjectDialog, &AddRemoteProjectDialog::StartObjectDownload, this, &ProjectsScreen::StartProjectDownload);
  446. if (addRemoteProjectDialog->exec() == QDialog::DialogCode::Accepted)
  447. {
  448. QString repoUri = addRemoteProjectDialog->GetRepoPath();
  449. if (repoUri.isEmpty())
  450. {
  451. QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
  452. return;
  453. }
  454. }
  455. }
  456. void ProjectsScreen::HandleOpenProject(const QString& projectPath)
  457. {
  458. if (!projectPath.isEmpty())
  459. {
  460. if (!WarnIfInBuildQueue(projectPath))
  461. {
  462. AZ::IO::FixedMaxPath fixedProjectPath = projectPath.toUtf8().constData();
  463. AZ::IO::FixedMaxPath editorExecutablePath = ProjectUtils::GetEditorExecutablePath(fixedProjectPath);
  464. if (editorExecutablePath.empty())
  465. {
  466. AZ_Error("ProjectManager", false, "Failed to locate editor");
  467. QMessageBox::critical(
  468. this, tr("Error"), tr("Failed to locate the Editor, please verify that it is built."));
  469. return;
  470. }
  471. AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
  472. processLaunchInfo.m_commandlineParameters = AZStd::vector<AZStd::string>{
  473. editorExecutablePath.String(),
  474. AZStd::string::format(R"(--regset="/Amazon/AzCore/Bootstrap/project_path=%s")", fixedProjectPath.c_str())
  475. };
  476. ;
  477. bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
  478. if (!launchSucceeded)
  479. {
  480. AZ_Error("ProjectManager", false, "Failed to launch editor");
  481. QMessageBox::critical(
  482. this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
  483. }
  484. else
  485. {
  486. // prevent the user from accidentally pressing the button while the editor is launching
  487. // and let them know what's happening
  488. ProjectButton* button = qobject_cast<ProjectButton*>(sender());
  489. if (button)
  490. {
  491. button->SetState(ProjectButtonState::Launching);
  492. }
  493. // enable the button after 3 seconds
  494. constexpr int waitTimeInMs = 3000;
  495. QTimer::singleShot(
  496. waitTimeInMs, this,
  497. [button]
  498. {
  499. if (button)
  500. {
  501. button->SetState(ProjectButtonState::ReadyToLaunch);
  502. }
  503. });
  504. }
  505. }
  506. }
  507. else
  508. {
  509. AZ_Error("ProjectManager", false, "Cannot open editor because an empty project path was provided");
  510. QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor because the project path is invalid."));
  511. }
  512. }
  513. void ProjectsScreen::HandleEditProject(const QString& projectPath)
  514. {
  515. if (!WarnIfInBuildQueue(projectPath))
  516. {
  517. emit NotifyCurrentProject(projectPath);
  518. emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
  519. }
  520. }
  521. void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
  522. {
  523. if (!WarnIfInBuildQueue(projectPath))
  524. {
  525. emit NotifyCurrentProject(projectPath);
  526. emit ChangeScreenRequest(ProjectManagerScreen::ProjectGemCatalog);
  527. }
  528. }
  529. void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
  530. {
  531. if (!WarnIfInBuildQueue(projectInfo.m_path))
  532. {
  533. ProjectInfo newProjectInfo(projectInfo);
  534. // Open file dialog and choose location for copied project then register copy with O3DE
  535. if (ProjectUtils::CopyProjectDialog(projectInfo.m_path, newProjectInfo, this))
  536. {
  537. emit NotifyBuildProject(newProjectInfo);
  538. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  539. }
  540. }
  541. }
  542. void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
  543. {
  544. if (!WarnIfInBuildQueue(projectPath))
  545. {
  546. // Unregister Project from O3DE and reload projects
  547. if (ProjectUtils::UnregisterProject(projectPath))
  548. {
  549. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  550. emit NotifyProjectRemoved(projectPath);
  551. }
  552. }
  553. }
  554. void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
  555. {
  556. if (!WarnIfInBuildQueue(projectPath))
  557. {
  558. QString projectName = tr("Project");
  559. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  560. if (getProjectResult)
  561. {
  562. projectName = getProjectResult.GetValue().m_displayName;
  563. }
  564. QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
  565. tr("Delete %1").arg(projectName),
  566. 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),
  567. QMessageBox::No | QMessageBox::Yes);
  568. if (warningResult == QMessageBox::Yes)
  569. {
  570. QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
  571. // Remove project from O3DE and delete from disk
  572. HandleRemoveProject(projectPath);
  573. ProjectUtils::DeleteProjectFiles(projectPath);
  574. QGuiApplication::restoreOverrideCursor();
  575. emit NotifyProjectRemoved(projectPath);
  576. }
  577. }
  578. }
  579. void ProjectsScreen::HandleOpenAndroidProjectGenerator(const QString& projectPath)
  580. {
  581. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath);
  582. AZ::Outcome projectBuildPathResult = ProjectUtils::GetProjectBuildPath(projectPath);
  583. auto engineInfo = engineInfoResult.TakeValue();
  584. auto buildPath = projectBuildPathResult.TakeValue();
  585. QString projectName = tr("Project");
  586. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  587. if (getProjectResult)
  588. {
  589. projectName = getProjectResult.GetValue().m_displayName;
  590. }
  591. const QString pythonPath = ProjectUtils::GetPythonExecutablePath(engineInfo.m_path);
  592. const QString apgPath = QString("%1/Code/Tools/Android/ProjectGenerator/main.py").arg(engineInfo.m_path);
  593. 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",
  594. projectName.toUtf8().constData(),
  595. projectPath.toUtf8().constData(),
  596. engineInfo.m_path.toUtf8().constData(),
  597. engineInfo.m_thirdPartyPath.toUtf8().constData(),
  598. buildPath.toUtf8().constData(),
  599. pythonPath.toUtf8().constData(),
  600. apgPath.toUtf8().constData());
  601. // Let's start the python script.
  602. QProcess process;
  603. process.setProgram(pythonPath);
  604. const QStringList commandArgs { apgPath,
  605. "--e", engineInfo.m_path,
  606. "--p", projectPath,
  607. "--b", buildPath,
  608. "--t", engineInfo.m_thirdPartyPath };
  609. process.setArguments(commandArgs);
  610. // It's important to dump the command details in the application log so the user
  611. // would know how to spawn the Android Project Generator from the command terminal
  612. // in case of errors and debugging is required.
  613. const QString commandArgsStr = QString("%1 %2").arg(pythonPath, commandArgs.join(" "));
  614. AZ_Printf("ProjectManager", "Will start the Android Project Generator with the following command:\n%s\n", commandArgsStr.toUtf8().constData());
  615. if (!process.startDetached())
  616. {
  617. QMessageBox::warning(
  618. this,
  619. tr("Tool Error"),
  620. tr("Failed to start Android Project Generator from path %1").arg(apgPath),
  621. QMessageBox::Ok);
  622. }
  623. }
  624. void ProjectsScreen::HandleOpenProjectExportSettings(const QString& projectPath)
  625. {
  626. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath);
  627. AZ::Outcome projectBuildPathResult = ProjectUtils::GetProjectBuildPath(projectPath);
  628. auto engineInfo = engineInfoResult.TakeValue();
  629. auto buildPath = projectBuildPathResult.TakeValue();
  630. QString projectName = tr("Project");
  631. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  632. if (getProjectResult)
  633. {
  634. projectName = getProjectResult.GetValue().m_displayName;
  635. }
  636. else
  637. {
  638. QMessageBox::critical(this, tr("Tool Error"), tr("Failed to retrieve project information."), QMessageBox::Ok);
  639. return;
  640. }
  641. const QString pythonPath = ProjectUtils::GetPythonExecutablePath(engineInfo.m_path);
  642. const QString o3dePath = QString("%1/scripts/o3de.py").arg(engineInfo.m_path);
  643. // Let's start the python script.
  644. QProcess process;
  645. process.setProgram(pythonPath);
  646. const QStringList commandArgs{ o3dePath, "export-project", "-pp", projectPath, "--configure" };
  647. process.setArguments(commandArgs);
  648. // It's important to dump the command details in the application log so the user
  649. // would know how to spawn the Export Configuration Panel from the command terminal
  650. // in case of errors and debugging is required.
  651. const QString commandArgsStr = QString("%1 %2").arg(pythonPath, commandArgs.join(" "));
  652. AZ_Printf(
  653. "ProjectManager",
  654. "Will start the Export Configuration Panel with the following command:\n%s\n",
  655. commandArgsStr.toUtf8().constData());
  656. if (!process.startDetached())
  657. {
  658. QMessageBox::critical(this, tr("Tool Error"), tr("Failed to start o3de.py from path %1").arg(o3dePath), QMessageBox::Ok);
  659. }
  660. }
  661. void ProjectsScreen::SuggestBuildProjectMsg(const ProjectInfo& projectInfo, bool showMessage)
  662. {
  663. if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end() || projectInfo.m_buildFailed)
  664. {
  665. m_requiresBuild.append(projectInfo);
  666. }
  667. UpdateIfCurrentScreen();
  668. if (showMessage)
  669. {
  670. QMessageBox::information(this,
  671. tr("Project should be rebuilt."),
  672. projectInfo.GetProjectDisplayName() + tr(" project likely needs to be rebuilt."));
  673. }
  674. }
  675. void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
  676. {
  677. SuggestBuildProjectMsg(projectInfo, true);
  678. }
  679. void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo, bool skipDialogBox)
  680. {
  681. auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
  682. if (requiredIter != m_requiresBuild.end())
  683. {
  684. m_requiresBuild.erase(requiredIter);
  685. }
  686. if (!BuildQueueContainsProject(projectInfo.m_path))
  687. {
  688. if (m_buildQueue.empty() && !m_currentBuilder)
  689. {
  690. StartProjectBuild(projectInfo, skipDialogBox);
  691. // Projects Content is already reset in function
  692. }
  693. else
  694. {
  695. m_buildQueue.append(projectInfo);
  696. UpdateIfCurrentScreen();
  697. }
  698. }
  699. }
  700. void ProjectsScreen::UnqueueBuildProject(const ProjectInfo& projectInfo)
  701. {
  702. m_buildQueue.removeAll(projectInfo);
  703. UpdateIfCurrentScreen();
  704. }
  705. void ProjectsScreen::QueueExportProject(const ProjectInfo& projectInfo, const QString& exportScript, bool skipDialogBox)
  706. {
  707. if (!ExportQueueContainsProject(projectInfo.m_path))
  708. {
  709. if (m_exportQueue.empty() && !m_currentExporter)
  710. {
  711. ProjectInfo info = projectInfo;
  712. info.m_currentExportScript = exportScript;
  713. StartProjectExport(info, skipDialogBox);
  714. //Projects Content should be reset in function
  715. }
  716. else
  717. {
  718. m_exportQueue.append(projectInfo);
  719. UpdateIfCurrentScreen();
  720. }
  721. }
  722. }
  723. void ProjectsScreen::UnqueueExportProject(const ProjectInfo& projectInfo)
  724. {
  725. m_exportQueue.removeAll(projectInfo);
  726. UpdateIfCurrentScreen();
  727. }
  728. void ProjectsScreen::StartProjectDownload(const QString& projectName, const QString& destinationPath, bool queueBuild)
  729. {
  730. m_downloadController->AddObjectDownload(projectName, destinationPath, DownloadController::DownloadObjectType::Project);
  731. UpdateIfCurrentScreen();
  732. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  733. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  734. {
  735. return (value.second->GetProjectInfo().m_projectName == projectName);
  736. });
  737. if (foundButton != m_projectButtons.end())
  738. {
  739. (*foundButton).second->SetState(queueBuild ? ProjectButtonState::DownloadingBuildQueued : ProjectButtonState::Downloading);
  740. }
  741. }
  742. void ProjectsScreen::HandleDownloadResult(const QString& projectName, bool succeeded)
  743. {
  744. auto foundButton = AZStd::ranges::find_if(
  745. m_projectButtons,
  746. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  747. {
  748. return (value.second->GetProjectInfo().m_projectName == projectName);
  749. });
  750. if (foundButton != m_projectButtons.end())
  751. {
  752. if (succeeded)
  753. {
  754. // Find the project info since it should now be local
  755. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  756. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  757. {
  758. for (const ProjectInfo& projectInfo : projectsResult.GetValue())
  759. {
  760. if (projectInfo.m_projectName == projectName)
  761. {
  762. (*foundButton).second->SetProject(projectInfo);
  763. if ((*foundButton).second->GetState() == ProjectButtonState::DownloadingBuildQueued)
  764. {
  765. QueueBuildProject(projectInfo, true);
  766. }
  767. else
  768. {
  769. (*foundButton).second->SetState(ProjectButtonState::NeedsToBuild);
  770. }
  771. }
  772. }
  773. }
  774. }
  775. else
  776. {
  777. (*foundButton).second->SetState(ProjectButtonState::NotDownloaded);
  778. }
  779. }
  780. else
  781. {
  782. UpdateIfCurrentScreen();
  783. }
  784. }
  785. void ProjectsScreen::HandleDownloadProgress(const QString& projectName, DownloadController::DownloadObjectType objectType, int bytesDownloaded, int totalBytes)
  786. {
  787. if (objectType != DownloadController::DownloadObjectType::Project)
  788. {
  789. return;
  790. }
  791. //Find button for project name
  792. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  793. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  794. {
  795. return (value.second->GetProjectInfo().m_projectName == projectName);
  796. });
  797. if (foundButton != m_projectButtons.end())
  798. {
  799. float percentage = static_cast<float>(bytesDownloaded) / totalBytes;
  800. (*foundButton).second->SetProgressBarPercentage(percentage);
  801. }
  802. }
  803. QVector<ProjectInfo> ProjectsScreen::GetAllProjects()
  804. {
  805. QVector<ProjectInfo> projects;
  806. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  807. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  808. {
  809. projects.append(projectsResult.GetValue());
  810. }
  811. auto remoteProjectsResult = PythonBindingsInterface::Get()->GetProjectsForAllRepos();
  812. if (remoteProjectsResult.IsSuccess() && !remoteProjectsResult.GetValue().isEmpty())
  813. {
  814. for (const ProjectInfo& remoteProject : remoteProjectsResult.TakeValue())
  815. {
  816. auto foundProject = AZStd::ranges::find_if( projects,
  817. [&remoteProject](const ProjectInfo& value)
  818. {
  819. return remoteProject.m_id == value.m_id;
  820. });
  821. if (foundProject == projects.end())
  822. {
  823. projects.append(remoteProject);
  824. }
  825. }
  826. }
  827. AZ::IO::Path buildProjectPath;
  828. if (m_currentBuilder)
  829. {
  830. buildProjectPath = AZ::IO::Path(m_currentBuilder->GetProjectInfo().m_path.toUtf8().constData());
  831. }
  832. // Sort the projects, putting currently building project in front, then queued projects, then sorts alphabetically
  833. AZStd::sort(projects.begin(), projects.end(), [buildProjectPath, this](const ProjectInfo& arg1, const ProjectInfo& arg2)
  834. {
  835. if (!buildProjectPath.empty())
  836. {
  837. if (AZ::IO::Path(arg1.m_path.toUtf8().constData()) == buildProjectPath)
  838. {
  839. return true;
  840. }
  841. else if (AZ::IO::Path(arg2.m_path.toUtf8().constData()) == buildProjectPath)
  842. {
  843. return false;
  844. }
  845. }
  846. bool arg1InBuildQueue = BuildQueueContainsProject(arg1.m_path);
  847. bool arg2InBuildQueue = BuildQueueContainsProject(arg2.m_path);
  848. if (arg1InBuildQueue && !arg2InBuildQueue)
  849. {
  850. return true;
  851. }
  852. else if (!arg1InBuildQueue && arg2InBuildQueue)
  853. {
  854. return false;
  855. }
  856. else if (arg1.m_displayName.compare(arg2.m_displayName, Qt::CaseInsensitive) == 0)
  857. {
  858. // handle case where names are the same
  859. return arg1.m_path.toLower() < arg2.m_path.toLower();
  860. }
  861. else
  862. {
  863. return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
  864. }
  865. });
  866. return projects;
  867. }
  868. void ProjectsScreen::NotifyCurrentScreen()
  869. {
  870. const QVector<ProjectInfo>& projects = GetAllProjects();
  871. const bool projectsFound = !projects.isEmpty();
  872. if (ShouldDisplayFirstTimeContent(projectsFound))
  873. {
  874. m_background.load(":/Backgrounds/FtueBackground.jpg");
  875. m_stack->setCurrentWidget(m_firstTimeContent);
  876. }
  877. else
  878. {
  879. m_background.load(":/Backgrounds/DefaultBackground.jpg");
  880. UpdateWithProjects(projects);
  881. }
  882. }
  883. bool ProjectsScreen::ShouldDisplayFirstTimeContent(bool projectsFound)
  884. {
  885. if (projectsFound)
  886. {
  887. return false;
  888. }
  889. // only show this screen once
  890. QSettings settings;
  891. bool displayFirstTimeContent = settings.value("displayFirstTimeContent", true).toBool();
  892. if (displayFirstTimeContent)
  893. {
  894. settings.setValue("displayFirstTimeContent", false);
  895. }
  896. return displayFirstTimeContent;
  897. }
  898. bool ProjectsScreen::StartProjectExport(const ProjectInfo& projectInfo, bool skipDialogBox)
  899. {
  900. bool proceedToExport = skipDialogBox;
  901. if (!proceedToExport)
  902. {
  903. QMessageBox::StandardButton buildProject = QMessageBox::information(
  904. this,
  905. tr("Exporting \"%1\"").arg(projectInfo.GetProjectDisplayName()),
  906. tr("Ready to export \"%1\"? Please ensure you have configured the export settings before proceeding.").arg(projectInfo.GetProjectDisplayName()),
  907. QMessageBox::No | QMessageBox::Yes);
  908. proceedToExport = buildProject == QMessageBox::Yes;
  909. }
  910. if (proceedToExport)
  911. {
  912. m_currentExporter = AZStd::make_unique<ProjectExportController>(projectInfo, nullptr, this);
  913. UpdateWithProjects(GetAllProjects());
  914. connect(m_currentExporter.get(), &ProjectExportController::Done, this, &ProjectsScreen::ProjectExportDone);
  915. m_currentExporter->Start();
  916. }
  917. else
  918. {
  919. return false;
  920. }
  921. return true;
  922. }
  923. bool ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo, bool skipDialogBox)
  924. {
  925. if (ProjectUtils::FindSupportedCompiler(projectInfo, this))
  926. {
  927. bool proceedToBuild = skipDialogBox;
  928. if (!proceedToBuild)
  929. {
  930. QMessageBox::StandardButton buildProject = QMessageBox::information(
  931. this,
  932. tr("Building \"%1\"").arg(projectInfo.GetProjectDisplayName()),
  933. tr("Ready to build \"%1\"?").arg(projectInfo.GetProjectDisplayName()),
  934. QMessageBox::No | QMessageBox::Yes);
  935. proceedToBuild = buildProject == QMessageBox::Yes;
  936. }
  937. if (proceedToBuild)
  938. {
  939. m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
  940. UpdateWithProjects(GetAllProjects());
  941. connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
  942. connect(m_currentBuilder, &ProjectBuilderController::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
  943. m_currentBuilder->Start();
  944. }
  945. else
  946. {
  947. SuggestBuildProjectMsg(projectInfo, false);
  948. return false;
  949. }
  950. return true;
  951. }
  952. return false;
  953. }
  954. void ProjectsScreen::ProjectBuildDone(bool success)
  955. {
  956. ProjectInfo currentBuilderProject;
  957. if (!success)
  958. {
  959. currentBuilderProject = m_currentBuilder->GetProjectInfo();
  960. }
  961. delete m_currentBuilder;
  962. m_currentBuilder = nullptr;
  963. if (!success)
  964. {
  965. SuggestBuildProjectMsg(currentBuilderProject, false);
  966. }
  967. if (!m_buildQueue.empty())
  968. {
  969. while (!StartProjectBuild(m_buildQueue.front()) && m_buildQueue.size() > 1)
  970. {
  971. m_buildQueue.pop_front();
  972. }
  973. m_buildQueue.pop_front();
  974. }
  975. UpdateIfCurrentScreen();
  976. }
  977. void ProjectsScreen::ProjectExportDone(bool success)
  978. {
  979. ProjectInfo currentExportProject;
  980. if (!success)
  981. {
  982. currentExportProject = m_currentExporter->GetProjectInfo();
  983. }
  984. m_currentExporter.reset();
  985. if (!m_exportQueue.empty())
  986. {
  987. while (!StartProjectExport(m_exportQueue.front()) && m_exportQueue.size() > 1)
  988. {
  989. m_exportQueue.pop_front();
  990. }
  991. m_exportQueue.pop_front();
  992. }
  993. UpdateIfCurrentScreen();
  994. }
  995. QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
  996. {
  997. QString nativeProjPath(QDir::toNativeSeparators(projectPath));
  998. auto projectIter = m_requiresBuild.begin();
  999. for (; projectIter != m_requiresBuild.end(); ++projectIter)
  1000. {
  1001. if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
  1002. {
  1003. break;
  1004. }
  1005. }
  1006. return projectIter;
  1007. }
  1008. bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
  1009. {
  1010. const AZ::IO::PathView path { projectPath.toUtf8().constData() };
  1011. for (const ProjectInfo& project : m_buildQueue)
  1012. {
  1013. if (AZ::IO::PathView(project.m_path.toUtf8().constData()) == path)
  1014. {
  1015. return true;
  1016. }
  1017. }
  1018. return false;
  1019. }
  1020. bool ProjectsScreen::ExportQueueContainsProject(const QString& projectPath)
  1021. {
  1022. const AZ::IO::PathView path { projectPath.toUtf8().constData() };
  1023. for (const ProjectInfo& project : m_exportQueue)
  1024. {
  1025. if (AZ::IO::PathView(project.m_path.toUtf8().constData()) == path)
  1026. {
  1027. return true;
  1028. }
  1029. }
  1030. return false;
  1031. }
  1032. bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
  1033. {
  1034. if (BuildQueueContainsProject(projectPath))
  1035. {
  1036. QMessageBox::warning(
  1037. this,
  1038. tr("Action Temporarily Disabled!"),
  1039. tr("Action not allowed on projects in build queue."));
  1040. return true;
  1041. }
  1042. return false;
  1043. }
  1044. } // namespace O3DE::ProjectManager