From 13847a2ac853e966b802f51a18726c744418b784 Mon Sep 17 00:00:00 2001 From: George Reales Date: Sat, 11 Apr 2020 16:02:46 +0200 Subject: [PATCH] Create juce_StandaloneFilterWindow.h JUCE 5 Standalone application code has been modified to make the window and menu look like macOS native on Mac builds. --- .../Standalone/juce_StandaloneFilterWindow.h | 902 ++++++++++++++++++ 1 file changed, 902 insertions(+) create mode 100644 Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h diff --git a/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h b/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h new file mode 100644 index 0000000..ba2afb1 --- /dev/null +++ b/Modules/juce_audio_plugin_client/Standalone/juce_StandaloneFilterWindow.h @@ -0,0 +1,902 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2017 - ROLI Ltd. + + 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 5 End-User License + Agreement and JUCE 5 Privacy Policy (both updated and effective as of the + 27th April 2017). + + End User License Agreement: www.juce.com/juce-5-licence + Privacy Policy: www.juce.com/juce-5-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. + + ============================================================================== +*/ + +#if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client +extern juce::AudioProcessor* JUCE_API JUCE_CALLTYPE createPluginFilterOfType (juce::AudioProcessor::WrapperType type); +#endif + +namespace juce +{ + +//============================================================================== +/** + An object that creates and plays a standalone instance of an AudioProcessor. + + The object will create your processor using the same createPluginFilter() + function that the other plugin wrappers use, and will run it through the + computer's audio/MIDI devices using AudioDeviceManager and AudioProcessorPlayer. + + @tags{Audio} +*/ +class StandalonePluginHolder : private AudioIODeviceCallback, + private Timer, + private Value::Listener +{ +public: + //============================================================================== + /** Structure used for the number of inputs and outputs. */ + struct PluginInOuts { short numIns, numOuts; }; + + //============================================================================== + /** Creates an instance of the default plugin. + + The settings object can be a PropertySet that the class should use to store its + settings - the takeOwnershipOfSettings indicates whether this object will delete + the settings automatically when no longer needed. The settings can also be nullptr. + + A default device name can be passed in. + + Preferably a complete setup options object can be used, which takes precedence over + the preferredDefaultDeviceName and allows you to select the input & output device names, + sample rate, buffer size etc. + + In all instances, the settingsToUse will take precedence over the "preferred" options if not null. + */ + StandalonePluginHolder (PropertySet* settingsToUse, + bool takeOwnershipOfSettings = true, + const String& preferredDefaultDeviceName = String(), + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, + const Array& channels = Array(), + #if JUCE_ANDROID || JUCE_IOS + bool shouldAutoOpenMidiDevices = true + #else + bool shouldAutoOpenMidiDevices = true + #endif + ) + + : settings (settingsToUse, takeOwnershipOfSettings), + channelConfiguration (channels), + autoOpenMidiDevices (shouldAutoOpenMidiDevices) + { + shouldMuteInput.addListener (this); + shouldMuteInput = ! isInterAppAudioConnected(); + + createPlugin(); + + auto inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns + : processor->getMainBusNumInputChannels()); + + if (preferredSetupOptions != nullptr) + options.reset (new AudioDeviceManager::AudioDeviceSetup (*preferredSetupOptions)); + + auto audioInputRequired = (inChannels > 0); + + if (audioInputRequired && RuntimePermissions::isRequired (RuntimePermissions::recordAudio) + && ! RuntimePermissions::isGranted (RuntimePermissions::recordAudio)) + RuntimePermissions::request (RuntimePermissions::recordAudio, + [this, preferredDefaultDeviceName] (bool granted) { init (granted, preferredDefaultDeviceName); }); + else + init (audioInputRequired, preferredDefaultDeviceName); + } + + void init (bool enableAudioInput, const String& preferredDefaultDeviceName) + { + setupAudioDevices (enableAudioInput, preferredDefaultDeviceName, options.get()); + reloadPluginState(); + startPlaying(); + + if (autoOpenMidiDevices) + startTimer (500); + } + + virtual ~StandalonePluginHolder() override + { + stopTimer(); + + deletePlugin(); + shutDownAudioDevices(); + } + + //============================================================================== + virtual void createPlugin() + { + #if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client + processor.reset (::createPluginFilterOfType (AudioProcessor::wrapperType_Standalone)); + #else + AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Standalone); + processor.reset (createPluginFilter()); + AudioProcessor::setTypeOfNextNewPlugin (AudioProcessor::wrapperType_Undefined); + #endif + jassert (processor != nullptr); // Your createPluginFilter() function must return a valid object! + + processor->disableNonMainBuses(); + processor->setRateAndBufferSizeDetails (44100, 512); + + int inChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numIns + : processor->getMainBusNumInputChannels()); + + int outChannels = (channelConfiguration.size() > 0 ? channelConfiguration[0].numOuts + : processor->getMainBusNumOutputChannels()); + + processorHasPotentialFeedbackLoop = (inChannels > 0 && outChannels > 0); + } + + virtual void deletePlugin() + { + stopPlaying(); + processor = nullptr; + } + + static String getFilePatterns (const String& fileSuffix) + { + if (fileSuffix.isEmpty()) + return {}; + + return (fileSuffix.startsWithChar ('.') ? "*" : "*.") + fileSuffix; + } + + //============================================================================== + Value& getMuteInputValue() { return shouldMuteInput; } + bool getProcessorHasPotentialFeedbackLoop() const { return processorHasPotentialFeedbackLoop; } + void valueChanged (Value& value) override { muteInput = (bool) value.getValue(); } + + //============================================================================== + File getLastFile() const + { + File f; + + if (settings != nullptr) + f = File (settings->getValue ("lastStateFile")); + + if (f == File()) + f = File::getSpecialLocation (File::userDocumentsDirectory); + + return f; + } + + void setLastFile (const FileChooser& fc) + { + if (settings != nullptr) + settings->setValue ("lastStateFile", fc.getResult().getFullPathName()); + } + + /** Pops up a dialog letting the user save the processor's state to a file. */ + void askUserToSaveState (const String& fileSuffix = String()) + { + #if JUCE_MODAL_LOOPS_PERMITTED + FileChooser fc (TRANS("Save current state"), getLastFile(), getFilePatterns (fileSuffix)); + + if (fc.browseForFileToSave (true)) + { + setLastFile (fc); + + MemoryBlock data; + processor->getStateInformation (data); + + if (! fc.getResult().replaceWithData (data.getData(), data.getSize())) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + TRANS("Error whilst saving"), + TRANS("Couldn't write to the specified file!")); + } + #else + ignoreUnused (fileSuffix); + #endif + } + + /** Pops up a dialog letting the user re-load the processor's state from a file. */ + void askUserToLoadState (const String& fileSuffix = String()) + { + #if JUCE_MODAL_LOOPS_PERMITTED + FileChooser fc (TRANS("Load a saved state"), getLastFile(), getFilePatterns (fileSuffix)); + + if (fc.browseForFileToOpen()) + { + setLastFile (fc); + + MemoryBlock data; + + if (fc.getResult().loadFileAsData (data)) + processor->setStateInformation (data.getData(), (int) data.getSize()); + else + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + TRANS("Error whilst loading"), + TRANS("Couldn't read from the specified file!")); + } + #else + ignoreUnused (fileSuffix); + #endif + } + + //============================================================================== + void startPlaying() + { + player.setProcessor (processor.get()); + + #if JucePlugin_Enable_IAA && JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + { + processor->setPlayHead (device->getAudioPlayHead()); + device->setMidiMessageCollector (&player.getMidiMessageCollector()); + } + #endif + } + + void stopPlaying() + { + player.setProcessor (nullptr); + } + + //============================================================================== + /** Shows an audio properties dialog box modally. */ + void showAudioSettingsDialog() + { + DialogWindow::LaunchOptions o; + + int maxNumInputs = 0, maxNumOutputs = 0; + + if (channelConfiguration.size() > 0) + { + auto& defaultConfig = channelConfiguration.getReference (0); + + maxNumInputs = jmax (0, (int) defaultConfig.numIns); + maxNumOutputs = jmax (0, (int) defaultConfig.numOuts); + } + + if (auto* bus = processor->getBus (true, 0)) + maxNumInputs = jmax (0, bus->getDefaultLayout().size()); + + if (auto* bus = processor->getBus (false, 0)) + maxNumOutputs = jmax (0, bus->getDefaultLayout().size()); + + o.content.setOwned (new SettingsComponent (*this, deviceManager, maxNumInputs, maxNumOutputs)); + o.content->setSize (500, 550); + + o.dialogTitle = TRANS("Audio/MIDI Settings"); + o.dialogBackgroundColour = o.content->getLookAndFeel().findColour (ResizableWindow::backgroundColourId); + o.escapeKeyTriggersCloseButton = true; + o.useNativeTitleBar = true; + o.resizable = false; + + o.launchAsync(); + } + + void saveAudioDeviceState() + { + if (settings != nullptr) + { + auto xml = deviceManager.createStateXml(); + + settings->setValue ("audioSetup", xml.get()); + + #if ! (JUCE_IOS || JUCE_ANDROID) + settings->setValue ("shouldMuteInput", (bool) shouldMuteInput.getValue()); + #endif + } + } + + void reloadAudioDeviceState (bool enableAudioInput, + const String& preferredDefaultDeviceName, + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) + { + std::unique_ptr savedState; + + if (settings != nullptr) + { + savedState = settings->getXmlValue ("audioSetup"); + + #if ! (JUCE_IOS || JUCE_ANDROID) + shouldMuteInput.setValue (settings->getBoolValue ("shouldMuteInput", true)); + #endif + } + + auto totalInChannels = processor->getMainBusNumInputChannels(); + auto totalOutChannels = processor->getMainBusNumOutputChannels(); + + if (channelConfiguration.size() > 0) + { + auto defaultConfig = channelConfiguration.getReference (0); + totalInChannels = defaultConfig.numIns; + totalOutChannels = defaultConfig.numOuts; + } + + deviceManager.initialise (enableAudioInput ? totalInChannels : 0, + totalOutChannels, + savedState.get(), + true, + preferredDefaultDeviceName, + preferredSetupOptions); + } + + //============================================================================== + void savePluginState() + { + if (settings != nullptr && processor != nullptr) + { + MemoryBlock data; + processor->getStateInformation (data); + + settings->setValue ("filterState", data.toBase64Encoding()); + } + } + + void reloadPluginState() + { + if (settings != nullptr) + { + MemoryBlock data; + + if (data.fromBase64Encoding (settings->getValue ("filterState")) && data.getSize() > 0) + processor->setStateInformation (data.getData(), (int) data.getSize()); + } + } + + //============================================================================== + void switchToHostApplication() + { + #if JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + device->switchApplication(); + #endif + } + + bool isInterAppAudioConnected() + { + #if JUCE_IOS + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + return device->isInterAppAudioConnected(); + #endif + + return false; + } + + #if JUCE_MODULE_AVAILABLE_juce_gui_basics + Image getIAAHostIcon (int size) + { + #if JUCE_IOS && JucePlugin_Enable_IAA + if (auto device = dynamic_cast (deviceManager.getCurrentAudioDevice())) + return device->getIcon (size); + #else + ignoreUnused (size); + #endif + + return {}; + } + #endif + + static StandalonePluginHolder* getInstance(); + + //============================================================================== + OptionalScopedPointer settings; + std::unique_ptr processor; + AudioDeviceManager deviceManager; + AudioProcessorPlayer player; + Array channelConfiguration; + + // avoid feedback loop by default + bool processorHasPotentialFeedbackLoop = true; + std::atomic muteInput { true }; + Value shouldMuteInput; + AudioBuffer emptyBuffer; + bool autoOpenMidiDevices; + + std::unique_ptr options; + Array lastMidiDevices; + +private: + //============================================================================== + class SettingsComponent : public Component + { + public: + SettingsComponent (StandalonePluginHolder& pluginHolder, + AudioDeviceManager& deviceManagerToUse, + int maxAudioInputChannels, + int maxAudioOutputChannels) + : owner (pluginHolder), + deviceSelector (deviceManagerToUse, + 0, maxAudioInputChannels, + 0, maxAudioOutputChannels, + true, + (pluginHolder.processor.get() != nullptr && pluginHolder.processor->producesMidi()), + true, false), + shouldMuteLabel ("Feedback Loop:", "Feedback Loop:"), + shouldMuteButton ("Mute audio input") + { + setOpaque (true); + + shouldMuteButton.setClickingTogglesState (true); + shouldMuteButton.getToggleStateValue().referTo (owner.shouldMuteInput); + + addAndMakeVisible (deviceSelector); + + if (owner.getProcessorHasPotentialFeedbackLoop()) + { + addAndMakeVisible (shouldMuteButton); + addAndMakeVisible (shouldMuteLabel); + + shouldMuteLabel.attachToComponent (&shouldMuteButton, true); + } + } + + void paint (Graphics& g) override + { + g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId)); + } + + void resized() override + { + auto r = getLocalBounds(); + + if (owner.getProcessorHasPotentialFeedbackLoop()) + { + auto itemHeight = deviceSelector.getItemHeight(); + auto extra = r.removeFromTop (itemHeight); + + auto seperatorHeight = (itemHeight >> 1); + shouldMuteButton.setBounds (Rectangle (extra.proportionOfWidth (0.35f), seperatorHeight, + extra.proportionOfWidth (0.60f), deviceSelector.getItemHeight())); + + r.removeFromTop (seperatorHeight); + } + + deviceSelector.setBounds (r); + } + + private: + //============================================================================== + StandalonePluginHolder& owner; + AudioDeviceSelectorComponent deviceSelector; + Label shouldMuteLabel; + ToggleButton shouldMuteButton; + + //============================================================================== + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SettingsComponent) + }; + + //============================================================================== + void audioDeviceIOCallback (const float** inputChannelData, + int numInputChannels, + float** outputChannelData, + int numOutputChannels, + int numSamples) override + { + if (muteInput) + { + emptyBuffer.clear(); + inputChannelData = emptyBuffer.getArrayOfReadPointers(); + } + + player.audioDeviceIOCallback (inputChannelData, numInputChannels, + outputChannelData, numOutputChannels, numSamples); + } + + void audioDeviceAboutToStart (AudioIODevice* device) override + { + emptyBuffer.setSize (device->getActiveInputChannels().countNumberOfSetBits(), device->getCurrentBufferSizeSamples()); + emptyBuffer.clear(); + + player.audioDeviceAboutToStart (device); + player.setMidiOutput (deviceManager.getDefaultMidiOutput()); + } + + void audioDeviceStopped() override + { + player.setMidiOutput (nullptr); + player.audioDeviceStopped(); + emptyBuffer.setSize (0, 0); + } + + //============================================================================== + void setupAudioDevices (bool enableAudioInput, + const String& preferredDefaultDeviceName, + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions) + { + deviceManager.addAudioCallback (this); + deviceManager.addMidiInputDeviceCallback ({}, &player); + + reloadAudioDeviceState (enableAudioInput, preferredDefaultDeviceName, preferredSetupOptions); + } + + void shutDownAudioDevices() + { + saveAudioDeviceState(); + + deviceManager.removeMidiInputDeviceCallback ({}, &player); + deviceManager.removeAudioCallback (this); + } + + void timerCallback() override + { + auto newMidiDevices = MidiInput::getAvailableDevices(); + + if (newMidiDevices != lastMidiDevices) + { + for (auto& oldDevice : lastMidiDevices) + if (! newMidiDevices.contains (oldDevice)) + deviceManager.setMidiInputDeviceEnabled (oldDevice.identifier, false); + + for (auto& newDevice : newMidiDevices) + if (! lastMidiDevices.contains (newDevice)) + deviceManager.setMidiInputDeviceEnabled (newDevice.identifier, true); + + lastMidiDevices = newMidiDevices; + } + } + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandalonePluginHolder) +}; + +//============================================================================== +/** + A class that can be used to run a simple standalone application containing your filter. + + Just create one of these objects in your JUCEApplicationBase::initialise() method, and + let it do its work. It will create your filter object using the same createPluginFilter() function + that the other plugin wrappers use. + + @tags{Audio} +*/ +class StandaloneFilterWindow : public DocumentWindow, + public Button::Listener +{ +public: + //============================================================================== + typedef StandalonePluginHolder::PluginInOuts PluginInOuts; + + //============================================================================== + /** Creates a window with a given title and colour. + The settings object can be a PropertySet that the class should use to + store its settings (it can also be null). If takeOwnershipOfSettings is + true, then the settings object will be owned and deleted by this object. + */ + StandaloneFilterWindow (const String& title, + Colour backgroundColour, + PropertySet* settingsToUse, + bool takeOwnershipOfSettings, + const String& preferredDefaultDeviceName = String(), + const AudioDeviceManager::AudioDeviceSetup* preferredSetupOptions = nullptr, + const Array& constrainToConfiguration = {}, + #if JUCE_ANDROID || JUCE_IOS + bool autoOpenMidiDevices = true + #else + bool autoOpenMidiDevices = true + #endif + ) + : DocumentWindow ("", Colours::black, DocumentWindow::minimiseButton | DocumentWindow::closeButton), + optionsButton ("Options") + { + #if JUCE_IOS || JUCE_ANDROID + setTitleBarHeight (0); + #else + setTitleBarButtonsRequired (DocumentWindow::minimiseButton | DocumentWindow::closeButton, false); +#if JUCE_MAC + // setUsingNativeTitleBar(true); + /* todo menu bar */ +#endif + Component::addAndMakeVisible (optionsButton); + optionsButton.addListener (this); + optionsButton.setTriggeredOnMouseDown (true); + #endif + + pluginHolder.reset (new StandalonePluginHolder (settingsToUse, takeOwnershipOfSettings, + preferredDefaultDeviceName, preferredSetupOptions, + constrainToConfiguration, autoOpenMidiDevices)); + + #if JUCE_IOS || JUCE_ANDROID + setFullScreen (true); + setContentOwned (new MainContentComponent (*this), false); + #else + + setContentOwned (new MainContentComponent (*this), true); + + if (auto* props = pluginHolder->settings.get()) + { + const int x = props->getIntValue ("windowX", -100); + const int y = props->getIntValue ("windowY", -100); + + if (x != -100 && y != -100) + setBoundsConstrained ({ x, y, getWidth(), getHeight() }); + else + centreWithSize (getWidth(), getHeight()); + } + else + { + centreWithSize (getWidth(), getHeight()); + } + #endif + } + + ~StandaloneFilterWindow() override + { + #if (! JUCE_IOS) && (! JUCE_ANDROID) + if (auto* props = pluginHolder->settings.get()) + { + props->setValue ("windowX", getX()); + props->setValue ("windowY", getY()); + } + #endif + pluginHolder->stopPlaying(); + clearContentComponent(); + pluginHolder = nullptr; + } + + //============================================================================== + AudioProcessor* getAudioProcessor() const noexcept { return pluginHolder->processor.get(); } + AudioDeviceManager& getDeviceManager() const noexcept { return pluginHolder->deviceManager; } + + /** Deletes and re-creates the plugin, resetting it to its default state. */ + void resetToDefaultState() + { + pluginHolder->stopPlaying(); + clearContentComponent(); + pluginHolder->deletePlugin(); + + if (auto* props = pluginHolder->settings.get()) + props->removeValue ("filterState"); + + pluginHolder->createPlugin(); + setContentOwned (new MainContentComponent (*this), true); + pluginHolder->startPlaying(); + } + + //============================================================================== + void closeButtonPressed() override + { + pluginHolder->savePluginState(); + + JUCEApplicationBase::quit(); + } + + void buttonClicked (Button*) override + { + pluginHolder->showAudioSettingsDialog(); + /* + PopupMenu m; + m.addItem (1, TRANS("Audio/MIDI Settings...")); + m.addSeparator(); + m.addItem (2, TRANS("Save current state...")); + m.addItem (3, TRANS("Load a saved state...")); + m.addSeparator(); + m.addItem (4, TRANS("Reset to default state")); + + m.showMenuAsync (PopupMenu::Options(), + ModalCallbackFunction::forComponent (menuCallback, this)); + */ + } + + void handleMenuResult (int result) + { + switch (result) + { + case 1: pluginHolder->showAudioSettingsDialog(); break; + case 2: pluginHolder->askUserToSaveState(); break; + case 3: pluginHolder->askUserToLoadState(); break; + case 4: resetToDefaultState(); break; + default: break; + } + } + + static void menuCallback (int result, StandaloneFilterWindow* button) + { + if (button != nullptr && result != 0) + button->handleMenuResult (result); + } + + void resized() override + { + DocumentWindow::resized(); + optionsButton.setBounds (8, 6, 60, getTitleBarHeight() - 8); + } + + virtual StandalonePluginHolder* getPluginHolder() { return pluginHolder.get(); } + + std::unique_ptr pluginHolder; + +private: + //============================================================================== + class MainContentComponent : public Component, + private Value::Listener, + private Button::Listener, + private ComponentListener + { + public: + MainContentComponent (StandaloneFilterWindow& filterWindow) + : owner (filterWindow), notification (this), + editor (owner.getAudioProcessor()->hasEditor() ? owner.getAudioProcessor()->createEditorIfNeeded() + : new GenericAudioProcessorEditor (*owner.getAudioProcessor())) + { + Value& inputMutedValue = owner.pluginHolder->getMuteInputValue(); + + if (editor != nullptr) + { + editor->addComponentListener (this); + componentMovedOrResized (*editor, false, true); + + addAndMakeVisible (editor.get()); + } + + addChildComponent (notification); + + if (owner.pluginHolder->getProcessorHasPotentialFeedbackLoop()) + { + inputMutedValue.addListener (this); + shouldShowNotification = inputMutedValue.getValue(); + } + + inputMutedChanged (shouldShowNotification); + } + + ~MainContentComponent() override + { + if (editor != nullptr) + { + editor->removeComponentListener (this); + owner.pluginHolder->processor->editorBeingDeleted (editor.get()); + editor = nullptr; + } + } + + void resized() override + { + auto r = getLocalBounds(); + + if (shouldShowNotification) + notification.setBounds (r.removeFromTop (NotificationArea::height)); + + if (editor != nullptr) + editor->setBounds (editor->getLocalArea (this, r) + .withPosition (r.getTopLeft().transformedBy (editor->getTransform().inverted()))); + } + + private: + //============================================================================== + class NotificationArea : public Component + { + public: + enum { height = 30 }; + + NotificationArea (Button::Listener* settingsButtonListener) + : notification ("notification", "Audio input is muted to avoid feedback loop"), + #if JUCE_IOS || JUCE_ANDROID + settingsButton ("Unmute Input") + #else + settingsButton ("Settings...") + #endif + { + setOpaque (true); + + notification.setColour (Label::textColourId, Colours::black); + + settingsButton.addListener (settingsButtonListener); + + addAndMakeVisible (notification); + addAndMakeVisible (settingsButton); + } + + void paint (Graphics& g) override + { + auto r = getLocalBounds(); + + g.setColour (Colours::darkgoldenrod); + g.fillRect (r.removeFromBottom (1)); + + g.setColour (Colours::lightgoldenrodyellow); + g.fillRect (r); + } + + void resized() override + { + auto r = getLocalBounds().reduced (5); + + settingsButton.setBounds (r.removeFromRight (70)); + notification.setBounds (r); + } + private: + Label notification; + TextButton settingsButton; + }; + + //============================================================================== + void inputMutedChanged (bool newInputMutedValue) + { + shouldShowNotification = newInputMutedValue; + notification.setVisible (shouldShowNotification); + + #if JUCE_IOS || JUCE_ANDROID + resized(); + #else + if (editor != nullptr) + { + auto rect = getSizeToContainEditor(); + + setSize (rect.getWidth(), + rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0)); + } + #endif + } + + void valueChanged (Value& value) override { inputMutedChanged (value.getValue()); } + void buttonClicked (Button*) override + { + #if JUCE_IOS || JUCE_ANDROID + owner.pluginHolder->getMuteInputValue().setValue (false); + #else + owner.pluginHolder->showAudioSettingsDialog(); + #endif + } + + //============================================================================== + void componentMovedOrResized (Component&, bool, bool) override + { + if (editor != nullptr) + { + auto rect = getSizeToContainEditor(); + + setSize (rect.getWidth(), + rect.getHeight() + (shouldShowNotification ? NotificationArea::height : 0)); + } + } + + Rectangle getSizeToContainEditor() const + { + if (editor != nullptr) + return getLocalArea (editor.get(), editor->getLocalBounds()); + + return {}; + } + + //============================================================================== + StandaloneFilterWindow& owner; + NotificationArea notification; + std::unique_ptr editor; + bool shouldShowNotification = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent) + }; + + //============================================================================== + TextButton optionsButton; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (StandaloneFilterWindow) +}; + +inline StandalonePluginHolder* StandalonePluginHolder::getInstance() +{ + #if JucePlugin_Enable_IAA || JucePlugin_Build_Standalone + if (PluginHostType::getPluginLoadedAs() == AudioProcessor::wrapperType_Standalone) + { + auto& desktop = Desktop::getInstance(); + const int numTopLevelWindows = desktop.getNumComponents(); + + for (int i = 0; i < numTopLevelWindows; ++i) + if (auto window = dynamic_cast (desktop.getComponent (i))) + return window->getPluginHolder(); + } + #endif + + return nullptr; +} + +} // namespace juce