CreateProjectCtrl.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  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 <CreateProjectCtrl.h>
  9. #include <ScreensCtrl.h>
  10. #include <PythonBindingsInterface.h>
  11. #include <NewProjectSettingsScreen.h>
  12. #include <ScreenHeaderWidget.h>
  13. #include <GemCatalog/GemModel.h>
  14. #include <ProjectGemCatalogScreen.h>
  15. #include <GemRepo/GemRepoScreen.h>
  16. #include <ProjectUtils.h>
  17. #include <DownloadController.h>
  18. #include <QDialogButtonBox>
  19. #include <QHBoxLayout>
  20. #include <QVBoxLayout>
  21. #include <QPushButton>
  22. #include <QMessageBox>
  23. #include <QStackedWidget>
  24. #include <QLabel>
  25. #include <QSizePolicy>
  26. #include <QFileInfo>
  27. namespace O3DE::ProjectManager
  28. {
  29. CreateProjectCtrl::CreateProjectCtrl(DownloadController* downloadController, QWidget* parent)
  30. : ScreenWidget(parent)
  31. {
  32. QVBoxLayout* vLayout = new QVBoxLayout();
  33. vLayout->setContentsMargins(0,0,0,0);
  34. m_header = new ScreenHeader(this);
  35. m_header->setTitle(tr("Create a New Project"));
  36. m_header->setSubTitle(tr("Enter Project Details"));
  37. connect(m_header->backButton(), &QPushButton::clicked, this, &CreateProjectCtrl::HandleBackButton);
  38. vLayout->addWidget(m_header);
  39. m_stack = new QStackedWidget(this);
  40. m_stack->setObjectName("body");
  41. m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred,QSizePolicy::Expanding));
  42. m_newProjectSettingsScreen = new NewProjectSettingsScreen(downloadController, this);
  43. m_stack->addWidget(m_newProjectSettingsScreen);
  44. m_projectGemCatalogScreen = new ProjectGemCatalogScreen(downloadController, this);
  45. m_stack->addWidget(m_projectGemCatalogScreen);
  46. m_gemRepoScreen = new GemRepoScreen(this);
  47. m_stack->addWidget(m_gemRepoScreen);
  48. vLayout->addWidget(m_stack);
  49. connect(m_projectGemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest);
  50. connect(static_cast<ScreensCtrl*>(parent), &ScreensCtrl::NotifyProjectRemoved, m_projectGemCatalogScreen, &GemCatalogScreen::NotifyProjectRemoved);
  51. // When there are multiple project templates present, we re-gather the gems when changing the selected the project template.
  52. connect(m_newProjectSettingsScreen, &NewProjectSettingsScreen::OnTemplateSelectionChanged, this, [=](int oldIndex, [[maybe_unused]] int newIndex)
  53. {
  54. const GemModel* gemModel = m_projectGemCatalogScreen->GetGemModel();
  55. const QVector<QModelIndex> toBeAdded = gemModel->GatherGemsToBeAdded();
  56. const QVector<QModelIndex> toBeRemoved = gemModel->GatherGemsToBeRemoved();
  57. if (!toBeAdded.isEmpty() || !toBeRemoved.isEmpty())
  58. {
  59. // In case the user enabled or disabled any gem and the current selection does not match the default from the
  60. // // project template anymore, we need to ask the user if they want to proceed as their modifications will be lost.
  61. const QString title = tr("Modifications will be lost");
  62. const QString text = tr("You selected a new project template after modifying the enabled gems.\n\n"
  63. "All modifications will be lost and the default from the new project template will be used.\n\n"
  64. "Do you want to proceed?");
  65. if (QMessageBox::warning(this, title, text, QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes)
  66. {
  67. // The users wants to proceed. Reinitialize based on the newly selected project template.
  68. ReinitGemCatalogForSelectedTemplate();
  69. }
  70. else
  71. {
  72. // Roll-back to the previously selected project template and
  73. // block signals so that we don't end up in this same callback again.
  74. m_newProjectSettingsScreen->SelectProjectTemplate(oldIndex, /*blockSignals=*/true);
  75. }
  76. }
  77. else
  78. {
  79. // In case the user did not enable or disable any gem and the currently enabled gems matches the previously selected
  80. // ones from the project template, we can just reinitialize based on the newly selected project template.
  81. ReinitGemCatalogForSelectedTemplate();
  82. }
  83. });
  84. QDialogButtonBox* buttons = new QDialogButtonBox();
  85. buttons->setObjectName("footer");
  86. vLayout->addWidget(buttons);
  87. m_primaryButton = buttons->addButton(tr("Create Project"), QDialogButtonBox::ApplyRole);
  88. m_primaryButton->setProperty("primary", true);
  89. connect(m_primaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandlePrimaryButton);
  90. connect(m_newProjectSettingsScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest);
  91. m_secondaryButton = buttons->addButton(tr("Back"), QDialogButtonBox::RejectRole);
  92. m_secondaryButton->setProperty("secondary", true);
  93. m_secondaryButton->setVisible(false);
  94. connect(m_secondaryButton, &QPushButton::clicked, this, &CreateProjectCtrl::HandleSecondaryButton);
  95. Update();
  96. setLayout(vLayout);
  97. }
  98. ProjectManagerScreen CreateProjectCtrl::GetScreenEnum()
  99. {
  100. return ProjectManagerScreen::CreateProject;
  101. }
  102. // Called when pressing "Create New Project"
  103. void CreateProjectCtrl::NotifyCurrentScreen()
  104. {
  105. ScreenWidget* currentScreen = static_cast<ScreenWidget*>(m_stack->currentWidget());
  106. if (currentScreen)
  107. {
  108. currentScreen->NotifyCurrentScreen();
  109. }
  110. // Gather the enabled gems from the default project template when starting the create new project workflow.
  111. ReinitGemCatalogForSelectedTemplate();
  112. // make sure the gem repo has the latest details
  113. m_gemRepoScreen->Reinit();
  114. }
  115. void CreateProjectCtrl::HandleBackButton()
  116. {
  117. if (m_stack->currentIndex() > 0)
  118. {
  119. PreviousScreen();
  120. }
  121. else
  122. {
  123. emit GoToPreviousScreenRequest();
  124. }
  125. }
  126. void CreateProjectCtrl::HandleSecondaryButton()
  127. {
  128. if (m_stack->currentIndex() > 0)
  129. {
  130. // return to Project Settings page
  131. PreviousScreen();
  132. }
  133. else
  134. {
  135. // Configure Gems
  136. NextScreen();
  137. }
  138. }
  139. void CreateProjectCtrl::Update()
  140. {
  141. if (m_stack->currentWidget() == m_projectGemCatalogScreen)
  142. {
  143. m_header->setSubTitle(tr("Configure project with Gems"));
  144. m_secondaryButton->setVisible(false);
  145. m_primaryButton->setVisible(true);
  146. }
  147. else if (m_stack->currentWidget() == m_gemRepoScreen)
  148. {
  149. m_header->setSubTitle(tr("Remote Sources"));
  150. m_secondaryButton->setVisible(true);
  151. m_secondaryButton->setText(tr("Back"));
  152. m_primaryButton->setVisible(false);
  153. }
  154. else
  155. {
  156. m_header->setSubTitle(tr("Enter Project Details"));
  157. m_secondaryButton->setVisible(true);
  158. m_secondaryButton->setText(tr("Configure Gems"));
  159. m_primaryButton->setVisible(true);
  160. }
  161. }
  162. void CreateProjectCtrl::OnChangeScreenRequest(ProjectManagerScreen screen)
  163. {
  164. if (screen == ProjectManagerScreen::ProjectGemCatalog)
  165. {
  166. HandleSecondaryButton();
  167. }
  168. else if (screen == ProjectManagerScreen::GemRepos)
  169. {
  170. NextScreen();
  171. }
  172. else
  173. {
  174. emit ChangeScreenRequest(screen);
  175. }
  176. }
  177. void CreateProjectCtrl::NextScreen()
  178. {
  179. if (m_stack->currentIndex() < m_stack->count())
  180. {
  181. // special case where we need to download the template before proceeding
  182. if (m_stack->currentWidget() == m_newProjectSettingsScreen)
  183. {
  184. if (m_newProjectSettingsScreen->GetProjectTemplatePath().isEmpty() &&
  185. !m_newProjectSettingsScreen->IsDownloadingTemplate())
  186. {
  187. m_newProjectSettingsScreen->ShowDownloadTemplateDialog();
  188. return;
  189. }
  190. else if (m_newProjectSettingsScreen->IsDownloadingTemplate())
  191. {
  192. QMessageBox::warning(this, tr("Cannot configure gems"), tr("Cannot configure gems until the template has finished downloading."));
  193. return;
  194. }
  195. }
  196. if(auto outcome = CurrentScreenIsValid(); outcome.IsSuccess())
  197. {
  198. m_stack->setCurrentIndex(m_stack->currentIndex() + 1);
  199. ScreenWidget* currentScreen = static_cast<ScreenWidget*>(m_stack->currentWidget());
  200. if (currentScreen)
  201. {
  202. currentScreen->NotifyCurrentScreen();
  203. }
  204. Update();
  205. }
  206. else if (!outcome.GetError().isEmpty())
  207. {
  208. QMessageBox::warning(this, tr("Cannot continue"), outcome.GetError());
  209. }
  210. else
  211. {
  212. QMessageBox::warning(this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
  213. }
  214. }
  215. }
  216. void CreateProjectCtrl::PreviousScreen()
  217. {
  218. // we don't require the current screen to be valid when moving back
  219. if (m_stack->currentIndex() > 0)
  220. {
  221. m_stack->setCurrentIndex(m_stack->currentIndex() - 1);
  222. ScreenWidget* currentScreen = static_cast<ScreenWidget*>(m_stack->currentWidget());
  223. if (currentScreen)
  224. {
  225. currentScreen->NotifyCurrentScreen();
  226. }
  227. Update();
  228. }
  229. }
  230. void CreateProjectCtrl::HandlePrimaryButton()
  231. {
  232. CreateProject();
  233. }
  234. AZ::Outcome<void, QString> CreateProjectCtrl::CurrentScreenIsValid()
  235. {
  236. if (m_stack->currentWidget() == m_newProjectSettingsScreen)
  237. {
  238. return m_newProjectSettingsScreen->Validate();
  239. }
  240. return AZ::Success();
  241. }
  242. void CreateProjectCtrl::CreateProject()
  243. {
  244. AZ::Outcome<void, QString> settingsValidation = m_newProjectSettingsScreen->Validate();
  245. if (settingsValidation.IsSuccess())
  246. {
  247. if (!m_projectGemCatalogScreen->GetDownloadController()->IsDownloadQueueEmpty())
  248. {
  249. QMessageBox::critical(this, tr("Gems downloading"), tr("You must wait for gems to finish downloading before continuing."));
  250. return;
  251. }
  252. ProjectInfo projectInfo = m_newProjectSettingsScreen->GetProjectInfo();
  253. QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
  254. // create in 2 steps for better error handling
  255. auto createResult = PythonBindingsInterface::Get()->CreateProject(projectTemplatePath, projectInfo, /*registerProject*/false);
  256. if (!createResult)
  257. {
  258. const IPythonBindings::ErrorPair& error = createResult.GetError();
  259. ProjectUtils::DisplayDetailedError(tr("Failed to create project"), error.first, error.second, this);
  260. return;
  261. }
  262. // RegisterProject will check compatibility and prompt user to continue if issues found
  263. // it will also handle detailed error messaging
  264. if(!ProjectUtils::RegisterProject(projectInfo.m_path, this))
  265. {
  266. // Since the project files were created during this workflow, but register project flow was cancelled or errored out,
  267. // clean up the created files here.
  268. [[maybe_unused]] bool filesDeleted = ProjectUtils::DeleteProjectFiles(projectInfo.m_path, /*force*/ true);
  269. AZ_Warning("O3DE", filesDeleted, "Unable to delete invalid new project files at %s", projectInfo.m_path.toUtf8().constData());
  270. return;
  271. }
  272. const ProjectGemCatalogScreen::ConfiguredGemsResult gemResult = m_projectGemCatalogScreen->ConfigureGemsForProject(projectInfo.m_path);
  273. if (gemResult == ProjectGemCatalogScreen::ConfiguredGemsResult::Failed)
  274. {
  275. QMessageBox::critical(this, tr("Failed to configure gems"), tr("Failed to configure gems for template."));
  276. }
  277. if (gemResult != ProjectGemCatalogScreen::ConfiguredGemsResult::Success)
  278. {
  279. return;
  280. }
  281. projectInfo.m_needsBuild = true;
  282. emit NotifyBuildProject(projectInfo);
  283. emit ChangeScreenRequest(ProjectManagerScreen::Projects);
  284. }
  285. else
  286. {
  287. const QString& errorMessage = settingsValidation.GetError();
  288. if (errorMessage.isEmpty())
  289. {
  290. QMessageBox::warning(
  291. this, tr("Invalid project settings"), tr("Please correct the indicated project settings and try again."));
  292. }
  293. else
  294. {
  295. QMessageBox::warning(this, tr("Invalid project settings"), errorMessage);
  296. }
  297. }
  298. }
  299. void CreateProjectCtrl::ReinitGemCatalogForSelectedTemplate()
  300. {
  301. const QString projectTemplatePath = m_newProjectSettingsScreen->GetProjectTemplatePath();
  302. if (projectTemplatePath.isEmpty())
  303. {
  304. return;
  305. }
  306. m_projectGemCatalogScreen->ReinitForProject(projectTemplatePath + "/Template");
  307. }
  308. } // namespace O3DE::ProjectManager