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.

644 lines
23KB

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