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.

641 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. if (callback != nullptr)
  107. callback (true);
  108. return;
  109. }
  110. parent->closeDocumentAsync (parent->components.getLast(),
  111. checkItsOkToCloseFirst,
  112. [parent, checkItsOkToCloseFirst, callback] (bool closeResult)
  113. {
  114. if (parent == nullptr)
  115. return;
  116. if (! closeResult)
  117. {
  118. if (callback != nullptr)
  119. callback (false);
  120. return;
  121. }
  122. parent->closeLastDocumentRecursive (parent, checkItsOkToCloseFirst, std::move (callback));
  123. });
  124. }
  125. void MultiDocumentPanel::closeAllDocumentsAsync (bool checkItsOkToCloseFirst, std::function<void (bool)> callback)
  126. {
  127. closeLastDocumentRecursive (this, checkItsOkToCloseFirst, std::move (callback));
  128. }
  129. #if JUCE_MODAL_LOOPS_PERMITTED
  130. bool MultiDocumentPanel::tryToCloseDocument (Component*)
  131. {
  132. // If you hit this assertion then you need to implement this method in a subclass.
  133. jassertfalse;
  134. return false;
  135. }
  136. #endif
  137. MultiDocumentPanelWindow* MultiDocumentPanel::createNewDocumentWindow()
  138. {
  139. return new MultiDocumentPanelWindow (backgroundColour);
  140. }
  141. void MultiDocumentPanel::addWindow (Component* component)
  142. {
  143. auto* dw = createNewDocumentWindow();
  144. dw->setResizable (true, false);
  145. dw->setContentNonOwned (component, true);
  146. dw->setName (component->getName());
  147. auto bkg = component->getProperties() ["mdiDocumentBkg_"];
  148. dw->setBackgroundColour (bkg.isVoid() ? backgroundColour : Colour ((uint32) static_cast<int> (bkg)));
  149. int x = 4;
  150. if (auto* topComp = getChildren().getLast())
  151. if (topComp->getX() == x && topComp->getY() == x)
  152. x += 16;
  153. dw->setTopLeftPosition (x, x);
  154. auto pos = component->getProperties() ["mdiDocumentPos_"];
  155. if (pos.toString().isNotEmpty())
  156. dw->restoreWindowStateFromString (pos.toString());
  157. addAndMakeVisible (dw);
  158. dw->toFront (true);
  159. }
  160. bool MultiDocumentPanel::addDocument (Component* const component,
  161. Colour docColour,
  162. const bool deleteWhenRemoved)
  163. {
  164. // If you try passing a full DocumentWindow or ResizableWindow in here, you'll end up
  165. // with a frame-within-a-frame! Just pass in the bare content component.
  166. jassert (dynamic_cast<ResizableWindow*> (component) == nullptr);
  167. if (component == nullptr || (maximumNumDocuments > 0 && components.size() >= maximumNumDocuments))
  168. return false;
  169. components.add (component);
  170. component->getProperties().set ("mdiDocumentDelete_", deleteWhenRemoved);
  171. component->getProperties().set ("mdiDocumentBkg_", (int) docColour.getARGB());
  172. component->addComponentListener (this);
  173. if (mode == FloatingWindows)
  174. {
  175. if (isFullscreenWhenOneDocument())
  176. {
  177. if (components.size() == 1)
  178. {
  179. addAndMakeVisible (component);
  180. }
  181. else
  182. {
  183. if (components.size() == 2)
  184. addWindow (components.getFirst());
  185. addWindow (component);
  186. }
  187. }
  188. else
  189. {
  190. addWindow (component);
  191. }
  192. }
  193. else
  194. {
  195. if (tabComponent == nullptr && components.size() > numDocsBeforeTabsUsed)
  196. {
  197. tabComponent.reset (new TabbedComponentInternal());
  198. addAndMakeVisible (tabComponent.get());
  199. auto temp = components;
  200. for (auto& c : temp)
  201. tabComponent->addTab (c->getName(), docColour, c, false);
  202. resized();
  203. }
  204. else
  205. {
  206. if (tabComponent != nullptr)
  207. tabComponent->addTab (component->getName(), docColour, component, false);
  208. else
  209. addAndMakeVisible (component);
  210. }
  211. setActiveDocument (component);
  212. }
  213. resized();
  214. updateActiveDocument (component);
  215. return true;
  216. }
  217. void MultiDocumentPanel::recreateLayout()
  218. {
  219. tabComponent.reset();
  220. for (int i = getNumChildComponents(); --i >= 0;)
  221. {
  222. std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
  223. if (dw != nullptr)
  224. {
  225. dw->getContentComponent()->getProperties().set ("mdiDocumentPos_", dw->getWindowStateAsString());
  226. dw->clearContentComponent();
  227. }
  228. }
  229. resized();
  230. auto tempComps = components;
  231. components.clear();
  232. {
  233. // We want to preserve the activeComponent, so we are blocking the changes originating
  234. // from addDocument()
  235. const ScopedValueSetter<bool> scope { isLayoutBeingChanged, true };
  236. for (auto* c : tempComps)
  237. addDocument (c,
  238. Colour ((uint32) static_cast<int> (c->getProperties().getWithDefault ("mdiDocumentBkg_",
  239. (int) Colours::white.getARGB()))),
  240. MultiDocHelpers::shouldDeleteComp (c));
  241. }
  242. if (activeComponent != nullptr)
  243. setActiveDocument (activeComponent);
  244. updateActiveDocumentFromUIState();
  245. }
  246. void MultiDocumentPanel::closeDocumentInternal (Component* componentToClose)
  247. {
  248. // Intellisense warns about component being uninitialised.
  249. // I'm not sure how a function argument could be uninitialised.
  250. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  251. const OptionalScopedPointer<Component> component { componentToClose,
  252. MultiDocHelpers::shouldDeleteComp (componentToClose) };
  253. component->removeComponentListener (this);
  254. component->getProperties().remove ("mdiDocumentDelete_");
  255. component->getProperties().remove ("mdiDocumentBkg_");
  256. const auto removedIndex = components.indexOf (component);
  257. if (removedIndex < 0)
  258. {
  259. jassertfalse;
  260. return;
  261. }
  262. components.remove (removedIndex);
  263. // See if the active document needs to change because of closing a document. It should only
  264. // change if we closed the active document. If so, the next active document should be the
  265. // subsequent one.
  266. if (component == activeComponent)
  267. {
  268. auto* newActiveComponent = components[jmin (removedIndex, components.size() - 1)];
  269. updateActiveDocument (newActiveComponent);
  270. }
  271. // We update the UI to reflect the new state, but we want to prevent the UI state callback
  272. // to change the active document.
  273. const ScopedValueSetter<bool> scope { isLayoutBeingChanged, true };
  274. if (mode == FloatingWindows)
  275. {
  276. for (auto* child : getChildren())
  277. {
  278. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  279. {
  280. if (dw->getContentComponent() == component)
  281. {
  282. std::unique_ptr<MultiDocumentPanelWindow> (dw)->clearContentComponent();
  283. break;
  284. }
  285. }
  286. }
  287. if (isFullscreenWhenOneDocument() && components.size() == 1)
  288. {
  289. for (int i = getNumChildComponents(); --i >= 0;)
  290. {
  291. std::unique_ptr<MultiDocumentPanelWindow> dw (dynamic_cast<MultiDocumentPanelWindow*> (getChildComponent (i)));
  292. if (dw != nullptr)
  293. dw->clearContentComponent();
  294. }
  295. addAndMakeVisible (getActiveDocument());
  296. }
  297. }
  298. else
  299. {
  300. if (tabComponent != nullptr)
  301. {
  302. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  303. if (tabComponent->getTabContentComponent (i) == component)
  304. tabComponent->removeTab (i);
  305. }
  306. else
  307. {
  308. removeChildComponent (component);
  309. }
  310. if (components.size() <= numDocsBeforeTabsUsed && getActiveDocument() != nullptr)
  311. {
  312. tabComponent.reset();
  313. addAndMakeVisible (getActiveDocument());
  314. }
  315. }
  316. resized();
  317. // This ensures that the active tab is painted properly when a tab is closed!
  318. if (auto* activeDocument = getActiveDocument())
  319. setActiveDocument (activeDocument);
  320. JUCE_END_IGNORE_WARNINGS_MSVC
  321. }
  322. #if JUCE_MODAL_LOOPS_PERMITTED
  323. bool MultiDocumentPanel::closeDocument (Component* component,
  324. const bool checkItsOkToCloseFirst)
  325. {
  326. // Intellisense warns about component being uninitialised.
  327. // I'm not sure how a function argument could be uninitialised.
  328. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  329. if (component == nullptr)
  330. return true;
  331. if (components.contains (component))
  332. {
  333. if (checkItsOkToCloseFirst && ! tryToCloseDocument (component))
  334. return false;
  335. closeDocumentInternal (component);
  336. }
  337. else
  338. {
  339. jassertfalse;
  340. }
  341. return true;
  342. JUCE_END_IGNORE_WARNINGS_MSVC
  343. }
  344. #endif
  345. void MultiDocumentPanel::closeDocumentAsync (Component* component,
  346. const bool checkItsOkToCloseFirst,
  347. std::function<void (bool)> callback)
  348. {
  349. // Intellisense warns about component being uninitialised.
  350. // I'm not sure how a function argument could be uninitialised.
  351. JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6001)
  352. if (component == nullptr)
  353. {
  354. if (callback != nullptr)
  355. callback (true);
  356. return;
  357. }
  358. if (components.contains (component))
  359. {
  360. if (checkItsOkToCloseFirst)
  361. {
  362. tryToCloseDocumentAsync (component,
  363. [parent = SafePointer<MultiDocumentPanel> { this }, component, callback] (bool closedSuccessfully)
  364. {
  365. if (parent == nullptr)
  366. return;
  367. if (closedSuccessfully)
  368. parent->closeDocumentInternal (component);
  369. if (callback != nullptr)
  370. callback (closedSuccessfully);
  371. });
  372. return;
  373. }
  374. closeDocumentInternal (component);
  375. }
  376. else
  377. {
  378. jassertfalse;
  379. }
  380. if (callback != nullptr)
  381. callback (true);
  382. JUCE_END_IGNORE_WARNINGS_MSVC
  383. }
  384. int MultiDocumentPanel::getNumDocuments() const noexcept
  385. {
  386. return components.size();
  387. }
  388. Component* MultiDocumentPanel::getDocument (const int index) const noexcept
  389. {
  390. return components [index];
  391. }
  392. Component* MultiDocumentPanel::getActiveDocument() const noexcept
  393. {
  394. return activeComponent;
  395. }
  396. void MultiDocumentPanel::setActiveDocument (Component* component)
  397. {
  398. jassert (component != nullptr);
  399. if (mode == FloatingWindows)
  400. {
  401. component = getContainerComp (component);
  402. if (component != nullptr)
  403. component->toFront (true);
  404. }
  405. else if (tabComponent != nullptr)
  406. {
  407. jassert (components.indexOf (component) >= 0);
  408. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  409. {
  410. if (tabComponent->getTabContentComponent (i) == component)
  411. {
  412. tabComponent->setCurrentTabIndex (i);
  413. break;
  414. }
  415. }
  416. }
  417. else
  418. {
  419. component->grabKeyboardFocus();
  420. }
  421. }
  422. void MultiDocumentPanel::activeDocumentChanged()
  423. {
  424. }
  425. void MultiDocumentPanel::setMaximumNumDocuments (const int newNumber)
  426. {
  427. maximumNumDocuments = newNumber;
  428. }
  429. void MultiDocumentPanel::useFullscreenWhenOneDocument (const bool shouldUseTabs)
  430. {
  431. const auto newNumDocsBeforeTabsUsed = shouldUseTabs ? 1 : 0;
  432. if (std::exchange (numDocsBeforeTabsUsed, newNumDocsBeforeTabsUsed) != newNumDocsBeforeTabsUsed)
  433. recreateLayout();
  434. }
  435. bool MultiDocumentPanel::isFullscreenWhenOneDocument() const noexcept
  436. {
  437. return numDocsBeforeTabsUsed != 0;
  438. }
  439. //==============================================================================
  440. void MultiDocumentPanel::setLayoutMode (const LayoutMode newLayoutMode)
  441. {
  442. if (std::exchange (mode, newLayoutMode) != newLayoutMode)
  443. recreateLayout();
  444. }
  445. void MultiDocumentPanel::setBackgroundColour (Colour newBackgroundColour)
  446. {
  447. if (backgroundColour != newBackgroundColour)
  448. {
  449. backgroundColour = newBackgroundColour;
  450. setOpaque (newBackgroundColour.isOpaque());
  451. repaint();
  452. }
  453. }
  454. //==============================================================================
  455. void MultiDocumentPanel::paint (Graphics& g)
  456. {
  457. g.fillAll (backgroundColour);
  458. }
  459. void MultiDocumentPanel::resized()
  460. {
  461. if (mode == MaximisedWindowsWithTabs || components.size() == numDocsBeforeTabsUsed)
  462. {
  463. for (auto* child : getChildren())
  464. child->setBounds (getLocalBounds());
  465. }
  466. setWantsKeyboardFocus (components.size() == 0);
  467. }
  468. Component* MultiDocumentPanel::getContainerComp (Component* c) const
  469. {
  470. if (mode == FloatingWindows)
  471. {
  472. for (auto* child : getChildren())
  473. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  474. if (dw->getContentComponent() == c)
  475. return dw;
  476. }
  477. return c;
  478. }
  479. void MultiDocumentPanel::componentNameChanged (Component&)
  480. {
  481. if (mode == FloatingWindows)
  482. {
  483. for (auto* child : getChildren())
  484. if (auto* dw = dynamic_cast<MultiDocumentPanelWindow*> (child))
  485. dw->setName (dw->getContentComponent()->getName());
  486. }
  487. else if (tabComponent != nullptr)
  488. {
  489. for (int i = tabComponent->getNumTabs(); --i >= 0;)
  490. tabComponent->setTabName (i, tabComponent->getTabContentComponent (i)->getName());
  491. }
  492. }
  493. void MultiDocumentPanel::updateActiveDocumentFromUIState()
  494. {
  495. auto* newActiveComponent = [&]() -> Component*
  496. {
  497. if (mode == FloatingWindows)
  498. {
  499. for (auto* c : components)
  500. {
  501. if (auto* window = static_cast<MultiDocumentPanelWindow*> (c->getParentComponent()))
  502. if (window->isActiveWindow())
  503. return c;
  504. }
  505. }
  506. if (tabComponent != nullptr)
  507. if (auto* current = tabComponent->getCurrentContentComponent())
  508. return current;
  509. return activeComponent;
  510. }();
  511. updateActiveDocument (newActiveComponent);
  512. }
  513. void MultiDocumentPanel::updateActiveDocument (Component* component)
  514. {
  515. if (isLayoutBeingChanged)
  516. return;
  517. if (std::exchange (activeComponent, component) != component)
  518. activeDocumentChanged();
  519. }
  520. } // namespace juce