| @@ -155,5 +155,118 @@ bool isOnTouchDevice() | |||
| return isTouch; | |||
| } | |||
| //============================================================================== | |||
| static AutoScale autoScaleFromString (StringRef str) | |||
| { | |||
| if (str.isEmpty()) return AutoScale::useDefault; | |||
| if (str == CharPointer_ASCII { "0" }) return AutoScale::scaled; | |||
| if (str == CharPointer_ASCII { "1" }) return AutoScale::unscaled; | |||
| jassertfalse; | |||
| return AutoScale::useDefault; | |||
| } | |||
| static const char* autoScaleToString (AutoScale autoScale) | |||
| { | |||
| if (autoScale == AutoScale::scaled) return "0"; | |||
| if (autoScale == AutoScale::unscaled) return "1"; | |||
| return {}; | |||
| } | |||
| AutoScale getAutoScaleValueForPlugin (const String& identifier) | |||
| { | |||
| if (identifier.isNotEmpty()) | |||
| { | |||
| auto plugins = StringArray::fromLines (getAppProperties().getUserSettings()->getValue ("autoScalePlugins")); | |||
| plugins.removeEmptyStrings(); | |||
| for (auto& plugin : plugins) | |||
| { | |||
| auto fromIdentifier = plugin.fromFirstOccurrenceOf (identifier, false, false); | |||
| if (fromIdentifier.isNotEmpty()) | |||
| return autoScaleFromString (fromIdentifier.fromFirstOccurrenceOf (":", false, false)); | |||
| } | |||
| } | |||
| return AutoScale::useDefault; | |||
| } | |||
| void setAutoScaleValueForPlugin (const String& identifier, AutoScale s) | |||
| { | |||
| auto plugins = StringArray::fromLines (getAppProperties().getUserSettings()->getValue ("autoScalePlugins")); | |||
| plugins.removeEmptyStrings(); | |||
| auto index = [identifier, plugins] | |||
| { | |||
| auto it = std::find_if (plugins.begin(), plugins.end(), | |||
| [&] (const String& str) { return str.startsWith (identifier); }); | |||
| return (int) std::distance (plugins.begin(), it); | |||
| }(); | |||
| if (s == AutoScale::useDefault && index != plugins.size()) | |||
| { | |||
| plugins.remove (index); | |||
| } | |||
| else | |||
| { | |||
| auto str = identifier + ":" + autoScaleToString (s); | |||
| if (index != plugins.size()) | |||
| plugins.getReference (index) = str; | |||
| else | |||
| plugins.add (str); | |||
| } | |||
| getAppProperties().getUserSettings()->setValue ("autoScalePlugins", plugins.joinIntoString ("\n")); | |||
| } | |||
| bool shouldAutoScalePlugin (const String& identifier) | |||
| { | |||
| if (! autoScaleOptionAvailable) | |||
| return false; | |||
| const auto scaleValue = getAutoScaleValueForPlugin (identifier); | |||
| return (scaleValue == AutoScale::scaled | |||
| || (scaleValue == AutoScale::useDefault | |||
| && getAppProperties().getUserSettings()->getBoolValue ("autoScalePluginWindows"))); | |||
| } | |||
| void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance* pluginInstance, | |||
| PopupMenu& menu) | |||
| { | |||
| if (pluginInstance == nullptr) | |||
| return; | |||
| auto description = pluginInstance->getPluginDescription(); | |||
| if (! description.pluginFormatName.contains ("VST")) | |||
| return; | |||
| auto identifier = description.fileOrIdentifier; | |||
| PopupMenu autoScaleMenu; | |||
| autoScaleMenu.addItem ("Default", | |||
| true, | |||
| getAutoScaleValueForPlugin (identifier) == AutoScale::useDefault, | |||
| [identifier] { setAutoScaleValueForPlugin (identifier, AutoScale::useDefault); }); | |||
| autoScaleMenu.addItem ("Enabled", | |||
| true, | |||
| getAutoScaleValueForPlugin (identifier) == AutoScale::scaled, | |||
| [identifier] { setAutoScaleValueForPlugin (identifier, AutoScale::scaled); }); | |||
| autoScaleMenu.addItem ("Disabled", | |||
| true, | |||
| getAutoScaleValueForPlugin (identifier) == AutoScale::unscaled, | |||
| [identifier] { setAutoScaleValueForPlugin (identifier, AutoScale::unscaled); }); | |||
| menu.addSubMenu ("Auto-scale window", autoScaleMenu); | |||
| } | |||
| // This kicks the whole thing off.. | |||
| START_JUCE_APPLICATION (PluginHostApp) | |||
| @@ -29,6 +29,11 @@ | |||
| #include "InternalPlugins.h" | |||
| #include "../UI/GraphEditorPanel.h" | |||
| static std::unique_ptr<ScopedDPIAwarenessDisabler> makeDPIAwarenessDisablerForPlugin (StringRef identifier) | |||
| { | |||
| return shouldAutoScalePlugin (identifier) ? std::make_unique<ScopedDPIAwarenessDisabler>() | |||
| : nullptr; | |||
| } | |||
| //============================================================================== | |||
| PluginGraph::PluginGraph (AudioPluginFormatManager& fm) | |||
| @@ -76,10 +81,12 @@ AudioProcessorGraph::Node::Ptr PluginGraph::getNodeForName (const String& name) | |||
| void PluginGraph::addPlugin (const PluginDescription& desc, Point<double> pos) | |||
| { | |||
| std::shared_ptr<ScopedDPIAwarenessDisabler> dpiDisabler = makeDPIAwarenessDisablerForPlugin (desc.fileOrIdentifier); | |||
| formatManager.createPluginInstanceAsync (desc, | |||
| graph.getSampleRate(), | |||
| graph.getBlockSize(), | |||
| [this, pos] (std::unique_ptr<AudioPluginInstance> instance, const String& error) | |||
| [this, pos, dpiDisabler] (std::unique_ptr<AudioPluginInstance> instance, const String& error) | |||
| { | |||
| addPluginCallback (std::move (instance), error, pos); | |||
| }); | |||
| @@ -156,18 +163,10 @@ PluginWindow* PluginGraph::getOrCreateWindowFor (AudioProcessorGraph::Node* node | |||
| getCommandManager().invokeDirectly (CommandIDs::showAudioSettings, false); | |||
| return nullptr; | |||
| } | |||
| } | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| if (! node->properties["DPIAware"] | |||
| && ! node->getProcessor()->getName().contains ("Kontakt")) // Kontakt doesn't behave correctly in DPI unaware mode... | |||
| { | |||
| ScopedDPIAwarenessDisabler disableDPIAwareness; | |||
| auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (description.fileOrIdentifier); | |||
| return activePluginWindows.add (new PluginWindow (node, type, activePluginWindows)); | |||
| } | |||
| #endif | |||
| return activePluginWindows.add (new PluginWindow (node, type, activePluginWindows)); | |||
| } | |||
| return nullptr; | |||
| @@ -332,9 +331,6 @@ static XmlElement* createNodeXml (AudioProcessorGraph::Node* const node) noexcep | |||
| e->setAttribute ("uid", (int) node->nodeID.uid); | |||
| e->setAttribute ("x", node->properties ["x"].toString()); | |||
| e->setAttribute ("y", node->properties ["y"].toString()); | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| e->setAttribute ("DPIAware", node->properties["DPIAware"].toString()); | |||
| #endif | |||
| for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i) | |||
| { | |||
| @@ -383,10 +379,16 @@ void PluginGraph::createNodeFromXml (const XmlElement& xml) | |||
| break; | |||
| } | |||
| String errorMessage; | |||
| auto createInstance = [this, pd] | |||
| { | |||
| String errorMessage; | |||
| auto localDpiDisabler = makeDPIAwarenessDisablerForPlugin (pd.fileOrIdentifier); | |||
| return formatManager.createPluginInstance (pd, graph.getSampleRate(), | |||
| graph.getBlockSize(), errorMessage); | |||
| }; | |||
| if (auto instance = formatManager.createPluginInstance (pd, graph.getSampleRate(), | |||
| graph.getBlockSize(), errorMessage)) | |||
| if (auto instance = createInstance()) | |||
| { | |||
| if (auto* layoutEntity = xml.getChildByName ("LAYOUT")) | |||
| { | |||
| @@ -408,11 +410,8 @@ void PluginGraph::createNodeFromXml (const XmlElement& xml) | |||
| node->getProcessor()->setStateInformation (m.getData(), (int) m.getSize()); | |||
| } | |||
| node->properties.set ("x", xml.getDoubleAttribute ("x")); | |||
| node->properties.set ("y", xml.getDoubleAttribute ("y")); | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| node->properties.set ("DPIAware", xml.getDoubleAttribute ("DPIAware")); | |||
| #endif | |||
| node->properties.set ("x", xml.getDoubleAttribute ("x")); | |||
| node->properties.set ("y", xml.getDoubleAttribute ("y")); | |||
| for (int i = 0; i < (int) PluginWindow::Type::numTypes; ++i) | |||
| { | |||
| @@ -403,21 +403,14 @@ struct GraphEditorPanel::PluginComponent : public Component, | |||
| menu->addItem (2, "Disconnect all pins"); | |||
| menu->addItem (3, "Toggle Bypass"); | |||
| if (getProcessor()->hasEditor()) | |||
| { | |||
| menu->addSeparator(); | |||
| menu->addItem (10, "Show plugin GUI"); | |||
| menu->addItem (11, "Show all programs"); | |||
| menu->addItem (12, "Show all parameters"); | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| auto isTicked = false; | |||
| if (auto* node = graph.graph.getNodeForId (pluginID)) | |||
| isTicked = node->properties["DPIAware"]; | |||
| menu->addItem (13, "Enable DPI awareness", true, isTicked); | |||
| #endif | |||
| menu->addItem (14, "Show debug log"); | |||
| } | |||
| menu->addSeparator(); | |||
| menu->addItem (10, "Show plugin GUI"); | |||
| menu->addItem (11, "Show all programs"); | |||
| menu->addItem (12, "Show all parameters"); | |||
| menu->addItem (13, "Show debug log"); | |||
| if (autoScaleOptionAvailable) | |||
| addPluginAutoScaleOptionsSubMenu (dynamic_cast<AudioPluginInstance*> (getProcessor()), *menu); | |||
| menu->addSeparator(); | |||
| menu->addItem (20, "Configure Audio I/O"); | |||
| @@ -441,15 +434,7 @@ struct GraphEditorPanel::PluginComponent : public Component, | |||
| case 10: showWindow (PluginWindow::Type::normal); break; | |||
| case 11: showWindow (PluginWindow::Type::programs); break; | |||
| case 12: showWindow (PluginWindow::Type::generic) ; break; | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| case 13: | |||
| { | |||
| if (auto* node = graph.graph.getNodeForId (pluginID)) | |||
| node->properties.set ("DPIAware", ! node->properties ["DPIAware"]); | |||
| break; | |||
| } | |||
| #endif | |||
| case 14: showWindow (PluginWindow::Type::debug); break; | |||
| case 13: showWindow (PluginWindow::Type::debug); break; | |||
| case 20: showWindow (PluginWindow::Type::audioIO); break; | |||
| case 21: testStateSaveLoad(); break; | |||
| @@ -277,9 +277,9 @@ PopupMenu MainHostWindow::getMenuForIndex (int topLevelMenuIndex, const String& | |||
| // "Plugins" menu | |||
| PopupMenu pluginsMenu; | |||
| addPluginsToMenu (pluginsMenu); | |||
| menu.addSubMenu ("Create plugin", pluginsMenu); | |||
| menu.addSubMenu ("Create Plug-in", pluginsMenu); | |||
| menu.addSeparator(); | |||
| menu.addItem (250, "Delete all plugins"); | |||
| menu.addItem (250, "Delete All Plug-ins"); | |||
| } | |||
| else if (topLevelMenuIndex == 2) | |||
| { | |||
| @@ -288,17 +288,20 @@ PopupMenu MainHostWindow::getMenuForIndex (int topLevelMenuIndex, const String& | |||
| menu.addCommandItem (&getCommandManager(), CommandIDs::showPluginListEditor); | |||
| PopupMenu sortTypeMenu; | |||
| sortTypeMenu.addItem (200, "List plugins in default order", true, pluginSortMethod == KnownPluginList::defaultOrder); | |||
| sortTypeMenu.addItem (201, "List plugins in alphabetical order", true, pluginSortMethod == KnownPluginList::sortAlphabetically); | |||
| sortTypeMenu.addItem (202, "List plugins by category", true, pluginSortMethod == KnownPluginList::sortByCategory); | |||
| sortTypeMenu.addItem (203, "List plugins by manufacturer", true, pluginSortMethod == KnownPluginList::sortByManufacturer); | |||
| sortTypeMenu.addItem (204, "List plugins based on the directory structure", true, pluginSortMethod == KnownPluginList::sortByFileSystemLocation); | |||
| menu.addSubMenu ("Plugin menu type", 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); | |||
| } | |||
| @@ -414,7 +417,8 @@ void MainHostWindow::getAllCommands (Array<CommandID>& commands) | |||
| CommandIDs::showAudioSettings, | |||
| CommandIDs::toggleDoublePrecision, | |||
| CommandIDs::aboutBox, | |||
| CommandIDs::allWindowsForward | |||
| CommandIDs::allWindowsForward, | |||
| CommandIDs::autoScalePluginWindows | |||
| }; | |||
| commands.addArray (ids, numElementsInArray (ids)); | |||
| @@ -451,12 +455,12 @@ void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationComma | |||
| #endif | |||
| case CommandIDs::showPluginListEditor: | |||
| result.setInfo ("Edit the list of available plug-Ins...", String(), category, 0); | |||
| 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", String(), category, 0); | |||
| result.setInfo ("Change the Audio Device Settings", {}, category, 0); | |||
| result.addDefaultKeypress ('a', ModifierKeys::commandModifier); | |||
| break; | |||
| @@ -465,7 +469,7 @@ void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationComma | |||
| break; | |||
| case CommandIDs::aboutBox: | |||
| result.setInfo ("About...", String(), category, 0); | |||
| result.setInfo ("About...", {}, category, 0); | |||
| break; | |||
| case CommandIDs::allWindowsForward: | |||
| @@ -473,6 +477,10 @@ void MainHostWindow::getCommandInfo (const CommandID commandID, ApplicationComma | |||
| result.addDefaultKeypress ('w', ModifierKeys::commandModifier); | |||
| break; | |||
| case CommandIDs::autoScalePluginWindows: | |||
| updateAutoScaleMenuItem (result); | |||
| break; | |||
| default: | |||
| break; | |||
| } | |||
| @@ -518,20 +526,30 @@ bool MainHostWindow::perform (const InvocationInfo& info) | |||
| case CommandIDs::toggleDoublePrecision: | |||
| if (auto* props = getAppProperties().getUserSettings()) | |||
| { | |||
| bool newIsDoublePrecision = ! isDoublePrecisionProcessing(); | |||
| auto newIsDoublePrecision = ! isDoublePrecisionProcessingEnabled(); | |||
| props->setValue ("doublePrecisionProcessing", var (newIsDoublePrecision)); | |||
| { | |||
| ApplicationCommandInfo cmdInfo (info.commandID); | |||
| updatePrecisionMenuItem (cmdInfo); | |||
| menuItemsChanged(); | |||
| } | |||
| 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; | |||
| @@ -633,7 +651,7 @@ void MainHostWindow::filesDropped (const StringArray& files, int x, int y) | |||
| } | |||
| } | |||
| bool MainHostWindow::isDoublePrecisionProcessing() | |||
| bool MainHostWindow::isDoublePrecisionProcessingEnabled() | |||
| { | |||
| if (auto* props = getAppProperties().getUserSettings()) | |||
| return props->getBoolValue ("doublePrecisionProcessing", false); | |||
| @@ -641,8 +659,22 @@ bool MainHostWindow::isDoublePrecisionProcessing() | |||
| 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", String(), "General", 0); | |||
| info.setTicked (isDoublePrecisionProcessing()); | |||
| 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()); | |||
| } | |||
| @@ -43,12 +43,34 @@ namespace CommandIDs | |||
| static const int aboutBox = 0x30300; | |||
| static const int allWindowsForward = 0x30400; | |||
| static const int toggleDoublePrecision = 0x30500; | |||
| static const int autoScalePluginWindows = 0x30600; | |||
| } | |||
| //============================================================================== | |||
| ApplicationCommandManager& getCommandManager(); | |||
| ApplicationProperties& getAppProperties(); | |||
| bool isOnTouchDevice(); | |||
| //============================================================================== | |||
| enum class AutoScale | |||
| { | |||
| scaled, | |||
| unscaled, | |||
| useDefault | |||
| }; | |||
| constexpr bool autoScaleOptionAvailable = | |||
| #if JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE | |||
| true; | |||
| #else | |||
| false; | |||
| #endif | |||
| AutoScale getAutoScaleValueForPlugin (const String&); | |||
| void setAutoScaleValueForPlugin (const String&, AutoScale); | |||
| bool shouldAutoScalePlugin (const String&); | |||
| void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance*, PopupMenu&); | |||
| //============================================================================== | |||
| class MainHostWindow : public DocumentWindow, | |||
| public MenuBarModel, | |||
| @@ -88,12 +110,18 @@ public: | |||
| void addPluginsToMenu (PopupMenu&); | |||
| PluginDescription getChosenType (int menuID) const; | |||
| bool isDoublePrecisionProcessing(); | |||
| void updatePrecisionMenuItem (ApplicationCommandInfo& info); | |||
| std::unique_ptr<GraphDocumentComponent> graphHolder; | |||
| private: | |||
| //============================================================================== | |||
| static bool isDoublePrecisionProcessingEnabled(); | |||
| static bool isAutoScalePluginWindowsEnabled(); | |||
| static void updatePrecisionMenuItem (ApplicationCommandInfo& info); | |||
| static void updateAutoScaleMenuItem (ApplicationCommandInfo& info); | |||
| void showAudioSettings(); | |||
| //============================================================================== | |||
| AudioDeviceManager deviceManager; | |||
| AudioPluginFormatManager formatManager; | |||
| @@ -106,7 +134,5 @@ private: | |||
| class PluginListWindow; | |||
| std::unique_ptr<PluginListWindow> pluginListWindow; | |||
| void showAudioSettings(); | |||
| JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainHostWindow) | |||
| }; | |||
| @@ -27,7 +27,7 @@ | |||
| #include <juce_audio_plugin_client/AAX/juce_AAX_Modifier_Injector.h> | |||
| #endif | |||
| #if JUCE_WIN_PER_MONITOR_DPI_AWARE && JUCE_MODULE_AVAILABLE_juce_gui_extra | |||
| #if JUCE_MODULE_AVAILABLE_juce_gui_extra | |||
| #include <juce_gui_extra/embedding/juce_ScopedDPIAwarenessDisabler.h> | |||
| #endif | |||
| @@ -470,7 +470,7 @@ static double getGlobalDPI() | |||
| } | |||
| //============================================================================== | |||
| #if JUCE_WIN_PER_MONITOR_DPI_AWARE && JUCE_MODULE_AVAILABLE_juce_gui_extra | |||
| #if JUCE_MODULE_AVAILABLE_juce_gui_extra | |||
| ScopedDPIAwarenessDisabler::ScopedDPIAwarenessDisabler() | |||
| { | |||
| if (! isPerMonitorDPIAwareThread()) | |||
| @@ -26,8 +26,6 @@ | |||
| namespace juce | |||
| { | |||
| #if (JUCE_WINDOWS && JUCE_WIN_PER_MONITOR_DPI_AWARE) || DOXYGEN | |||
| //============================================================================== | |||
| /** | |||
| A Windows-specific class that temporarily sets the DPI awareness context of | |||
| @@ -52,6 +50,5 @@ public: | |||
| private: | |||
| void* previousContext = nullptr; | |||
| }; | |||
| #endif | |||
| } // namespace juce | |||
| @@ -188,3 +188,9 @@ | |||
| #include "native/juce_android_WebBrowserComponent.cpp" | |||
| #endif | |||
| #endif | |||
| //============================================================================== | |||
| #if ! JUCE_WINDOWS | |||
| juce::ScopedDPIAwarenessDisabler::ScopedDPIAwarenessDisabler() { ignoreUnused (previousContext); } | |||
| juce::ScopedDPIAwarenessDisabler::~ScopedDPIAwarenessDisabler() {} | |||
| #endif | |||