3
0

ProjectsScreen.cpp 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037
  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 File") };
  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::getOpenFileName(this, title, defaultPath, ProjectUtils::ProjectJsonFilename.data()));
  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. path.remove(ProjectUtils::ProjectJsonFilename.data());
  400. if (ProjectUtils::RegisterProject(path, this))
  401. {
  402. // notify the user the project was added successfully
  403. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  404. QMessageBox::information(this, "Project added", "Project added successfully");
  405. }
  406. }
  407. }
  408. void ProjectsScreen::HandleAddRemoteProjectButton()
  409. {
  410. AddRemoteProjectDialog* addRemoteProjectDialog = new AddRemoteProjectDialog(this);
  411. connect(addRemoteProjectDialog, &AddRemoteProjectDialog::StartObjectDownload, this, &ProjectsScreen::StartProjectDownload);
  412. if (addRemoteProjectDialog->exec() == QDialog::DialogCode::Accepted)
  413. {
  414. QString repoUri = addRemoteProjectDialog->GetRepoPath();
  415. if (repoUri.isEmpty())
  416. {
  417. QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
  418. return;
  419. }
  420. }
  421. }
  422. void ProjectsScreen::HandleOpenProject(const QString& projectPath)
  423. {
  424. if (!projectPath.isEmpty())
  425. {
  426. if (!WarnIfInBuildQueue(projectPath))
  427. {
  428. AZ::IO::FixedMaxPath fixedProjectPath = projectPath.toUtf8().constData();
  429. AZ::IO::FixedMaxPath editorExecutablePath = ProjectUtils::GetEditorExecutablePath(fixedProjectPath);
  430. if (editorExecutablePath.empty())
  431. {
  432. AZ_Error("ProjectManager", false, "Failed to locate editor");
  433. QMessageBox::critical(
  434. this, tr("Error"), tr("Failed to locate the Editor, please verify that it is built."));
  435. return;
  436. }
  437. AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
  438. processLaunchInfo.m_commandlineParameters = AZStd::vector<AZStd::string>{
  439. editorExecutablePath.String(),
  440. AZStd::string::format(R"(--regset="/Amazon/AzCore/Bootstrap/project_path=%s")", fixedProjectPath.c_str())
  441. };
  442. ;
  443. bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
  444. if (!launchSucceeded)
  445. {
  446. AZ_Error("ProjectManager", false, "Failed to launch editor");
  447. QMessageBox::critical(
  448. this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
  449. }
  450. else
  451. {
  452. // prevent the user from accidentally pressing the button while the editor is launching
  453. // and let them know what's happening
  454. ProjectButton* button = qobject_cast<ProjectButton*>(sender());
  455. if (button)
  456. {
  457. button->SetState(ProjectButtonState::Launching);
  458. }
  459. // enable the button after 3 seconds
  460. constexpr int waitTimeInMs = 3000;
  461. QTimer::singleShot(
  462. waitTimeInMs, this,
  463. [button]
  464. {
  465. if (button)
  466. {
  467. button->SetState(ProjectButtonState::ReadyToLaunch);
  468. }
  469. });
  470. }
  471. }
  472. }
  473. else
  474. {
  475. AZ_Error("ProjectManager", false, "Cannot open editor because an empty project path was provided");
  476. QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor because the project path is invalid."));
  477. }
  478. }
  479. void ProjectsScreen::HandleEditProject(const QString& projectPath)
  480. {
  481. if (!WarnIfInBuildQueue(projectPath))
  482. {
  483. emit NotifyCurrentProject(projectPath);
  484. emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
  485. }
  486. }
  487. void ProjectsScreen::HandleEditProjectGems(const QString& projectPath)
  488. {
  489. if (!WarnIfInBuildQueue(projectPath))
  490. {
  491. emit NotifyCurrentProject(projectPath);
  492. emit ChangeScreenRequest(ProjectManagerScreen::ProjectGemCatalog);
  493. }
  494. }
  495. void ProjectsScreen::HandleCopyProject(const ProjectInfo& projectInfo)
  496. {
  497. if (!WarnIfInBuildQueue(projectInfo.m_path))
  498. {
  499. ProjectInfo newProjectInfo(projectInfo);
  500. // Open file dialog and choose location for copied project then register copy with O3DE
  501. if (ProjectUtils::CopyProjectDialog(projectInfo.m_path, newProjectInfo, this))
  502. {
  503. emit NotifyBuildProject(newProjectInfo);
  504. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  505. }
  506. }
  507. }
  508. void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
  509. {
  510. if (!WarnIfInBuildQueue(projectPath))
  511. {
  512. // Unregister Project from O3DE and reload projects
  513. if (ProjectUtils::UnregisterProject(projectPath))
  514. {
  515. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  516. emit NotifyProjectRemoved(projectPath);
  517. }
  518. }
  519. }
  520. void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
  521. {
  522. if (!WarnIfInBuildQueue(projectPath))
  523. {
  524. QString projectName = tr("Project");
  525. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  526. if (getProjectResult)
  527. {
  528. projectName = getProjectResult.GetValue().m_displayName;
  529. }
  530. QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
  531. tr("Delete %1").arg(projectName),
  532. 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),
  533. QMessageBox::No | QMessageBox::Yes);
  534. if (warningResult == QMessageBox::Yes)
  535. {
  536. QGuiApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
  537. // Remove project from O3DE and delete from disk
  538. HandleRemoveProject(projectPath);
  539. ProjectUtils::DeleteProjectFiles(projectPath);
  540. QGuiApplication::restoreOverrideCursor();
  541. emit NotifyProjectRemoved(projectPath);
  542. }
  543. }
  544. }
  545. void ProjectsScreen::HandleOpenAndroidProjectGenerator(const QString& projectPath)
  546. {
  547. AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetProjectEngine(projectPath);
  548. AZ::Outcome projectBuildPathResult = ProjectUtils::GetProjectBuildPath(projectPath);
  549. auto engineInfo = engineInfoResult.TakeValue();
  550. auto buildPath = projectBuildPathResult.TakeValue();
  551. QString projectName = tr("Project");
  552. auto getProjectResult = PythonBindingsInterface::Get()->GetProject(projectPath);
  553. if (getProjectResult)
  554. {
  555. projectName = getProjectResult.GetValue().m_displayName;
  556. }
  557. const QString pythonPath = ProjectUtils::GetPythonExecutablePath(engineInfo.m_path);
  558. const QString apgPath = QString("%1/Code/Tools/Android/ProjectGenerator/main.py").arg(engineInfo.m_path);
  559. 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",
  560. projectName.toUtf8().constData(),
  561. projectPath.toUtf8().constData(),
  562. engineInfo.m_path.toUtf8().constData(),
  563. engineInfo.m_thirdPartyPath.toUtf8().constData(),
  564. buildPath.toUtf8().constData(),
  565. pythonPath.toUtf8().constData(),
  566. apgPath.toUtf8().constData());
  567. // Let's start the python script.
  568. QProcess process;
  569. process.setProgram(pythonPath);
  570. const QStringList commandArgs { apgPath,
  571. "--e", engineInfo.m_path,
  572. "--p", projectPath,
  573. "--b", buildPath,
  574. "--t", engineInfo.m_thirdPartyPath };
  575. process.setArguments(commandArgs);
  576. // It's important to dump the command details in the application log so the user
  577. // would know how to spawn the Android Project Generator from the command terminal
  578. // in case of errors and debugging is required.
  579. const QString commandArgsStr = QString("%1 %2").arg(pythonPath, commandArgs.join(" "));
  580. AZ_Printf("ProjectManager", "Will start the Android Project Generator with the following command:\n%s\n", commandArgsStr.toUtf8().constData());
  581. if (!process.startDetached())
  582. {
  583. QMessageBox::warning(
  584. this,
  585. tr("Tool Error"),
  586. tr("Failed to start Android Project Generator from path %1").arg(apgPath),
  587. QMessageBox::Ok);
  588. }
  589. }
  590. void ProjectsScreen::SuggestBuildProjectMsg(const ProjectInfo& projectInfo, bool showMessage)
  591. {
  592. if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end() || projectInfo.m_buildFailed)
  593. {
  594. m_requiresBuild.append(projectInfo);
  595. }
  596. UpdateIfCurrentScreen();
  597. if (showMessage)
  598. {
  599. QMessageBox::information(this,
  600. tr("Project should be rebuilt."),
  601. projectInfo.GetProjectDisplayName() + tr(" project likely needs to be rebuilt."));
  602. }
  603. }
  604. void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
  605. {
  606. SuggestBuildProjectMsg(projectInfo, true);
  607. }
  608. void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo, bool skipDialogBox)
  609. {
  610. auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
  611. if (requiredIter != m_requiresBuild.end())
  612. {
  613. m_requiresBuild.erase(requiredIter);
  614. }
  615. if (!BuildQueueContainsProject(projectInfo.m_path))
  616. {
  617. if (m_buildQueue.empty() && !m_currentBuilder)
  618. {
  619. StartProjectBuild(projectInfo, skipDialogBox);
  620. // Projects Content is already reset in function
  621. }
  622. else
  623. {
  624. m_buildQueue.append(projectInfo);
  625. UpdateIfCurrentScreen();
  626. }
  627. }
  628. }
  629. void ProjectsScreen::UnqueueBuildProject(const ProjectInfo& projectInfo)
  630. {
  631. m_buildQueue.removeAll(projectInfo);
  632. UpdateIfCurrentScreen();
  633. }
  634. void ProjectsScreen::StartProjectDownload(const QString& projectName, const QString& destinationPath, bool queueBuild)
  635. {
  636. m_downloadController->AddObjectDownload(projectName, destinationPath, DownloadController::DownloadObjectType::Project);
  637. UpdateIfCurrentScreen();
  638. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  639. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  640. {
  641. return (value.second->GetProjectInfo().m_projectName == projectName);
  642. });
  643. if (foundButton != m_projectButtons.end())
  644. {
  645. (*foundButton).second->SetState(queueBuild ? ProjectButtonState::DownloadingBuildQueued : ProjectButtonState::Downloading);
  646. }
  647. }
  648. void ProjectsScreen::HandleDownloadResult(const QString& projectName, bool succeeded)
  649. {
  650. auto foundButton = AZStd::ranges::find_if(
  651. m_projectButtons,
  652. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  653. {
  654. return (value.second->GetProjectInfo().m_projectName == projectName);
  655. });
  656. if (foundButton != m_projectButtons.end())
  657. {
  658. if (succeeded)
  659. {
  660. // Find the project info since it should now be local
  661. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  662. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  663. {
  664. for (const ProjectInfo& projectInfo : projectsResult.GetValue())
  665. {
  666. if (projectInfo.m_projectName == projectName)
  667. {
  668. (*foundButton).second->SetProject(projectInfo);
  669. if ((*foundButton).second->GetState() == ProjectButtonState::DownloadingBuildQueued)
  670. {
  671. QueueBuildProject(projectInfo, true);
  672. }
  673. else
  674. {
  675. (*foundButton).second->SetState(ProjectButtonState::NeedsToBuild);
  676. }
  677. }
  678. }
  679. }
  680. }
  681. else
  682. {
  683. (*foundButton).second->SetState(ProjectButtonState::NotDownloaded);
  684. }
  685. }
  686. else
  687. {
  688. UpdateIfCurrentScreen();
  689. }
  690. }
  691. void ProjectsScreen::HandleDownloadProgress(const QString& projectName, DownloadController::DownloadObjectType objectType, int bytesDownloaded, int totalBytes)
  692. {
  693. if (objectType != DownloadController::DownloadObjectType::Project)
  694. {
  695. return;
  696. }
  697. //Find button for project name
  698. auto foundButton = AZStd::ranges::find_if(m_projectButtons,
  699. [&projectName](const AZStd::unordered_map<AZ::IO::Path, ProjectButton*>::value_type& value)
  700. {
  701. return (value.second->GetProjectInfo().m_projectName == projectName);
  702. });
  703. if (foundButton != m_projectButtons.end())
  704. {
  705. float percentage = static_cast<float>(bytesDownloaded) / totalBytes;
  706. (*foundButton).second->SetProgressBarPercentage(percentage);
  707. }
  708. }
  709. QVector<ProjectInfo> ProjectsScreen::GetAllProjects()
  710. {
  711. QVector<ProjectInfo> projects;
  712. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  713. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  714. {
  715. projects.append(projectsResult.GetValue());
  716. }
  717. auto remoteProjectsResult = PythonBindingsInterface::Get()->GetProjectsForAllRepos();
  718. if (remoteProjectsResult.IsSuccess() && !remoteProjectsResult.GetValue().isEmpty())
  719. {
  720. for (const ProjectInfo& remoteProject : remoteProjectsResult.TakeValue())
  721. {
  722. auto foundProject = AZStd::ranges::find_if( projects,
  723. [&remoteProject](const ProjectInfo& value)
  724. {
  725. return remoteProject.m_id == value.m_id;
  726. });
  727. if (foundProject == projects.end())
  728. {
  729. projects.append(remoteProject);
  730. }
  731. }
  732. }
  733. AZ::IO::Path buildProjectPath;
  734. if (m_currentBuilder)
  735. {
  736. buildProjectPath = AZ::IO::Path(m_currentBuilder->GetProjectInfo().m_path.toUtf8().constData());
  737. }
  738. // Sort the projects, putting currently building project in front, then queued projects, then sorts alphabetically
  739. AZStd::sort(projects.begin(), projects.end(), [buildProjectPath, this](const ProjectInfo& arg1, const ProjectInfo& arg2)
  740. {
  741. if (!buildProjectPath.empty())
  742. {
  743. if (AZ::IO::Path(arg1.m_path.toUtf8().constData()) == buildProjectPath)
  744. {
  745. return true;
  746. }
  747. else if (AZ::IO::Path(arg2.m_path.toUtf8().constData()) == buildProjectPath)
  748. {
  749. return false;
  750. }
  751. }
  752. bool arg1InBuildQueue = BuildQueueContainsProject(arg1.m_path);
  753. bool arg2InBuildQueue = BuildQueueContainsProject(arg2.m_path);
  754. if (arg1InBuildQueue && !arg2InBuildQueue)
  755. {
  756. return true;
  757. }
  758. else if (!arg1InBuildQueue && arg2InBuildQueue)
  759. {
  760. return false;
  761. }
  762. else if (arg1.m_displayName.compare(arg2.m_displayName, Qt::CaseInsensitive) == 0)
  763. {
  764. // handle case where names are the same
  765. return arg1.m_path.toLower() < arg2.m_path.toLower();
  766. }
  767. else
  768. {
  769. return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
  770. }
  771. });
  772. return projects;
  773. }
  774. void ProjectsScreen::NotifyCurrentScreen()
  775. {
  776. const QVector<ProjectInfo>& projects = GetAllProjects();
  777. const bool projectsFound = !projects.isEmpty();
  778. if (ShouldDisplayFirstTimeContent(projectsFound))
  779. {
  780. m_background.load(":/Backgrounds/FtueBackground.jpg");
  781. m_stack->setCurrentWidget(m_firstTimeContent);
  782. }
  783. else
  784. {
  785. m_background.load(":/Backgrounds/DefaultBackground.jpg");
  786. UpdateWithProjects(projects);
  787. }
  788. }
  789. bool ProjectsScreen::ShouldDisplayFirstTimeContent(bool projectsFound)
  790. {
  791. if (projectsFound)
  792. {
  793. return false;
  794. }
  795. // only show this screen once
  796. QSettings settings;
  797. bool displayFirstTimeContent = settings.value("displayFirstTimeContent", true).toBool();
  798. if (displayFirstTimeContent)
  799. {
  800. settings.setValue("displayFirstTimeContent", false);
  801. }
  802. return displayFirstTimeContent;
  803. }
  804. bool ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo, bool skipDialogBox)
  805. {
  806. if (ProjectUtils::FindSupportedCompiler(this))
  807. {
  808. bool proceedToBuild = skipDialogBox;
  809. if (!proceedToBuild)
  810. {
  811. QMessageBox::StandardButton buildProject = QMessageBox::information(
  812. this,
  813. tr("Building \"%1\"").arg(projectInfo.GetProjectDisplayName()),
  814. tr("Ready to build \"%1\"?").arg(projectInfo.GetProjectDisplayName()),
  815. QMessageBox::No | QMessageBox::Yes);
  816. proceedToBuild = buildProject == QMessageBox::Yes;
  817. }
  818. if (proceedToBuild)
  819. {
  820. m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
  821. UpdateWithProjects(GetAllProjects());
  822. connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
  823. connect(m_currentBuilder, &ProjectBuilderController::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
  824. m_currentBuilder->Start();
  825. }
  826. else
  827. {
  828. SuggestBuildProjectMsg(projectInfo, false);
  829. return false;
  830. }
  831. return true;
  832. }
  833. return false;
  834. }
  835. void ProjectsScreen::ProjectBuildDone(bool success)
  836. {
  837. ProjectInfo currentBuilderProject;
  838. if (!success)
  839. {
  840. currentBuilderProject = m_currentBuilder->GetProjectInfo();
  841. }
  842. delete m_currentBuilder;
  843. m_currentBuilder = nullptr;
  844. if (!success)
  845. {
  846. SuggestBuildProjectMsg(currentBuilderProject, false);
  847. }
  848. if (!m_buildQueue.empty())
  849. {
  850. while (!StartProjectBuild(m_buildQueue.front()) && m_buildQueue.size() > 1)
  851. {
  852. m_buildQueue.pop_front();
  853. }
  854. m_buildQueue.pop_front();
  855. }
  856. UpdateIfCurrentScreen();
  857. }
  858. QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
  859. {
  860. QString nativeProjPath(QDir::toNativeSeparators(projectPath));
  861. auto projectIter = m_requiresBuild.begin();
  862. for (; projectIter != m_requiresBuild.end(); ++projectIter)
  863. {
  864. if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
  865. {
  866. break;
  867. }
  868. }
  869. return projectIter;
  870. }
  871. bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
  872. {
  873. const AZ::IO::PathView path { projectPath.toUtf8().constData() };
  874. for (const ProjectInfo& project : m_buildQueue)
  875. {
  876. if (AZ::IO::PathView(project.m_path.toUtf8().constData()) == path)
  877. {
  878. return true;
  879. }
  880. }
  881. return false;
  882. }
  883. bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
  884. {
  885. if (BuildQueueContainsProject(projectPath))
  886. {
  887. QMessageBox::warning(
  888. this,
  889. tr("Action Temporarily Disabled!"),
  890. tr("Action not allowed on projects in build queue."));
  891. return true;
  892. }
  893. return false;
  894. }
  895. } // namespace O3DE::ProjectManager