GemRepoScreen.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  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 <GemRepo/GemRepoScreen.h>
  9. #include <GemRepo/GemRepoItemDelegate.h>
  10. #include <GemRepo/GemRepoListView.h>
  11. #include <GemRepo/GemRepoModel.h>
  12. #include <GemRepo/GemRepoAddDialog.h>
  13. #include <GemRepo/GemRepoInspector.h>
  14. #include <GemRepo/GemRepoProxyModel.h>
  15. #include <PythonBindingsInterface.h>
  16. #include <ProjectManagerDefs.h>
  17. #include <ProjectUtils.h>
  18. #include <AdjustableHeaderWidget.h>
  19. #include <ProjectManagerDefs.h>
  20. #include <QVBoxLayout>
  21. #include <QHBoxLayout>
  22. #include <QPushButton>
  23. #include <QTimer>
  24. #include <QMessageBox>
  25. #include <QLabel>
  26. #include <QHeaderView>
  27. #include <QTableWidget>
  28. #include <QFrame>
  29. #include <QStackedWidget>
  30. #include <QMessageBox>
  31. #include <QItemSelectionModel>
  32. namespace O3DE::ProjectManager
  33. {
  34. GemRepoScreen::GemRepoScreen(QWidget* parent)
  35. : ScreenWidget(parent)
  36. {
  37. m_gemRepoModel = new GemRepoModel(this);
  38. m_gemRepoModel->setSortRole(GemRepoModel::UserRole::RoleName);
  39. connect(m_gemRepoModel, &GemRepoModel::ShowToastNotification, this, &GemRepoScreen::ShowStandardToastNotification);
  40. QVBoxLayout* vLayout = new QVBoxLayout();
  41. vLayout->setMargin(0);
  42. vLayout->setSpacing(0);
  43. setLayout(vLayout);
  44. m_contentStack = new QStackedWidget(this);
  45. m_noRepoContent = CreateNoReposContent();
  46. m_contentStack->addWidget(m_noRepoContent);
  47. m_repoContent = CreateReposContent();
  48. m_contentStack->addWidget(m_repoContent);
  49. vLayout->addWidget(m_contentStack);
  50. m_notificationsView = AZStd::make_unique<AzToolsFramework::ToastNotificationsView>(this, AZ_CRC_CE("ReposNotificationsView"));
  51. m_notificationsView->SetOffset(QPoint(10, 10));
  52. m_notificationsView->SetMaxQueuedNotifications(1);
  53. m_notificationsView->SetRejectDuplicates(false); // we want to show notifications if a user repeats actions
  54. ScreensCtrl* screensCtrl = GetScreensCtrl(this);
  55. if (screensCtrl)
  56. {
  57. connect(this, &GemRepoScreen::NotifyRemoteContentRefreshed, screensCtrl, &ScreensCtrl::NotifyRemoteContentRefreshed);
  58. }
  59. }
  60. void GemRepoScreen::NotifyCurrentScreen()
  61. {
  62. constexpr bool downloadMissingOnly = true;
  63. PythonBindingsInterface::Get()->RefreshAllGemRepos(downloadMissingOnly);
  64. Reinit();
  65. // we might have downloading missing data so make sure to update the GemCatalog
  66. emit NotifyRemoteContentRefreshed();
  67. }
  68. void GemRepoScreen::Reinit()
  69. {
  70. QString selectedRepoUri;
  71. QPersistentModelIndex selectedIndex = m_selectionModel->currentIndex();
  72. if (selectedIndex.isValid())
  73. {
  74. selectedIndex = m_sortProxyModel->mapToSource(selectedIndex);
  75. selectedRepoUri = GemRepoModel::GetRepoUri(selectedIndex);
  76. }
  77. disconnect(m_gemRepoModel, &GemRepoModel::dataChanged, this, &GemRepoScreen::OnModelDataChanged);
  78. m_gemRepoModel->clear();
  79. FillModel();
  80. connect(m_gemRepoModel, &GemRepoModel::dataChanged, this, &GemRepoScreen::OnModelDataChanged);
  81. // If model contains any data show the repos
  82. if (m_gemRepoModel->rowCount())
  83. {
  84. m_contentStack->setCurrentWidget(m_repoContent);
  85. QPersistentModelIndex modelIndex;
  86. if (!selectedRepoUri.isEmpty())
  87. {
  88. // attempt to re-select the row with the unique RepoURI if it still exists
  89. modelIndex = m_gemRepoModel->FindModelIndexByRepoUri(selectedRepoUri);
  90. modelIndex = m_sortProxyModel->mapFromSource(modelIndex);
  91. }
  92. if (!modelIndex.isValid())
  93. {
  94. // fallback to selecting the first item in the list
  95. modelIndex = m_sortProxyModel->index(0, 0);
  96. }
  97. m_gemRepoListView->selectionModel()->setCurrentIndex(modelIndex, QItemSelectionModel::ClearAndSelect);
  98. }
  99. else
  100. {
  101. m_contentStack->setCurrentWidget(m_noRepoContent);
  102. }
  103. }
  104. void GemRepoScreen::HandleAddRepoButton()
  105. {
  106. GemRepoAddDialog* repoAddDialog = new GemRepoAddDialog(this);
  107. if (repoAddDialog->exec() == QDialog::DialogCode::Accepted)
  108. {
  109. QString repoUri = repoAddDialog->GetRepoPath();
  110. if (repoUri.isEmpty())
  111. {
  112. QMessageBox::warning(this, tr("No Input"), tr("Please provide a repo Uri."));
  113. return;
  114. }
  115. auto addGemRepoResult = PythonBindingsInterface::Get()->AddGemRepo(repoUri);
  116. if (addGemRepoResult.IsSuccess())
  117. {
  118. ShowStandardToastNotification(tr("Repo added successfully!"));
  119. Reinit();
  120. emit NotifyRemoteContentRefreshed();
  121. }
  122. else
  123. {
  124. QString failureMessage = tr("Failed to add gem repo: %1.").arg(repoUri);
  125. ProjectUtils::DisplayDetailedError(failureMessage, addGemRepoResult, this);
  126. AZ_Error("Project Manager", false, failureMessage.toUtf8());
  127. }
  128. }
  129. }
  130. void GemRepoScreen::HandleRemoveRepoButton(const QModelIndex& modelIndex)
  131. {
  132. QString repoName = m_gemRepoModel->GetName(modelIndex);
  133. QMessageBox::StandardButton warningResult = QMessageBox::warning(
  134. this, tr("Remove Repo"), tr("Are you sure you would like to remove gem repo: %1?").arg(repoName),
  135. QMessageBox::No | QMessageBox::Yes);
  136. if (warningResult == QMessageBox::Yes)
  137. {
  138. QString repoUri = m_gemRepoModel->GetRepoUri(modelIndex);
  139. bool removeGemRepoResult = PythonBindingsInterface::Get()->RemoveGemRepo(repoUri);
  140. if (removeGemRepoResult)
  141. {
  142. ShowStandardToastNotification(tr("Repo removed"));
  143. Reinit();
  144. emit NotifyRemoteContentRefreshed();
  145. }
  146. else
  147. {
  148. QString failureMessage = tr("Failed to remove gem repo: %1.").arg(repoUri);
  149. QMessageBox::critical(this, tr("Operation failed"), failureMessage);
  150. AZ_Error("Project Manger", false, failureMessage.toUtf8());
  151. }
  152. }
  153. }
  154. void GemRepoScreen::HandleRefreshAllButton()
  155. {
  156. // re-download everything when the user presses the refresh all button
  157. constexpr bool downloadMissingOnly = false;
  158. bool refreshResult = PythonBindingsInterface::Get()->RefreshAllGemRepos(downloadMissingOnly);
  159. Reinit();
  160. emit NotifyRemoteContentRefreshed();
  161. if (refreshResult)
  162. {
  163. ShowStandardToastNotification(tr("Repos updated"));
  164. }
  165. else
  166. {
  167. QMessageBox::critical(
  168. this, tr("Operation failed"), QString("Some repos failed to refresh."));
  169. }
  170. }
  171. void GemRepoScreen::HandleRefreshRepoButton(const QModelIndex& modelIndex)
  172. {
  173. const QString repoUri = m_gemRepoModel->GetRepoUri(modelIndex);
  174. const QString repoName = m_gemRepoModel->GetName(modelIndex);
  175. // re-download everything when the user presses the refresh all button
  176. constexpr bool downloadMissingOnly = false;
  177. AZ::Outcome<void, AZStd::string> refreshResult = PythonBindingsInterface::Get()->RefreshGemRepo(repoUri, downloadMissingOnly);
  178. if (refreshResult.IsSuccess())
  179. {
  180. Reinit();
  181. emit NotifyRemoteContentRefreshed();
  182. ShowStandardToastNotification(tr("%1 updated").arg(repoName));
  183. }
  184. else
  185. {
  186. QMessageBox::critical(
  187. this, tr("Operation failed"),
  188. QString("Failed to refresh gem repo %1<br>Error:<br>%2")
  189. .arg(m_gemRepoModel->GetName(modelIndex), refreshResult.GetError().c_str()));
  190. }
  191. }
  192. void GemRepoScreen::FillModel()
  193. {
  194. AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> allGemRepoInfosResult = PythonBindingsInterface::Get()->GetAllGemRepoInfos();
  195. if (allGemRepoInfosResult.IsSuccess())
  196. {
  197. // Add all available repos to the model
  198. const QVector<GemRepoInfo> allGemRepoInfos = allGemRepoInfosResult.GetValue();
  199. QDateTime oldestRepoUpdate;
  200. if (!allGemRepoInfos.isEmpty())
  201. {
  202. oldestRepoUpdate = allGemRepoInfos[0].m_lastUpdated;
  203. }
  204. for (const GemRepoInfo& gemRepoInfo : allGemRepoInfos)
  205. {
  206. m_gemRepoModel->AddGemRepo(gemRepoInfo);
  207. // Find least recently updated repo
  208. if (gemRepoInfo.m_lastUpdated < oldestRepoUpdate)
  209. {
  210. oldestRepoUpdate = gemRepoInfo.m_lastUpdated;
  211. }
  212. }
  213. if (!allGemRepoInfos.isEmpty())
  214. {
  215. // get the month day and year in the preferred locale's format (QLocale defaults to the OS locale)
  216. QString monthDayYear = oldestRepoUpdate.toString(QLocale().dateFormat(QLocale::ShortFormat));
  217. // always show 12 hour + minutes + am/pm
  218. QString hourMinuteAMPM = oldestRepoUpdate.toString("h:mmap");
  219. QString repoUpdatedDate = QString("%1 %2").arg(monthDayYear, hourMinuteAMPM);
  220. m_lastAllUpdateLabel->setText(tr("Last Updated: %1").arg(repoUpdatedDate));
  221. }
  222. else
  223. {
  224. m_lastAllUpdateLabel->setText(tr("Last Updated: Never"));
  225. }
  226. m_sortProxyModel->sort(/*column*/0);
  227. }
  228. else
  229. {
  230. QMessageBox::critical(this, tr("Operation failed"), QString("Cannot retrieve gem repos for engine.<br>Error:<br>%2").arg(allGemRepoInfosResult.GetError().c_str()));
  231. }
  232. }
  233. void GemRepoScreen::OnModelDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector<int>& roles)
  234. {
  235. if (roles.isEmpty() || roles.at(0) == GemRepoModel::UserRole::RoleIsEnabled)
  236. {
  237. QItemSelection updatedItems(topLeft, bottomRight);
  238. for (const QModelIndex& modelIndex : updatedItems.indexes())
  239. {
  240. const bool isEnabled = GemRepoModel::IsEnabled(modelIndex);
  241. QString repoUri = GemRepoModel::GetRepoUri(modelIndex);
  242. PythonBindingsInterface::Get()->SetRepoEnabled(repoUri, isEnabled);
  243. const QString repoName = m_gemRepoModel->GetName(modelIndex);
  244. if (isEnabled)
  245. {
  246. ShowStandardToastNotification(tr("%1 activated").arg(repoName));
  247. }
  248. else
  249. {
  250. ShowStandardToastNotification(tr("%1 deactivated").arg(repoName));
  251. }
  252. }
  253. }
  254. }
  255. QFrame* GemRepoScreen::CreateNoReposContent()
  256. {
  257. QFrame* contentFrame = new QFrame(this);
  258. QVBoxLayout* vLayout = new QVBoxLayout();
  259. vLayout->setAlignment(Qt::AlignHCenter);
  260. vLayout->setMargin(0);
  261. vLayout->setSpacing(0);
  262. contentFrame->setLayout(vLayout);
  263. vLayout->addStretch();
  264. QLabel* noRepoLabel = new QLabel(tr("No repositories have been added yet."), this);
  265. noRepoLabel->setObjectName("gemRepoNoReposLabel");
  266. vLayout->addWidget(noRepoLabel);
  267. vLayout->setAlignment(noRepoLabel, Qt::AlignHCenter);
  268. vLayout->addSpacing(20);
  269. // Size hint for button is wrong so horizontal layout with stretch is used to center it
  270. QHBoxLayout* hLayout = new QHBoxLayout();
  271. hLayout->setMargin(0);
  272. hLayout->setSpacing(0);
  273. hLayout->addStretch();
  274. QPushButton* addRepoButton = new QPushButton(tr("Add Repository"), this);
  275. addRepoButton->setObjectName("gemRepoAddButton");
  276. addRepoButton->setProperty("secondary", true);
  277. addRepoButton->setMinimumWidth(120);
  278. hLayout->addWidget(addRepoButton);
  279. connect(addRepoButton, &QPushButton::clicked, this, &GemRepoScreen::HandleAddRepoButton);
  280. hLayout->addStretch();
  281. vLayout->addLayout(hLayout);
  282. vLayout->addStretch();
  283. return contentFrame;
  284. }
  285. QFrame* GemRepoScreen::CreateReposContent()
  286. {
  287. constexpr int inspectorWidth = 240;
  288. constexpr int middleLayoutIndent = 60;
  289. QFrame* contentFrame = new QFrame(this);
  290. QHBoxLayout* hLayout = new QHBoxLayout();
  291. hLayout->setMargin(0);
  292. hLayout->setSpacing(0);
  293. contentFrame->setLayout(hLayout);
  294. hLayout->addSpacing(middleLayoutIndent);
  295. QVBoxLayout* middleVLayout = new QVBoxLayout();
  296. middleVLayout->setMargin(0);
  297. middleVLayout->setSpacing(0);
  298. middleVLayout->addSpacing(30);
  299. QHBoxLayout* topMiddleHLayout = new QHBoxLayout();
  300. topMiddleHLayout->setMargin(0);
  301. topMiddleHLayout->setSpacing(0);
  302. m_lastAllUpdateLabel = new QLabel(tr("Last Updated: Never"), this);
  303. m_lastAllUpdateLabel->setObjectName("gemRepoHeaderLabel");
  304. topMiddleHLayout->addWidget(m_lastAllUpdateLabel);
  305. topMiddleHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum));
  306. QPushButton* updateAllButton = new QPushButton(QIcon(":/Refresh.svg").pixmap(16, 16), tr("Update All"), this);
  307. updateAllButton->setObjectName("gemRepoAddButton");
  308. updateAllButton->setProperty("secondary", true);
  309. topMiddleHLayout->addWidget(updateAllButton);
  310. connect(updateAllButton, &QPushButton::clicked, this, &GemRepoScreen::HandleRefreshAllButton);
  311. topMiddleHLayout->addSpacing(10);
  312. QPushButton* addRepoButton = new QPushButton(tr("Add Repository"), this);
  313. addRepoButton->setObjectName("gemRepoAddButton");
  314. addRepoButton->setProperty("secondary", true);
  315. topMiddleHLayout->addWidget(addRepoButton);
  316. connect(addRepoButton, &QPushButton::clicked, this, &GemRepoScreen::HandleAddRepoButton);
  317. middleVLayout->addLayout(topMiddleHLayout);
  318. middleVLayout->addSpacing(30);
  319. constexpr int minHeaderSectionWidth = 80;
  320. m_gemRepoHeaderTable = new AdjustableHeaderWidget(
  321. QStringList{ tr("Repository Name"), tr("Creator"), "", tr("Updated Date"), tr("Status") },
  322. QVector<int>{
  323. GemRepoItemDelegate::s_nameDefaultWidth + GemRepoItemDelegate::s_contentMargins.left(),
  324. GemRepoItemDelegate::s_creatorDefaultWidth,
  325. GemRepoItemDelegate::s_badgeDefaultWidth,
  326. GemRepoItemDelegate::s_updatedDefaultWidth,
  327. // Include invisible header for delete button
  328. GemRepoItemDelegate::s_buttonsDefaultWidth + GemRepoItemDelegate::s_contentMargins.right()
  329. },
  330. minHeaderSectionWidth,
  331. QVector<QHeaderView::ResizeMode>
  332. {
  333. QHeaderView::ResizeMode::Interactive,
  334. QHeaderView::ResizeMode::Stretch,
  335. QHeaderView::ResizeMode::Fixed,
  336. QHeaderView::ResizeMode::Fixed,
  337. QHeaderView::ResizeMode::Fixed
  338. },
  339. this);
  340. middleVLayout->addWidget(m_gemRepoHeaderTable);
  341. m_sortProxyModel = new GemRepoProxyModel(this);
  342. m_sortProxyModel->setSourceModel(m_gemRepoModel);
  343. m_sortProxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
  344. m_sortProxyModel->setSortRole(GemRepoModel::UserRole::RoleName);
  345. m_selectionModel = new QItemSelectionModel(m_sortProxyModel, this);
  346. m_gemRepoListView = new GemRepoListView(m_sortProxyModel, m_selectionModel, m_gemRepoHeaderTable, this);
  347. connect(m_gemRepoListView, &GemRepoListView::RefreshRepo, this, &GemRepoScreen::HandleRefreshRepoButton);
  348. middleVLayout->addWidget(m_gemRepoListView);
  349. hLayout->addLayout(middleVLayout);
  350. hLayout->addSpacing(middleLayoutIndent);
  351. m_gemRepoInspector = new GemRepoInspector(m_gemRepoModel, m_selectionModel, this);
  352. connect(m_gemRepoInspector, &GemRepoInspector::RemoveRepo, this, &GemRepoScreen::HandleRemoveRepoButton);
  353. connect(m_gemRepoInspector, &GemRepoInspector::ShowToastNotification, this, &GemRepoScreen::ShowStandardToastNotification);
  354. m_gemRepoInspector->setFixedWidth(inspectorWidth);
  355. hLayout->addWidget(m_gemRepoInspector);
  356. return contentFrame;
  357. }
  358. void GemRepoScreen::ShowStandardToastNotification(const QString& notification)
  359. {
  360. AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, "");
  361. toastConfiguration.m_customIconImage = ":/Info.svg";
  362. toastConfiguration.m_borderRadius = 4;
  363. toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000);
  364. m_notificationsView->ShowToastNotification(toastConfiguration);
  365. }
  366. ProjectManagerScreen GemRepoScreen::GetScreenEnum()
  367. {
  368. return ProjectManagerScreen::GemRepos;
  369. }
  370. } // namespace O3DE::ProjectManager