/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2020 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 6 End-User License Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). End User License Agreement: www.juce.com/juce-6-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include #include "MainHostWindow.h" #include "../Plugins/InternalPlugins.h" //============================================================================== class MainHostWindow::PluginListWindow : public DocumentWindow { public: PluginListWindow (MainHostWindow& mw, AudioPluginFormatManager& pluginFormatManager) : DocumentWindow ("Available Plugins", LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId), DocumentWindow::minimiseButton | DocumentWindow::closeButton), owner (mw) { auto deadMansPedalFile = getAppProperties().getUserSettings() ->getFile().getSiblingFile ("RecentlyCrashedPluginsList"); setContentOwned (new PluginListComponent (pluginFormatManager, owner.knownPluginList, deadMansPedalFile, getAppProperties().getUserSettings(), true), true); setResizable (true, false); setResizeLimits (300, 400, 800, 1500); setTopLeftPosition (60, 60); restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("listWindowPos")); setVisible (true); } ~PluginListWindow() override { getAppProperties().getUserSettings()->setValue ("listWindowPos", getWindowStateAsString()); clearContentComponent(); } void closeButtonPressed() override { owner.pluginListWindow = nullptr; } private: MainHostWindow& owner; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginListWindow) }; //============================================================================== MainHostWindow::MainHostWindow() : DocumentWindow (JUCEApplication::getInstance()->getApplicationName(), LookAndFeel::getDefaultLookAndFeel().findColour (ResizableWindow::backgroundColourId), DocumentWindow::allButtons) { formatManager.addDefaultFormats(); formatManager.addFormat (new InternalPluginFormat()); auto safeThis = SafePointer (this); RuntimePermissions::request (RuntimePermissions::recordAudio, [safeThis] (bool granted) mutable { auto savedState = getAppProperties().getUserSettings()->getXmlValue ("audioDeviceState"); safeThis->deviceManager.initialise (granted ? 256 : 0, 256, savedState.get(), true); }); #if JUCE_IOS || JUCE_ANDROID setFullScreen (true); #else setResizable (true, false); setResizeLimits (500, 400, 10000, 10000); centreWithSize (800, 600); #endif graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList)); setContentNonOwned (graphHolder.get(), false); restoreWindowStateFromString (getAppProperties().getUserSettings()->getValue ("mainWindowPos")); setVisible (true); InternalPluginFormat internalFormat; internalTypes = internalFormat.getAllTypes(); if (auto savedPluginList = getAppProperties().getUserSettings()->getXmlValue ("pluginList")) knownPluginList.recreateFromXml (*savedPluginList); for (auto& t : internalTypes) knownPluginList.addType (t); pluginSortMethod = (KnownPluginList::SortMethod) getAppProperties().getUserSettings() ->getIntValue ("pluginSortMethod", KnownPluginList::sortByManufacturer); knownPluginList.addChangeListener (this); if (auto* g = graphHolder->graph.get()) g->addChangeListener (this); addKeyListener (getCommandManager().getKeyMappings()); Process::setPriority (Process::HighPriority); #if JUCE_IOS || JUCE_ANDROID graphHolder->burgerMenu.setModel (this); #else #if JUCE_MAC setMacMainMenu (this); #else setMenuBar (this); #endif #endif getCommandManager().setFirstCommandTarget (this); } MainHostWindow::~MainHostWindow() { pluginListWindow = nullptr; knownPluginList.removeChangeListener (this); if (auto* g = graphHolder->graph.get()) g->removeChangeListener (this); getAppProperties().getUserSettings()->setValue ("mainWindowPos", getWindowStateAsString()); clearContentComponent(); #if ! (JUCE_ANDROID || JUCE_IOS) #if JUCE_MAC setMacMainMenu (nullptr); #else setMenuBar (nullptr); #endif #endif graphHolder = nullptr; } void MainHostWindow::closeButtonPressed() { tryToQuitApplication(); } struct AsyncQuitRetrier : private Timer { AsyncQuitRetrier() { startTimer (500); } void timerCallback() override { stopTimer(); delete this; if (auto app = JUCEApplicationBase::getInstance()) app->systemRequestedQuit(); } }; void MainHostWindow::tryToQuitApplication() { if (graphHolder->closeAnyOpenPluginWindows()) { // Really important thing to note here: if the last call just deleted any plugin windows, // we won't exit immediately - instead we'll use our AsyncQuitRetrier to let the message // loop run for another brief moment, then try again. This will give any plugins a chance // to flush any GUI events that may have been in transit before the app forces them to // be unloaded new AsyncQuitRetrier(); return; } if (ModalComponentManager::getInstance()->cancelAllModalComponents()) { new AsyncQuitRetrier(); return; } if (graphHolder != nullptr) { auto releaseAndQuit = [this] { // Some plug-ins do not want [NSApp stop] to be called // before the plug-ins are not deallocated. graphHolder->releaseGraph(); JUCEApplication::quit(); }; #if JUCE_ANDROID || JUCE_IOS if (graphHolder->graph->saveDocument (PluginGraph::getDefaultGraphDocumentOnMobile())) releaseAndQuit(); #else SafePointer parent { this }; graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent, releaseAndQuit] (FileBasedDocument::SaveResult r) { if (parent == nullptr) return; if (r == FileBasedDocument::savedOk) releaseAndQuit(); }); #endif return; } JUCEApplication::quit(); } void MainHostWindow::changeListenerCallback (ChangeBroadcaster* changed) { if (changed == &knownPluginList) { menuItemsChanged(); // save the plugin list every time it gets changed, so that if we're scanning // and it crashes, we've still saved the previous ones if (auto savedPluginList = std::unique_ptr (knownPluginList.createXml())) { getAppProperties().getUserSettings()->setValue ("pluginList", savedPluginList.get()); getAppProperties().saveIfNeeded(); } } else if (graphHolder != nullptr && changed == graphHolder->graph.get()) { auto title = JUCEApplication::getInstance()->getApplicationName(); auto f = graphHolder->graph->getFile(); if (f.existsAsFile()) title = f.getFileName() + " - " + title; setName (title); } } StringArray MainHostWindow::getMenuBarNames() { StringArray names; names.add ("File"); names.add ("Plugins"); names.add ("Options"); names.add ("Windows"); return names; } PopupMenu MainHostWindow::getMenuForIndex (int topLevelMenuIndex, const String& /*menuName*/) { PopupMenu menu; if (topLevelMenuIndex == 0) { // "File" menu #if ! (JUCE_IOS || JUCE_ANDROID) menu.addCommandItem (&getCommandManager(), CommandIDs::newFile); menu.addCommandItem (&getCommandManager(), CommandIDs::open); #endif RecentlyOpenedFilesList recentFiles; recentFiles.restoreFromString (getAppProperties().getUserSettings() ->getValue ("recentFilterGraphFiles")); PopupMenu recentFilesMenu; recentFiles.createPopupMenuItems (recentFilesMenu, 100, true, true); menu.addSubMenu ("Open recent file", recentFilesMenu); #if ! (JUCE_IOS || JUCE_ANDROID) menu.addCommandItem (&getCommandManager(), CommandIDs::save); menu.addCommandItem (&getCommandManager(), CommandIDs::saveAs); #endif menu.addSeparator(); menu.addCommandItem (&getCommandManager(), StandardApplicationCommandIDs::quit); } else if (topLevelMenuIndex == 1) { // "Plugins" menu PopupMenu pluginsMenu; addPluginsToMenu (pluginsMenu); menu.addSubMenu ("Create Plug-in", pluginsMenu); menu.addSeparator(); menu.addItem (250, "Delete All Plug-ins"); } else if (topLevelMenuIndex == 2) { // "Options" menu menu.addCommandItem (&getCommandManager(), CommandIDs::showPluginListEditor); PopupMenu sortTypeMenu; sortTypeMenu.addItem (200, "List Plug-ins in Default Order", true, pluginSortMethod == KnownPluginList::defaultOrder); sortTypeMenu.addItem (201, "List Plug-ins in Alphabetical Order", true, pluginSortMethod == KnownPluginList::sortAlphabetically); sortTypeMenu.addItem (202, "List Plug-ins by Category", true, pluginSortMethod == KnownPluginList::sortByCategory); sortTypeMenu.addItem (203, "List Plug-ins by Manufacturer", true, pluginSortMethod == KnownPluginList::sortByManufacturer); sortTypeMenu.addItem (204, "List Plug-ins Based on the Directory Structure", true, pluginSortMethod == KnownPluginList::sortByFileSystemLocation); menu.addSubMenu ("Plug-in Menu Type", sortTypeMenu); menu.addSeparator(); menu.addCommandItem (&getCommandManager(), CommandIDs::showAudioSettings); menu.addCommandItem (&getCommandManager(), CommandIDs::toggleDoublePrecision); if (autoScaleOptionAvailable) menu.addCommandItem (&getCommandManager(), CommandIDs::autoScalePluginWindows); menu.addSeparator(); menu.addCommandItem (&getCommandManager(), CommandIDs::aboutBox); } else if (topLevelMenuIndex == 3) { menu.addCommandItem (&getCommandManager(), CommandIDs::allWindowsForward); } return menu; } void MainHostWindow::menuItemSelected (int menuItemID, int /*topLevelMenuIndex*/) { if (menuItemID == 250) { if (graphHolder != nullptr) if (auto* graph = graphHolder->graph.get()) graph->clear(); } #if ! (JUCE_ANDROID || JUCE_IOS) else if (menuItemID >= 100 && menuItemID < 200) { RecentlyOpenedFilesList recentFiles; recentFiles.restoreFromString (getAppProperties().getUserSettings() ->getValue ("recentFilterGraphFiles")); if (graphHolder != nullptr) { if (auto* graph = graphHolder->graph.get()) { SafePointer parent { this }; graph->saveIfNeededAndUserAgreesAsync ([parent, recentFiles, menuItemID] (FileBasedDocument::SaveResult r) { if (parent == nullptr) return; if (r == FileBasedDocument::savedOk) parent->graphHolder->graph->loadFrom (recentFiles.getFile (menuItemID - 100), true); }); } } } #endif else if (menuItemID >= 200 && menuItemID < 210) { if (menuItemID == 200) pluginSortMethod = KnownPluginList::defaultOrder; else if (menuItemID == 201) pluginSortMethod = KnownPluginList::sortAlphabetically; else if (menuItemID == 202) pluginSortMethod = KnownPluginList::sortByCategory; else if (menuItemID == 203) pluginSortMethod = KnownPluginList::sortByManufacturer; else if (menuItemID == 204) pluginSortMethod = KnownPluginList::sortByFileSystemLocation; getAppProperties().getUserSettings()->setValue ("pluginSortMethod", (int) pluginSortMethod); menuItemsChanged(); } else { if (KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuItemID) >= 0) createPlugin (getChosenType (menuItemID), { proportionOfWidth (0.3f + Random::getSystemRandom().nextFloat() * 0.6f), proportionOfHeight (0.3f + Random::getSystemRandom().nextFloat() * 0.6f) }); } } void MainHostWindow::menuBarActivated (bool isActivated) { if (isActivated && graphHolder != nullptr) graphHolder->unfocusKeyboardComponent(); } void MainHostWindow::createPlugin (const PluginDescription& desc, Point pos) { if (graphHolder != nullptr) graphHolder->createNewPlugin (desc, pos); } void MainHostWindow::addPluginsToMenu (PopupMenu& m) { if (graphHolder != nullptr) { int i = 0; for (auto& t : internalTypes) m.addItem (++i, t.name + " (" + t.pluginFormatName + ")"); } m.addSeparator(); pluginDescriptions = knownPluginList.getTypes(); // This avoids showing the internal types again later on in the list pluginDescriptions.removeIf ([] (PluginDescription& desc) { return desc.pluginFormatName == InternalPluginFormat::getIdentifier(); }); KnownPluginList::addToMenu (m, pluginDescriptions, pluginSortMethod); } PluginDescription MainHostWindow::getChosenType (const int menuID) const { if (menuID >= 1 && menuID < (int) (1 + internalTypes.size())) return internalTypes[(size_t) (menuID - 1)]; return pluginDescriptions[KnownPluginList::getIndexChosenByMenu (pluginDescriptions, menuID)]; } //============================================================================== ApplicationCommandTarget* MainHostWindow::getNextCommandTarget() { return findFirstTargetParentComponent(); } void MainHostWindow::getAllCommands (Array& commands) { // this returns the set of all commands that this target can perform.. const CommandID ids[] = { #if ! (JUCE_IOS || JUCE_ANDROID) CommandIDs::newFile, CommandIDs::open, CommandIDs::save, CommandIDs::saveAs, #endif CommandIDs::showPluginListEditor, CommandIDs::showAudioSettings, CommandIDs::toggleDoublePrecision, CommandIDs::aboutBox, CommandIDs::allWindowsForward, CommandIDs::autoScalePluginWindows }; commands.addArray (ids, numElementsInArray (ids)); } void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationCommandInfo& result) { const String category ("General"); switch (commandID) { #if ! (JUCE_IOS || JUCE_ANDROID) case CommandIDs::newFile: result.setInfo ("New", "Creates a new filter graph file", category, 0); result.defaultKeypresses.add(KeyPress('n', ModifierKeys::commandModifier, 0)); break; case CommandIDs::open: result.setInfo ("Open...", "Opens a filter graph file", category, 0); result.defaultKeypresses.add (KeyPress ('o', ModifierKeys::commandModifier, 0)); break; case CommandIDs::save: result.setInfo ("Save", "Saves the current graph to a file", category, 0); result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::commandModifier, 0)); break; case CommandIDs::saveAs: result.setInfo ("Save As...", "Saves a copy of the current graph to a file", category, 0); result.defaultKeypresses.add (KeyPress ('s', ModifierKeys::shiftModifier | ModifierKeys::commandModifier, 0)); break; #endif case CommandIDs::showPluginListEditor: result.setInfo ("Edit the List of Available Plug-ins...", {}, category, 0); result.addDefaultKeypress ('p', ModifierKeys::commandModifier); break; case CommandIDs::showAudioSettings: result.setInfo ("Change the Audio Device Settings", {}, category, 0); result.addDefaultKeypress ('a', ModifierKeys::commandModifier); break; case CommandIDs::toggleDoublePrecision: updatePrecisionMenuItem (result); break; case CommandIDs::aboutBox: result.setInfo ("About...", {}, category, 0); break; case CommandIDs::allWindowsForward: result.setInfo ("All Windows Forward", "Bring all plug-in windows forward", category, 0); result.addDefaultKeypress ('w', ModifierKeys::commandModifier); break; case CommandIDs::autoScalePluginWindows: updateAutoScaleMenuItem (result); break; default: break; } } bool MainHostWindow::perform (const InvocationInfo& info) { switch (info.commandID) { #if ! (JUCE_IOS || JUCE_ANDROID) case CommandIDs::newFile: if (graphHolder != nullptr && graphHolder->graph != nullptr) { SafePointer parent { this }; graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent] (FileBasedDocument::SaveResult r) { if (parent == nullptr) return; if (r == FileBasedDocument::savedOk) parent->graphHolder->graph->newDocument(); }); } break; case CommandIDs::open: if (graphHolder != nullptr && graphHolder->graph != nullptr) { SafePointer parent { this }; graphHolder->graph->saveIfNeededAndUserAgreesAsync ([parent] (FileBasedDocument::SaveResult r) { if (parent == nullptr) return; if (r == FileBasedDocument::savedOk) parent->graphHolder->graph->loadFromUserSpecifiedFileAsync (true, [] (Result) {}); }); } break; case CommandIDs::save: if (graphHolder != nullptr && graphHolder->graph != nullptr) graphHolder->graph->saveAsync (true, true, nullptr); break; case CommandIDs::saveAs: if (graphHolder != nullptr && graphHolder->graph != nullptr) graphHolder->graph->saveAsAsync ({}, true, true, true, nullptr); break; #endif case CommandIDs::showPluginListEditor: if (pluginListWindow == nullptr) pluginListWindow.reset (new PluginListWindow (*this, formatManager)); pluginListWindow->toFront (true); break; case CommandIDs::showAudioSettings: showAudioSettings(); break; case CommandIDs::toggleDoublePrecision: if (auto* props = getAppProperties().getUserSettings()) { auto newIsDoublePrecision = ! isDoublePrecisionProcessingEnabled(); props->setValue ("doublePrecisionProcessing", var (newIsDoublePrecision)); ApplicationCommandInfo cmdInfo (info.commandID); updatePrecisionMenuItem (cmdInfo); menuItemsChanged(); if (graphHolder != nullptr) graphHolder->setDoublePrecision (newIsDoublePrecision); } break; case CommandIDs::autoScalePluginWindows: if (auto* props = getAppProperties().getUserSettings()) { auto newAutoScale = ! isAutoScalePluginWindowsEnabled(); props->setValue ("autoScalePluginWindows", var (newAutoScale)); ApplicationCommandInfo cmdInfo (info.commandID); updateAutoScaleMenuItem (cmdInfo); menuItemsChanged(); } break; case CommandIDs::aboutBox: // TODO break; case CommandIDs::allWindowsForward: { auto& desktop = Desktop::getInstance(); for (int i = 0; i < desktop.getNumComponents(); ++i) desktop.getComponent (i)->toBehind (this); break; } default: return false; } return true; } void MainHostWindow::showAudioSettings() { auto* audioSettingsComp = new AudioDeviceSelectorComponent (deviceManager, 0, 256, 0, 256, true, true, true, false); audioSettingsComp->setSize (500, 450); DialogWindow::LaunchOptions o; o.content.setOwned (audioSettingsComp); o.dialogTitle = "Audio Settings"; o.componentToCentreAround = this; o.dialogBackgroundColour = getLookAndFeel().findColour (ResizableWindow::backgroundColourId); o.escapeKeyTriggersCloseButton = true; o.useNativeTitleBar = false; o.resizable = false; auto* w = o.create(); auto safeThis = SafePointer (this); w->enterModalState (true, ModalCallbackFunction::create ([safeThis] (int) { auto audioState = safeThis->deviceManager.createStateXml(); getAppProperties().getUserSettings()->setValue ("audioDeviceState", audioState.get()); getAppProperties().getUserSettings()->saveIfNeeded(); if (safeThis->graphHolder != nullptr) if (safeThis->graphHolder->graph != nullptr) safeThis->graphHolder->graph->graph.removeIllegalConnections(); }), true); } bool MainHostWindow::isInterestedInFileDrag (const StringArray&) { return true; } void MainHostWindow::fileDragEnter (const StringArray&, int, int) { } void MainHostWindow::fileDragMove (const StringArray&, int, int) { } void MainHostWindow::fileDragExit (const StringArray&) { } void MainHostWindow::filesDropped (const StringArray& files, int x, int y) { if (graphHolder != nullptr) { #if ! (JUCE_ANDROID || JUCE_IOS) File firstFile { files[0] }; if (files.size() == 1 && firstFile.hasFileExtension (PluginGraph::getFilenameSuffix())) { if (auto* g = graphHolder->graph.get()) { SafePointer parent; g->saveIfNeededAndUserAgreesAsync ([parent, g, firstFile] (FileBasedDocument::SaveResult r) { if (parent == nullptr) return; if (r == FileBasedDocument::savedOk) g->loadFrom (firstFile, true); }); } } else #endif { OwnedArray typesFound; knownPluginList.scanAndAddDragAndDroppedFiles (formatManager, files, typesFound); auto pos = graphHolder->getLocalPoint (this, Point (x, y)); for (int i = 0; i < jmin (5, typesFound.size()); ++i) if (auto* desc = typesFound.getUnchecked(i)) createPlugin (*desc, pos); } } } bool MainHostWindow::isDoublePrecisionProcessingEnabled() { if (auto* props = getAppProperties().getUserSettings()) return props->getBoolValue ("doublePrecisionProcessing", false); return false; } bool MainHostWindow::isAutoScalePluginWindowsEnabled() { if (auto* props = getAppProperties().getUserSettings()) return props->getBoolValue ("autoScalePluginWindows", false); return false; } void MainHostWindow::updatePrecisionMenuItem (ApplicationCommandInfo& info) { info.setInfo ("Double Floating-Point Precision Rendering", {}, "General", 0); info.setTicked (isDoublePrecisionProcessingEnabled()); } void MainHostWindow::updateAutoScaleMenuItem (ApplicationCommandInfo& info) { info.setInfo ("Auto-Scale Plug-in Windows", {}, "General", 0); info.setTicked (isAutoScalePluginWindowsEnabled()); }