ProjectsScreen.cpp 40 KB

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