OK, so without writing and launching an external process, and without investigating that whole thing, I wanted to see if I could insert a “custom scanner” into the process of PluginListComponent. It seems really difficult and klugey. I’m wondering if I’m just wrong on this whole idea…
Basically, if you wanted to ignore a certain plugin, you need to override doNextScan():
bool doNextScan()
{
// -----------------------------
// this is essentially the only modification that required sub-classing
// the whole thing
String s = scanner->getNextPluginFileThatWillBeScanned();
if (s.contains(“myPlugin”))
{
scanner->skipNextFile();
progress = scanner->getProgress();
DBG(" >>>>PLComponent2: doNextScan SKIPPING " + s + ", progress " + String(progress)); return true;
}
// -----------------------------
if (scanner->scanNextFile (true, pluginBeingScanned))
{
progress = scanner->getProgress();
return true;
}
finished = true;
return false;
}
But doNextScan() is a member function of a private class of PluginListComponent::Scanner. You can’t sub-class it, unless I’m wrong.
So to get to that point, you need to sub-class PluginListComponent, and duplicate/replace the entire Scanner class. Not only that, you need to duplicate a bunch of private member variables of PluginListComponent and have them shadow/hide the ones in the super-class, because you need to pass them to the scanFor() function and they are private.
And the real klugey thing was replacing the actions (lambdas) in the Options Popup Menu so that they pass a pointer to the sub-class and call the override of scanFor():
PopupMenu PluginListComponent2::createOptionsMenu()
{
// first, create the entire menu in the super-class
PopupMenu menu = PluginListComponent::createOptionsMenu();
// find the "scan for" items and replace the lambdas
// with pointers to this object (sub-class)
PopupMenu::MenuItemIterator iter(menu);
while(iter.next())
{
auto& item = iter.getItem(); // find the first "scan for" item
if (item.text.contains("Scan for new or updated"))
{
// run the same loop for format types as the original,
// and replace all of the lambdas
for (auto format : formatManager.getFormats())
{
// can't use the same variable inside this loop for some reason
auto& thisItem = iter.getItem();
if (format->canScanForPlugins())
thisItem.setAction ([this, format] { scanFor (*format); });
if (! iter.next()) // advance to next item; if none will exit
break;
}
break;
}
}
return menu;
}
Maybe I’m just being an idiot here, I’m not an expert with inheritance… but here’s what I had to do (in total) to simply get to an override of doNextScan() (it DOES work, BTW):
PluginListComponent2.h
/*
==============================================================================
PluginListComponent2.h
Created: 27 Feb 2020 7:14:45pm
==============================================================================
*/
#pragma once
#include "JuceHeader.h"
class PluginListComponent2 : public PluginListComponent
{
public:
PluginListComponent2(AudioPluginFormatManager& formatManager,
KnownPluginList& listToRepresent,
const File& deadMansPedalFile,
PropertiesFile* propertiesToUse,
bool allowPluginsWhichRequireAsynchronousInstantiation = false);
PopupMenu createOptionsMenu();
void scanFor (AudioPluginFormat&);
void scanFor (AudioPluginFormat&, const StringArray& filesOrIdentifiersToScan);
private:
// all of these duplicate and hide member variables in the super-class
// because we need to pass them in scanFor() and they are private
AudioPluginFormatManager& formatManager;
KnownPluginList& list;
TableListBox table;
File deadMansPedalFile;
PropertiesFile* propertiesToUse;
String dialogTitle, dialogText;
bool allowAsync;
int numThreads;
// this hides and overrides the Scanner with our Scanner class
class Scanner;
std::unique_ptr<Scanner> currentScanner;
void scanFinished (const StringArray&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginListComponent2)
};
PluginListComponent2.cpp
/*
==============================================================================
PluginListComponent2.cpp
Created: 27 Feb 2020 7:14:45pm
Author: Stephen Kay
==============================================================================
*/
#include "PluginListComponent2.h"
//==============================================================================
class PluginListComponent2::Scanner : private Timer
{
public:
Scanner (PluginListComponent2& plc, AudioPluginFormat& format, const StringArray& filesOrIdentifiers,
PropertiesFile* properties, bool allowPluginsWhichRequireAsynchronousInstantiation, int threads,
const String& title, const String& text)
: owner (plc), formatToScan (format), filesOrIdentifiersToScan (filesOrIdentifiers), propertiesToUse (properties),
pathChooserWindow (TRANS("Select folders to scan..."), String(), AlertWindow::NoIcon),
progressWindow (title, text, AlertWindow::NoIcon),
numThreads (threads), allowAsync (allowPluginsWhichRequireAsynchronousInstantiation)
{
FileSearchPath path (formatToScan.getDefaultLocationsToSearch());
// You need to use at least one thread when scanning plug-ins asynchronously
jassert (! allowAsync || (numThreads > 0));
// If the filesOrIdentifiersToScan argument isn't empty, we should only scan these
// If the path is empty, then paths aren't used for this format.
if (filesOrIdentifiersToScan.isEmpty() && path.getNumPaths() > 0)
{
#if ! JUCE_IOS
if (propertiesToUse != nullptr)
path = getLastSearchPath (*propertiesToUse, formatToScan);
#endif
pathList.setSize (500, 300);
pathList.setPath (path);
pathChooserWindow.addCustomComponent (&pathList);
pathChooserWindow.addButton (TRANS("Scan"), 1, KeyPress (KeyPress::returnKey));
pathChooserWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
pathChooserWindow.enterModalState (true,
ModalCallbackFunction::forComponent (startScanCallback,
&pathChooserWindow, this),
false);
}
else
{
startScan();
}
}
~Scanner() override
{
if (pool != nullptr)
{
pool->removeAllJobs (true, 60000);
pool.reset();
}
}
private:
PluginListComponent2& owner;
AudioPluginFormat& formatToScan;
StringArray filesOrIdentifiersToScan;
PropertiesFile* propertiesToUse;
std::unique_ptr<PluginDirectoryScanner> scanner;
AlertWindow pathChooserWindow, progressWindow;
FileSearchPathListComponent pathList;
String pluginBeingScanned;
double progress = 0;
int numThreads;
bool allowAsync, finished = false, timerReentrancyCheck = false;
std::unique_ptr<ThreadPool> pool;
static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner)
{
if (alert != nullptr && scanner != nullptr)
{
if (result != 0)
scanner->warnUserAboutStupidPaths();
else
scanner->finishedScan();
}
}
// Try to dissuade people from to scanning their entire C: drive, or other system folders.
void warnUserAboutStupidPaths()
{
for (int i = 0; i < pathList.getPath().getNumPaths(); ++i)
{
auto f = pathList.getPath()[i];
if (isStupidPath (f))
{
AlertWindow::showOkCancelBox (AlertWindow::WarningIcon,
TRANS("Plugin Scanning"),
TRANS("If you choose to scan folders that contain non-plugin files, "
"then scanning may take a long time, and can cause crashes when "
"attempting to load unsuitable files.")
+ newLine
+ TRANS ("Are you sure you want to scan the folder \"XYZ\"?")
.replace ("XYZ", f.getFullPathName()),
TRANS ("Scan"),
String(),
nullptr,
ModalCallbackFunction::create (warnAboutStupidPathsCallback, this));
return;
}
}
startScan();
}
static bool isStupidPath (const File& f)
{
Array<File> roots;
File::findFileSystemRoots (roots);
if (roots.contains (f))
return true;
File::SpecialLocationType pathsThatWouldBeStupidToScan[]
= { File::globalApplicationsDirectory,
File::userHomeDirectory,
File::userDocumentsDirectory,
File::userDesktopDirectory,
File::tempDirectory,
File::userMusicDirectory,
File::userMoviesDirectory,
File::userPicturesDirectory };
for (auto location : pathsThatWouldBeStupidToScan)
{
auto sillyFolder = File::getSpecialLocation (location);
if (f == sillyFolder || sillyFolder.isAChildOf (f))
return true;
}
return false;
}
static void warnAboutStupidPathsCallback (int result, Scanner* scanner)
{
if (result != 0)
scanner->startScan();
else
scanner->finishedScan();
}
void startScan()
{
pathChooserWindow.setVisible (false);
scanner.reset (new PluginDirectoryScanner (owner.list, formatToScan, pathList.getPath(),
true, owner.deadMansPedalFile, allowAsync));
if (! filesOrIdentifiersToScan.isEmpty())
{
scanner->setFilesOrIdentifiersToScan (filesOrIdentifiersToScan);
}
else if (propertiesToUse != nullptr)
{
setLastSearchPath (*propertiesToUse, formatToScan, pathList.getPath());
propertiesToUse->saveIfNeeded();
}
progressWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
progressWindow.addProgressBarComponent (progress);
progressWindow.enterModalState();
if (numThreads > 0)
{
pool.reset (new ThreadPool (numThreads));
for (int i = numThreads; --i >= 0;)
pool->addJob (new ScanJob (*this), true);
}
startTimer (20);
}
void finishedScan()
{
owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles()
: StringArray());
}
void timerCallback() override
{
if (timerReentrancyCheck)
return;
if (pool == nullptr)
{
const ScopedValueSetter<bool> setter (timerReentrancyCheck, true);
if (doNextScan())
startTimer (20);
}
if (! progressWindow.isCurrentlyModal())
finished = true;
if (finished)
finishedScan();
else
progressWindow.setMessage (TRANS("Testing") + ":\n\n" + pluginBeingScanned);
}
bool doNextScan()
{
// -----------------------------
// this is essentially the only modification that required sub-classing
// the whole thing
String s = scanner->getNextPluginFileThatWillBeScanned();
if (s.contains(“myPlugin”))
{
scanner->skipNextFile();
progress = scanner->getProgress();
DBG(" >>>>PLComponent2: doNextScan SKIPPING " + s + ", progress " + String(progress));
return true;
}
// -----------------------------
if (scanner->scanNextFile (true, pluginBeingScanned))
{
progress = scanner->getProgress();
return true;
}
finished = true;
return false;
}
struct ScanJob : public ThreadPoolJob
{
ScanJob (Scanner& s) : ThreadPoolJob ("pluginscan"), scanner (s) {}
JobStatus runJob()
{
while (scanner.doNextScan() && ! shouldExit())
{}
return jobHasFinished;
}
Scanner& scanner;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
};
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Scanner)
};
PluginListComponent2::PluginListComponent2 (AudioPluginFormatManager& manager, KnownPluginList& listToEdit,
const File& deadMansPedal, PropertiesFile* const props,
bool allowPluginsWhichRequireAsynchronousInstantiation)
: PluginListComponent (manager, listToEdit,
deadMansPedal, props,
allowPluginsWhichRequireAsynchronousInstantiation),
formatManager (manager),
list (listToEdit),
deadMansPedalFile (deadMansPedal),
propertiesToUse (props),
allowAsync (allowPluginsWhichRequireAsynchronousInstantiation),
numThreads (allowAsync ? 1 : 0)
{
//addAndMakeVisible(optionsButton);
getOptionsButton().onClick = [this]
{
createOptionsMenu().showMenuAsync (PopupMenu::Options()
.withDeletionCheck (*this)
.withTargetComponent (getOptionsButton()));
};
}
PopupMenu PluginListComponent2::createOptionsMenu()
{
// first, create the entire menu in the super-class
PopupMenu menu = PluginListComponent::createOptionsMenu();
// find the "scan for" items and replace the lambdas
// with pointers to this object (sub-class)
PopupMenu::MenuItemIterator iter(menu);
while(iter.next())
{
auto& item = iter.getItem(); // find the first "scan for" item
if (item.text.contains("Scan for new or updated"))
{
// run the same loop for format types as the original,
// and replace all of the lambdas
for (auto format : formatManager.getFormats())
{
// can't use the same variable inside this loop for some reason
auto& thisItem = iter.getItem();
if (format->canScanForPlugins())
thisItem.setAction ([this, format] { scanFor (*format); });
if (! iter.next()) // advance to next item; if none will exit
break;
}
break;
}
}
return menu;
}
void PluginListComponent2::scanFor (AudioPluginFormat& format)
{
DBG("PL2:scanFor format " + format.getName());
scanFor (format, StringArray());
}
void PluginListComponent2::scanFor (AudioPluginFormat& format, const StringArray& filesOrIdentifiersToScan)
{
currentScanner.reset (new Scanner (*this, format, filesOrIdentifiersToScan, propertiesToUse, allowAsync, numThreads,
dialogTitle.isNotEmpty() ? dialogTitle : TRANS("Scanning for plug-ins..."),
dialogText.isNotEmpty() ? dialogText : TRANS("Searching for all possible plug-in files...")));
}
void PluginListComponent2::scanFinished (const StringArray& failedFiles)
{
StringArray shortNames;
for (auto& f : failedFiles)
shortNames.add (File::createFileWithoutCheckingPath (f).getFileName());
currentScanner.reset(); // mustn't delete this before using the failed files array
if (shortNames.size() > 0)
AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon,
TRANS("Scan complete"),
TRANS("Note that the following files appeared to be plugin files, but failed to load correctly")
+ ":\n\n"
+ shortNames.joinIntoString (", "));
}