Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions include/element/plugins.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,55 @@ class PluginManager : public juce::ChangeBroadcaster {
/** Get the first node factory by format. e.g. "LV2" */
NodeProvider* getProvider (const juce::String& format) noexcept;

//==============================================================================
/** Returns true if the plugin is hidden from the "Add Plugin" surfaces.

Hidden state is user curation stored separately from the known plugins
list (keyed by juce::PluginDescription::createIdentifierString()) so it
survives plugin rescans.

@param desc the plugin to query
@return true if the plugin is currently hidden
*/
bool isPluginHidden (const juce::PluginDescription& desc) const;

/** Shows or hides a plugin. Persists immediately and broadcasts a change.

@param desc the plugin to update
@param hidden true to hide the plugin from "Add Plugin" surfaces
*/
void setPluginHidden (const juce::PluginDescription& desc, bool hidden);

/** Returns true if the plugin is marked as a favorite.

@param desc the plugin to query
@return true if the plugin is a favorite
*/
bool isPluginFavorite (const juce::PluginDescription& desc) const;

/** Marks or unmarks a plugin as a favorite. Persists immediately and
broadcasts a change.

@param desc the plugin to update
@param favorite true to mark the plugin as a favorite
*/
void setPluginFavorite (const juce::PluginDescription& desc, bool favorite);

/** Returns all known plugin types with hidden ones removed.

This is the shared filter that every "Add Plugin" surface should route
through. The result preserves the order of getKnownPlugins().getTypes().

@return the visible (non-hidden) plugin types
*/
juce::Array<juce::PluginDescription> getVisiblePluginTypes() const;

/** Returns the visible plugin types that are marked as favorites.

@return the favorite plugin types (excludes hidden plugins)
*/
juce::Array<juce::PluginDescription> getFavoritePluginTypes() const;

private:
friend class PluginScanner;
juce::PropertiesFile* props = nullptr;
Expand Down
3 changes: 3 additions & 0 deletions include/element/tags.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ EL_TAG (Script)
EL_TAG (Session)

EL_TAG (AudioChannelSet)
EL_TAG (PluginMetadata)
} // namespace types

