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.

642 lines
23KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE 6 technical preview.
  4. Copyright (c) 2020 - Raw Material Software Limited
  5. You may use this code under the terms of the GPL v3
  6. (see www.gnu.org/licenses).
  7. For this technical preview, this file is not subject to commercial licensing.
  8. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  9. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  10. DISCLAIMED.
  11. ==============================================================================
  12. */
  13. #include <JuceHeader.h>
  14. #include "MainHostWindow.h"
  15. #include "../Plugins/InternalPlugins.h"
  16. //==============================================================================
  17. class MainHostWindow::PluginListWindow : public DocumentWindow
  18. {
  19. public:
  20. PluginListWindow (MainHostWindow& mw, AudioPluginFormatManager& pluginFormatManager)
  21. : DocumentWindow ("Available Plugins",
  22. LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId),
  23. DocumentWindow::minimiseButton | DocumentWindow::closeButton),
  24. owner (mw)
  25. {
  26. auto deadMansPedalFile = getAppProperties().getUserSettings()
  27. ->getFile().getSiblingFile ("RecentlyCrashedPluginsList");
  28. setContentOwned (new PluginListComponent (pluginFormatManager,
  29. owner.knownPluginList,
  30. deadMansPedalFile,
  31. getAppProperties().getUserSettings(), true), true);
  32. setResizable (true, false);
  33. setResizeLimits (300, 400, 800, 1500);
  34. setTopLeftPosition (60, 60);
  35. restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("listWindowPos"));
  36. setVisible (true);
  37. }
  38. ~PluginListWindow() override
  39. {
  40. getAppProperties().getUserSettings()->setValue ("listWindowPos", getWindowStateAsString());
  41. clearContentComponent();
  42. }
  43. void closeButtonPressed() override
  44. {
  45. owner.pluginListWindow = nullptr;
  46. }
  47. private:
  48. MainHostWindow& owner;
  49. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginListWindow)
  50. };
  51. //==============================================================================
  52. MainHostWindow::MainHostWindow()
  53. : DocumentWindow (JUCEApplication::getInstance()->getApplicationName(),
  54. LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId),
  55. DocumentWindow::allButtons)
  56. {
  57. formatManager.addDefaultFormats();
  58. formatManager.addFormat (new InternalPluginFormat());
  59. auto safeThis = SafePointer<MainHostWindow> (this);
  60. RuntimePermissions::request (RuntimePermissions::recordAudio,
  61. [safeThis] (bool granted) mutable
  62. {
  63. auto savedState = getAppProperties().getUserSettings()->getXmlValue ("audioDeviceState");
  64. safeThis->deviceManager.initialise (granted ? 256 : 0, 256, savedState.get(), true);
  65. });
  66. #if JUCE_IOS || JUCE_ANDROID
  67. setFullScreen (true);
  68. #else
  69. setResizable (true, false);
  70. setResizeLimits (500, 400, 10000, 10000);
  71. centreWithSize (800, 600);
  72. #endif
  73. graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList));
  74. setContentNonOwned (graphHolder.get(), false);
  75. restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("mainWindowPos"));
  76. setVisible (true);
  77. InternalPluginFormat internalFormat;
  78. internalTypes = internalFormat.getAllTypes();
  79. if (auto savedPluginList = getAppProperties().getUserSettings()->getXmlValue ("pluginList"))
  80. knownPluginList.recreateFromXml (*savedPluginList);
  81. for (auto& t : internalTypes)
  82. knownPluginList.addType (t);
  83. pluginSortMethod = (KnownPluginList::SortMethod) getAppProperties().getUserSettings()
  84. ->getIntValue ("pluginSortMethod", KnownPluginList::sortByManufacturer);
  85. knownPluginList.addChangeListener (this);
  86. if (auto* g = graphHolder->graph.get())
  87. g->addChangeListener (this);
  88. addKeyListener (getCommandManager().getKeyMappings());
  89. Process::setPriority (Process::HighPriority);
  90. #if JUCE_IOS || JUCE_ANDROID
  91. graphHolder->burgerMenu.setModel (this);
  92. #else
  93. #if JUCE_MAC
  94. setMacMainMenu (this);
  95. #else
  96. setMenuBar (this);
  97. #endif
  98. #endif
  99. getCommandManager().setFirstCommandTarget (this);
  100. }
  101. MainHostWindow::~MainHostWindow()
  102. {
  103. pluginListWindow = nullptr;
  104. knownPluginList.removeChangeListener (this);
  105. if (auto* g = graphHolder->graph.get())
  106. g->removeChangeListener (this);
  107. getAppProperties().getUserSettings()->setValue ("mainWindowPos", getWindowStateAsString());
  108. clearContentComponent();
  109. #if ! (JUCE_ANDROID || JUCE_IOS)
  110. #if JUCE_MAC
  111. setMacMainMenu (nullptr);
  112. #else
  113. setMenuBar (nullptr);
  114. #endif
  115. #endif
  116. graphHolder = nullptr;
  117. }
  118. void MainHostWindow::closeButtonPressed()
  119. {
  120. tryToQuitApplication();
  121. }
  122. struct AsyncQuitRetrier : private Timer
  123. {
  124. AsyncQuitRetrier() { startTimer (500); }
  125. void timerCallback() override
  126. {
  127. stopTimer();
  128. delete this;
  129. if (auto app = JUCEApplicationBase::getInstance())
  130. app->systemRequestedQuit();
  131. }
  132. };
  133. void MainHostWindow::tryToQuitApplication()
  134. {
  135. if (graphHolder->closeAnyOpenPluginWindows())
  136. {
  137. // Really important thing to note here: if the last call just deleted any plugin windows,
  138. // we won't exit immediately - instead we'll use our AsyncQuitRetrier to let the message
  139. // loop run for another brief moment, then try again. This will give any plugins a chance
  140. // to flush any GUI events that may have been in transit before the app forces them to
  141. // be unloaded
  142. new AsyncQuitRetrier();
  143. }
  144. else if (ModalComponentManager::getInstance()->cancelAllModalComponents())
  145. {
  146. new AsyncQuitRetrier();
  147. }
  148. #if JUCE_ANDROID || JUCE_IOS
  149. else if (graphHolder == nullptr || graphHolder->graph->saveDocument (PluginGraph::getDefaultGraphDocumentOnMobile()))
  150. #else
  151. else if (graphHolder == nullptr || graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
  152. #endif
  153. {
  154. // Some plug-ins do not want [NSApp stop] to be called
  155. // before the plug-ins are not deallocated.
  156. graphHolder->releaseGraph();
  157. JUCEApplication::quit();
  158. }
  159. }
  160. void MainHostWindow::changeListenerCallback (ChangeBroadcaster* changed)
  161. {
  162. if (changed == &knownPluginList)
  163. {
  164. menuItemsChanged();
  165. // save the plugin list every time it gets changed, so that if we're scanning
  166. // and it crashes, we've still saved the previous ones
  167. if (auto savedPluginList = std::unique_ptr<XmlElement> (knownPluginList.createXml()))
  168. {
  169. getAppProperties().getUserSettings()->setValue ("pluginList", savedPluginList.get());
  170. getAppProperties().saveIfNeeded();
  171. }
  172. }
  173. else if (graphHolder != nullptr && changed == graphHolder->graph.get())
  174. {
  175. auto title = JUCEApplication::getInstance()->getApplicationName();
  176. auto f = graphHolder->graph->getFile();
  177. if (f.existsAsFile())
  178. title = f.getFileName() + " - " + title;
  179. setName (title);
  180. }
  181. }
  182. StringArray MainHostWindow::getMenuBarNames()
  183. {
  184. StringArray names;
  185. names.add ("File");
  186. names.add ("Plugins");
  187. names.add ("Options");
  188. names.add ("Windows");
  189. return names;
  190. }
  191. PopupMenu MainHostWindow::getMenuForIndex (int topLevelMenuIndex, const String& /*menuName*/)
  192. {
  193. PopupMenu menu;
  194. if (topLevelMenuIndex == 0)
  195. {
  196. // "File" menu
  197. #if ! (JUCE_IOS || JUCE_ANDROID)
  198. menu.addCommandItem (&getCommandManager(), CommandIDs::newFile);
  199. menu.addCommandItem (&getCommandManager(), CommandIDs::open);
  200. #endif
  201. RecentlyOpenedFilesList recentFiles;
  202. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  203. ->getValue ("recentFilterGraphFiles"));
  204. PopupMenu recentFilesMenu;
  205. recentFiles.createPopupMenuItems (recentFilesMenu, 100, true, true);
  206. menu.addSubMenu ("Open recent file", recentFilesMenu);
  207. #if ! (JUCE_IOS || JUCE_ANDROID)
  208. menu.addCommandItem (&getCommandManager(), CommandIDs::save);
  209. menu.addCommandItem (&getCommandManager(), CommandIDs::saveAs);
  210. #endif
  211. menu.addSeparator();
  212. menu.addCommandItem (&getCommandManager(), StandardApplicationCommandIDs::quit);
  213. }
  214. else if (topLevelMenuIndex == 1)
  215. {
  216. // "Plugins" menu
  217. PopupMenu pluginsMenu;
  218. addPluginsToMenu (pluginsMenu);
  219. menu.addSubMenu ("Create plugin", pluginsMenu);
  220. menu.addSeparator();
  221. menu.addItem (250, "Delete all plugins");
  222. }
  223. else if (topLevelMenuIndex == 2)
  224. {
  225. // "Options" menu
  226. menu.addCommandItem (&getCommandManager(), CommandIDs::showPluginListEditor);
  227. PopupMenu sortTypeMenu;
  228. sortTypeMenu.addItem (200, "List plugins in default order", true, pluginSortMethod == KnownPluginList::defaultOrder);
  229. sortTypeMenu.addItem (201, "List plugins in alphabetical order", true, pluginSortMethod == KnownPluginList::sortAlphabetically);
  230. sortTypeMenu.addItem (202, "List plugins by category", true, pluginSortMethod == KnownPluginList::sortByCategory);
  231. sortTypeMenu.addItem (203, "List plugins by manufacturer", true, pluginSortMethod == KnownPluginList::sortByManufacturer);
  232. sortTypeMenu.addItem (204, "List plugins based on the directory structure", true, pluginSortMethod == KnownPluginList::sortByFileSystemLocation);
  233. menu.addSubMenu ("Plugin menu type", sortTypeMenu);
  234. menu.addSeparator();
  235. menu.addCommandItem (&getCommandManager(), CommandIDs::showAudioSettings);
  236. menu.addCommandItem (&getCommandManager(), CommandIDs::toggleDoublePrecision);
  237. menu.addSeparator();
  238. menu.addCommandItem (&getCommandManager(), CommandIDs::aboutBox);
  239. }
  240. else if (topLevelMenuIndex == 3)
  241. {
  242. menu.addCommandItem (&getCommandManager(), CommandIDs::allWindowsForward);
  243. }
  244. return menu;
  245. }
  246. void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/)
  247. {
  248. if (menuItemID == 250)
  249. {
  250. if (graphHolder != nullptr)
  251. if (auto* graph = graphHolder->graph.get())
  252. graph->clear();
  253. }
  254. #if ! (JUCE_ANDROID || JUCE_IOS)
  255. else if (menuItemID >= 100 && menuItemID < 200)
  256. {
  257. RecentlyOpenedFilesList recentFiles;
  258. recentFiles.restoreFromString (getAppProperties().getUserSettings()
  259. ->getValue ("recentFilterGraphFiles"));
  260. if (graphHolder != nullptr)
  261. if (auto* graph = graphHolder->graph.get())
  262. if (graph != nullptr && graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
  263. graph->loadFrom (recentFiles.getFile (menuItemID - 100), true);
  264. }
  265. #endif
  266. else if (menuItemID >= 200 && menuItemID < 210)
  267. {
  268. if (menuItemID == 200) pluginSortMethod = KnownPluginList::defaultOrder;
  269. else if (menuItemID == 201) pluginSortMethod = KnownPluginList::sortAlphabetically;
  270. else if (menuItemID == 202) pluginSortMethod = KnownPluginList::sortByCategory;
  271. else if (menuItemID == 203) pluginSortMethod = KnownPluginList::sortByManufacturer;
  272. else if (menuItemID == 204) pluginSortMethod = KnownPluginList::sortByFileSystemLocation;
  273. getAppProperties().getUserSettings()->setValue ("pluginSortMethod", (int) pluginSortMethod);
  274. menuItemsChanged();
  275. }
  276. else
  277. {
  278. if (KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuItemID) >= 0)
  279. createPlugin (getChosenType (menuItemID), { proportionOfWidth (0.3f + Random::getSystemRandom().nextFloat() * 0.6f),
  280. proportionOfHeight (0.3f + Random::getSystemRandom().nextFloat() * 0.6f) });
  281. }
  282. }
  283. void MainHostWindow::menuBarActivated (bool isActivated)
  284. {
  285. if (isActivated && graphHolder != nullptr)
  286. graphHolder->unfocusKeyboardComponent();
  287. }
  288. void MainHostWindow::createPlugin (const PluginDescription& desc, Point<int> pos)
  289. {
  290. if (graphHolder != nullptr)
  291. graphHolder->createNewPlugin (desc, pos);
  292. }
  293. void MainHostWindow::addPluginsToMenu (PopupMenu& m)
  294. {
  295. if (graphHolder != nullptr)
  296. {
  297. int i = 0;
  298. for (auto& t : internalTypes)
  299. m.addItem (++i, t.name + " (" + t.pluginFormatName + ")");
  300. }
  301. m.addSeparator();
  302. pluginDescriptions = knownPluginList.getTypes();
  303. // This avoids showing the internal types again later on in the list
  304. pluginDescriptions.removeIf ([] (PluginDescription& desc)
  305. {
  306. return desc.pluginFormatName == InternalPluginFormat::getIdentifier();
  307. });
  308. KnownPluginList::addToMenu (m, pluginDescriptions, pluginSortMethod);
  309. }
  310. PluginDescription MainHostWindow::getChosenType (const int menuID) const
  311. {
  312. if (menuID >= 1 && menuID < (int) (1 + internalTypes.size()))
  313. return internalTypes[(size_t) (menuID - 1)];
  314. return pluginDescriptions[KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuID)];
  315. }
  316. //==============================================================================
  317. ApplicationCommandTarget* MainHostWindow::getNextCommandTarget()
  318. {
  319. return findFirstTargetParentComponent();
  320. }
  321. void MainHostWindow::getAllCommands (Array<CommandID>& commands)
  322. {
  323. // this returns the set of all commands that this target can perform..
  324. const CommandID ids[] = {
  325. #if ! (JUCE_IOS || JUCE_ANDROID)
  326. CommandIDs::newFile,
  327. CommandIDs::open,
  328. CommandIDs::save,
  329. CommandIDs::saveAs,
  330. #endif
  331. CommandIDs::showPluginListEditor,
  332. CommandIDs::showAudioSettings,
  333. CommandIDs::toggleDoublePrecision,
  334. CommandIDs::aboutBox,
  335. CommandIDs::allWindowsForward
  336. };
  337. commands.addArray (ids, numElementsInArray (ids));
  338. }
  339. void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result)
  340. {
  341. const String category ("General");
  342. switch (commandID)
  343. {
  344. #if ! (JUCE_IOS || JUCE_ANDROID)
  345. case CommandIDs::newFile:
  346. result.setInfo ("New", "Creates a new filter graph file", category, 0);
  347. result.defaultKeypresses.add(KeyPress('n', ModifierKeys::commandModifier, 0));
  348. break;
  349. case CommandIDs::open:
  350. result.setInfo ("Open...", "Opens a filter graph file", category, 0);
  351. result.defaultKeypresses.add (KeyPress ('o', ModifierKeys::commandModifier, 0));
  352. break;
  353. case CommandIDs::save:
  354. result.setInfo ("Save", "Saves the current graph to a file", category, 0);
  355. result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::commandModifier, 0));
  356. break;
  357. case CommandIDs::saveAs:
  358. result.setInfo ("Save As...",
  359. "Saves a copy of the current graph to a file",
  360. category, 0);
  361. result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::shiftModifier | ModifierKeys::commandModifier, 0));
  362. break;
  363. #endif
  364. case CommandIDs::showPluginListEditor:
  365. result.setInfo ("Edit the list of available plug-Ins...", String(), category, 0);
  366. result.addDefaultKeypress ('p', ModifierKeys::commandModifier);
  367. break;
  368. case CommandIDs::showAudioSettings:
  369. result.setInfo ("Change the audio device settings", String(), category, 0);
  370. result.addDefaultKeypress ('a', ModifierKeys::commandModifier);
  371. break;
  372. case CommandIDs::toggleDoublePrecision:
  373. updatePrecisionMenuItem (result);
  374. break;
  375. case CommandIDs::aboutBox:
  376. result.setInfo ("About...", String(), category, 0);
  377. break;
  378. case CommandIDs::allWindowsForward:
  379. result.setInfo ("All Windows Forward", "Bring all plug-in windows forward", category, 0);
  380. result.addDefaultKeypress ('w', ModifierKeys::commandModifier);
  381. break;
  382. default:
  383. break;
  384. }
  385. }
  386. bool MainHostWindow::perform (const InvocationInfo& info)
  387. {
  388. switch (info.commandID)
  389. {
  390. #if ! (JUCE_IOS || JUCE_ANDROID)
  391. case CommandIDs::newFile:
  392. if (graphHolder != nullptr && graphHolder->graph != nullptr && graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
  393. graphHolder->graph->newDocument();
  394. break;
  395. case CommandIDs::open:
  396. if (graphHolder != nullptr && graphHolder->graph != nullptr && graphHolder->graph->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
  397. graphHolder->graph->loadFromUserSpecifiedFile (true);
  398. break;
  399. case CommandIDs::save:
  400. if (graphHolder != nullptr && graphHolder->graph != nullptr)
  401. graphHolder->graph->save (true, true);
  402. break;
  403. case CommandIDs::saveAs:
  404. if (graphHolder != nullptr && graphHolder->graph != nullptr)
  405. graphHolder->graph->saveAs (File(), true, true, true);
  406. break;
  407. #endif
  408. case CommandIDs::showPluginListEditor:
  409. if (pluginListWindow == nullptr)
  410. pluginListWindow.reset (new PluginListWindow (*this, formatManager));
  411. pluginListWindow->toFront (true);
  412. break;
  413. case CommandIDs::showAudioSettings:
  414. showAudioSettings();
  415. break;
  416. case CommandIDs::toggleDoublePrecision:
  417. if (auto* props = getAppProperties().getUserSettings())
  418. {
  419. bool newIsDoublePrecision = ! isDoublePrecisionProcessing();
  420. props->setValue ("doublePrecisionProcessing", var (newIsDoublePrecision));
  421. {
  422. ApplicationCommandInfo cmdInfo (info.commandID);
  423. updatePrecisionMenuItem (cmdInfo);
  424. menuItemsChanged();
  425. }
  426. if (graphHolder != nullptr)
  427. graphHolder->setDoublePrecision (newIsDoublePrecision);
  428. }
  429. break;
  430. case CommandIDs::aboutBox:
  431. // TODO
  432. break;
  433. case CommandIDs::allWindowsForward:
  434. {
  435. auto& desktop = Desktop::getInstance();
  436. for (int i = 0; i < desktop.getNumComponents(); ++i)
  437. desktop.getComponent (i)->toBehind (this);
  438. break;
  439. }
  440. default:
  441. return false;
  442. }
  443. return true;
  444. }
  445. void MainHostWindow::showAudioSettings()
  446. {
  447. auto* audioSettingsComp = new AudioDeviceSelectorComponent (deviceManager,
  448. 0, 256,
  449. 0, 256,
  450. true, true,
  451. true, false);
  452. audioSettingsComp->setSize (500, 450);
  453. DialogWindow::LaunchOptions o;
  454. o.content.setOwned (audioSettingsComp);
  455. o.dialogTitle = "Audio Settings";
  456. o.componentToCentreAround = this;
  457. o.dialogBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId);
  458. o.escapeKeyTriggersCloseButton = true;
  459. o.useNativeTitleBar = false;
  460. o.resizable = false;
  461. auto* w = o.create();
  462. auto safeThis = SafePointer<MainHostWindow> (this);
  463. w->enterModalState (true,
  464. ModalCallbackFunction::create
  465. ([safeThis] (int)
  466. {
  467. auto audioState = safeThis->deviceManager.createStateXml();
  468. getAppProperties().getUserSettings()->setValue ("audioDeviceState", audioState.get());
  469. getAppProperties().getUserSettings()->saveIfNeeded();
  470. if (safeThis->graphHolder != nullptr)
  471. if (safeThis->graphHolder->graph != nullptr)
  472. safeThis->graphHolder->graph->graph.removeIllegalConnections();
  473. }), true);
  474. }
  475. bool MainHostWindow::isInterestedInFileDrag (const StringArray&)
  476. {
  477. return true;
  478. }
  479. void MainHostWindow::fileDragEnter (const StringArray&, int, int)
  480. {
  481. }
  482. void MainHostWindow::fileDragMove (const StringArray&, int, int)
  483. {
  484. }
  485. void MainHostWindow::fileDragExit (const StringArray&)
  486. {
  487. }
  488. void MainHostWindow::filesDropped (const StringArray& files, int x, int y)
  489. {
  490. if (graphHolder != nullptr)
  491. {
  492. #if ! (JUCE_ANDROID || JUCE_IOS)
  493. if (files.size() == 1 && File (files[0]).hasFileExtension (PluginGraph::getFilenameSuffix()))
  494. {
  495. if (auto* g = graphHolder->graph.get())
  496. if (g->saveIfNeededAndUserAgrees() == FileBasedDocument::savedOk)
  497. g->loadFrom (File (files[0]), true);
  498. }
  499. else
  500. #endif
  501. {
  502. OwnedArray<PluginDescription> typesFound;
  503. knownPluginList.scanAndAddDragAndDroppedFiles (formatManager, files, typesFound);
  504. auto pos = graphHolder->getLocalPoint (this, Point<int> (x, y));
  505. for (int i = 0; i < jmin (5, typesFound.size()); ++i)
  506. if (auto* desc = typesFound.getUnchecked(i))
  507. createPlugin (*desc, pos);
  508. }
  509. }
  510. }
  511. bool MainHostWindow::isDoublePrecisionProcessing()
  512. {
  513. if (auto* props = getAppProperties().getUserSettings())
  514. return props->getBoolValue ("doublePrecisionProcessing", false);
  515. return false;
  516. }
  517. void MainHostWindow::updatePrecisionMenuItem (ApplicationCommandInfo& info)
  518. {
  519. info.setInfo ("Double floating point precision rendering", String(), "General", 0);
  520. info.setTicked (isDoublePrecisionProcessing());
  521. }