ProjectsScreen.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /*
  2. * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. * its licensors.
  4. *
  5. * For complete copyright and license terms please see the LICENSE at the root of this
  6. * distribution (the "License"). All use of this software is governed by the License,
  7. * or, if provided, by the license below or the license accompanying this file. Do not
  8. * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  9. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. *
  11. */
  12. #include <ProjectsScreen.h>
  13. #include <ProjectButtonWidget.h>
  14. #include <PythonBindingsInterface.h>
  15. #include <ProjectUtils.h>
  16. #include <ProjectBuilder.h>
  17. #include <ScreensCtrl.h>
  18. #include <AzQtComponents/Components/FlowLayout.h>
  19. #include <AzCore/Platform.h>
  20. #include <AzCore/IO/SystemFile.h>
  21. #include <AzFramework/AzFramework_Traits_Platform.h>
  22. #include <AzFramework/Process/ProcessCommon.h>
  23. #include <AzFramework/Process/ProcessWatcher.h>
  24. #include <AzCore/Utils/Utils.h>
  25. #include <QVBoxLayout>
  26. #include <QHBoxLayout>
  27. #include <QLabel>
  28. #include <QPushButton>
  29. #include <QMenu>
  30. #include <QListView>
  31. #include <QSpacerItem>
  32. #include <QListWidget>
  33. #include <QListWidgetItem>
  34. #include <QFileInfo>
  35. #include <QScrollArea>
  36. #include <QStackedWidget>
  37. #include <QFrame>
  38. #include <QIcon>
  39. #include <QPixmap>
  40. #include <QSettings>
  41. #include <QMessageBox>
  42. #include <QTimer>
  43. #include <QQueue>
  44. #include <QDir>
  45. //#define DISPLAY_PROJECT_DEV_DATA true
  46. namespace O3DE::ProjectManager
  47. {
  48. ProjectsScreen::ProjectsScreen(QWidget* parent)
  49. : ScreenWidget(parent)
  50. {
  51. QVBoxLayout* vLayout = new QVBoxLayout();
  52. vLayout->setAlignment(Qt::AlignTop);
  53. vLayout->setContentsMargins(s_contentMargins, 0, s_contentMargins, 0);
  54. setLayout(vLayout);
  55. m_background.load(":/Backgrounds/FirstTimeBackgroundImage.jpg");
  56. m_stack = new QStackedWidget(this);
  57. m_firstTimeContent = CreateFirstTimeContent();
  58. m_stack->addWidget(m_firstTimeContent);
  59. m_projectsContent = CreateProjectsContent();
  60. m_stack->addWidget(m_projectsContent);
  61. vLayout->addWidget(m_stack);
  62. connect(reinterpret_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyBuildProject, this, &ProjectsScreen::SuggestBuildProject);
  63. }
  64. ProjectsScreen::~ProjectsScreen()
  65. {
  66. delete m_currentBuilder;
  67. }
  68. QFrame* ProjectsScreen::CreateFirstTimeContent()
  69. {
  70. QFrame* frame = new QFrame(this);
  71. frame->setObjectName("firstTimeContent");
  72. {
  73. QVBoxLayout* layout = new QVBoxLayout();
  74. layout->setContentsMargins(0, 0, 0, 0);
  75. layout->setAlignment(Qt::AlignTop);
  76. frame->setLayout(layout);
  77. QLabel* titleLabel = new QLabel(tr("Ready. Set. Create."), this);
  78. titleLabel->setObjectName("titleLabel");
  79. layout->addWidget(titleLabel);
  80. QLabel* introLabel = new QLabel(this);
  81. introLabel->setObjectName("introLabel");
  82. introLabel->setText(tr("Welcome to O3DE! Start something new by creating a project. Not sure what to create? \nExplore what's "
  83. "available by downloading our sample project."));
  84. layout->addWidget(introLabel);
  85. QHBoxLayout* buttonLayout = new QHBoxLayout();
  86. buttonLayout->setAlignment(Qt::AlignLeft);
  87. buttonLayout->setSpacing(s_spacerSize);
  88. // use a newline to force the text up
  89. QPushButton* createProjectButton = new QPushButton(tr("Create a Project\n"), this);
  90. createProjectButton->setObjectName("createProjectButton");
  91. buttonLayout->addWidget(createProjectButton);
  92. QPushButton* addProjectButton = new QPushButton(tr("Add a Project\n"), this);
  93. addProjectButton->setObjectName("addProjectButton");
  94. buttonLayout->addWidget(addProjectButton);
  95. connect(createProjectButton, &QPushButton::clicked, this, &ProjectsScreen::HandleNewProjectButton);
  96. connect(addProjectButton, &QPushButton::clicked, this, &ProjectsScreen::HandleAddProjectButton);
  97. layout->addLayout(buttonLayout);
  98. }
  99. return frame;
  100. }
  101. QFrame* ProjectsScreen::CreateProjectsContent(QString buildProjectPath, ProjectButton** projectButton)
  102. {
  103. QFrame* frame = new QFrame(this);
  104. frame->setObjectName("projectsContent");
  105. {
  106. QVBoxLayout* layout = new QVBoxLayout();
  107. layout->setAlignment(Qt::AlignTop);
  108. layout->setContentsMargins(0, 0, 0, 0);
  109. frame->setLayout(layout);
  110. QFrame* header = new QFrame(this);
  111. QHBoxLayout* headerLayout = new QHBoxLayout();
  112. {
  113. QLabel* titleLabel = new QLabel(tr("My Projects"), this);
  114. titleLabel->setObjectName("titleLabel");
  115. headerLayout->addWidget(titleLabel);
  116. QMenu* newProjectMenu = new QMenu(this);
  117. m_createNewProjectAction = newProjectMenu->addAction("Create New Project");
  118. m_addExistingProjectAction = newProjectMenu->addAction("Add Existing Project");
  119. connect(m_createNewProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleNewProjectButton);
  120. connect(m_addExistingProjectAction, &QAction::triggered, this, &ProjectsScreen::HandleAddProjectButton);
  121. QPushButton* newProjectMenuButton = new QPushButton(tr("New Project..."), this);
  122. newProjectMenuButton->setObjectName("newProjectButton");
  123. newProjectMenuButton->setMenu(newProjectMenu);
  124. newProjectMenuButton->setDefault(true);
  125. headerLayout->addWidget(newProjectMenuButton);
  126. }
  127. header->setLayout(headerLayout);
  128. layout->addWidget(header);
  129. // Get all projects and create a horizontal scrolling list of them
  130. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  131. if (projectsResult.IsSuccess() && !projectsResult.GetValue().isEmpty())
  132. {
  133. QScrollArea* projectsScrollArea = new QScrollArea(this);
  134. QWidget* scrollWidget = new QWidget();
  135. FlowLayout* flowLayout = new FlowLayout(0, s_spacerSize, s_spacerSize);
  136. scrollWidget->setLayout(flowLayout);
  137. projectsScrollArea->setWidget(scrollWidget);
  138. projectsScrollArea->setWidgetResizable(true);
  139. #ifndef DISPLAY_PROJECT_DEV_DATA
  140. // Iterate once to insert building project first
  141. if (!buildProjectPath.isEmpty())
  142. {
  143. buildProjectPath = QDir::fromNativeSeparators(buildProjectPath);
  144. for (auto project : projectsResult.GetValue())
  145. {
  146. if (QDir::fromNativeSeparators(project.m_path) == buildProjectPath)
  147. {
  148. ProjectButton* buildingProjectButton = CreateProjectButton(project, flowLayout, true);
  149. if (projectButton)
  150. {
  151. *projectButton = buildingProjectButton;
  152. }
  153. break;
  154. }
  155. }
  156. }
  157. for (auto project : projectsResult.GetValue())
  158. #else
  159. ProjectInfo project = projectsResult.GetValue().at(0);
  160. for (int i = 0; i < 15; i++)
  161. #endif
  162. {
  163. // Add all other projects skipping building project
  164. // Safe if no building project because it is just an empty string
  165. if (project.m_path != buildProjectPath)
  166. {
  167. ProjectButton* projectButtonWidget = CreateProjectButton(project, flowLayout);
  168. if (RequiresBuildProjectIterator(project.m_path) != m_requiresBuild.end())
  169. {
  170. projectButtonWidget->ShowBuildButton(true);
  171. }
  172. }
  173. }
  174. layout->addWidget(projectsScrollArea);
  175. }
  176. }
  177. return frame;
  178. }
  179. ProjectButton* ProjectsScreen::CreateProjectButton(ProjectInfo& project, QLayout* flowLayout, bool processing)
  180. {
  181. ProjectButton* projectButton;
  182. QString projectPreviewPath = project.m_path + m_projectPreviewImagePath;
  183. QFileInfo doesPreviewExist(projectPreviewPath);
  184. if (doesPreviewExist.exists() && doesPreviewExist.isFile())
  185. {
  186. project.m_imagePath = projectPreviewPath;
  187. }
  188. projectButton = new ProjectButton(project, this, processing);
  189. flowLayout->addWidget(projectButton);
  190. if (!processing)
  191. {
  192. connect(projectButton, &ProjectButton::OpenProject, this, &ProjectsScreen::HandleOpenProject);
  193. connect(projectButton, &ProjectButton::EditProject, this, &ProjectsScreen::HandleEditProject);
  194. connect(projectButton, &ProjectButton::CopyProject, this, &ProjectsScreen::HandleCopyProject);
  195. connect(projectButton, &ProjectButton::RemoveProject, this, &ProjectsScreen::HandleRemoveProject);
  196. connect(projectButton, &ProjectButton::DeleteProject, this, &ProjectsScreen::HandleDeleteProject);
  197. }
  198. connect(projectButton, &ProjectButton::BuildProject, this, &ProjectsScreen::QueueBuildProject);
  199. return projectButton;
  200. }
  201. void ProjectsScreen::ResetProjectsContent()
  202. {
  203. // refresh the projects content by re-creating it for now
  204. if (m_projectsContent)
  205. {
  206. m_stack->removeWidget(m_projectsContent);
  207. m_projectsContent->deleteLater();
  208. }
  209. // Make sure to update builder with latest Project Button
  210. if (m_currentBuilder)
  211. {
  212. ProjectButton* projectButtonPtr;
  213. m_projectsContent = CreateProjectsContent(m_currentBuilder->GetProjectPath(), &projectButtonPtr);
  214. m_currentBuilder->SetProjectButton(projectButtonPtr);
  215. }
  216. else
  217. {
  218. m_projectsContent = CreateProjectsContent();
  219. }
  220. m_stack->addWidget(m_projectsContent);
  221. m_stack->setCurrentWidget(m_projectsContent);
  222. }
  223. ProjectManagerScreen ProjectsScreen::GetScreenEnum()
  224. {
  225. return ProjectManagerScreen::Projects;
  226. }
  227. bool ProjectsScreen::IsTab()
  228. {
  229. return true;
  230. }
  231. QString ProjectsScreen::GetTabText()
  232. {
  233. return tr("Projects");
  234. }
  235. void ProjectsScreen::paintEvent([[maybe_unused]] QPaintEvent* event)
  236. {
  237. // we paint the background here because qss does not support background cover scaling
  238. QPainter painter(this);
  239. auto winSize = size();
  240. auto pixmapRatio = (float)m_background.width() / m_background.height();
  241. auto windowRatio = (float)winSize.width() / winSize.height();
  242. if (pixmapRatio > windowRatio)
  243. {
  244. auto newWidth = (int)(winSize.height() * pixmapRatio);
  245. auto offset = (newWidth - winSize.width()) / -2;
  246. painter.drawPixmap(offset, 0, newWidth, winSize.height(), m_background);
  247. }
  248. else
  249. {
  250. auto newHeight = (int)(winSize.width() / pixmapRatio);
  251. painter.drawPixmap(0, 0, winSize.width(), newHeight, m_background);
  252. }
  253. }
  254. void ProjectsScreen::HandleNewProjectButton()
  255. {
  256. emit ResetScreenRequest(ProjectManagerScreen::CreateProject);
  257. emit ChangeScreenRequest(ProjectManagerScreen::CreateProject);
  258. }
  259. void ProjectsScreen::HandleAddProjectButton()
  260. {
  261. if (ProjectUtils::AddProjectDialog(this))
  262. {
  263. ResetProjectsContent();
  264. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  265. }
  266. }
  267. void ProjectsScreen::HandleOpenProject(const QString& projectPath)
  268. {
  269. if (!projectPath.isEmpty())
  270. {
  271. if (!WarnIfInBuildQueue(projectPath))
  272. {
  273. AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
  274. AZStd::string executableFilename = "Editor";
  275. AZ::IO::FixedMaxPath editorExecutablePath = executableDirectory / (executableFilename + AZ_TRAIT_OS_EXECUTABLE_EXTENSION);
  276. auto cmdPath = AZ::IO::FixedMaxPathString::format(
  277. "%s -regset=\"/Amazon/AzCore/Bootstrap/project_path=%s\"", editorExecutablePath.c_str(),
  278. projectPath.toStdString().c_str());
  279. AzFramework::ProcessLauncher::ProcessLaunchInfo processLaunchInfo;
  280. processLaunchInfo.m_commandlineParameters = cmdPath;
  281. bool launchSucceeded = AzFramework::ProcessLauncher::LaunchUnwatchedProcess(processLaunchInfo);
  282. if (!launchSucceeded)
  283. {
  284. AZ_Error("ProjectManager", false, "Failed to launch editor");
  285. QMessageBox::critical(
  286. this, tr("Error"), tr("Failed to launch the Editor, please verify the project settings are valid."));
  287. }
  288. else
  289. {
  290. // prevent the user from accidentally pressing the button while the editor is launching
  291. // and let them know what's happening
  292. ProjectButton* button = qobject_cast<ProjectButton*>(sender());
  293. if (button)
  294. {
  295. button->SetLaunchButtonEnabled(false);
  296. button->SetButtonOverlayText(tr("Opening Editor..."));
  297. }
  298. // enable the button after 3 seconds
  299. constexpr int waitTimeInMs = 3000;
  300. QTimer::singleShot(
  301. waitTimeInMs, this,
  302. [this, button]
  303. {
  304. if (button)
  305. {
  306. button->SetLaunchButtonEnabled(true);
  307. }
  308. });
  309. }
  310. }
  311. }
  312. else
  313. {
  314. AZ_Error("ProjectManager", false, "Cannot open editor because an empty project path was provided");
  315. QMessageBox::critical( this, tr("Error"), tr("Failed to launch the Editor because the project path is invalid."));
  316. }
  317. }
  318. void ProjectsScreen::HandleEditProject(const QString& projectPath)
  319. {
  320. if (!WarnIfInBuildQueue(projectPath))
  321. {
  322. emit NotifyCurrentProject(projectPath);
  323. emit ChangeScreenRequest(ProjectManagerScreen::UpdateProject);
  324. }
  325. }
  326. void ProjectsScreen::HandleCopyProject(const QString& projectPath)
  327. {
  328. if (!WarnIfInBuildQueue(projectPath))
  329. {
  330. // Open file dialog and choose location for copied project then register copy with O3DE
  331. if (ProjectUtils::CopyProjectDialog(projectPath, this))
  332. {
  333. ResetProjectsContent();
  334. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  335. }
  336. }
  337. }
  338. void ProjectsScreen::HandleRemoveProject(const QString& projectPath)
  339. {
  340. if (!WarnIfInBuildQueue(projectPath))
  341. {
  342. // Unregister Project from O3DE and reload projects
  343. if (ProjectUtils::UnregisterProject(projectPath))
  344. {
  345. ResetProjectsContent();
  346. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  347. }
  348. }
  349. }
  350. void ProjectsScreen::HandleDeleteProject(const QString& projectPath)
  351. {
  352. if (!WarnIfInBuildQueue(projectPath))
  353. {
  354. QMessageBox::StandardButton warningResult = QMessageBox::warning(this,
  355. tr("Delete Project"),
  356. tr("Are you sure?\nProject will be unregistered from O3DE and project directory will be deleted from your disk."),
  357. QMessageBox::No | QMessageBox::Yes);
  358. if (warningResult == QMessageBox::Yes)
  359. {
  360. // Remove project from O3DE and delete from disk
  361. HandleRemoveProject(projectPath);
  362. ProjectUtils::DeleteProjectFiles(projectPath);
  363. }
  364. }
  365. }
  366. void ProjectsScreen::SuggestBuildProject(const ProjectInfo& projectInfo)
  367. {
  368. if (projectInfo.m_needsBuild)
  369. {
  370. if (RequiresBuildProjectIterator(projectInfo.m_path) == m_requiresBuild.end())
  371. {
  372. m_requiresBuild.append(projectInfo);
  373. }
  374. ResetProjectsContent();
  375. }
  376. else
  377. {
  378. QMessageBox::information(this,
  379. tr("Project Should be rebuilt."),
  380. projectInfo.m_projectName + tr(" project likely needs to be rebuilt."));
  381. }
  382. }
  383. void ProjectsScreen::QueueBuildProject(const ProjectInfo& projectInfo)
  384. {
  385. auto requiredIter = RequiresBuildProjectIterator(projectInfo.m_path);
  386. if (requiredIter != m_requiresBuild.end())
  387. {
  388. m_requiresBuild.erase(requiredIter);
  389. }
  390. if (!BuildQueueContainsProject(projectInfo.m_path))
  391. {
  392. if (m_buildQueue.empty() && !m_currentBuilder)
  393. {
  394. StartProjectBuild(projectInfo);
  395. }
  396. else
  397. {
  398. m_buildQueue.append(projectInfo);
  399. }
  400. }
  401. }
  402. void ProjectsScreen::NotifyCurrentScreen()
  403. {
  404. if (ShouldDisplayFirstTimeContent())
  405. {
  406. m_stack->setCurrentWidget(m_firstTimeContent);
  407. }
  408. else
  409. {
  410. ResetProjectsContent();
  411. }
  412. }
  413. bool ProjectsScreen::ShouldDisplayFirstTimeContent()
  414. {
  415. auto projectsResult = PythonBindingsInterface::Get()->GetProjects();
  416. if (!projectsResult.IsSuccess() || projectsResult.GetValue().isEmpty())
  417. {
  418. return true;
  419. }
  420. QSettings settings;
  421. bool displayFirstTimeContent = settings.value("displayFirstTimeContent", true).toBool();
  422. if (displayFirstTimeContent)
  423. {
  424. settings.setValue("displayFirstTimeContent", false);
  425. }
  426. return displayFirstTimeContent;
  427. }
  428. void ProjectsScreen::StartProjectBuild(const ProjectInfo& projectInfo)
  429. {
  430. if (ProjectUtils::IsVS2019Installed())
  431. {
  432. QMessageBox::StandardButton buildProject = QMessageBox::information(
  433. this,
  434. tr("Building \"%1\"").arg(projectInfo.m_projectName),
  435. tr("Ready to build \"%1\"?").arg(projectInfo.m_projectName),
  436. QMessageBox::No | QMessageBox::Yes);
  437. if (buildProject == QMessageBox::Yes)
  438. {
  439. m_currentBuilder = new ProjectBuilderController(projectInfo, nullptr, this);
  440. ResetProjectsContent();
  441. connect(m_currentBuilder, &ProjectBuilderController::Done, this, &ProjectsScreen::ProjectBuildDone);
  442. m_currentBuilder->Start();
  443. }
  444. else
  445. {
  446. ProjectBuildDone();
  447. }
  448. }
  449. }
  450. void ProjectsScreen::ProjectBuildDone()
  451. {
  452. delete m_currentBuilder;
  453. m_currentBuilder = nullptr;
  454. if (!m_buildQueue.empty())
  455. {
  456. StartProjectBuild(m_buildQueue.front());
  457. m_buildQueue.pop_front();
  458. }
  459. else
  460. {
  461. ResetProjectsContent();
  462. }
  463. }
  464. QList<ProjectInfo>::iterator ProjectsScreen::RequiresBuildProjectIterator(const QString& projectPath)
  465. {
  466. QString nativeProjPath(QDir::toNativeSeparators(projectPath));
  467. auto projectIter = m_requiresBuild.begin();
  468. for (; projectIter != m_requiresBuild.end(); ++projectIter)
  469. {
  470. if (QDir::toNativeSeparators(projectIter->m_path) == nativeProjPath)
  471. {
  472. break;
  473. }
  474. }
  475. return projectIter;
  476. }
  477. bool ProjectsScreen::BuildQueueContainsProject(const QString& projectPath)
  478. {
  479. QString nativeProjPath(QDir::toNativeSeparators(projectPath));
  480. for (const ProjectInfo& project : m_buildQueue)
  481. {
  482. if (QDir::toNativeSeparators(project.m_path) == nativeProjPath)
  483. {
  484. return true;
  485. }
  486. }
  487. return false;
  488. }
  489. bool ProjectsScreen::WarnIfInBuildQueue(const QString& projectPath)
  490. {
  491. if (BuildQueueContainsProject(projectPath))
  492. {
  493. QMessageBox::warning(
  494. this,
  495. tr("Action Temporarily Disabled!"),
  496. tr("Action not allowed on projects in build queue."));
  497. return true;
  498. }
  499. return false;
  500. }
  501. } // namespace O3DE::ProjectManager