AtomToolsDocumentMainWindow.cpp 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857
  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 <Atom/RPI.Edit/Common/AssetUtils.h>
  9. #include <Atom/RPI.Reflect/Asset/AssetUtils.h>
  10. #include <AtomToolsFramework/Document/AtomToolsDocumentMainWindow.h>
  11. #include <AtomToolsFramework/Document/AtomToolsDocumentRequestBus.h>
  12. #include <AtomToolsFramework/Document/AtomToolsDocumentSystemRequestBus.h>
  13. #include <AtomToolsFramework/Document/CreateDocumentDialog.h>
  14. #include <AtomToolsFramework/SettingsDialog/SettingsDialog.h>
  15. #include <AtomToolsFramework/Util/Util.h>
  16. #include <AzCore/Utils/Utils.h>
  17. #include <AzFramework/StringFunc/StringFunc.h>
  18. AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT
  19. #include <QApplication>
  20. #include <QByteArray>
  21. #include <QCloseEvent>
  22. #include <QDesktopServices>
  23. #include <QDragEnterEvent>
  24. #include <QDragLeaveEvent>
  25. #include <QDropEvent>
  26. #include <QInputDialog>
  27. #include <QLayout>
  28. #include <QMenu>
  29. #include <QMenuBar>
  30. #include <QMessageBox>
  31. #include <QMimeData>
  32. #include <QTimer>
  33. #include <QWindow>
  34. AZ_POP_DISABLE_WARNING
  35. namespace AtomToolsFramework
  36. {
  37. AtomToolsDocumentMainWindow::AtomToolsDocumentMainWindow(const AZ::Crc32& toolId, const QString& objectName, QWidget* parent)
  38. : Base(toolId, objectName, parent)
  39. {
  40. AddDocumentTabBar();
  41. // Register a handler with the asset browser that attempts to open the first compatible document type for the selected path.
  42. m_assetBrowser->SetOpenHandler([this](const AZStd::string& absolutePath) {
  43. DocumentTypeInfoVector documentTypes;
  44. AtomToolsDocumentSystemRequestBus::EventResult(
  45. documentTypes, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::GetRegisteredDocumentTypes);
  46. for (const auto& documentType : documentTypes)
  47. {
  48. if (documentType.IsSupportedExtensionToOpen(absolutePath))
  49. {
  50. AZ::SystemTickBus::QueueFunction([toolId = m_toolId, absolutePath]() {
  51. AtomToolsDocumentSystemRequestBus::Event(toolId, &AtomToolsDocumentSystemRequestBus::Events::OpenDocument, absolutePath);
  52. });
  53. return;
  54. }
  55. }
  56. // If there was no compatible document type I tend to open the file using standard OS file openers
  57. QDesktopServices::openUrl(QUrl::fromLocalFile(absolutePath.c_str()));
  58. });
  59. // Enable dragging and dropping of files onto this window.
  60. setAcceptDrops(true);
  61. AtomToolsDocumentNotificationBus::Handler::BusConnect(m_toolId);
  62. }
  63. AtomToolsDocumentMainWindow::~AtomToolsDocumentMainWindow()
  64. {
  65. AtomToolsDocumentNotificationBus::Handler::BusDisconnect();
  66. }
  67. void AtomToolsDocumentMainWindow::CreateMenus(QMenuBar* menuBar)
  68. {
  69. Base::CreateMenus(menuBar);
  70. // Generating the main menu manually because it's easier and we will have some dynamic or data driven entries
  71. QAction* insertPostion = !m_menuFile->actions().empty() ? m_menuFile->actions().front() : nullptr;
  72. BuildCreateMenu(insertPostion);
  73. BuildOpenMenu(insertPostion);
  74. m_menuOpenRecent = new QMenu("Open Recent", menuBar);
  75. connect(m_menuOpenRecent, &QMenu::aboutToShow, menuBar, [this]() {
  76. UpdateRecentFileMenu();
  77. });
  78. m_menuFile->insertMenu(insertPostion, m_menuOpenRecent);
  79. m_menuFile->insertSeparator(insertPostion);
  80. m_actionSave = CreateActionAtPosition(m_menuFile, insertPostion, "&Save", [this]() {
  81. SaveDocument(GetCurrentDocumentId());
  82. }, QKeySequence::Save);
  83. m_actionSaveAsCopy = CreateActionAtPosition(m_menuFile, insertPostion, "Save &As...", [this]() {
  84. const AZ::Uuid documentId = GetCurrentDocumentId();
  85. const QString documentPath = GetDocumentPath(documentId);
  86. if (const auto& savePath = GetSaveDocumentParams(documentPath.toUtf8().constData(), documentId); !savePath.empty())
  87. {
  88. bool result = false;
  89. AtomToolsDocumentSystemRequestBus::EventResult(
  90. result, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::SaveDocumentAsCopy, documentId, savePath);
  91. if (!result)
  92. {
  93. SetStatusError(tr("Document save failed: %1").arg(documentPath).toUtf8().constData());
  94. }
  95. }
  96. }, QKeySequence::SaveAs);
  97. m_actionSaveAsChild = CreateActionAtPosition(m_menuFile, insertPostion, "Save As &Child...", [this]() {
  98. const AZ::Uuid documentId = GetCurrentDocumentId();
  99. const QString documentPath = GetDocumentPath(documentId);
  100. if (const auto& savePath = GetSaveDocumentParams(documentPath.toUtf8().constData(), documentId); !savePath.empty())
  101. {
  102. bool result = false;
  103. AtomToolsDocumentSystemRequestBus::EventResult(
  104. result, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::SaveDocumentAsChild, documentId, savePath);
  105. if (!result)
  106. {
  107. SetStatusError(tr("Document save failed: %1").arg(documentPath).toUtf8().constData());
  108. }
  109. }
  110. });
  111. m_actionSaveAll = CreateActionAtPosition(m_menuFile, insertPostion, "Save A&ll", [this]() {
  112. for (const auto& documentId : GetOpenDocumentIds())
  113. {
  114. if (!SaveDocument(documentId))
  115. {
  116. // Stop if there is any save failed or cancel
  117. break;
  118. }
  119. }
  120. });
  121. m_menuFile->insertSeparator(insertPostion);
  122. m_actionClose = CreateActionAtPosition(m_menuFile, insertPostion, "&Close", [this]() {
  123. CloseDocuments({GetCurrentDocumentId()});
  124. }, QKeySequence::Close);
  125. m_actionCloseAll = CreateActionAtPosition(m_menuFile, insertPostion, "Close All", [this]() {
  126. CloseDocuments(GetOpenDocumentIds());
  127. });
  128. m_actionCloseOthers = CreateActionAtPosition(m_menuFile, insertPostion, "Close Others", [this]() {
  129. auto documentIds = GetOpenDocumentIds();
  130. AZStd::erase(documentIds, GetCurrentDocumentId());
  131. CloseDocuments(documentIds);
  132. });
  133. m_menuFile->insertSeparator(insertPostion);
  134. insertPostion = !m_menuEdit->actions().empty() ? m_menuEdit->actions().front() : nullptr;
  135. m_actionUndo = CreateActionAtPosition(m_menuEdit, insertPostion, "&Undo", [this]() {
  136. const AZ::Uuid documentId = GetCurrentDocumentId();
  137. bool result = false;
  138. AtomToolsDocumentRequestBus::EventResult(result, documentId, &AtomToolsDocumentRequestBus::Events::Undo);
  139. if (!result)
  140. {
  141. SetStatusError(tr("Document undo failed: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  142. }
  143. }, QKeySequence::Undo);
  144. m_actionRedo = CreateActionAtPosition(m_menuEdit, insertPostion, "&Redo", [this]() {
  145. const AZ::Uuid documentId = GetCurrentDocumentId();
  146. bool result = false;
  147. AtomToolsDocumentRequestBus::EventResult(result, documentId, &AtomToolsDocumentRequestBus::Events::Redo);
  148. if (!result)
  149. {
  150. SetStatusError(tr("Document redo failed: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  151. }
  152. }, QKeySequence::Redo);
  153. m_menuEdit->insertSeparator(insertPostion);
  154. insertPostion = !m_menuView->actions().empty() ? m_menuView->actions().front() : nullptr;
  155. m_actionPreviousTab = CreateActionAtPosition(m_menuView, insertPostion, "&Previous Tab", [this]() {
  156. SelectPrevDocumentTab();
  157. }, 0x0 | Qt::CTRL | Qt::SHIFT | Qt::Key_Tab); //QKeySequence::PreviousChild is mapped incorrectly in Qt
  158. m_actionNextTab = CreateActionAtPosition(m_menuView, insertPostion, "&Next Tab", [this]() {
  159. SelectNextDocumentTab();
  160. }, 0x0 | Qt::CTRL | Qt::Key_Tab); //QKeySequence::NextChild works as expected but mirroring Previous
  161. m_menuView->insertSeparator(insertPostion);
  162. }
  163. bool AtomToolsDocumentMainWindow::SaveDocument(const AZ::Uuid& documentId)
  164. {
  165. AZStd::string documentPath;
  166. AtomToolsDocumentRequestBus::EventResult(documentPath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
  167. DocumentTypeInfo documentInfo;
  168. AtomToolsDocumentRequestBus::EventResult(documentInfo, documentId, &AtomToolsDocumentRequestBus::Events::GetDocumentTypeInfo);
  169. // Attempt to save using the current path if it is not empty and has a supported save extension.
  170. if (documentInfo.IsSupportedExtensionToSave(documentPath))
  171. {
  172. bool result = false;
  173. AtomToolsDocumentSystemRequestBus::EventResult(
  174. result, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::SaveDocument, documentId);
  175. if (!result)
  176. {
  177. SetStatusError(tr("Document save failed: %1").arg(documentPath.c_str()).toUtf8().constData());
  178. return false;
  179. }
  180. return true;
  181. }
  182. // If the path is empty or the extension is not valid for saving, do a save as operation, which prompts the user for a path.
  183. if (const auto& savePath = GetSaveDocumentParams(documentPath, documentId); !savePath.empty())
  184. {
  185. bool result = false;
  186. AtomToolsDocumentSystemRequestBus::EventResult(
  187. result, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::SaveDocumentAsCopy, documentId, savePath);
  188. if (!result)
  189. {
  190. SetStatusError(tr("Document save failed: %1").arg(documentPath.c_str()).toUtf8().constData());
  191. return false;
  192. }
  193. return true;
  194. }
  195. // Cancel the save if no valid path was selected by this point.
  196. return false;
  197. }
  198. bool AtomToolsDocumentMainWindow::CloseDocumentCheck(const AZ::Uuid& documentId)
  199. {
  200. AZStd::string documentPath;
  201. AtomToolsDocumentRequestBus::EventResult(documentPath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
  202. bool isModified = false;
  203. AtomToolsDocumentRequestBus::EventResult(isModified, documentId, &AtomToolsDocumentRequestBus::Events::IsModified);
  204. if (isModified)
  205. {
  206. auto selection = QMessageBox::question(
  207. GetToolMainWindow(),
  208. QObject::tr("Document has unsaved changes"),
  209. QObject::tr("Do you want to save changes to\n%1?").arg(documentPath.c_str()),
  210. QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
  211. if (selection == QMessageBox::Cancel)
  212. {
  213. AZ_TracePrintf("AtomToolsDocument", "Close document canceled: %s\n", documentPath.c_str());
  214. return false;
  215. }
  216. if (selection == QMessageBox::Yes)
  217. {
  218. if (!SaveDocument(documentId))
  219. {
  220. const QString title = QObject::tr("Document could not be closed");
  221. const QString text = QObject::tr("Close document failed because document was not saved: \n%1").arg(documentPath.c_str());
  222. AZ_Error("AtomToolsDocumentMainWindow", false, "%s: %s", title.toUtf8().constData(), text.toUtf8().constData());
  223. QMessageBox::critical(
  224. GetToolMainWindow(), title, QObject::tr("%1").arg(text));
  225. return false;
  226. }
  227. }
  228. }
  229. return true;
  230. }
  231. bool AtomToolsDocumentMainWindow::CloseDocuments(const AZStd::vector<AZ::Uuid>& documentIds)
  232. {
  233. for (const auto& documentId : documentIds)
  234. {
  235. if (!CloseDocumentCheck(documentId))
  236. {
  237. return false;
  238. }
  239. bool result = false;
  240. AtomToolsDocumentSystemRequestBus::EventResult(result, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::CloseDocument, documentId);
  241. if (!result)
  242. {
  243. return false;
  244. }
  245. }
  246. return true;
  247. }
  248. const AZStd::vector<AZ::Uuid> AtomToolsDocumentMainWindow::GetOpenDocumentIds() const
  249. {
  250. AZStd::vector<AZ::Uuid> documentIds;
  251. documentIds.reserve(m_tabWidget->count());
  252. for (int index = 0; index < m_tabWidget->count(); ++index)
  253. {
  254. documentIds.push_back(GetDocumentTabId(index));
  255. }
  256. return documentIds;
  257. }
  258. void AtomToolsDocumentMainWindow::UpdateMenus(QMenuBar* menuBar)
  259. {
  260. Base::UpdateMenus(menuBar);
  261. const AZ::Uuid documentId = GetCurrentDocumentId();
  262. bool isOpen = false;
  263. AtomToolsDocumentRequestBus::EventResult(isOpen, documentId, &AtomToolsDocumentRequestBus::Events::IsOpen);
  264. bool canSaveAsChild = false;
  265. AtomToolsDocumentRequestBus::EventResult(canSaveAsChild, documentId, &AtomToolsDocumentRequestBus::Events::CanSaveAsChild);
  266. bool canUndo = false;
  267. AtomToolsDocumentRequestBus::EventResult(canUndo, documentId, &AtomToolsDocumentRequestBus::Events::CanUndo);
  268. bool canRedo = false;
  269. AtomToolsDocumentRequestBus::EventResult(canRedo, documentId, &AtomToolsDocumentRequestBus::Events::CanRedo);
  270. const bool hasTabs = m_tabWidget->count() > 0;
  271. // Update menu options
  272. m_actionClose->setEnabled(hasTabs);
  273. m_actionCloseAll->setEnabled(hasTabs);
  274. m_actionCloseOthers->setEnabled(hasTabs);
  275. m_actionSave->setEnabled(isOpen);
  276. m_actionSaveAsCopy->setEnabled(isOpen);
  277. m_actionSaveAsChild->setEnabled(canSaveAsChild);
  278. m_actionSaveAsChild->setVisible(canSaveAsChild);
  279. m_actionSaveAll->setEnabled(hasTabs);
  280. m_actionUndo->setEnabled(canUndo);
  281. m_actionRedo->setEnabled(canRedo);
  282. m_actionPreviousTab->setEnabled(m_tabWidget->count() > 1);
  283. m_actionNextTab->setEnabled(m_tabWidget->count() > 1);
  284. }
  285. void AtomToolsDocumentMainWindow::PopulateSettingsInspector(InspectorWidget* inspector) const
  286. {
  287. Base::PopulateSettingsInspector(inspector);
  288. m_documentSystemSettingsGroup = CreateSettingsPropertyGroup(
  289. "Document System Settings",
  290. "Document System Settings",
  291. { CreateSettingsPropertyValue(
  292. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/DisplayWarningMessageDialogs",
  293. "Display Warning Message Dialogs",
  294. "Display message boxes for warnings opening documents",
  295. true),
  296. CreateSettingsPropertyValue(
  297. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/DisplayErrorMessageDialogs",
  298. "Display Error Message Dialogs",
  299. "Display message boxes for errors opening documents",
  300. true),
  301. CreateSettingsPropertyValue(
  302. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/EnableAutomaticReload",
  303. "Enable Automatic Reload",
  304. "Automatically reload documents after external modifications",
  305. true),
  306. CreateSettingsPropertyValue(
  307. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/EnableAutomaticReloadPrompts",
  308. "Enable Automatic Reload Prompts",
  309. "Confirm before automatically reloading modified documents",
  310. true),
  311. CreateSettingsPropertyValue(
  312. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/AutoSaveEnabled",
  313. "Enable Auto Save",
  314. "Automatically save documents after they are modified",
  315. false),
  316. CreateSettingsPropertyValue(
  317. "/O3DE/AtomToolsFramework/AtomToolsDocumentSystem/AutoSaveInterval",
  318. "Auto Save Interval",
  319. "How often (in milliseconds) auto save occurs",
  320. aznumeric_cast<AZ::s64>(250),
  321. aznumeric_cast<AZ::s64>(0),
  322. aznumeric_cast<AZ::s64>(1000)) });
  323. inspector->AddGroup(
  324. m_documentSystemSettingsGroup->m_name,
  325. m_documentSystemSettingsGroup->m_displayName,
  326. m_documentSystemSettingsGroup->m_description,
  327. new InspectorPropertyGroupWidget(
  328. m_documentSystemSettingsGroup.get(), m_documentSystemSettingsGroup.get(), azrtti_typeid<DynamicPropertyGroup>()));
  329. }
  330. void AtomToolsDocumentMainWindow::BuildCreateMenu(QAction* insertPostion)
  331. {
  332. DocumentTypeInfoVector documentTypes;
  333. AtomToolsDocumentSystemRequestBus::EventResult(
  334. documentTypes, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::GetRegisteredDocumentTypes);
  335. // If there is more than one document type then we create a sub menu to insert all of the actions
  336. auto parentMenu = m_menuFile;
  337. if (documentTypes.size() > 1)
  338. {
  339. parentMenu = new QMenu("&New", this);
  340. m_menuFile->insertMenu(insertPostion, parentMenu);
  341. }
  342. bool isFirstDocumentTypeAdded = true;
  343. for (const auto& documentType : documentTypes)
  344. {
  345. const QString name = tr("New %1 Document...").arg(documentType.m_documentTypeName.c_str());
  346. CreateActionAtPosition(parentMenu, insertPostion, name, [documentType, toolId = m_toolId, this]() {
  347. // Open the create document dialog with labels and filters configured from the document type info.
  348. CreateDocumentDialog dialog(
  349. documentType, AZStd::string::format("%s/Assets", AZ::Utils::GetProjectPath().c_str()).c_str(), this);
  350. dialog.adjustSize();
  351. if (dialog.exec() == QDialog::Accepted)
  352. {
  353. AtomToolsDocumentSystemRequestBus::Event(
  354. toolId,
  355. &AtomToolsDocumentSystemRequestBus::Events::CreateDocumentFromFilePath,
  356. dialog.m_sourcePath.toUtf8().constData(),
  357. dialog.m_targetPath.toUtf8().constData());
  358. }
  359. }, isFirstDocumentTypeAdded ? QKeySequence::New : QKeySequence());
  360. isFirstDocumentTypeAdded = false;
  361. }
  362. }
  363. void AtomToolsDocumentMainWindow::BuildOpenMenu(QAction* insertPostion)
  364. {
  365. DocumentTypeInfoVector documentTypes;
  366. AtomToolsDocumentSystemRequestBus::EventResult(
  367. documentTypes, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::GetRegisteredDocumentTypes);
  368. // If there is more than one document type then we create a sub menu to insert all of the actions
  369. auto parentMenu = m_menuFile;
  370. if (documentTypes.size() > 1)
  371. {
  372. parentMenu = new QMenu("&Open", this);
  373. m_menuFile->insertMenu(insertPostion, parentMenu);
  374. }
  375. bool isFirstDocumentTypeAdded = true;
  376. for (const auto& documentType : documentTypes)
  377. {
  378. if (!documentType.m_supportedExtensionsToOpen.empty())
  379. {
  380. // Create a menu action for each document type instead of one action for all document types to reduce the number of
  381. // extensions displayed in the dialog
  382. const QString name = tr("Open %1 Document...").arg(documentType.m_documentTypeName.c_str());
  383. CreateActionAtPosition(parentMenu, insertPostion, name, [documentType, toolId = m_toolId]() {
  384. // Visual Studio 2022 flags toolId as unused even though it is passed to the QueueFunction lambda below.
  385. // Use AZ_UNUSED to prevent the compiler error.
  386. AZ_UNUSED(toolId);
  387. // Open all files selected in the dialog
  388. const auto& paths =
  389. GetOpenFilePathsFromDialog({}, documentType.m_supportedExtensionsToOpen, documentType.m_documentTypeName, true);
  390. AZ::SystemTickBus::QueueFunction([toolId, paths]() {
  391. for (const auto& path : paths)
  392. {
  393. AtomToolsDocumentSystemRequestBus::Event(toolId, &AtomToolsDocumentSystemRequestBus::Events::OpenDocument, path);
  394. }
  395. });
  396. }, isFirstDocumentTypeAdded ? QKeySequence::Open : QKeySequence());
  397. isFirstDocumentTypeAdded = false;
  398. }
  399. }
  400. }
  401. void AtomToolsDocumentMainWindow::AddDocumentTabBar()
  402. {
  403. m_tabWidget = new AzQtComponents::TabWidget(centralWidget());
  404. m_tabWidget->setObjectName("TabWidget");
  405. m_tabWidget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
  406. m_tabWidget->setContentsMargins(0, 0, 0, 0);
  407. // The tab bar should only be visible if it has active documents
  408. m_tabWidget->setVisible(false);
  409. m_tabWidget->setTabBarAutoHide(false);
  410. m_tabWidget->setMovable(true);
  411. m_tabWidget->setTabsClosable(true);
  412. m_tabWidget->setUsesScrollButtons(true);
  413. // Update document tab styling to fix the close button and be conformant with similar windows
  414. AzQtComponents::TabWidget::applySecondaryStyle(m_tabWidget);
  415. // This signal will be triggered whenever a tab is added, removed, selected, clicked, dragged
  416. // When the last tab is removed tabIndex will be -1 and the document ID will be null
  417. // This should automatically clear the active document
  418. connect(m_tabWidget, &QTabWidget::currentChanged, this, [this]() {
  419. const AZ::Uuid documentId = GetCurrentDocumentId();
  420. AtomToolsDocumentNotificationBus::Event(m_toolId, &AtomToolsDocumentNotificationBus::Events::OnDocumentOpened, documentId);
  421. if (auto viewWidget = m_tabWidget->currentWidget())
  422. {
  423. viewWidget->setFocus();
  424. }
  425. });
  426. connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) {
  427. CloseDocuments({ GetDocumentTabId(index) });
  428. });
  429. // Add context menu for right-clicking on tabs
  430. m_tabWidget->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu);
  431. connect(m_tabWidget, &QWidget::customContextMenuRequested, this, [this]() {
  432. OpenDocumentTabContextMenu();
  433. });
  434. centralWidget()->layout()->addWidget(m_tabWidget);
  435. }
  436. void AtomToolsDocumentMainWindow::UpdateRecentFileMenu()
  437. {
  438. m_menuOpenRecent->clear();
  439. AZStd::vector<AZStd::string> absolutePaths;
  440. AtomToolsDocumentSystemRequestBus::EventResult(
  441. absolutePaths, m_toolId, &AtomToolsDocumentSystemRequestBus::Handler::GetRecentFilePaths);
  442. for (const AZStd::string& path : absolutePaths)
  443. {
  444. if (QFile::exists(path.c_str()))
  445. {
  446. m_menuOpenRecent->addAction(tr("&%1: %2").arg(m_menuOpenRecent->actions().size()).arg(path.c_str()), [this, path]() {
  447. // Deferring execution with timer to not corrupt menu after document is opened.
  448. AZ::SystemTickBus::QueueFunction([toolId = m_toolId, path]() {
  449. AtomToolsDocumentSystemRequestBus::Event(toolId, &AtomToolsDocumentSystemRequestBus::Events::OpenDocument, path);
  450. });
  451. });
  452. }
  453. }
  454. m_menuOpenRecent->addAction(tr("Clear Recent Files"), [this]() {
  455. AZ::SystemTickBus::QueueFunction([toolId = m_toolId]() {
  456. AtomToolsDocumentSystemRequestBus::Event(toolId, &AtomToolsDocumentSystemRequestBus::Handler::ClearRecentFilePaths);
  457. });
  458. });
  459. }
  460. QString AtomToolsDocumentMainWindow::GetDocumentPath(const AZ::Uuid& documentId) const
  461. {
  462. AZStd::string absolutePath;
  463. AtomToolsDocumentRequestBus::EventResult(absolutePath, documentId, &AtomToolsDocumentRequestBus::Handler::GetAbsolutePath);
  464. return absolutePath.c_str();
  465. }
  466. AZ::Uuid AtomToolsDocumentMainWindow::GetDocumentTabId(const int tabIndex) const
  467. {
  468. const QVariant tabData = m_tabWidget->tabBar()->tabData(tabIndex);
  469. if (!tabData.isNull())
  470. {
  471. // We need to be able to convert between a UUID and a string to store and retrieve a document ID from the tab bar
  472. const QString documentIdString = tabData.toString();
  473. const QByteArray documentIdBytes = documentIdString.toUtf8();
  474. const AZ::Uuid documentId(documentIdBytes.data(), documentIdBytes.size());
  475. return documentId;
  476. }
  477. return AZ::Uuid::CreateNull();
  478. }
  479. AZ::Uuid AtomToolsDocumentMainWindow::GetCurrentDocumentId() const
  480. {
  481. return GetDocumentTabId(m_tabWidget->currentIndex());
  482. }
  483. int AtomToolsDocumentMainWindow::GetDocumentTabIndex(const AZ::Uuid& documentId) const
  484. {
  485. for (int tabIndex = 0; tabIndex < m_tabWidget->count(); ++tabIndex)
  486. {
  487. if (documentId == GetDocumentTabId(tabIndex))
  488. {
  489. return tabIndex;
  490. }
  491. }
  492. return -1;
  493. }
  494. bool AtomToolsDocumentMainWindow::HasDocumentTab(const AZ::Uuid& documentId) const
  495. {
  496. return GetDocumentTabIndex(documentId) >= 0;
  497. }
  498. bool AtomToolsDocumentMainWindow::AddDocumentTab(const AZ::Uuid& documentId, QWidget* viewWidget)
  499. {
  500. if (!documentId.IsNull() && viewWidget)
  501. {
  502. // Blocking signals from the tab bar so the currentChanged signal is not sent while a document is already being opened.
  503. // This prevents the OnDocumentOpened notification from being sent recursively.
  504. const QSignalBlocker blocker(m_tabWidget);
  505. // If a tab for this document already exists then select it instead of creating a new one
  506. if (const int tabIndex = GetDocumentTabIndex(documentId); tabIndex >= 0)
  507. {
  508. m_tabWidget->setVisible(true);
  509. m_tabWidget->setCurrentIndex(tabIndex);
  510. UpdateDocumentTab(documentId);
  511. delete viewWidget;
  512. return false;
  513. }
  514. // The user can manually reorder tabs which will invalidate any association by index.
  515. // We need to store the document ID with the tab using the tab instead of a separate mapping.
  516. const int tabIndex = m_tabWidget->addTab(viewWidget, QString());
  517. m_tabWidget->tabBar()->setTabData(tabIndex, QVariant(QString::fromUtf8(documentId.ToFixedString().c_str())));
  518. m_tabWidget->setVisible(true);
  519. m_tabWidget->setCurrentIndex(tabIndex);
  520. UpdateDocumentTab(documentId);
  521. QueueUpdateMenus(true);
  522. return true;
  523. }
  524. delete viewWidget;
  525. return false;
  526. }
  527. void AtomToolsDocumentMainWindow::RemoveDocumentTab(const AZ::Uuid& documentId)
  528. {
  529. // We are not blocking signals here because we want closing tabs to close the document and automatically select the next document.
  530. if (const int tabIndex = GetDocumentTabIndex(documentId); tabIndex >= 0)
  531. {
  532. // removeTab does not destroy the widget contained in a tab. It must be manually deleted.
  533. auto viewWidget = m_tabWidget->widget(tabIndex);
  534. m_tabWidget->removeTab(tabIndex);
  535. m_tabWidget->setVisible(m_tabWidget->count() > 0);
  536. m_tabWidget->repaint();
  537. delete viewWidget;
  538. QueueUpdateMenus(true);
  539. }
  540. }
  541. void AtomToolsDocumentMainWindow::UpdateDocumentTab(const AZ::Uuid& documentId)
  542. {
  543. // Whenever a document is opened, saved, or modified we need to update the tab label
  544. if (const int tabIndex = GetDocumentTabIndex(documentId); tabIndex >= 0)
  545. {
  546. bool isModified = false;
  547. AtomToolsDocumentRequestBus::EventResult(isModified, documentId, &AtomToolsDocumentRequestBus::Events::IsModified);
  548. AZStd::string absolutePath;
  549. AtomToolsDocumentRequestBus::EventResult(absolutePath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
  550. AZStd::string filename;
  551. AzFramework::StringFunc::Path::GetFullFileName(absolutePath.c_str(), filename);
  552. if (filename.empty())
  553. {
  554. filename = "(untitled)";
  555. }
  556. // We use an asterisk prepended to the file name to denote modified document.
  557. // Appending is standard and preferred but the tabs elide from the end (instead of middle) and cut it off.
  558. const AZStd::string label = isModified ? "* " + filename : filename;
  559. m_tabWidget->setTabText(tabIndex, label.c_str());
  560. m_tabWidget->setTabToolTip(tabIndex, absolutePath.c_str());
  561. m_tabWidget->repaint();
  562. }
  563. }
  564. void AtomToolsDocumentMainWindow::SelectPrevDocumentTab()
  565. {
  566. if (m_tabWidget->count() > 1)
  567. {
  568. // Adding count to wrap around when index <= 0
  569. m_tabWidget->setCurrentIndex((m_tabWidget->currentIndex() + m_tabWidget->count() - 1) % m_tabWidget->count());
  570. }
  571. }
  572. void AtomToolsDocumentMainWindow::SelectNextDocumentTab()
  573. {
  574. if (m_tabWidget->count() > 1)
  575. {
  576. m_tabWidget->setCurrentIndex((m_tabWidget->currentIndex() + 1) % m_tabWidget->count());
  577. }
  578. }
  579. void AtomToolsDocumentMainWindow::OpenDocumentTabContextMenu()
  580. {
  581. const QTabBar* tabBar = m_tabWidget->tabBar();
  582. const QPoint position = tabBar->mapFromGlobal(QCursor::pos());
  583. const int clickedTabIndex = tabBar->tabAt(position);
  584. if (const AZ::Uuid documentId = GetDocumentTabId(clickedTabIndex); !documentId.IsNull())
  585. {
  586. QMenu menu;
  587. PopulateTabContextMenu(documentId, menu);
  588. menu.exec(QCursor::pos());
  589. }
  590. }
  591. void AtomToolsDocumentMainWindow::PopulateTabContextMenu(const AZ::Uuid& documentId, QMenu& menu)
  592. {
  593. menu.addAction("Select", [this, documentId]() {
  594. AtomToolsDocumentNotificationBus::Event(m_toolId, &AtomToolsDocumentNotificationBus::Events::OnDocumentOpened, documentId);
  595. });
  596. menu.addAction("Close", [this, documentId]() {
  597. CloseDocuments({documentId});
  598. });
  599. menu.addAction("Close Others", [this, documentId]() {
  600. auto documentIds = GetOpenDocumentIds();
  601. AZStd::erase(documentIds, documentId);
  602. CloseDocuments(documentIds);
  603. })->setEnabled(m_tabWidget->tabBar()->count() > 1);
  604. }
  605. AZStd::string AtomToolsDocumentMainWindow::GetSaveDocumentParams(const AZStd::string& initialPath, const AZ::Uuid& documentId) const
  606. {
  607. DocumentTypeInfo documentType;
  608. AtomToolsDocumentRequestBus::EventResult(
  609. documentType, documentId, &AtomToolsDocumentRequestBus::Events::GetDocumentTypeInfo);
  610. return GetSaveFilePathFromDialog(initialPath, documentType.m_supportedExtensionsToSave, documentType.m_documentTypeName);
  611. }
  612. void AtomToolsDocumentMainWindow::OnDocumentOpened(const AZ::Uuid& documentId)
  613. {
  614. AZStd::string absolutePath;
  615. AtomToolsDocumentRequestBus::EventResult(absolutePath, documentId, &AtomToolsDocumentRequestBus::Events::GetAbsolutePath);
  616. UpdateDocumentTab(documentId);
  617. ActivateWindow();
  618. QueueUpdateMenus(true);
  619. // Whenever a document is opened or selected select the corresponding tab
  620. m_tabWidget->setCurrentIndex(GetDocumentTabIndex(documentId));
  621. if (!absolutePath.empty())
  622. {
  623. // Find and select the file path in the asset browser
  624. m_assetBrowser->SelectEntries(absolutePath);
  625. SetStatusMessage(tr("Document opened: %1").arg(absolutePath.c_str()).toUtf8().constData());
  626. }
  627. }
  628. void AtomToolsDocumentMainWindow::OnDocumentClosed(const AZ::Uuid& documentId)
  629. {
  630. RemoveDocumentTab(documentId);
  631. SetStatusMessage(tr("Document closed: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  632. }
  633. void AtomToolsDocumentMainWindow::OnDocumentCleared(const AZ::Uuid& documentId)
  634. {
  635. UpdateDocumentTab(documentId);
  636. QueueUpdateMenus(true);
  637. SetStatusMessage(tr("Document cleared: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  638. }
  639. void AtomToolsDocumentMainWindow::OnDocumentError(const AZ::Uuid& documentId)
  640. {
  641. UpdateDocumentTab(documentId);
  642. QueueUpdateMenus(true);
  643. SetStatusError(tr("Document error: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  644. }
  645. void AtomToolsDocumentMainWindow::OnDocumentDestroyed(const AZ::Uuid& documentId)
  646. {
  647. RemoveDocumentTab(documentId);
  648. }
  649. void AtomToolsDocumentMainWindow::OnDocumentModified(const AZ::Uuid& documentId)
  650. {
  651. UpdateDocumentTab(documentId);
  652. }
  653. void AtomToolsDocumentMainWindow::OnDocumentUndoStateChanged(const AZ::Uuid& documentId)
  654. {
  655. if (documentId == GetCurrentDocumentId())
  656. {
  657. QueueUpdateMenus(false);
  658. }
  659. }
  660. void AtomToolsDocumentMainWindow::OnDocumentSaved(const AZ::Uuid& documentId)
  661. {
  662. UpdateDocumentTab(documentId);
  663. SetStatusMessage(tr("Document saved: %1").arg(GetDocumentPath(documentId)).toUtf8().constData());
  664. }
  665. void AtomToolsDocumentMainWindow::closeEvent(QCloseEvent* closeEvent)
  666. {
  667. if (!CloseDocuments(GetOpenDocumentIds()))
  668. {
  669. closeEvent->ignore();
  670. return;
  671. }
  672. closeEvent->accept();
  673. Base::closeEvent(closeEvent);
  674. }
  675. void AtomToolsDocumentMainWindow::dragEnterEvent(QDragEnterEvent* event)
  676. {
  677. // Check for files matching supported document types being dragged into the main window
  678. for (const AZStd::string& path : GetPathsFromMimeData(event->mimeData()))
  679. {
  680. DocumentTypeInfoVector documentTypes;
  681. AtomToolsDocumentSystemRequestBus::EventResult(
  682. documentTypes, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::GetRegisteredDocumentTypes);
  683. for (const auto& documentType : documentTypes)
  684. {
  685. if (documentType.IsSupportedExtensionToOpen(path))
  686. {
  687. event->setAccepted(true);
  688. event->acceptProposedAction();
  689. Base::dragEnterEvent(event);
  690. return;
  691. }
  692. }
  693. }
  694. event->setAccepted(false);
  695. Base::dragEnterEvent(event);
  696. }
  697. void AtomToolsDocumentMainWindow::dragMoveEvent(QDragMoveEvent* event)
  698. {
  699. // Files dragged into the main window must only be accepted if they are within the client area
  700. event->setAccepted(centralWidget() && centralWidget()->geometry().contains(event->pos()));
  701. Base::dragMoveEvent(event);
  702. }
  703. void AtomToolsDocumentMainWindow::dragLeaveEvent(QDragLeaveEvent* event)
  704. {
  705. Base::dragLeaveEvent(event);
  706. }
  707. void AtomToolsDocumentMainWindow::dropEvent(QDropEvent* event)
  708. {
  709. // If supported document files are dragged into the main window client area attempt to open them
  710. if (centralWidget() && centralWidget()->geometry().contains(event->pos()))
  711. {
  712. AZStd::vector<AZStd::string> acceptedPaths;
  713. for (const AZStd::string& path : GetPathsFromMimeData(event->mimeData()))
  714. {
  715. DocumentTypeInfoVector documentTypes;
  716. AtomToolsDocumentSystemRequestBus::EventResult(
  717. documentTypes, m_toolId, &AtomToolsDocumentSystemRequestBus::Events::GetRegisteredDocumentTypes);
  718. for (const auto& documentType : documentTypes)
  719. {
  720. if (documentType.IsSupportedExtensionToOpen(path))
  721. {
  722. acceptedPaths.push_back(path);
  723. }
  724. }
  725. }
  726. if (!acceptedPaths.empty())
  727. {
  728. AZ::SystemTickBus::QueueFunction([toolId = m_toolId, acceptedPaths]() {
  729. for (const AZStd::string& path : acceptedPaths)
  730. {
  731. AtomToolsDocumentSystemRequestBus::Event(toolId, &AtomToolsDocumentSystemRequestBus::Events::OpenDocument, path);
  732. }
  733. });
  734. event->acceptProposedAction();
  735. }
  736. }
  737. Base::dropEvent(event);
  738. }
  739. template<typename Functor>
  740. QAction* AtomToolsDocumentMainWindow::CreateActionAtPosition(
  741. QMenu* menu, QAction* position, const QString& name, Functor fn, const QKeySequence& shortcut)
  742. {
  743. QAction* action = new QAction(name, menu);
  744. action->setShortcut(shortcut);
  745. action->setShortcutContext(Qt::WindowShortcut);
  746. QObject::connect(action, &QAction::triggered, menu, fn);
  747. menu->insertAction(position, action);
  748. return action;
  749. }
  750. } // namespace AtomToolsFramework