namespace tags {
Expand Down Expand Up @@ -152,6 +153,8 @@ static const juce::Identifier renderMode = "renderMode";
static const juce::Identifier staticPos = "staticPos";

static const juce::Identifier plugin = "plugin";
static const juce::Identifier hidden = "hidden";
static const juce::Identifier favorite = "favorite";

static const juce::Identifier windowOnTop = "windowOnTop";
static const juce::Identifier windowVisible = "windowVisible";
Expand Down
104 changes: 104 additions & 0 deletions src/pluginmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ static const char* pluginListKey() { return Settings::pluginListKey; }
/* noop. prevent OS error dialogs from child process */
static void pluginScannerCrashHandler (void*) {}
static File pluginsXmlFile() { return DataPath::applicationDataDir().getChildFile ("plugins.xml"); }
static File pluginMetadataXmlFile() { return DataPath::applicationDataDir().getChildFile ("plugin-metadata.xml"); }

static FileSearchPath readSearchPath (const PropertiesFile& props, const String& f)
{
Expand Down Expand Up @@ -638,6 +639,53 @@ class PluginManager::Private : public PluginScanner::Listener
unverified.searchForPlugins (props);
}

/** Load hidden/favorite curation from a metadata file (keyed by
PluginDescription::createIdentifierString()). */
void loadPluginMetadata (const File& file)
{
hiddenPlugins.clearQuick();
favoritePlugins.clearQuick();

auto xml = XmlDocument::parse (file);
if (xml == nullptr || ! xml->hasTagName (types::PluginMetadata.toString()))
return;

for (auto* const e : xml->getChildWithTagNameIterator (tags::plugin.toString()))
{
const auto id = e->getStringAttribute (tags::identifier.toString());
if (id.isEmpty())
continue;
if (e->getBoolAttribute (tags::hidden.toString()))
hiddenPlugins.addIfNotAlreadyThere (id);
if (e->getBoolAttribute (tags::favorite.toString()))
favoritePlugins.addIfNotAlreadyThere (id);
}
}

/** Persist hidden/favorite curation. Only non-default entries are written. */
void savePluginMetadata (const File& file)
{
XmlElement xml (types::PluginMetadata.toString());

StringArray ids;
ids.addArray (hiddenPlugins);
ids.addArray (favoritePlugins);
ids.removeDuplicates (false);

for (const auto& id : ids)
{
auto* const e = xml.createNewChildElement (tags::plugin.toString());
e->setAttribute (tags::identifier.toString(), id);
if (hiddenPlugins.contains (id))
e->setAttribute (tags::hidden.toString(), 1);
if (favoritePlugins.contains (id))
e->setAttribute (tags::favorite.toString(), 1);
}

file.getParentDirectory().createDirectory();
xml.writeTo (file);
}

void getUnverifiedPlugins (const String& format, OwnedArray<PluginDescription>& plugs)
{
unverified.getPlugins (plugs, format, allPlugins);
Expand All @@ -648,6 +696,7 @@ class PluginManager::Private : public PluginScanner::Listener
PluginManager& owner;
AudioPluginFormatManager formats;
KnownPluginList allPlugins;
StringArray hiddenPlugins, favoritePlugins;
File deadAudioPlugins;
UnverifiedPlugins unverified;
NodeFactory nodes;
Expand Down Expand Up @@ -951,6 +1000,7 @@ void PluginManager::restoreUserPlugins (const XmlElement& xml)
priv->allPlugins.recreateFromXml (xml);
scanInternalPlugins();
priv->updateBlacklistedAudioPlugins();
priv->loadPluginMetadata (detail::pluginMetadataXmlFile());
if (props == nullptr)
return;
}
Expand Down Expand Up @@ -1119,4 +1169,58 @@ NodeProvider* PluginManager::getProvider (const String& format) noexcept
return nullptr;
}

//==============================================================================
bool PluginManager::isPluginHidden (const PluginDescription& desc) const
{
return priv->hiddenPlugins.contains (desc.createIdentifierString());
}

void PluginManager::setPluginHidden (const PluginDescription& desc, bool hidden)
{
const auto id = desc.createIdentifierString();
if (hidden)
priv->hiddenPlugins.addIfNotAlreadyThere (id);
else
priv->hiddenPlugins.removeString (id);
priv->savePluginMetadata (detail::pluginMetadataXmlFile());
priv->allPlugins.sendChangeMessage();
}

bool PluginManager::isPluginFavorite (const PluginDescription& desc) const
{
return priv->favoritePlugins.contains (desc.createIdentifierString());
}

void PluginManager::setPluginFavorite (const PluginDescription& desc, bool favorite)
{
const auto id = desc.createIdentifierString();
if (favorite)
priv->favoritePlugins.addIfNotAlreadyThere (id);
else
priv->favoritePlugins.removeString (id);
priv->savePluginMetadata (detail::pluginMetadataXmlFile());
priv->allPlugins.sendChangeMessage();
}

Array<PluginDescription> PluginManager::getVisiblePluginTypes() const
{
Array<PluginDescription> result;
for (const auto& desc : priv->allPlugins.getTypes())
if (! priv->hiddenPlugins.contains (desc.createIdentifierString()))
result.add (desc);
return result;
}

Array<PluginDescription> PluginManager::getFavoritePluginTypes() const
{
Array<PluginDescription> result;
for (const auto& desc : priv->allPlugins.getTypes())
{
const auto id = desc.createIdentifierString();
if (priv->favoritePlugins.contains (id) && ! priv->hiddenPlugins.contains (id))
result.add (desc);
}
return result;
}

} // namespace element
4 changes: 3 additions & 1 deletion src/ui/block.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,9 @@ void BlockComponent::mouseDown (const MouseEvent& e)
const int result = menu.show();
colorSelector.removeChangeListener (this);

