GemInspector.cpp 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  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 <GemCatalog/GemInspector.h>
  9. #include <GemCatalog/GemItemDelegate.h>
  10. #include <ProjectManagerDefs.h>
  11. #include <ProjectUtils.h>
  12. #include <QDir>
  13. #include <QFrame>
  14. #include <QLabel>
  15. #include <QSpacerItem>
  16. #include <QHBoxLayout>
  17. #include <QVBoxLayout>
  18. #include <QIcon>
  19. #include <QPushButton>
  20. #include <QComboBox>
  21. #include <QClipboard>
  22. #include <QGuiApplication>
  23. namespace O3DE::ProjectManager
  24. {
  25. GemInspectorWorker::GemInspectorWorker() : QObject()
  26. {
  27. }
  28. void GemInspectorWorker::GetDirSize(QDir dir, quint64& sizeTotal)
  29. {
  30. const QDir::Filters fileFilters = QDir::Files | QDir::System | QDir::Hidden;
  31. for (const QString& filePath : dir.entryList(fileFilters))
  32. {
  33. sizeTotal += QFileInfo(dir, filePath).size();
  34. }
  35. const QDir::Filters dirFilters = QDir::Dirs | QDir::NoDotAndDotDot | QDir::System | QDir::Hidden;
  36. for (const QString& childDirPath : dir.entryList(dirFilters))
  37. {
  38. GetDirSize(dir.filePath(childDirPath), sizeTotal);
  39. }
  40. }
  41. void GemInspectorWorker::SetDir(QString dir)
  42. {
  43. quint64 size = 0;
  44. GetDirSize(QDir(dir), size);
  45. emit Done(QLocale().formattedDataSize(size, QLocale::DataSizeTraditionalFormat));
  46. }
  47. GemInspector::GemInspector(GemModel* model, QWidget* parent, bool readOnly)
  48. : QScrollArea(parent)
  49. , m_model(model)
  50. , m_readOnly(readOnly)
  51. {
  52. setObjectName("GemCatalogInspector");
  53. setWidgetResizable(true);
  54. setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
  55. setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
  56. m_mainWidget = new QWidget();
  57. if (parent)
  58. {
  59. m_mainWidget->setFixedWidth(parent->width());
  60. }
  61. setWidget(m_mainWidget);
  62. m_mainLayout = new QVBoxLayout();
  63. m_mainLayout->setMargin(15);
  64. m_mainLayout->setAlignment(Qt::AlignTop);
  65. m_mainWidget->setLayout(m_mainLayout);
  66. InitMainWidget();
  67. // worker for calculating folder sizes
  68. m_worker.moveToThread(&m_workerThread);
  69. connect(&m_worker, &GemInspectorWorker::Done, this, &GemInspector::OnDirSizeSet, Qt::QueuedConnection);
  70. m_workerThread.start();
  71. connect(m_model->GetSelectionModel(), &QItemSelectionModel::selectionChanged, this, &GemInspector::OnSelectionChanged);
  72. Update({});
  73. }
  74. GemInspector::~GemInspector()
  75. {
  76. m_workerThread.quit();
  77. m_workerThread.wait();
  78. }
  79. void GemInspector::OnDirSizeSet(QString size)
  80. {
  81. m_binarySizeLabel->setText(tr("Binary Size: %1").arg(size));
  82. }
  83. void GemInspector::OnSelectionChanged(const QItemSelection& selected, [[maybe_unused]] const QItemSelection& deselected)
  84. {
  85. const QModelIndexList selectedIndices = selected.indexes();
  86. if (selectedIndices.empty())
  87. {
  88. Update({});
  89. return;
  90. }
  91. Update(selectedIndices[0]);
  92. }
  93. void SetLabelElidedText(QLabel* label, const QString& text, int labelWidth = 0)
  94. {
  95. QFontMetrics nameFontMetrics(label->font());
  96. if (!labelWidth)
  97. {
  98. labelWidth = label->width();
  99. }
  100. // Don't elide if the widgets are sized too small (sometimes occurs when loading gem catalog)
  101. if (labelWidth > 100)
  102. {
  103. label->setText(nameFontMetrics.elidedText(text, Qt::ElideRight, labelWidth));
  104. }
  105. else
  106. {
  107. label->setText(text);
  108. }
  109. }
  110. void GemInspector::Update(const QPersistentModelIndex& modelIndex, const QString& version, const QString& path)
  111. {
  112. m_curModelIndex = modelIndex;
  113. if (!modelIndex.isValid())
  114. {
  115. m_mainWidget->hide();
  116. return;
  117. }
  118. // use the provided version if available
  119. QString displayVersion = version;
  120. QString activeVersion = m_model->GetNewVersion(modelIndex);
  121. if (activeVersion.isEmpty())
  122. {
  123. // fallback to the current version
  124. activeVersion = m_model->GetVersion(modelIndex);
  125. }
  126. if (displayVersion.isEmpty())
  127. {
  128. displayVersion = activeVersion;
  129. }
  130. const GemInfo& gemInfo = m_model->GetGemInfo(modelIndex, displayVersion, path);
  131. const bool isMissing = gemInfo.m_path.isEmpty();
  132. SetLabelElidedText(m_nameLabel, gemInfo.m_displayName.isEmpty() ? gemInfo.m_name : gemInfo.m_displayName);
  133. SetLabelElidedText(m_creatorLabel, gemInfo.m_origin);
  134. m_summaryLabel->setText(gemInfo.m_summary);
  135. m_summaryLabel->adjustSize();
  136. m_licenseLinkLabel->SetText(gemInfo.m_licenseText);
  137. m_licenseLinkLabel->SetUrl(gemInfo.m_licenseLink);
  138. m_directoryLinkLabel->SetUrl(gemInfo.m_directoryLink);
  139. m_documentationLinkLabel->SetUrl(gemInfo.m_documentationLink);
  140. m_requirementsTextLabel->setVisible(!gemInfo.m_requirement.isEmpty());
  141. m_requirementsTextLabel->setText(gemInfo.m_requirement);
  142. // Depending gems
  143. const QVector<Tag>& dependingGemTags = m_model->GetDependingGemTags(modelIndex);
  144. if (!dependingGemTags.isEmpty())
  145. {
  146. m_dependingGemsSpacer->changeSize(0, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
  147. m_dependingGems->show();
  148. m_dependingGems->Update(tr("Depending Gems"), tr("The following Gems will be automatically enabled with this Gem."), dependingGemTags);
  149. }
  150. else
  151. {
  152. m_dependingGems->hide();
  153. m_dependingGemsSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed);
  154. }
  155. // Additional information
  156. m_lastUpdatedLabel->setText(tr("Last Updated: %1").arg(gemInfo.m_lastUpdatedDate));
  157. m_copyDownloadLinkLabel->setVisible(!gemInfo.m_downloadSourceUri.isEmpty());
  158. if (gemInfo.m_binarySizeInKB)
  159. {
  160. m_binarySizeLabel->setText(tr("Binary Size: %1 KB").arg(gemInfo.m_binarySizeInKB));
  161. }
  162. else if (gemInfo.m_downloadStatus == GemInfo::Downloaded && !isMissing)
  163. {
  164. m_binarySizeLabel->setText(tr("Binary Size: ..."));
  165. QMetaObject::invokeMethod(&m_worker, "SetDir", Qt::QueuedConnection, Q_ARG(QString, gemInfo.m_path));
  166. }
  167. else
  168. {
  169. m_binarySizeLabel->setText(tr("Binary Size: Unknown"));
  170. }
  171. // Versions
  172. disconnect(m_versionComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GemInspector::OnVersionChanged);
  173. m_versionComboBox->clear();
  174. const auto& gemVersions = m_model->GetGemVersions(modelIndex);
  175. if (gemInfo.m_isEngineGem || gemVersions.count() < 2)
  176. {
  177. m_versionComboBox->setVisible(false);
  178. m_versionLabel->setText(gemInfo.m_version);
  179. m_versionLabel->setVisible(true);
  180. m_updateVersionButton->setVisible(false);
  181. }
  182. else
  183. {
  184. m_versionLabel->setVisible(false);
  185. m_versionComboBox->setVisible(true);
  186. for (const auto& gemVersion : gemVersions)
  187. {
  188. const GemInfo& gemVersionInfo = gemVersion.value<GemInfo>();
  189. m_versionComboBox->addItem(gemVersionInfo.m_version, gemVersionInfo.m_path);
  190. }
  191. if (m_versionComboBox->count() == 0)
  192. {
  193. m_versionComboBox->insertItem(0, "Unknown");
  194. }
  195. auto foundIndex = path.isEmpty()? m_versionComboBox->findText(displayVersion) : m_versionComboBox->findData(path);
  196. m_versionComboBox->setCurrentIndex(foundIndex > -1 ? foundIndex : 0);
  197. bool versionChanged = displayVersion != activeVersion && !m_readOnly && m_model->IsAdded(modelIndex);
  198. m_updateVersionButton->setVisible(versionChanged);
  199. if (versionChanged)
  200. {
  201. m_updateVersionButton->setText(tr("Use Version %1").arg(GetVersion()));
  202. }
  203. connect(m_versionComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &GemInspector::OnVersionChanged);
  204. }
  205. m_compatibilityTextLabel->setVisible(!gemInfo.IsCompatible());
  206. if(!gemInfo.IsCompatible())
  207. {
  208. if(!gemInfo.m_compatibleEngines.isEmpty())
  209. {
  210. if (m_readOnly)
  211. {
  212. m_compatibilityTextLabel->setText(tr("This version is not known to be compatible with the current engine"));
  213. }
  214. else
  215. {
  216. m_compatibilityTextLabel->setText(tr("This version is not known to be compatible with the engine this project uses"));
  217. }
  218. }
  219. else
  220. {
  221. m_compatibilityTextLabel->setText(tr("This version has missing or incompatible gem dependencies"));
  222. }
  223. }
  224. // Compatible engines
  225. m_enginesTitleLabel->setVisible(!gemInfo.m_isEngineGem);
  226. m_enginesLabel->setVisible(!gemInfo.m_isEngineGem);
  227. if (!gemInfo.m_isEngineGem)
  228. {
  229. if (gemInfo.m_compatibleEngines.isEmpty())
  230. {
  231. // reduce to one line for common case
  232. m_enginesTitleLabel->setText(tr("Compatible Engines: All engines"));
  233. m_enginesLabel->setVisible(false);
  234. }
  235. else
  236. {
  237. m_enginesTitleLabel->setText(tr("Compatible Engines:"));
  238. QString compatibleEngines;
  239. for (int i = 0; i < gemInfo.m_compatibleEngines.size(); ++i)
  240. {
  241. if (i > 0)
  242. {
  243. compatibleEngines.append("\n");
  244. }
  245. compatibleEngines.append(ProjectUtils::GetDependencyString(gemInfo.m_compatibleEngines[i]));
  246. }
  247. m_enginesLabel->setText(compatibleEngines);
  248. }
  249. }
  250. const bool isRemote = gemInfo.m_gemOrigin == GemInfo::Remote;
  251. // for now we don't count engine or project gems as local so they cannot be removed
  252. // currently we don't have a way for users to add gems to the project or engine through
  253. // the project manager if they remove/unregister them by accident
  254. const bool isLocal = gemInfo.m_gemOrigin == GemInfo::Local && !gemInfo.m_isEngineGem && !gemInfo.m_isProjectGem;
  255. const bool isDownloaded = gemInfo.m_downloadStatus == GemInfo::Downloaded ||
  256. gemInfo.m_downloadStatus == GemInfo::DownloadSuccessful;
  257. m_updateGemButton->setVisible(isRemote && isDownloaded && !isMissing);
  258. m_uninstallGemButton->setText(isRemote ? tr("Uninstall Gem") : tr("Remove Gem"));
  259. m_uninstallGemButton->setVisible(!isMissing && ((isRemote && isDownloaded) || isLocal));
  260. m_editGemButton->setVisible(!isMissing && (!isRemote || (isRemote && isDownloaded)));
  261. m_downloadGemButton->setVisible(isRemote && !isDownloaded);
  262. m_mainWidget->adjustSize();
  263. m_mainWidget->show();
  264. }
  265. QLabel* GemInspector::CreateStyledLabel(QLayout* layout, int fontSize, const QString& colorCodeString)
  266. {
  267. QLabel* result = new QLabel();
  268. result->setStyleSheet(QString("font-size: %1px; color: %2;").arg(QString::number(fontSize), colorCodeString));
  269. layout->addWidget(result);
  270. return result;
  271. }
  272. void GemInspector::OnVersionChanged([[maybe_unused]] int index)
  273. {
  274. Update(m_curModelIndex, GetVersion(), GetVersionPath());
  275. // we don't update the version in the gem list when read only
  276. // because it can cause the row to disappear due to changing filters
  277. // but in a project-specific view it is necessary because
  278. // the checkbox to activate the gem is on the row
  279. if (!GemModel::IsAdded(m_curModelIndex) && !m_readOnly)
  280. {
  281. GemModel::UpdateWithVersion(*m_model, m_curModelIndex, GetVersion(), GetVersionPath());
  282. }
  283. }
  284. void GemInspector::OnCopyDownloadLinkClicked()
  285. {
  286. const GemInfo& gemInfo = m_model->GetGemInfo(m_curModelIndex, GetVersion(), GetVersionPath());
  287. if (!gemInfo.m_downloadSourceUri.isEmpty())
  288. {
  289. if(QClipboard* clipboard = QGuiApplication::clipboard(); clipboard != nullptr)
  290. {
  291. clipboard->setText(gemInfo.m_downloadSourceUri);
  292. QString displayname = gemInfo.m_displayName.isEmpty() ? gemInfo.m_name : gemInfo.m_displayName;
  293. if (gemInfo.m_version.isEmpty() || gemInfo.m_displayName.contains(gemInfo.m_version) || gemInfo.m_version.contains("Unknown", Qt::CaseInsensitive))
  294. {
  295. emit ShowToastNotification(tr("%1 download URL copied to clipboard").arg(displayname));
  296. }
  297. else
  298. {
  299. emit ShowToastNotification(tr("%1 %2 download URL copied to clipboard").arg(displayname, gemInfo.m_version));
  300. }
  301. }
  302. else
  303. {
  304. emit ShowToastNotification("Failed to copy download URL to clipboard");
  305. }
  306. }
  307. }
  308. QString GemInspector::GetVersion() const
  309. {
  310. return m_versionComboBox->count() > 0 ? m_versionComboBox->currentText() : m_model->GetVersion(m_curModelIndex);
  311. }
  312. QString GemInspector::GetVersionPath() const
  313. {
  314. return m_versionComboBox->count() > 0 ? m_versionComboBox->currentData().toString() : m_model->GetGemInfo(m_curModelIndex).m_path;
  315. }
  316. void GemInspector::InitMainWidget()
  317. {
  318. // Gem name, creator and summary
  319. m_nameLabel = CreateStyledLabel(m_mainLayout, 18, s_headerColor);
  320. m_creatorLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_headerColor);
  321. // Version
  322. {
  323. m_versionWidget = new QWidget();
  324. m_versionWidget->setObjectName("GemCatalogVersion");
  325. auto versionVLayout = new QVBoxLayout();
  326. versionVLayout->setMargin(0);
  327. versionVLayout->addSpacing(5);
  328. auto versionHLayout = new QHBoxLayout();
  329. versionHLayout->setMargin(0);
  330. versionVLayout->addLayout(versionHLayout);
  331. m_versionWidget->setLayout(versionVLayout);
  332. m_mainLayout->addWidget(m_versionWidget);
  333. auto versionLabelTitle = new QLabel(tr("Version: "));
  334. versionLabelTitle->setProperty("class", "label");
  335. versionHLayout->addWidget(versionLabelTitle);
  336. m_versionLabel = new QLabel();
  337. versionHLayout->addWidget(m_versionLabel);
  338. m_versionComboBox = new QComboBox();
  339. versionHLayout->addWidget(m_versionComboBox);
  340. m_updateVersionButton = new QPushButton(tr("Use Version"));
  341. m_updateVersionButton->setProperty("secondary", true);
  342. versionVLayout->addWidget(m_updateVersionButton);
  343. connect(m_updateVersionButton, &QPushButton::clicked, this , [this]{
  344. GemModel::SetIsAdded(*m_model, m_curModelIndex, true, GetVersion());
  345. GemModel::UpdateWithVersion(*m_model, m_curModelIndex, GetVersion(), GetVersionPath());
  346. const GemInfo& gemInfo = GemModel::GetGemInfo(m_curModelIndex);
  347. if (!gemInfo.m_repoUri.isEmpty())
  348. {
  349. // this gem comes from a remote repository, see if we should download it
  350. const auto downloadStatus = GemModel::GetDownloadStatus(m_curModelIndex);
  351. if (downloadStatus == GemInfo::NotDownloaded || downloadStatus == GemInfo::DownloadFailed)
  352. {
  353. emit DownloadGem(m_curModelIndex, GetVersion(), GetVersionPath());
  354. }
  355. }
  356. m_updateVersionButton->setVisible(false);
  357. });
  358. // Compatibility
  359. m_compatibilityTextLabel = new QLabel();
  360. m_compatibilityTextLabel->setObjectName("GemCatalogCompatibilityWarning");
  361. m_compatibilityTextLabel->setWordWrap(true);
  362. m_compatibilityTextLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
  363. m_compatibilityTextLabel->setOpenExternalLinks(true);
  364. versionVLayout->addWidget(m_compatibilityTextLabel);
  365. m_enginesTitleLabel = new QLabel(tr("Compatible Engines: "));
  366. versionVLayout->addWidget(m_enginesTitleLabel);
  367. m_enginesLabel = new QLabel();
  368. versionVLayout->addWidget(m_enginesLabel);
  369. }
  370. m_mainLayout->addSpacing(5);
  371. // TODO: QLabel seems to have issues determining the right sizeHint() for our font with the given font size.
  372. // This results into squeezed elements in the layout in case the text is a little longer than a sentence.
  373. m_summaryLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_headerColor);
  374. m_mainLayout->addWidget(m_summaryLabel);
  375. m_summaryLabel->setWordWrap(true);
  376. m_summaryLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
  377. m_summaryLabel->setOpenExternalLinks(true);
  378. m_mainLayout->addSpacing(5);
  379. // License
  380. {
  381. QHBoxLayout* licenseHLayout = new QHBoxLayout();
  382. licenseHLayout->setMargin(0);
  383. licenseHLayout->setAlignment(Qt::AlignLeft);
  384. m_mainLayout->addLayout(licenseHLayout);
  385. m_licenseLabel = CreateStyledLabel(licenseHLayout, s_baseFontSize, s_headerColor);
  386. m_licenseLabel->setText(tr("License: "));
  387. m_licenseLinkLabel = new LinkLabel("", QUrl(), s_baseFontSize);
  388. licenseHLayout->addWidget(m_licenseLinkLabel);
  389. m_mainLayout->addSpacing(5);
  390. }
  391. // Directory and documentation links
  392. {
  393. QHBoxLayout* linksHLayout = new QHBoxLayout();
  394. linksHLayout->setMargin(0);
  395. m_mainLayout->addLayout(linksHLayout);
  396. linksHLayout->addStretch();
  397. m_directoryLinkLabel = new LinkLabel(tr("View in Directory"));
  398. linksHLayout->addWidget(m_directoryLinkLabel);
  399. linksHLayout->addWidget(new QLabel("|"));
  400. m_documentationLinkLabel = new LinkLabel(tr("Read Documentation"));
  401. linksHLayout->addWidget(m_documentationLinkLabel);
  402. linksHLayout->addStretch();
  403. m_mainLayout->addSpacing(8);
  404. }
  405. // Separating line
  406. QFrame* hLine = new QFrame();
  407. hLine->setFrameShape(QFrame::HLine);
  408. hLine->setObjectName("horizontalSeparatingLine");
  409. m_mainLayout->addWidget(hLine);
  410. m_mainLayout->addSpacing(10);
  411. // Requirements
  412. m_requirementsTextLabel = new QLabel();
  413. m_requirementsTextLabel->setObjectName("GemCatalogRequirements");
  414. m_requirementsTextLabel->setWordWrap(true);
  415. m_requirementsTextLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
  416. m_requirementsTextLabel->setOpenExternalLinks(true);
  417. m_mainLayout->addWidget(m_requirementsTextLabel);
  418. // Additional information
  419. auto additionalInfoLabel = new QLabel(tr("Additional Information"));
  420. additionalInfoLabel->setProperty("class", "title");
  421. m_mainLayout->addWidget(additionalInfoLabel);
  422. m_lastUpdatedLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor);
  423. m_binarySizeLabel = CreateStyledLabel(m_mainLayout, s_baseFontSize, s_textColor);
  424. m_copyDownloadLinkLabel = new LinkLabel(tr("Copy Download URL"));
  425. m_mainLayout->addWidget(m_copyDownloadLinkLabel);
  426. connect(m_copyDownloadLinkLabel, &LinkLabel::clicked, this, &GemInspector::OnCopyDownloadLinkClicked);
  427. m_mainLayout->addSpacing(20);
  428. // Depending gems
  429. m_dependingGems = new GemsSubWidget();
  430. connect(m_dependingGems, &GemsSubWidget::TagClicked, this, [this](const Tag& tag){ emit TagClicked(tag); });
  431. m_mainLayout->addWidget(m_dependingGems);
  432. m_dependingGemsSpacer = new QSpacerItem(0, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
  433. m_mainLayout->addSpacerItem(m_dependingGemsSpacer);
  434. // Update and Uninstall buttons
  435. m_updateGemButton = new QPushButton(tr("Update Gem"));
  436. m_updateGemButton->setProperty("secondary", true);
  437. m_mainLayout->addWidget(m_updateGemButton);
  438. connect(m_updateGemButton, &QPushButton::clicked, this , [this]{ emit UpdateGem(m_curModelIndex, GetVersion(), GetVersionPath()); });
  439. m_mainLayout->addSpacing(10);
  440. m_editGemButton = new QPushButton(tr("Edit Gem"));
  441. m_editGemButton->setProperty("secondary", true);
  442. m_mainLayout->addWidget(m_editGemButton);
  443. connect(m_editGemButton, &QPushButton::clicked, this , [this]{ emit EditGem(m_curModelIndex, GetVersionPath()); });
  444. m_mainLayout->addSpacing(10);
  445. m_uninstallGemButton = new QPushButton(tr("Uninstall Gem"));
  446. m_uninstallGemButton->setProperty("danger", true);
  447. m_mainLayout->addWidget(m_uninstallGemButton);
  448. connect(m_uninstallGemButton, &QPushButton::clicked, this , [this]{ emit UninstallGem(m_curModelIndex, GetVersionPath()); });
  449. m_downloadGemButton = new QPushButton(tr("Download Gem"));
  450. m_downloadGemButton->setProperty("primary", true);
  451. m_mainLayout->addWidget(m_downloadGemButton);
  452. connect(m_downloadGemButton, &QPushButton::clicked, this , [this]{ emit DownloadGem(m_curModelIndex, GetVersion(), GetVersionPath()); });
  453. }
  454. } // namespace O3DE::ProjectManager