NewProjectSettingsScreen.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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 <NewProjectSettingsScreen.h>
  9. #include <ProjectManagerDefs.h>
  10. #include <PythonBindingsInterface.h>
  11. #include <FormBrowseEditWidget.h>
  12. #include <FormLineEditWidget.h>
  13. #include <TemplateButtonWidget.h>
  14. #include <PathValidator.h>
  15. #include <EngineInfo.h>
  16. #include <CreateProjectCtrl.h>
  17. #include <TagWidget.h>
  18. #include <ProjectUtils.h>
  19. #include <AddRemoteTemplateDialog.h>
  20. #include <DownloadRemoteTemplateDialog.h>
  21. #include <AzCore/Math/Uuid.h>
  22. #include <AzCore/std/ranges/ranges_algorithm.h>
  23. #include <AzQtComponents/Components/FlowLayout.h>
  24. #include <QVBoxLayout>
  25. #include <QHBoxLayout>
  26. #include <QFileDialog>
  27. #include <QLabel>
  28. #include <QLineEdit>
  29. #include <QRadioButton>
  30. #include <QButtonGroup>
  31. #include <QPushButton>
  32. #include <QSpacerItem>
  33. #include <QFrame>
  34. #include <QScrollArea>
  35. #include <QAbstractButton>
  36. namespace O3DE::ProjectManager
  37. {
  38. constexpr const char* k_templateIndexProperty = "TemplateIndex";
  39. constexpr const char* k_templateNameProperty = "TemplateName";
  40. NewProjectSettingsScreen::NewProjectSettingsScreen(DownloadController* downloadController, QWidget* parent)
  41. : ProjectSettingsScreen(parent)
  42. , m_downloadController(downloadController)
  43. {
  44. const QString defaultName = GetDefaultProjectName();
  45. const QString defaultPath = QDir::toNativeSeparators(ProjectUtils::GetDefaultProjectPath() + "/" + defaultName);
  46. m_projectName->lineEdit()->setText(defaultName);
  47. m_projectPath->lineEdit()->setText(defaultPath);
  48. // if we don't use a QFrame we cannot "contain" the widgets inside and move them around
  49. // as a group
  50. QFrame* projectTemplateWidget = new QFrame(this);
  51. projectTemplateWidget->setObjectName("projectTemplate");
  52. QVBoxLayout* containerLayout = new QVBoxLayout();
  53. containerLayout->setAlignment(Qt::AlignTop);
  54. {
  55. QLabel* projectTemplateLabel = new QLabel(tr("Select a Project Template"));
  56. projectTemplateLabel->setObjectName("projectTemplateLabel");
  57. containerLayout->addWidget(projectTemplateLabel);
  58. QLabel* projectTemplateDetailsLabel = new QLabel(tr("Project templates are pre-configured with relevant Gems that provide "
  59. "additional functionality and content to the project."));
  60. projectTemplateDetailsLabel->setWordWrap(true);
  61. projectTemplateDetailsLabel->setObjectName("projectTemplateDetailsLabel");
  62. containerLayout->addWidget(projectTemplateDetailsLabel);
  63. // we might have enough templates that we need to scroll
  64. QScrollArea* templatesScrollArea = new QScrollArea(this);
  65. QWidget* scrollWidget = new QWidget();
  66. m_templateFlowLayout = new FlowLayout(0, s_spacerSize, s_spacerSize);
  67. scrollWidget->setLayout(m_templateFlowLayout);
  68. templatesScrollArea->setWidget(scrollWidget);
  69. templatesScrollArea->setWidgetResizable(true);
  70. m_projectTemplateButtonGroup = new QButtonGroup(this);
  71. m_projectTemplateButtonGroup->setObjectName("templateButtonGroup");
  72. // QButtonGroup has overloaded buttonClicked methods so we need the QOverload
  73. connect(
  74. m_projectTemplateButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this,
  75. [=](QAbstractButton* button)
  76. {
  77. if (button && button->property(k_templateIndexProperty).isValid())
  78. {
  79. int projectTemplateIndex = button->property(k_templateIndexProperty).toInt();
  80. if (m_selectedTemplateIndex != projectTemplateIndex)
  81. {
  82. const int oldIndex = m_selectedTemplateIndex;
  83. m_selectedTemplateIndex = projectTemplateIndex;
  84. UpdateTemplateDetails(m_templates.at(m_selectedTemplateIndex));
  85. emit OnTemplateSelectionChanged(/*oldIndex=*/oldIndex, /*newIndex=*/m_selectedTemplateIndex);
  86. }
  87. }
  88. else if (button == m_remoteTemplateButton)
  89. {
  90. AddRemoteTemplateDialog* addRemoteTemplateDialog = new AddRemoteTemplateDialog(this);
  91. if (addRemoteTemplateDialog->exec() == QDialog::DialogCode::Accepted)
  92. {
  93. auto remoteTemplatesResult =
  94. PythonBindingsInterface::Get()->GetProjectTemplatesForRepo(addRemoteTemplateDialog->GetRepoPath());
  95. if (remoteTemplatesResult.IsSuccess() && !remoteTemplatesResult.GetValue().isEmpty())
  96. {
  97. // remove remote template button from layout so we can insert the new templates before it
  98. m_templateFlowLayout->removeWidget(m_remoteTemplateButton);
  99. int currentTemplateIndex = m_templates.size();
  100. const QVector<ProjectTemplateInfo>& remoteTemplates = remoteTemplatesResult.GetValue();
  101. for (const ProjectTemplateInfo& remoteTemplate : remoteTemplates)
  102. {
  103. m_templates.push_back(remoteTemplate);
  104. // create template button
  105. QString projectPreviewPath = QDir(remoteTemplate.m_path).filePath(ProjectPreviewImagePath);
  106. QFileInfo doesPreviewExist(projectPreviewPath);
  107. if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
  108. {
  109. projectPreviewPath = ":/DefaultTemplate.png";
  110. }
  111. TemplateButton* templateButton =
  112. new TemplateButton(projectPreviewPath, remoteTemplate.m_displayName, this);
  113. templateButton->SetIsRemote(remoteTemplate.m_isRemote);
  114. templateButton->setCheckable(true);
  115. templateButton->setProperty(k_templateIndexProperty, currentTemplateIndex);
  116. templateButton->setProperty(k_templateNameProperty, remoteTemplate.m_name);
  117. m_projectTemplateButtonGroup->addButton(templateButton);
  118. m_templateFlowLayout->addWidget(templateButton);
  119. m_templateButtons.append(templateButton);
  120. ++currentTemplateIndex;
  121. }
  122. // add remote template button back to layout
  123. m_templateFlowLayout->addWidget(m_remoteTemplateButton);
  124. }
  125. }
  126. }
  127. });
  128. containerLayout->addWidget(templatesScrollArea);
  129. }
  130. projectTemplateWidget->setLayout(containerLayout);
  131. m_verticalLayout->addWidget(projectTemplateWidget);
  132. QFrame* projectTemplateDetails = CreateTemplateDetails(s_templateDetailsContentMargin);
  133. projectTemplateDetails->setObjectName("projectTemplateDetails");
  134. m_horizontalLayout->addWidget(projectTemplateDetails);
  135. connect(m_downloadController, &DownloadController::Done, this, &NewProjectSettingsScreen::HandleDownloadResult);
  136. connect(m_downloadController, &DownloadController::ObjectDownloadProgress, this, &NewProjectSettingsScreen::HandleDownloadProgress);
  137. }
  138. bool NewProjectSettingsScreen::IsDownloadingTemplate() const
  139. {
  140. if (m_selectedTemplateIndex < 0 || (m_selectedTemplateIndex > m_templates.size() - 1))
  141. {
  142. return false;
  143. }
  144. const ProjectTemplateInfo& templateInfo = m_templates.at(m_selectedTemplateIndex);
  145. return m_downloadController->IsDownloadingObject(templateInfo.m_name, DownloadController::Template);
  146. }
  147. void NewProjectSettingsScreen::HandleDownloadResult(const QString& templateName, bool succeeded)
  148. {
  149. auto foundButton = AZStd::ranges::find_if(
  150. m_templateButtons,
  151. [&templateName](const QAbstractButton* value)
  152. {
  153. return value->property(k_templateNameProperty) == templateName;
  154. });
  155. if (foundButton != m_templateButtons.end() && succeeded)
  156. {
  157. // Convert button to point at the now downloaded template
  158. auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
  159. if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
  160. {
  161. QVector<ProjectTemplateInfo> templates = templatesResult.GetValue();
  162. auto foundTemplate = AZStd::ranges::find_if(
  163. templates,
  164. [&templateName](const ProjectTemplateInfo& value)
  165. {
  166. return value.m_name == templateName;
  167. });
  168. if (foundTemplate != templates.end())
  169. {
  170. int templateIndex = (*foundButton)->property(k_templateIndexProperty).toInt();
  171. m_templates[templateIndex] = (*foundTemplate);
  172. (*foundButton)->SetIsRemote(false);
  173. }
  174. }
  175. }
  176. else if (foundButton != m_templateButtons.end())
  177. {
  178. (*foundButton)->ShowDownloadProgress(false);
  179. }
  180. }
  181. void NewProjectSettingsScreen::HandleDownloadProgress(const QString& templateName, DownloadController::DownloadObjectType objectType, int bytesDownloaded, int totalBytes)
  182. {
  183. if (objectType != DownloadController::DownloadObjectType::Template)
  184. {
  185. return;
  186. }
  187. auto foundButton = AZStd::ranges::find_if(
  188. m_templateButtons,
  189. [&templateName](const QAbstractButton* value)
  190. {
  191. return value->property(k_templateNameProperty) == templateName;
  192. });
  193. if (foundButton != m_templateButtons.end())
  194. {
  195. float percentage = static_cast<float>(bytesDownloaded) / totalBytes;
  196. (*foundButton)->SetProgressPercentage(percentage);
  197. }
  198. }
  199. QString NewProjectSettingsScreen::GetDefaultProjectName()
  200. {
  201. return "NewProject";
  202. }
  203. QString NewProjectSettingsScreen::GetProjectAutoPath()
  204. {
  205. const QString projectName = m_projectName->lineEdit()->text();
  206. return QDir::toNativeSeparators(ProjectUtils::GetDefaultProjectPath() + "/" + projectName);
  207. }
  208. ProjectManagerScreen NewProjectSettingsScreen::GetScreenEnum()
  209. {
  210. return ProjectManagerScreen::NewProjectSettings;
  211. }
  212. void NewProjectSettingsScreen::AddTemplateButtons()
  213. {
  214. auto templatesResult = PythonBindingsInterface::Get()->GetProjectTemplates();
  215. if (templatesResult.IsSuccess() && !templatesResult.GetValue().isEmpty())
  216. {
  217. m_templates = templatesResult.GetValue();
  218. // Add in remote templates
  219. auto remoteTemplatesResult = PythonBindingsInterface::Get()->GetProjectTemplatesForAllRepos();
  220. if (remoteTemplatesResult.IsSuccess() && !remoteTemplatesResult.GetValue().isEmpty())
  221. {
  222. const QVector<ProjectTemplateInfo>& remoteTemplates = remoteTemplatesResult.GetValue();
  223. for (const ProjectTemplateInfo& remoteTemplate : remoteTemplates)
  224. {
  225. const auto found = AZStd::ranges::find_if(m_templates,
  226. [remoteTemplate](const ProjectTemplateInfo& value)
  227. {
  228. return remoteTemplate.m_name == value.m_name;
  229. });
  230. if (found == m_templates.end())
  231. {
  232. m_templates.append(remoteTemplate);
  233. }
  234. }
  235. }
  236. // sort alphabetically by display name (but putting Standard first) because they could be in any order
  237. std::sort(m_templates.begin(), m_templates.end(), [](const ProjectTemplateInfo& arg1, const ProjectTemplateInfo& arg2)
  238. {
  239. if (arg1.m_displayName == "Standard")
  240. {
  241. return true;
  242. }
  243. else if (arg2.m_displayName == "Standard")
  244. {
  245. return false;
  246. }
  247. else
  248. {
  249. return arg1.m_displayName.toLower() < arg2.m_displayName.toLower();
  250. }
  251. });
  252. for (int index = 0; index < m_templates.size(); ++index)
  253. {
  254. ProjectTemplateInfo projectTemplate = m_templates.at(index);
  255. QString projectPreviewPath = QDir(projectTemplate.m_path).filePath(ProjectPreviewImagePath);
  256. QFileInfo doesPreviewExist(projectPreviewPath);
  257. if (!doesPreviewExist.exists() || !doesPreviewExist.isFile())
  258. {
  259. projectPreviewPath = ":/DefaultTemplate.png";
  260. }
  261. TemplateButton* templateButton = new TemplateButton(projectPreviewPath, projectTemplate.m_displayName, this);
  262. templateButton->SetIsRemote(projectTemplate.m_isRemote);
  263. templateButton->setCheckable(true);
  264. templateButton->setProperty(k_templateIndexProperty, index);
  265. templateButton->setProperty(k_templateNameProperty, projectTemplate.m_name);
  266. m_projectTemplateButtonGroup->addButton(templateButton);
  267. m_templateFlowLayout->addWidget(templateButton);
  268. m_templateButtons.append(templateButton);
  269. }
  270. // Insert the add a remote template button
  271. m_remoteTemplateButton = new TemplateButton(":/DefaultTemplate.png", tr("Add remote Template"), this);
  272. m_projectTemplateButtonGroup->addButton(m_remoteTemplateButton);
  273. m_templateFlowLayout->addWidget(m_remoteTemplateButton);
  274. // Select the first project template (default selection).
  275. SelectProjectTemplate(0, /*blockSignals=*/true);
  276. }
  277. }
  278. void NewProjectSettingsScreen::NotifyCurrentScreen()
  279. {
  280. if (m_templates.isEmpty())
  281. {
  282. AddTemplateButtons();
  283. }
  284. if (!m_templates.isEmpty())
  285. {
  286. UpdateTemplateDetails(m_templates.first());
  287. }
  288. Validate();
  289. }
  290. QString NewProjectSettingsScreen::GetProjectTemplatePath()
  291. {
  292. AZ_Assert(m_selectedTemplateIndex == m_projectTemplateButtonGroup->checkedButton()->property(k_templateIndexProperty).toInt(),
  293. "Selected template index not in sync with the currently checked project template button.");
  294. const ProjectTemplateInfo& templateInfo = m_templates.at(m_selectedTemplateIndex);
  295. if (templateInfo.m_isRemote)
  296. {
  297. // if this is a remote template that has not been downloaded we cannot return a path
  298. return "";
  299. }
  300. return templateInfo.m_path;
  301. }
  302. QFrame* NewProjectSettingsScreen::CreateTemplateDetails(int margin)
  303. {
  304. QFrame* projectTemplateDetails = new QFrame(this);
  305. projectTemplateDetails->setObjectName("projectTemplateDetails");
  306. QVBoxLayout* templateDetailsLayout = new QVBoxLayout();
  307. templateDetailsLayout->setContentsMargins(margin, margin, margin, margin);
  308. templateDetailsLayout->setAlignment(Qt::AlignTop);
  309. {
  310. m_templateDisplayName = new QLabel(this);
  311. m_templateDisplayName->setObjectName("displayName");
  312. m_templateDisplayName->setWordWrap(true);
  313. templateDetailsLayout->addWidget(m_templateDisplayName);
  314. m_templateSummary = new QLabel(this);
  315. m_templateSummary->setObjectName("summary");
  316. m_templateSummary->setWordWrap(true);
  317. templateDetailsLayout->addWidget(m_templateSummary);
  318. QLabel* includedGemsTitle = new QLabel(tr("Included Gems"), this);
  319. includedGemsTitle->setObjectName("includedGemsTitle");
  320. templateDetailsLayout->addWidget(includedGemsTitle);
  321. m_templateIncludedGems = new TagContainerWidget(this);
  322. m_templateIncludedGems->setObjectName("includedGems");
  323. templateDetailsLayout->addWidget(m_templateIncludedGems);
  324. QLabel* moreGemsLabel = new QLabel(tr("Looking for more Gems?"), this);
  325. moreGemsLabel->setObjectName("moreGems");
  326. templateDetailsLayout->addWidget(moreGemsLabel);
  327. QLabel* browseCatalogLabel = new QLabel(tr("Browse the Gems Catalog to further customize your project."), this);
  328. browseCatalogLabel->setObjectName("browseCatalog");
  329. browseCatalogLabel->setWordWrap(true);
  330. templateDetailsLayout->addWidget(browseCatalogLabel);
  331. m_downloadTemplateButton = new QPushButton(tr("Download Template"), this);
  332. m_downloadTemplateButton->setVisible(false);
  333. templateDetailsLayout->addWidget(m_downloadTemplateButton);
  334. QPushButton* configureGemsButton = new QPushButton(tr("Configure with more Gems"), this);
  335. connect(configureGemsButton, &QPushButton::clicked, this, [=]()
  336. {
  337. emit ChangeScreenRequest(ProjectManagerScreen::ProjectGemCatalog);
  338. });
  339. templateDetailsLayout->addWidget(configureGemsButton);
  340. }
  341. projectTemplateDetails->setLayout(templateDetailsLayout);
  342. return projectTemplateDetails;
  343. }
  344. void NewProjectSettingsScreen::StartTemplateDownload(const QString& templateName, const QString& destinationPath)
  345. {
  346. AZ_Assert(m_downloadController, "DownloadController must exist.");
  347. m_downloadController->AddObjectDownload(templateName, destinationPath, DownloadController::DownloadObjectType::Template);
  348. auto foundButton = AZStd::ranges::find_if(
  349. m_templateButtons,
  350. [&templateName](const QAbstractButton* value)
  351. {
  352. return value->property(k_templateNameProperty) == templateName;
  353. });
  354. if (foundButton != m_templateButtons.end())
  355. {
  356. (*foundButton)->ShowDownloadProgress(true);
  357. }
  358. }
  359. const ProjectTemplateInfo NewProjectSettingsScreen::GetSelectedProjectTemplateInfo() const
  360. {
  361. if(m_selectedTemplateIndex < 0 || m_selectedTemplateIndex >= m_templates.size())
  362. {
  363. return {};
  364. }
  365. return m_templates[m_selectedTemplateIndex];
  366. }
  367. void NewProjectSettingsScreen::ShowDownloadTemplateDialog(const ProjectTemplateInfo& templateInfo)
  368. {
  369. ProjectTemplateInfo resolvedTemplateInfo = templateInfo.IsValid() ? templateInfo : GetSelectedProjectTemplateInfo();
  370. if (!resolvedTemplateInfo.IsValid())
  371. {
  372. QMessageBox::critical(this, tr("Failed to find project template"), tr("The remote project template info for %1 could not be found or is invalid.\n\nPlease try refreshing the remote repository it came from, or download the template and register it through the o3de CLI.").arg(templateInfo.m_name));
  373. return;
  374. }
  375. DownloadRemoteTemplateDialog* dialog = new DownloadRemoteTemplateDialog(resolvedTemplateInfo, this);
  376. if (dialog->exec() == QDialog::DialogCode::Accepted)
  377. {
  378. StartTemplateDownload(resolvedTemplateInfo.m_name, dialog->GetInstallPath());
  379. }
  380. }
  381. void NewProjectSettingsScreen::UpdateTemplateDetails(const ProjectTemplateInfo& templateInfo)
  382. {
  383. m_templateDisplayName->setText(templateInfo.m_displayName);
  384. m_templateSummary->setText(templateInfo.m_summary);
  385. m_templateIncludedGems->Update(templateInfo.m_includedGems);
  386. m_downloadTemplateButton->setVisible(templateInfo.m_isRemote);
  387. m_downloadTemplateButton->disconnect();
  388. connect(m_downloadTemplateButton, &QPushButton::clicked, this, [&, templateInfo]()
  389. {
  390. ShowDownloadTemplateDialog(templateInfo);
  391. });
  392. }
  393. void NewProjectSettingsScreen::SelectProjectTemplate(int index, bool blockSignals)
  394. {
  395. const QList<QAbstractButton*> buttons = m_projectTemplateButtonGroup->buttons();
  396. if (index >= buttons.size())
  397. {
  398. return;
  399. }
  400. if (blockSignals)
  401. {
  402. m_projectTemplateButtonGroup->blockSignals(true);
  403. }
  404. QAbstractButton* button = buttons.at(index);
  405. button->setChecked(true);
  406. m_selectedTemplateIndex = button->property(k_templateIndexProperty).toInt();
  407. if (blockSignals)
  408. {
  409. m_projectTemplateButtonGroup->blockSignals(false);
  410. }
  411. }
  412. AZ::Outcome<void, QString> NewProjectSettingsScreen::Validate() const
  413. {
  414. if (m_selectedTemplateIndex != -1 && m_templates[m_selectedTemplateIndex].m_isRemote)
  415. {
  416. return AZ::Failure<QString>(tr("You cannot create a new project or configure gems with a template that has not been downloaded. Please download it before proceeding."));
  417. }
  418. return ProjectSettingsScreen::Validate();
  419. }
  420. void NewProjectSettingsScreen::OnProjectNameUpdated()
  421. {
  422. if (ValidateProjectName() && !m_userChangedProjectPath)
  423. {
  424. m_projectPath->setText(GetProjectAutoPath());
  425. }
  426. }
  427. void NewProjectSettingsScreen::OnProjectPathUpdated()
  428. {
  429. const QString defaultPath =
  430. QDir::toNativeSeparators(ProjectUtils::GetDefaultProjectPath() + "/" + GetDefaultProjectName());
  431. const QString autoPath = GetProjectAutoPath();
  432. const QString path = m_projectPath->lineEdit()->text();
  433. m_userChangedProjectPath = path != defaultPath && path != autoPath;
  434. ValidateProjectPath();
  435. }
  436. } // namespace O3DE::ProjectManager