The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

633 lines
19KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. MultiDocumentPanelWindow::MultiDocumentPanelWindow (Colour backgroundColour)
  21. : DocumentWindow (String(), backgroundColour,
  22. DocumentWindow::maximiseButton | DocumentWindow::closeButton, false)
  23. {
  24. }
  25. MultiDocumentPanelWindow::~MultiDocumentPanelWindow()
  26. {
  27. }
  28. //==============================================================================
  29. void MultiDocumentPanelWindow::maximiseButtonPressed()
  30. {
  31. if (auto* owner = getOwner())
  32. owner->setLayoutMode (MultiDocumentPanel::MaximisedWindowsWithTabs);
  33. else
  34. jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel!
  35. }
  36. void MultiDocumentPanelWindow::closeButtonPressed()
  37. {
  38. if (auto* owner = getOwner())
  39. owner->closeDocumentAsync (getContentComponent(), true, nullptr);
  40. else
  41. jassertfalse; // these windows are only designed to be used inside a MultiDocumentPanel!
  42. }
  43. void MultiDocumentPanelWindow::activeWindowStatusChanged()
  44. {
  45. DocumentWindow::activeWindowStatusChanged();
  46. updateActiveDocument();
  47. }
  48. void MultiDocumentPanelWindow::broughtToFront()
  49. {
  50. DocumentWindow::broughtToFront();
  51. updateActiveDocument();
  52. }
  53. void MultiDocumentPanelWindow::updateActiveDocument()
  54. {
  55. if (auto* owner = getOwner())
  56. owner->updateActiveDocumentFromUIState();
  57. }
  58. MultiDocumentPanel* MultiDocumentPanelWindow::getOwner() const noexcept
  59. {
  60. return findParentComponentOfClass<MultiDocumentPanel>();
  61. }
  62. //==============================================================================
  63. struct MultiDocumentPanel::TabbedComponentInternal : public TabbedComponent
  64. {
  65. TabbedComponentInternal() : TabbedComponent (TabbedButtonBar::TabsAtTop) {}
  66. void currentTabChanged (int, const String&) override
  67. {
  68. if (auto* owner = findParentComponentOfClass<MultiDocumentPanel>())
  69. owner->updateActiveDocumentFromUIState();
  70. }
  71. };
  72. //==============================================================================
  73. MultiDocumentPanel::MultiDocumentPanel()
  74. {
  75. setOpaque (true);
  76. }
  77. MultiDocumentPanel::~MultiDocumentPanel()
  78. {
  79. for (int i = components.size(); --i >= 0;)
  80. if (auto* component = components[i])
  81. closeDocumentInternal (component);
  82. }
  83. //==============================================================================
  84. namespace MultiDocHelpers
  85. {
  86. static bool shouldDeleteComp (Component* const c)
  87. {
  88. return c->getProperties() ["mdiDocumentDelete_"];
  89. }
  90. }
  91. #if JUCE_MODAL_LOOPS_PERMITTED
  92. bool MultiDocumentPanel::closeAllDocuments (const bool checkItsOkToCloseFirst)
  93. {
  94. while (! components.isEmpty())
  95. if (! closeDocument (components.getLast(), checkItsOkToCloseFirst))
  96. return false;
  97. return true;
  98. }
  99. #endif
  100. void MultiDocumentPanel::closeLastDocumentRecursive (SafePointer<MultiDocumentPanel> parent,
  101. bool checkItsOkToCloseFirst,
  102. std::function<void (bool)> callback)
  103. {
  104. if (parent->components.isEmpty())
  105. {
  106. NullCheckedInvocation::invoke (callback, true);
  107. return;
  108. }
  109. parent->closeDocumentAsync (parent->components.getLast(),
  110. checkItsOkToCloseFirst,
  111. [parent, checkItsOkToCloseFirst, callback] (bool closeResult)
  112. {
  113. if (parent == nullptr)
  114. return;
  115. if (! closeResult)
  116. {
  117. NullCheckedInvocation::invoke (callback, false);
  118. return;
  119. }
  120. parent->closeLastDocumentRecursive (parent, checkItsOkToCloseFirst, std::move (callback));
  121. });
  122. }
  123. void MultiDocumentPanel::closeAllDocumentsAsync (bool checkItsOkToCloseFirst, std::function<void (bool)> callback)
  124. {
  125. closeLastDocumentRecursive (this, checkItsOkToCloseFirst, std::move (callback));
  126. }
  127. #if JUCE_MODAL_LOOPS_PERMITTED
  128. bool MultiDocumentPanel::tryToCloseDocument (Component*)
  129. {
  130. // If you hit this assertion then you need to implement this method in a subclass.
  131. jassertfalse;
  132. return false;
  133. }
  134. #endif
  135. MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow()
  136. {
  137. return new MultiDocumentPanelWindow (backgroundColour);
  138. }
  139. void MultiDocumentPanel::addWindow (Component* component)
  140. {
  141. auto* dw = createNewDocumentWindow();
  142. dw->setResizable (true, false);
  143. dw->setContentNonOwned (component, true);
  144. dw->setName (component->getName());
  145. auto bkg = component->getProperties() ["mdiDocumentBkg_"];
  146. dw->setBackgroundColour (bkg.isVoid() ? backgroundColour : Colour ((uint32) static_cast<int> (bkg)));
  147. int x = 4;
  148. if (auto* topComp = getChildren().getLast())
  149. if (topComp->getX() == x && topComp->getY() == x)
  150. x += 16;
  151. dw->setTopLeftPosition (x, x);
  152. auto pos = component->getProperties() ["mdiDocumentPos_"];
  153. if (pos.toString().isNotEmpty())
  154. dw->restoreWindowStateFromString (pos.toString());
  155. addAndMakeVisible (dw);
  156. dw->toFront (true);
  157. }
  158. bool MultiDocumentPanel::addDocument (Component* const component,
  159. Colour docColour,
  160. const bool deleteWhenRemoved)
  161. {
  162. // If you try passing a full DocumentWindow or ResizableWindow in here, you'll end up
  163. // with a frame-within-a-frame! Just pass in the bare content component.
  164. jassert (dynamic_cast<ResizableWindow*> (component) == nullptr);
  165. if (component == nullptr || (maximumNumDocuments > 0 && components.size() >= maximumNumDocuments))
  166. return false;
  167. components.add (component);
  168. component->getProperties().set ("mdiDocumentDelete_", deleteWhenRemoved);
  169. component->getProperties().set ("mdiDocumentBkg_", (int) docColour.getARGB());
  170. component->addComponentListener (this);
  171. if (mode == FloatingWindows)
  172. {
  173. if (isFullscreenWhenOneDocument())
  174. {
  175. if (components.size() == 1)
  176. {
  177. addAndMakeVisible (component);
  178. }
  179. else
  180. {
  181. if (components.size() == 2)
  182. addWindow (components.getFirst());
  183. addWindow (component);
  184. }
  185. }
  186. else
  187. {
  188. addWindow (component);
  189. }
  190. }
  191. else
  192. {
  193. if (tabComponent == nullptr && components.size() > numDocsBeforeTabsUsed)
  194. {
  195. tabComponent.reset (new TabbedComponentInternal());
  196. addAndMakeVisible (tabComponent.get());
  197. auto temp = components;
  198. for (auto& c : temp)
  199. tabComponent->addTab (c->getName(), docColour, c, false);
  200. resized();
  201. }
  202. else
  203. {
  204. if (tabComponent != nullptr)
  205. tabComponent->addTab (component->getName(), docColour, component, false);
  206. else
  207. addAndMakeVisible (component);
  208. }
  209. setActiveDocument (component);
  210. }
  211. resized();
  212. updateActiveDocument (component);
  213. return true;
  214. }
  215. void MultiDocumentPanel::recreateLayout()
  216. {
  217. tabComponent.reset();
  218. for (int i = getNumChildComponents(); --i >= 0;)
  219. {
  220. std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
  221. if (dw != nullptr)
  222. {
  223. dw->getContentComponent()->getProperties().set ("mdiDocumentPos_", dw->getWindowStateAsString());
  224. dw->clearContentComponent();
  225. }
  226. }
  227. resized();
  228. auto tempComps = components;
  229. components.clear();
  230. {
  231. // We want to preserve the activeComponent, so we are blocking the changes originating
  232. // from addDocument()
  233. const ScopedValueSetter<bool> scope { isLayoutBeingChanged, true };
  234. for (auto* c : tempComps)
  235. addDocument (c,
  236. Colour ((uint32) static_cast<int> (c->getProperties().getWithDefault ("mdiDocumentBkg_",
  237. (int) Colours::white.getARGB()))),
  238. MultiDocHelpers::shouldDeleteComp (c));
  239. }
  240. if (activeComponent != nullptr)
  241. setActiveDocument (activeComponent);
  242. updateActiveDocumentFromUIState();
  243. }
  244. void MultiDocumentPanel::closeDocumentInternal (Component* componentToClose)
  245. {
  246. // Intellisense warns about component being uninitialised.
  247. // I'm not sure how a function argument could be uninitialised.
  248. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  249. const OptionalScopedPointer<Component> component { componentToClose,
  250. MultiDocHelpers::shouldDeleteComp (componentToClose) };
  251. component->removeComponentListener (this);
  252. component->getProperties().remove ("mdiDocumentDelete_");
  253. component->getProperties().remove ("mdiDocumentBkg_");
  254. const auto removedIndex = components.indexOf (component);
  255. if (removedIndex < 0)
  256. {
  257. jassertfalse;
  258. return;
  259. }
  260. components.remove (removedIndex);
  261. // See if the active document needs to change because of closing a document. It should only
  262. // change if we closed the active document. If so, the next active document should be the
  263. // subsequent one.
  264. if (component == activeComponent)
  265. {
  266. auto* newActiveComponent = components[jmin (removedIndex, components.size() - 1)];
  267. updateActiveDocument (newActiveComponent);
  268. }
  269. // We update the UI to reflect the new state, but we want to prevent the UI state callback
  270. // to change the active document.
  271. const ScopedValueSetter<bool> scope { isLayoutBeingChanged, true };
  272. if (mode == FloatingWindows)
  273. {
  274. for (auto* child : getChildren())
  275. {
  276. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  277. {
  278. if (dw->getContentComponent() == component)
  279. {
  280. std::unique_ptr<MultiDocumentPanelWindow> (dw)->clearContentComponent();
  281. break;
  282. }
  283. }
  284. }
  285. if (isFullscreenWhenOneDocument() && components.size() == 1)
  286. {
  287. for (int i = getNumChildComponents(); --i >= 0;)
  288. {
  289. std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
  290. if (dw != nullptr)
  291. dw->clearContentComponent();
  292. }
  293. addAndMakeVisible (getActiveDocument());
  294. }
  295. }
  296. else
  297. {
  298. if (tabComponent != nullptr)
  299. {
  300. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  301. if (tabComponent->getTabContentComponent (i) == component)
  302. tabComponent->removeTab (i);
  303. }
  304. else
  305. {
  306. removeChildComponent (component);
  307. }
  308. if (components.size() <= numDocsBeforeTabsUsed && getActiveDocument() != nullptr)
  309. {
  310. tabComponent.reset();
  311. addAndMakeVisible (getActiveDocument());
  312. }
  313. }
  314. resized();
  315. // This ensures that the active tab is painted properly when a tab is closed!
  316. if (auto* activeDocument = getActiveDocument())
  317. setActiveDocument (activeDocument);
  318. JUCE_END_IGNORE_WARNINGS_MSVC
  319. }
  320. #if JUCE_MODAL_LOOPS_PERMITTED
  321. bool MultiDocumentPanel::closeDocument (Component* component,
  322. const bool checkItsOkToCloseFirst)
  323. {
  324. // Intellisense warns about component being uninitialised.
  325. // I'm not sure how a function argument could be uninitialised.
  326. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  327. if (component == nullptr)
  328. return true;
  329. if (components.contains (component))
  330. {
  331. if (checkItsOkToCloseFirst && ! tryToCloseDocument (component))
  332. return false;
  333. closeDocumentInternal (component);
  334. }
  335. else
  336. {
  337. jassertfalse;
  338. }
  339. return true;
  340. JUCE_END_IGNORE_WARNINGS_MSVC
  341. }
  342. #endif
  343. void MultiDocumentPanel::closeDocumentAsync (Component* component,
  344. const bool checkItsOkToCloseFirst,
  345. std::function<void (bool)> callback)
  346. {
  347. // Intellisense warns about component being uninitialised.
  348. // I'm not sure how a function argument could be uninitialised.
  349. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  350. if (component == nullptr)
  351. {
  352. NullCheckedInvocation::invoke (callback, true);
  353. return;
  354. }
  355. if (components.contains (component))
  356. {
  357. if (checkItsOkToCloseFirst)
  358. {
  359. tryToCloseDocumentAsync (component,
  360. [parent = SafePointer<MultiDocumentPanel> { this }, component, callback] (bool closedSuccessfully)
  361. {
  362. if (parent == nullptr)
  363. return;
  364. if (closedSuccessfully)
  365. parent->closeDocumentInternal (component);
  366. NullCheckedInvocation::invoke (callback, closedSuccessfully);
  367. });
  368. return;
  369. }
  370. closeDocumentInternal (component);
  371. }
  372. else
  373. {
  374. jassertfalse;
  375. }
  376. NullCheckedInvocation::invoke (callback, true);
  377. JUCE_END_IGNORE_WARNINGS_MSVC
  378. }
  379. int MultiDocumentPanel::getNumDocuments() const noexcept
  380. {
  381. return components.size();
  382. }
  383. Component* MultiDocumentPanel::getDocument (const int index) const noexcept
  384. {
  385. return components [index];
  386. }
  387. Component* MultiDocumentPanel::getActiveDocument() const noexcept
  388. {
  389. return activeComponent;
  390. }
  391. void MultiDocumentPanel::setActiveDocument (Component* component)
  392. {
  393. jassert (component != nullptr);
  394. if (mode == FloatingWindows)
  395. {
  396. component = getContainerComp (component);
  397. if (component != nullptr)
  398. component->toFront (true);
  399. }
  400. else if (tabComponent != nullptr)
  401. {
  402. jassert (components.indexOf (component) >= 0);
  403. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  404. {
  405. if (tabComponent->getTabContentComponent (i) == component)
  406. {
  407. tabComponent->setCurrentTabIndex (i);
  408. break;
  409. }
  410. }
  411. }
  412. else
  413. {
  414. component->grabKeyboardFocus();
  415. }
  416. }
  417. void MultiDocumentPanel::activeDocumentChanged()
  418. {
  419. }
  420. void MultiDocumentPanel::setMaximumNumDocuments (const int newNumber)
  421. {
  422. maximumNumDocuments = newNumber;
  423. }
  424. void MultiDocumentPanel::useFullscreenWhenOneDocument (const bool shouldUseTabs)
  425. {
  426. const auto newNumDocsBeforeTabsUsed = shouldUseTabs ? 1 : 0;
  427. if (std::exchange (numDocsBeforeTabsUsed, newNumDocsBeforeTabsUsed) != newNumDocsBeforeTabsUsed)
  428. recreateLayout();
  429. }
  430. bool MultiDocumentPanel::isFullscreenWhenOneDocument() const noexcept
  431. {
  432. return numDocsBeforeTabsUsed != 0;
  433. }
  434. //==============================================================================
  435. void MultiDocumentPanel::setLayoutMode (const LayoutMode newLayoutMode)
  436. {
  437. if (std::exchange (mode, newLayoutMode) != newLayoutMode)
  438. recreateLayout();
  439. }
  440. void MultiDocumentPanel::setBackgroundColour (Colour newBackgroundColour)
  441. {
  442. if (backgroundColour != newBackgroundColour)
  443. {
  444. backgroundColour = newBackgroundColour;
  445. setOpaque (newBackgroundColour.isOpaque());
  446. repaint();
  447. }
  448. }
  449. //==============================================================================
  450. void MultiDocumentPanel::paint (Graphics& g)
  451. {
  452. g.fillAll (backgroundColour);
  453. }
  454. void MultiDocumentPanel::resized()
  455. {
  456. if (mode == MaximisedWindowsWithTabs || components.size() == numDocsBeforeTabsUsed)
  457. {
  458. for (auto* child : getChildren())
  459. child->setBounds (getLocalBounds());
  460. }
  461. setWantsKeyboardFocus (components.size() == 0);
  462. }
  463. Component* MultiDocumentPanel::getContainerComp (Component* c) const
  464. {
  465. if (mode == FloatingWindows)
  466. {
  467. for (auto* child : getChildren())
  468. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  469. if (dw->getContentComponent() == c)
  470. return dw;
  471. }
  472. return c;
  473. }
  474. void MultiDocumentPanel::componentNameChanged (Component&)
  475. {
  476. if (mode == FloatingWindows)
  477. {
  478. for (auto* child : getChildren())
  479. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  480. dw->setName (dw->getContentComponent()->getName());
  481. }
  482. else if (tabComponent != nullptr)
  483. {
  484. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  485. tabComponent->setTabName (i, tabComponent->getTabContentComponent (i)->getName());
  486. }
  487. }
  488. void MultiDocumentPanel::updateActiveDocumentFromUIState()
  489. {
  490. auto* newActiveComponent = [&]() -> Component*
  491. {
  492. if (mode == FloatingWindows)
  493. {
  494. for (auto* c : components)
  495. {
  496. if (auto* window = static_cast<MultiDocumentPanelWindow*> (c->getParentComponent()))
  497. if (window->isActiveWindow())
  498. return c;
  499. }
  500. }
  501. if (tabComponent != nullptr)
  502. if (auto* current = tabComponent->getCurrentContentComponent())
  503. return current;
  504. return activeComponent;
  505. }();
  506. updateActiveDocument (newActiveComponent);
  507. }
  508. void MultiDocumentPanel::updateActiveDocument (Component* component)
  509. {
  510. if (isLayoutBeingChanged)
  511. return;
  512. if (std::exchange (activeComponent, component) != component)
  513. activeDocumentChanged();
  514. }
  515. } // namespace juce