diff --git a/CHANGELOG.md b/CHANGELOG.md index fcc434815..fcc38bbb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,16 @@ ## [1.2.x] +### Added +- CLAP plugins now appear in the Unverified plugin menu and can be added to a graph without a prior scan. +- Graph Mixer: channel strips can be hidden via right-click and restored from the mixer background's right-click menu. + ### Changed - Disconnected audio devices are no longer silently replaced with another device: Element closes the device, shows its status in the status bar, and automatically restores it when it reconnects. Double-click the status label to open audio settings. +- Graph Mixer: larger fonts for node names, dB readouts and the meter scale, plus a higher-contrast layout. +- Graph Mixer: channel strip name headers now use the node's block color from the graph editor. +- Double-clicking a channel strip fader resets it to 0 dB. +- Selected nodes now show the same accent outline in both the graph editor and the Graph Mixer. - Note names throughout the UI now use scientific pitch notation (middle C = C4), matching the convention used by most DAWs. ### Fixed diff --git a/include/element/node.hpp b/include/element/node.hpp index e1b535815..7191819b1 100644 --- a/include/element/node.hpp +++ b/include/element/node.hpp @@ -199,7 +199,7 @@ class EL_API Node : public Model { juce::Value getMutedValue() { return getPropertyAsValue (tags::mute); } /** Returns true if inputs are muted */ - bool isMutingInputs() const { return (bool) getProperty ("muteInput", false); } + bool isMutingInputs() const { return (bool) getProperty (tags::muteInput, false); } /** Change the mute status of this Node */ void setMuted (bool); @@ -458,6 +458,18 @@ class EL_API Node : public Model { return getUIValueTree().hasProperty ("color") ? getColor() : fallback; } + /** Change whether this node's strip is hidden in the graph mixer. */ + void setHiddenInMixer (bool hidden) + { + getUIValueTree().setProperty (tags::hiddenInMixer, hidden, nullptr); + } + + /** Returns true if this node's strip should be hidden in the graph mixer. */ + bool isHiddenInMixer() const noexcept + { + return (bool) getUIValueTree().getProperty (tags::hiddenInMixer, false); + } + /** Add a script to this node. If failure, the returned will be invalid; */ diff --git a/include/element/porttype.hpp b/include/element/porttype.hpp index 686aafa33..fc50f7ce5 100644 --- a/include/element/porttype.hpp +++ b/include/element/porttype.hpp @@ -15,7 +15,7 @@ #endif #ifndef EL_INVALID_PORT - #define EL_INVALID_PORT ((uint32_t) -1) + #define EL_INVALID_PORT ((uint32_t) - 1) #endif #ifndef EL_INVALID_NODE diff --git a/include/element/tags.hpp b/include/element/tags.hpp index 19c768280..b10e1bf68 100644 --- a/include/element/tags.hpp +++ b/include/element/tags.hpp @@ -106,6 +106,7 @@ static const juce::Identifier lastDisplayMode = "lastDisplayMode"; static const juce::Identifier enabled = "enabled"; static const juce::Identifier gain = "gain"; static const juce::Identifier graphs = "graphs"; +static const juce::Identifier hiddenInMixer = "hiddenInMixer"; static const juce::Identifier hiddenPorts = "hiddenPorts"; static const juce::Identifier inputGain = "inputGain"; static const juce::Identifier mappingData = "mappingData"; @@ -113,6 +114,7 @@ static const juce::Identifier map = "map"; static const juce::Identifier maps = "maps"; static const juce::Identifier missing = "missing"; static const juce::Identifier mute = "mute"; +static const juce::Identifier muteInput = "muteInput"; static const juce::Identifier node = "node"; static const juce::Identifier nodes = "nodes"; static const juce::Identifier notes = "notes"; diff --git a/include/element/ui/style.hpp b/include/element/ui/style.hpp index ed76e3b92..7938d3440 100644 --- a/include/element/ui/style.hpp +++ b/include/element/ui/style.hpp @@ -48,6 +48,12 @@ struct Style { e.g. standard Labels, Textboxes, ComboBoxes, etc etc */ static constexpr float fontSizeDefault = 12.0f; + /** Font size for fine print such as scales and small annotations. */ + static constexpr float fontSizeSmall = 9.0f; + + /** Font size for emphasized readouts such as the channel strip's dB value. */ + static constexpr float fontSizeLarge = 14.0f; + /** Draws text rotated by 90 or -90 degrees */ static void drawVerticalText (juce::Graphics& g, const juce::String& text, diff --git a/src/engine/clapprovider.cpp b/src/engine/clapprovider.cpp index 4f731f2f3..a253cbe51 100644 --- a/src/engine/clapprovider.cpp +++ b/src/engine/clapprovider.cpp @@ -389,7 +389,9 @@ class CLAPHost final : public CLAPBaseHost pfds.reserve (fds.size()); for (const auto& f : fds) { - struct pollfd p {}; + struct pollfd p + { + }; p.fd = f.first; if (f.second & CLAP_POSIX_FD_READ) p.events |= POLLIN; diff --git a/src/filesystemwatcher.cpp b/src/filesystemwatcher.cpp index 7941b62bf..9d47af2cb 100644 --- a/src/filesystemwatcher.cpp +++ b/src/filesystemwatcher.cpp @@ -140,7 +140,9 @@ class FileSystemWatcher::Impl CFArrayRef paths { nullptr }; dispatch_queue_t queue { nullptr }; FSEventStreamRef stream { nullptr }; - struct FSEventStreamContext context {}; + struct FSEventStreamContext context + { + }; }; #endif diff --git a/src/node.cpp b/src/node.cpp index bf570a154..fd33d19ee 100644 --- a/src/node.cpp +++ b/src/node.cpp @@ -915,7 +915,7 @@ void Node::restorePluginState() obj->setMidiProgramsState (getProperty (tags::midiProgramsState).toString().trim()); obj->setMuted ((bool) getProperty (tags::mute, obj->isMuted())); - obj->setMuteInput ((bool) getProperty ("muteInput", obj->isMutingInputs())); + obj->setMuteInput ((bool) getProperty (tags::muteInput, obj->isMutingInputs())); if (hasProperty (tags::transpose)) obj->setTransposeOffset (getProperty (tags::transpose)); @@ -1006,7 +1006,7 @@ void Node::savePluginState() setProperty (tags::globalMidiPrograms, obj->useGlobalMidiPrograms()); setProperty (tags::midiProgramsEnabled, obj->areMidiProgramsEnabled()); setProperty (tags::mute, obj->isMuted()); - setProperty ("muteInput", obj->isMutingInputs()); + setProperty (tags::muteInput, obj->isMutingInputs()); String mps; obj->getMidiProgramsState (mps); setProperty (tags::midiProgramsState, mps); @@ -1194,7 +1194,7 @@ void Node::setMuted (bool shouldBeMuted) void Node::setMuteInput (bool shouldMuteInputs) { if (shouldMuteInputs != isMutingInputs()) - setProperty ("muteInput", shouldMuteInputs); + setProperty (tags::muteInput, shouldMuteInputs); if (auto* obj = getObject()) obj->setMuteInput (isMutingInputs()); } diff --git a/src/pluginmanager.cpp b/src/pluginmanager.cpp index 0487df962..bb2a2b29f 100644 --- a/src/pluginmanager.cpp +++ b/src/pluginmanager.cpp @@ -901,9 +901,18 @@ class UnverifiedPlugins : private Thread ScopedLock sl (lock); if (plugins.contains (format)) { + const auto types = list.getTypes(); for (const auto& file : plugins.getReference (format)) { - if (nullptr != list.getTypeForFile (file)) + bool known = nullptr != list.getTypeForFile (file); + // Provider formats (e.g. CLAP) store types as "pluginID:filePath". + for (int i = types.size(); --i >= 0 && ! known;) + { + const auto& d = types.getReference (i); + known = d.pluginFormatName == format + && d.fileOrIdentifier.fromFirstOccurrenceOf (":", false, false) == file; + } + if (known) continue; auto* const desc = plugs.add (new PluginDescription()); desc->pluginFormatName = format; @@ -931,6 +940,7 @@ class UnverifiedPlugins : private Thread cancelFlag.set (0); PluginManager pluginManager; + pluginManager.getNodeFactory().add (new CLAPProvider()); pluginManager.addDefaultFormats(); auto& manager (pluginManager.getAudioPluginFormats()); @@ -951,9 +961,21 @@ class UnverifiedPlugins : private Thread // Element Node Providers auto& factory = pluginManager.getNodeFactory(); - for (auto provider : factory.providers()) + for (auto* const provider : factory.providers()) { - // FIXME: CLAP and other unverified support. + if (threadShouldExit() || cancelFlag.get() != 0) + break; + + const auto formatName = provider->format(); + FileSearchPath path = paths[formatName]; + path.addPath (provider->defaultSearchPath()); + // Providers with no search path (e.g. internal nodes) aren't file-based. + if (path.getNumPaths() <= 0) + continue; + const auto found = provider->findTypes (path, true, false); + + ScopedLock sl (lock); + plugins.set (formatName, found); } cancelFlag.set (0); diff --git a/src/services/engineservice.cpp b/src/services/engineservice.cpp index 4e3c5c524..381595dc6 100644 --- a/src/services/engineservice.cpp +++ b/src/services/engineservice.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,42 @@ static void initializeRootGraphPorts (RootGraph* root, const Node& model) root->setNumPorts (PortType::Midi, ins.size(), true, false); root->setNumPorts (PortType::Midi, outs.size(), false, false); } + +/** Scans an unverified plugin and registers it with the known plugins list. + + Handles both JUCE audio plugin formats and Element node providers such + as CLAP, whose identifiers from the unverified scan are bare file paths. + + @param plugins the plugin manager to verify against. + @param desc description of the unverified plugin to scan. + @param out receives the full descriptions produced by the scan. + @return true if at least one description was found and added. +*/ +static bool verifyPlugin (PluginManager& plugins, const PluginDescription& desc, OwnedArray& out) +{ + auto& list = plugins.getKnownPlugins(); + + if (auto* const format = plugins.getAudioPluginFormat (desc.pluginFormatName)) + { + list.removeFromBlacklist (desc.fileOrIdentifier); + list.removeType (desc); + return list.scanAndAddFile (desc.fileOrIdentifier, false, out, *format); + } + + if (auto* const provider = plugins.getProvider (desc.pluginFormatName)) + { + list.removeFromBlacklist (desc.fileOrIdentifier); + provider->scan (desc.fileOrIdentifier, out); + for (auto* const d : out) + { + list.removeType (*d); + list.addType (*d); + } + return ! out.isEmpty(); + } + + return false; +} } // namespace detail struct RootGraphHolder @@ -596,12 +633,7 @@ Node EngineService::addPlugin (const PluginDescription& desc, const bool verifie OwnedArray plugs; if (! verified) { - auto* format = context().plugins().getAudioPluginFormat (desc.pluginFormatName); - jassert (format != nullptr); - auto& list (context().plugins().getKnownPlugins()); - list.removeFromBlacklist (desc.fileOrIdentifier); - list.removeType (desc); - if (list.scanAndAddFile (desc.fileOrIdentifier, false, plugs, *format)) + if (detail::verifyPlugin (context().plugins(), desc, plugs)) { context().plugins().saveUserPlugins (context().settings()); } @@ -875,12 +907,7 @@ Node EngineService::addPlugin (const Node& graph, const PluginDescription& desc, } else { - auto* format = context().plugins().getAudioPluginFormat (desc.pluginFormatName); - jassert (format != nullptr); - - list.removeFromBlacklist (desc.fileOrIdentifier); - - if (list.scanAndAddFile (desc.fileOrIdentifier, false, plugs, *format)) + if (detail::verifyPlugin (context().plugins(), desc, plugs)) { context().plugins().saveUserPlugins (context().settings()); } diff --git a/src/ui/block.cpp b/src/ui/block.cpp index 0626ab950..72e342dd5 100644 --- a/src/ui/block.cpp +++ b/src/ui/block.cpp @@ -832,7 +832,7 @@ void BlockComponent::paint (Graphics& g) if (isSelected()) { - bgc = bgc.brighter (0.55f); + bgc = bgc.brighter (0.1f); } if (colorize) @@ -841,7 +841,7 @@ void BlockComponent::paint (Graphics& g) { case Compact: case Small: { - g.setColour (selected ? barColor.brighter (0.275f) : barColor); + g.setColour (selected ? barColor.brighter (0.1f) : barColor); g.fillRoundedRectangle (box.toFloat(), cornerSize); break; } @@ -874,6 +874,12 @@ void BlockComponent::paint (Graphics& g) g.drawRoundedRectangle (box.toFloat(), cornerSize, 1.3f); } + if (isSelected()) + { + g.setColour (Colors::toggleBlue); + g.drawRoundedRectangle (box.toFloat(), cornerSize, 1.5f); + } + auto displayName = node.getDisplayName(); auto subName = node.hasModifiedName() ? node.getPluginName() : String(); diff --git a/src/ui/buttons.cpp b/src/ui/buttons.cpp index 2fc8bc7d6..3aca68d9a 100644 --- a/src/ui/buttons.cpp +++ b/src/ui/buttons.cpp @@ -55,7 +55,7 @@ void SettingButton::paintButton (Graphics& g, bool isMouseOverButton, bool isBut if (text.isEmpty() && getClickingTogglesState()) text = (getToggleState()) ? yes : no; - g.setFont (12.f); + g.setFont (Style::fontSizeDefault); g.setColour (getTextColour()); g.drawText (text, getLocalBounds(), Justification::centred); } diff --git a/src/ui/buttons.hpp b/src/ui/buttons.hpp index 4658a303f..5ad6084c7 100644 --- a/src/ui/buttons.hpp +++ b/src/ui/buttons.hpp @@ -201,7 +201,7 @@ class DragableIntLabel : public Component, if (text.isNotEmpty()) { - g.setFont (12.f); + g.setFont (fontSize); g.setColour (isEnabled() ? Colours::black : Colours::darkgrey); g.drawText (text, getLocalBounds(), Justification::centred); } @@ -247,6 +247,13 @@ class DragableIntLabel : public Component, void setDragable (const bool yn) { dragable = yn; } + /** Change the font size used to draw the value. */ + void setFontSize (float newSize) + { + fontSize = newSize; + repaint(); + } + inline void setValue (double value) { if (useMinMax) @@ -298,6 +305,7 @@ class DragableIntLabel : public Component, int places = 0; bool useMinMax = false; double minValue = 0.0, maxValue = 0.0; + float fontSize = Style::fontSizeDefault; private: bool dragable = true; @@ -332,7 +340,7 @@ class TimeSignatureSetting : public Component if (text.isNotEmpty()) { - g.setFont (12.f); + g.setFont (Style::fontSizeDefault); g.setColour (isEnabled() ? Colours::black : Colours::darkgrey); g.drawText (text, getLocalBounds(), Justification::centred); } diff --git a/src/ui/channelstrip.cpp b/src/ui/channelstrip.cpp index 31ffa5c5e..4f51854c0 100644 --- a/src/ui/channelstrip.cpp +++ b/src/ui/channelstrip.cpp @@ -49,30 +49,27 @@ ChannelStripComponent::ChannelStripComponent() fader.setSkewFactor (2); fader.addListener (this); fader.setColour (Slider::trackColourId, Colours::black); - setColour (Slider::thumbColourId, Colours::black.brighter (0.2f)); + fader.setColour (Slider::thumbColourId, Colours::black.brighter (0.2f)); + fader.setDoubleClickReturnValue (true, 0.0); fader.setLookAndFeel (_fstyle.get()); addAndMakeVisible (meter, 100); addAndMakeVisible (scale, 101); - addAndMakeVisible (name); - name.setFont (name.getFont().withHeight (14)); - name.setJustificationType (Justification::centred); - name.setText ("Name", dontSendNotification); + addAndMakeVisible (powerButton); + powerButton.setColour (SettingButton::backgroundOnColourId, Colors::toggleBlue); + powerButton.setButtonText ("M"); + powerButton.addListener (this); - addAndMakeVisible (mute); - mute.setColour (SettingButton::backgroundOnColourId, Colors::toggleBlue); - mute.setButtonText ("M"); - mute.addListener (this); - - addAndMakeVisible (mute2); - mute2.setYesNoText ("M", "M"); - mute2.setButtonText ("M"); - mute2.setColour (SettingButton::backgroundOnColourId, Colors::toggleRed); - mute2.setColour (SettingButton::textColourId, Colours::black); - mute2.addListener (this); + addAndMakeVisible (muteButton); + muteButton.setYesNoText ("M", "M"); + muteButton.setButtonText ("M"); + muteButton.setColour (SettingButton::backgroundOnColourId, Colors::toggleRed); + muteButton.setColour (SettingButton::textColourId, Colours::black); + muteButton.addListener (this); addAndMakeVisible (volume); + volume.setFontSize (Style::fontSizeLarge); volume.setNumDecimalPlaces (1); volume.setMinMax (fader.getMinimum(), fader.getMaximum()); volume.setValue (fader.getValue()); @@ -112,7 +109,7 @@ void ChannelStripComponent::resized() auto r2 = r1.removeFromRight (r1.getWidth() / 2); r1.removeFromTop (4); - volume.setBounds (r1.removeFromTop (18).withSizeKeepingCentre (30, 18)); + volume.setBounds (r1.removeFromTop (18).withSizeKeepingCentre (40, 18)); r1.removeFromBottom (4); for (auto* const button : extraButtons) @@ -121,12 +118,12 @@ void ChannelStripComponent::resized() r1.removeFromBottom (1); } - mute.setBounds (r1.removeFromBottom (18).withSizeKeepingCentre (26, 18)); + powerButton.setBounds (r1.removeFromBottom (18).withSizeKeepingCentre (26, 18)); - if (mute2.isVisible()) + if (muteButton.isVisible()) { r1.removeFromBottom (1); - mute2.setBounds (r1.removeFromBottom (18).withSizeKeepingCentre (26, 18)); + muteButton.setBounds (r1.removeFromBottom (18).withSizeKeepingCentre (26, 18)); } const int quarter = r2.getWidth() / 2; @@ -140,14 +137,14 @@ void ChannelStripComponent::resized() void ChannelStripComponent::buttonClicked (Button* b) { - if (b == &mute) + if (b == &powerButton) { - mute.setToggleState (! mute.getToggleState(), juce::dontSendNotification); + powerButton.setToggleState (! powerButton.getToggleState(), juce::dontSendNotification); powerChanged(); } - else if (b == &mute2) + else if (b == &muteButton) { - mute2.setToggleState (! mute2.getToggleState(), juce::dontSendNotification); + muteButton.setToggleState (! muteButton.getToggleState(), juce::dontSendNotification); muteChanged(); } } diff --git a/src/ui/channelstrip.hpp b/src/ui/channelstrip.hpp index e2c3048a9..f165e5170 100644 --- a/src/ui/channelstrip.hpp +++ b/src/ui/channelstrip.hpp @@ -31,29 +31,29 @@ class ChannelStripComponent : public Component, inline void setPower (const bool powerOn, const bool notify = true) { - if (powerOn == mute.getToggleState()) + if (powerOn == powerButton.getToggleState()) return; - mute.setToggleState (powerOn, notify ? juce::sendNotification : juce::dontSendNotification); + powerButton.setToggleState (powerOn, notify ? juce::sendNotification : juce::dontSendNotification); if (notify) powerChanged(); } inline void setMuted (const bool muted, const bool notify = true) { - if (muted == mute2.getToggleState()) + if (muted == muteButton.getToggleState()) return; - mute2.setToggleState (muted, notify ? juce::sendNotification : juce::dontSendNotification); + muteButton.setToggleState (muted, notify ? juce::sendNotification : juce::dontSendNotification); if (notify) muteChanged(); } - inline bool isPowerOn() const { return mute.getToggleState(); } + inline bool isPowerOn() const { return powerButton.getToggleState(); } inline bool isPowerOff() const { return ! isPowerOn(); } - inline bool isMuted() const { return mute2.getToggleState(); } + inline bool isMuted() const { return muteButton.getToggleState(); } inline void setMuteButtonVisible (bool visible) { - mute2.setVisible (visible); + muteButton.setVisible (visible); resized(); } @@ -64,6 +64,9 @@ class ChannelStripComponent : public Component, void setMinMaxDecibels (double minDb, double maxDb); + /** Returns the minimum decibel value of the fader. */ + double getMinDecibels() const { return fader.getMinimum(); } + void addButton (Component*); /** @internal */ @@ -87,7 +90,6 @@ class ChannelStripComponent : public Component, Slider fader; SimpleMeter meter; DecibelScale scale; - Label name; struct FaderStyle; std::unique_ptr _fstyle; @@ -99,8 +101,8 @@ class ChannelStripComponent : public Component, void settingLabelDoubleClicked() override; } volume; - PowerButton mute; - SettingButton mute2; + PowerButton powerButton; + SettingButton muteButton; OwnedArray extraButtons; diff --git a/src/ui/contextmenus.hpp b/src/ui/contextmenus.hpp index ae3cdfd4c..c04c94648 100644 --- a/src/ui/contextmenus.hpp +++ b/src/ui/contextmenus.hpp @@ -97,6 +97,18 @@ class PluginsPopupMenu : public PopupMenu for (int i = lastSize; i < unverified.size(); ++i) menu.addItem (i + 20000, unverified[i]->name); } + else + { + // Provider formats (e.g. CLAP): identifiers are file paths. + for (int i = lastSize; i < unverified.size(); ++i) + { + const auto& fid = unverified.getUnchecked (i)->fileOrIdentifier; + menu.addItem (i + 20000, + juce::File::isAbsolutePath (fid) + ? juce::File (fid).getFileNameWithoutExtension() + : fid); + } + } if (menu.getNumItems() > 0) unvMenu.addSubMenu (name, menu); diff --git a/src/ui/decibelscale.cpp b/src/ui/decibelscale.cpp index 1e01ab1c3..47395f05e 100644 --- a/src/ui/decibelscale.cpp +++ b/src/ui/decibelscale.cpp @@ -2,16 +2,17 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include using namespace juce; namespace element { DecibelScale::DecibelScale() - : font (FontOptions (7.0f)), scale (0.0f), lastY (0) + : font (FontOptions (Style::fontSizeSmall)), scale (0.0f), lastY (0) { zeromem (levels, sizeof (int) * LevelCount); - setColour (markerColourId, Colour (0xFFCCCCCC)); + setColour (markerColourId, Colors::textColor); } DecibelScale::~DecibelScale() diff --git a/src/ui/graphmixerview.cpp b/src/ui/graphmixerview.cpp index 20fac55aa..6156ca00d 100644 --- a/src/ui/graphmixerview.cpp +++ b/src/ui/graphmixerview.cpp @@ -18,7 +18,7 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, public ComponentListener { public: - std::function onReordered; + std::function onRefreshNeeded; GraphMixerChannelStrip (GuiService& gui) : NodeChannelStripComponent (gui, false) { @@ -43,6 +43,9 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, void mouseDown (const MouseEvent& ev) override { + if (ev.mods.isPopupMenu()) + return; + if (! down) { down = true; @@ -51,6 +54,20 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, } } + void showContextMenu() + { + PopupMenu menu; + menu.addItem (1, TRANS ("Hide from mixer")); + menu.showMenuAsync (PopupMenu::Options().withTargetComponent (this).withMousePosition(), + [strip = Component::SafePointer (this)] (int result) { + if (strip == nullptr || result != 1) + return; + strip->getNode().setHiddenInMixer (true); + if (strip->onRefreshNeeded) + strip->onRefreshNeeded(); + }); + } + void mouseDrag (const MouseEvent& ev) override { if (down && ! dragging) @@ -106,7 +123,7 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, if (selected || (hover && ! dragging && ! down)) { g.setColour (Colors::toggleBlue); - g.drawRect (0.f, 0.f, (float) getWidth(), (float) getHeight(), selected ? 1.4 : 1.0); + g.drawRect (0.f, 0.f, (float) getWidth(), (float) getHeight(), selected ? 1.5f : 1.0f); } } @@ -129,8 +146,8 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, if (myIndex >= 0 && dIndex >= 0) { parent.moveChild (dIndex, myIndex, nullptr); - if (onReordered) - onReordered(); + if (onRefreshNeeded) + onRefreshNeeded(); } } @@ -157,19 +174,16 @@ class GraphMixerChannelStrip : public NodeChannelStripComponent, ChildListener (GraphMixerChannelStrip& o) : owner (o) {} void mouseDown (const MouseEvent& ev) override { - owner.selectInGuiController(); + if (ev.mods.isPopupMenu()) + owner.showContextMenu(); + else + owner.selectInGuiController(); } GraphMixerChannelStrip& owner; }; std::unique_ptr listener; - -#if 0 - virtual void itemDragEnter (const SourceDetails& dragSourceDetails); - virtual void itemDragMove (const SourceDetails& dragSourceDetails); - virtual void itemDragExit (const SourceDetails& dragSourceDetails); -#endif }; class GraphMixerListBoxModel : public ListBoxModel @@ -198,14 +212,14 @@ class GraphMixerListBoxModel : public ListBoxModel GraphMixerChannelStrip* const strip = existing == nullptr ? new GraphMixerChannelStrip (gui) : dynamic_cast (existing); - strip->onReordered = std::bind (&GraphMixerListBoxModel::onReordered, this); + strip->onRefreshNeeded = std::bind (&GraphMixerListBoxModel::refresh, this); auto node = getNode (rowNumber); strip->setNode (node); strip->setSelected (node == gui.getSelectedNode()); return strip; } - void onReordered() + void refresh() { refreshNodes(); box.updateContent(); @@ -230,7 +244,8 @@ class GraphMixerListBoxModel : public ListBoxModel // clang-format off if (n.isMidiIONode() || n.getIdentifier() == EL_NODE_ID_MIDI_INPUT_DEVICE || - n.getIdentifier() == EL_NODE_ID_MIDI_OUTPUT_DEVICE) + n.getIdentifier() == EL_NODE_ID_MIDI_OUTPUT_DEVICE || + n.isHiddenInMixer()) { continue; } @@ -239,18 +254,48 @@ class GraphMixerListBoxModel : public ListBoxModel nodes.add (n); } } -#if 0 - virtual void listBoxItemClicked (int row, const MouseEvent&); - virtual void listBoxItemDoubleClicked (int row, const MouseEvent&); - virtual void backgroundClicked (const MouseEvent&); - virtual void selectedRowsChanged (int lastRowSelected); - virtual void deleteKeyPressed (int lastRowSelected); - virtual void returnKeyPressed (int lastRowSelected); - virtual void listWasScrolled(); - virtual var getDragSourceDescription (const SparseSet& rowsToDescribe); - virtual String getTooltipForRow (int row); - virtual MouseCursor getMouseCursorForRow (int row); -#endif + + void backgroundClicked (const MouseEvent& ev) override + { + if (! ev.mods.isPopupMenu()) + return; + + const auto graph = _node.isGraph() ? _node + : gui.context().session()->getActiveGraph(); + NodeArray hidden; + for (int i = 0; i < graph.getNumNodes(); ++i) + { + const auto n = graph.getNode (i); + if (n.isHiddenInMixer()) + hidden.add (n); + } + + PopupMenu menu; + for (int i = 0; i < hidden.size(); ++i) + menu.addItem (i + 2, TRANS ("Show") + " " + hidden.getReference (i).getDisplayName()); + if (hidden.size() > 1) + { + menu.addSeparator(); + menu.addItem (1, TRANS ("Show all")); + } + else if (hidden.isEmpty()) + { + menu.addItem (1, TRANS ("No hidden channels"), false); + } + + menu.showMenuAsync (PopupMenu::Options(), + [safeBox = Component::SafePointer (&box), this, hidden] (int result) { + if (safeBox == nullptr || result <= 0) + return; + if (result == 1) + for (auto n : hidden) + n.setHiddenInMixer (false); + else if (result - 2 < hidden.size()) + hidden[result - 2].setHiddenInMixer (false); + refresh(); + }); + } + private: GuiService& gui; HorizontalListBox& box; @@ -304,7 +349,7 @@ class GraphMixerView::Content : public Component, void paint (Graphics& g) override { - g.setColour (Colors::widgetBackgroundColor.darker()); + g.setColour (Colors::contentBackgroundColor); g.fillAll(); if (model->getNumRows() <= 0) @@ -329,7 +374,6 @@ class GraphMixerView::Content : public Component, SessionPtr session; [[maybe_unused]] GraphMixerView& view; std::unique_ptr model; - ChannelStripComponent channelStrip; HorizontalListBox box; std::vector _conns; }; diff --git a/src/ui/nodechannelstrip.hpp b/src/ui/nodechannelstrip.hpp index 5f232762d..3d84bfa17 100644 --- a/src/ui/nodechannelstrip.hpp +++ b/src/ui/nodechannelstrip.hpp @@ -28,7 +28,7 @@ class NodeChannelStripComponent : public Component, nodeName.setText ("", dontSendNotification); nodeName.setJustificationType (Justification::centredBottom); nodeName.setEditable (false, true, false); - nodeName.setFont (Font (FontOptions (10.f))); + nodeName.setFont (Font (FontOptions (Style::fontSizeDefault))); nodeName.onTextChange = [this] { if (node.isValid()) node.setProperty (tags::name, nodeName.getText()); @@ -60,6 +60,7 @@ class NodeChannelStripComponent : public Component, { unbindSignals(); displayName.addListener (this); + nodeColor.addListener (this); flowBox.addListener (this); if (listenForNodeSelected) nodeSelectedConnection = gui.nodeSelected.connect ( @@ -81,6 +82,7 @@ class NodeChannelStripComponent : public Component, _conns.clear(); displayName.removeListener (this); + nodeColor.removeListener (this); flowBox.removeListener (this); nodeSelectedConnection.disconnect(); volumeChangedConnection.disconnect(); @@ -106,8 +108,8 @@ class NodeChannelStripComponent : public Component, { g.setColour (Colors::widgetBackgroundColor); g.fillAll(); - g.setColour (Colors::contentBackgroundColor); - g.drawLine (getWidth() - 1.f, 0.0, getWidth() - 1.f, getHeight()); + g.setColour (Colors::backgroundColor); + g.fillRect (getWidth() - 1, 0, 1, getHeight()); } inline void timerCallback() override @@ -193,6 +195,7 @@ class NodeChannelStripComponent : public Component, audioOuts.clearQuick(); node.getPorts (audioIns, audioOuts, PortType::Audio); displayName.referTo (node.getPropertyAsValue (tags::name)); + nodeColor.referTo (node.getUIValueTree().getPropertyAsValue ("color", nullptr)); stabilizeContent(); startTimerHz (meterSpeedHz); @@ -234,7 +237,7 @@ class NodeChannelStripComponent : public Component, return 0.f; float gain = isMonitoringInputs() || isAudioOutNode ? object->getInputGain() : object->getGain(); - return Decibels::gainToDecibels (gain, -60.f); + return Decibels::gainToDecibels (gain, (float) channelStrip.getMinDecibels()); } private: @@ -256,6 +259,7 @@ class NodeChannelStripComponent : public Component, [[maybe_unused]] bool monoMeter = false; Value displayName; + Value nodeColor; SignalConnection nodeSelectedConnection; SignalConnection volumeChangedConnection; @@ -269,7 +273,7 @@ class NodeChannelStripComponent : public Component, void valueChanged (Value& value) override { - if (value.refersToSameSourceAs (displayName)) + if (value.refersToSameSourceAs (displayName) || value.refersToSameSourceAs (nodeColor)) updateNodeName(); } @@ -282,6 +286,14 @@ class NodeChannelStripComponent : public Component, if (node.hasModifiedName()) tooltip << " (" << node.getPluginName() << ")"; nodeName.setTooltip (tooltip); + + const auto color = node.getColor (Colours::transparentBlack); + const bool colorize = color != Colour (0x00000000); + nodeName.setColour (Label::backgroundColourId, + colorize ? color : Colours::transparentBlack); + nodeName.setColour (Label::textColourId, + colorize ? Colours::white.overlaidWith (color).contrasting() + : Colors::textColor); } } @@ -365,20 +377,20 @@ class NodeChannelStripComponent : public Component, if (ProcessorPtr object = node.getObject()) { - auto gain = Decibels::decibelsToGain (value, -60.0); + auto gain = Decibels::decibelsToGain (value, channelStrip.getMinDecibels()); if (isAudioOutNode || isMonitoringInputs()) { - if (gain != (double) node.getProperty ("inputGain", gain) || gain != (double) object->getInputGain()) + if (gain != (double) node.getProperty (tags::inputGain, gain) || gain != (double) object->getInputGain()) { - node.setProperty ("inputGain", gain); + node.setProperty (tags::inputGain, gain); object->setInputGain (static_cast (gain)); } } else { - if (gain != (double) node.getProperty ("gain", gain) || gain != (double) object->getGain()) + if (gain != (double) node.getProperty (tags::gain, gain) || gain != (double) object->getGain()) { - node.setProperty ("gain", gain); + node.setProperty (tags::gain, gain); object->setGain (static_cast (gain)); } } diff --git a/src/ui/simplemeter.cpp b/src/ui/simplemeter.cpp index 46fdf4206..2c26f21eb 100644 --- a/src/ui/simplemeter.cpp +++ b/src/ui/simplemeter.cpp @@ -254,9 +254,11 @@ void SimpleMeter::resized() } } +static const Colour simpleMeterBackgroundColor (0xFF141414); + void SimpleMeter::paint (Graphics& g) { - g.setColour (Colour (0xFF202020)); + g.setColour (simpleMeterBackgroundColor); g.fillAll(); } @@ -265,7 +267,7 @@ void SimpleMeter::paintOverChildren (juce::Graphics& g) if (portCount < 2) return; - g.setColour (Colour (0xFF202020).darker (0.2f)); + g.setColour (simpleMeterBackgroundColor.darker (0.2f)); const int size = (horizontal ? getHeight() : getWidth()) / portCount; if (! horizontal) { diff --git a/test/NodeTests.cpp b/test/NodeTests.cpp index 649783abf..097e0dc68 100644 --- a/test/NodeTests.cpp +++ b/test/NodeTests.cpp @@ -36,6 +36,16 @@ BOOST_AUTO_TEST_CASE (DefaultGraph) BOOST_REQUIRE (node.getName().isEmpty()); } +BOOST_AUTO_TEST_CASE (HiddenInMixer) +{ + Node node (types::Node); + BOOST_REQUIRE (! node.isHiddenInMixer()); + node.setHiddenInMixer (true); + BOOST_REQUIRE (node.isHiddenInMixer()); + node.setHiddenInMixer (false); + BOOST_REQUIRE (! node.isHiddenInMixer()); +} + BOOST_AUTO_TEST_CASE (HiddenBlockPorts) { Node node (types::Node); diff --git a/test/PluginManagerTests.cpp b/test/PluginManagerTests.cpp index 9a35b11a1..ed4a188f6 100644 --- a/test/PluginManagerTests.cpp +++ b/test/PluginManagerTests.cpp @@ -5,12 +5,28 @@ #include #include +#include #include "engine/clapprovider.hpp" #include "utils.hpp" using namespace element; +namespace { + +/** Returns true if the unverified CLAP list contains the given file path. */ +static bool unverifiedContains (PluginManager& manager, const juce::String& path) +{ + juce::OwnedArray plugs; + manager.getUnverifiedPlugins ("CLAP", plugs); + for (const auto* const d : plugs) + if (d->fileOrIdentifier == path) + return true; + return false; +} + +} // namespace + BOOST_AUTO_TEST_SUITE (PluginManagerTests) BOOST_AUTO_TEST_CASE (SupportedFormats) @@ -25,4 +41,50 @@ BOOST_AUTO_TEST_CASE (SupportedFormats) BOOST_REQUIRE_MESSAGE (manager.isAudioPluginFormatSupported (supported), supported.toStdString()); } +#if ! JUCE_MAC +BOOST_AUTO_TEST_CASE (UnverifiedClapPlugins) +{ + const auto tempDir = juce::File::getSpecialLocation (juce::File::tempDirectory) + .getChildFile ("element-unverified-claps"); + tempDir.deleteRecursively(); + BOOST_REQUIRE (tempDir.createDirectory().wasOk()); + const auto fakeClap = tempDir.getChildFile ("Fake.clap"); + BOOST_REQUIRE (fakeClap.create().wasOk()); + + const auto propsFile = tempDir.getChildFile ("test.properties"); + juce::PropertiesFile::Options opts; + opts.storageFormat = juce::PropertiesFile::storeAsXML; + juce::PropertiesFile props (propsFile, opts); + props.setValue (juce::String (Settings::lastPluginScanPathPrefix) + "CLAP", + tempDir.getFullPathName()); + + PluginManager manager; + manager.getNodeFactory().add (new CLAPProvider()); + manager.addDefaultFormats(); + manager.setPropertiesFile (&props); + manager.searchUnverifiedPlugins(); + + bool found = false; + for (int retry = 0; ! found && retry < 250; ++retry) + { + found = unverifiedContains (manager, fakeClap.getFullPathName()); + if (! found) + juce::Thread::sleep (20); + } + BOOST_REQUIRE_MESSAGE (found, "unverified scan should find the CLAP file"); + + // Once known with an "id:path" identifier, it is no longer unverified. + juce::PluginDescription desc; + desc.pluginFormatName = "CLAP"; + desc.name = "Fake"; + desc.fileOrIdentifier = "com.fake.id:" + fakeClap.getFullPathName(); + manager.getKnownPlugins().addType (desc); + BOOST_REQUIRE (! unverifiedContains (manager, fakeClap.getFullPathName())); + + manager.setPropertiesFile (nullptr); + props.setNeedsToBeSaved (false); + tempDir.deleteRecursively(); +} +#endif + BOOST_AUTO_TEST_SUITE_END()