const auto types = plugins.getKnownPlugins().getTypes();
// Must match the array used by NodePopupMenu::addReplaceSubmenu so the
// menu index resolves to the correct plugin.
const auto types = plugins.getVisiblePluginTypes();

if (auto* message = menu.createMessageForResultCode (result))
{
Expand Down
35 changes: 30 additions & 5 deletions src/ui/contextmenus.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,32 @@ class PluginsPopupMenu : public PopupMenu
jassert (cc != nullptr);
plugins = &cc->context().plugins();
jassert (plugins != nullptr);
available = plugins->getKnownPlugins().getTypes();
available = plugins->getVisiblePluginTypes();
favorites = plugins->getFavoritePluginTypes();
}

bool isPluginResultCode (const int resultCode)
{
// clang-format off
return (plugins->getKnownPlugins().getIndexChosenByMenu (available, resultCode) >= 0) ||
return (isPositiveAndBelow (int (resultCode - favoriteResultOffset), favorites.size())) ||
(plugins->getKnownPlugins().getIndexChosenByMenu (available, resultCode) >= 0) ||
(isPositiveAndBelow (int (resultCode - 20000), unverified.size()));
// clang-format on
}

PluginDescription getPluginDescription (int resultCode, bool& verified)
{
jassert (plugins != nullptr);
int index = plugins->getKnownPlugins().getIndexChosenByMenu (available, resultCode);

// Favorites submenu — a subset of the visible/known plugins, so verified.
int index = resultCode - favoriteResultOffset;
if (isPositiveAndBelow (index, favorites.size()))
{
verified = true;
return favorites.getReference (index);
}

index = plugins->getKnownPlugins().getIndexChosenByMenu (available, resultCode);
if (isPositiveAndBelow (index, available.size()))
{
verified = true;
Expand All @@ -57,6 +68,16 @@ class PluginsPopupMenu : public PopupMenu
if (hasAddedPlugins)
return;
hasAddedPlugins = true;

if (! favorites.isEmpty())
{
PopupMenu favMenu;
for (int i = 0; i < favorites.size(); ++i)
favMenu.addItem (favoriteResultOffset + i, favorites.getReference (i).name);
addSubMenu ("Favorites", favMenu);
addSeparator();
}

plugins->getKnownPlugins().addToMenu (*this, available, KnownPluginList::sortByManufacturer);

PopupMenu unvMenu;
Expand Down Expand Up @@ -89,7 +110,11 @@ class PluginsPopupMenu : public PopupMenu
}

private:
Array<PluginDescription> available;
// Favorites use a private menu-id range so they never collide with the
// main list (which uses KnownPluginList's large menuIdBase) or the
// unverified submenu (which uses the 20000 range).
static constexpr int favoriteResultOffset = 10000;
Array<PluginDescription> available, favorites;
OwnedArray<PluginDescription> unverified;
PluginManager* plugins { nullptr };
bool hasAddedPlugins = false;
Expand Down Expand Up @@ -222,7 +247,7 @@ class NodePopupMenu : public PopupMenu
#if ! ELEMENT_SE
PopupMenu menu;
KnownPluginList::addToMenu (menu,
plugins.getKnownPlugins().getTypes(),
plugins.getVisiblePluginTypes(),
KnownPluginList::sortByManufacturer,
node.getFileOrIdentifier().toString());
addSubMenu ("Replace", menu);
Expand Down
2 changes: 1 addition & 1 deletion src/ui/navigationview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class PluginsNavigationItem : public TreeItemBase
PluginsNavigationItem (PluginManager& pm)
: plugins (pm)
{
types = plugins.getKnownPlugins().getTypes();
types = plugins.getVisiblePluginTypes();
}

~PluginsNavigationItem() {}
Expand Down
Loading
Loading