VST3: fetch midi mappings all at once, use it for note/sound-off
[carla.git] / source / modules / juce_audio_processors / scanning / juce_PluginListComponent.cpp
blob6af4db76405543d5ebd6c7deeda03c70802f2154
1 /*
2 ==============================================================================
4 This file is part of the JUCE library.
5 Copyright (c) 2022 - Raw Material Software Limited
7 JUCE is an open source library subject to commercial or open-source
8 licensing.
10 By using JUCE, you agree to the terms of both the JUCE 7 End-User License
11 Agreement and JUCE Privacy Policy.
13 End User License Agreement: www.juce.com/juce-7-licence
14 Privacy Policy: www.juce.com/juce-privacy-policy
16 Or: You may also use this code under the terms of the GPL v3 (see
17 www.gnu.org/licenses).
19 JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20 EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21 DISCLAIMED.
23 ==============================================================================
26 namespace juce
29 class PluginListComponent::TableModel : public TableListBoxModel
31 public:
32 TableModel (PluginListComponent& c, KnownPluginList& l) : owner (c), list (l) {}
34 int getNumRows() override
36 return list.getNumTypes() + list.getBlacklistedFiles().size();
39 void paintRowBackground (Graphics& g, int /*rowNumber*/, int /*width*/, int /*height*/, bool rowIsSelected) override
41 const auto defaultColour = owner.findColour (ListBox::backgroundColourId);
42 const auto c = rowIsSelected ? defaultColour.interpolatedWith (owner.findColour (ListBox::textColourId), 0.5f)
43 : defaultColour;
45 g.fillAll (c);
48 enum
50 nameCol = 1,
51 typeCol = 2,
52 categoryCol = 3,
53 manufacturerCol = 4,
54 descCol = 5
57 void paintCell (Graphics& g, int row, int columnId, int width, int height, bool /*rowIsSelected*/) override
59 String text;
60 bool isBlacklisted = row >= list.getNumTypes();
62 if (isBlacklisted)
64 if (columnId == nameCol)
65 text = list.getBlacklistedFiles() [row - list.getNumTypes()];
66 else if (columnId == descCol)
67 text = TRANS("Deactivated after failing to initialise correctly");
69 else
71 auto desc = list.getTypes()[row];
73 switch (columnId)
75 case nameCol: text = desc.name; break;
76 case typeCol: text = desc.pluginFormatName; break;
77 case categoryCol: text = desc.category.isNotEmpty() ? desc.category : "-"; break;
78 case manufacturerCol: text = desc.manufacturerName; break;
79 case descCol: text = getPluginDescription (desc); break;
81 default: jassertfalse; break;
85 if (text.isNotEmpty())
87 const auto defaultTextColour = owner.findColour (ListBox::textColourId);
88 g.setColour (isBlacklisted ? Colours::red
89 : columnId == nameCol ? defaultTextColour
90 : defaultTextColour.interpolatedWith (Colours::transparentBlack, 0.3f));
91 g.setFont (Font ((float) height * 0.7f, Font::bold));
92 g.drawFittedText (text, 4, 0, width - 6, height, Justification::centredLeft, 1, 0.9f);
96 void cellClicked (int rowNumber, int columnId, const juce::MouseEvent& e) override
98 TableListBoxModel::cellClicked (rowNumber, columnId, e);
100 if (rowNumber >= 0 && rowNumber < getNumRows() && e.mods.isPopupMenu())
101 owner.createMenuForRow (rowNumber).showMenuAsync (PopupMenu::Options().withDeletionCheck (owner));
104 void deleteKeyPressed (int) override
106 owner.removeSelectedPlugins();
109 void sortOrderChanged (int newSortColumnId, bool isForwards) override
111 switch (newSortColumnId)
113 case nameCol: list.sort (KnownPluginList::sortAlphabetically, isForwards); break;
114 case typeCol: list.sort (KnownPluginList::sortByFormat, isForwards); break;
115 case categoryCol: list.sort (KnownPluginList::sortByCategory, isForwards); break;
116 case manufacturerCol: list.sort (KnownPluginList::sortByManufacturer, isForwards); break;
117 case descCol: break;
119 default: jassertfalse; break;
123 static String getPluginDescription (const PluginDescription& desc)
125 StringArray items;
127 if (desc.descriptiveName != desc.name)
128 items.add (desc.descriptiveName);
130 items.add (desc.version);
132 items.removeEmptyStrings();
133 return items.joinIntoString (" - ");
136 PluginListComponent& owner;
137 KnownPluginList& list;
139 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableModel)
142 //==============================================================================
143 PluginListComponent::PluginListComponent (AudioPluginFormatManager& manager, KnownPluginList& listToEdit,
144 const File& deadMansPedal, PropertiesFile* const props,
145 bool allowPluginsWhichRequireAsynchronousInstantiation)
146 : formatManager (manager),
147 list (listToEdit),
148 deadMansPedalFile (deadMansPedal),
149 optionsButton ("Options..."),
150 propertiesToUse (props),
151 allowAsync (allowPluginsWhichRequireAsynchronousInstantiation),
152 numThreads (allowAsync ? 1 : 0)
154 tableModel.reset (new TableModel (*this, listToEdit));
156 TableHeaderComponent& header = table.getHeader();
158 header.addColumn (TRANS("Name"), TableModel::nameCol, 200, 100, 700, TableHeaderComponent::defaultFlags | TableHeaderComponent::sortedForwards);
159 header.addColumn (TRANS("Format"), TableModel::typeCol, 80, 80, 80, TableHeaderComponent::notResizable);
160 header.addColumn (TRANS("Category"), TableModel::categoryCol, 100, 100, 200);
161 header.addColumn (TRANS("Manufacturer"), TableModel::manufacturerCol, 200, 100, 300);
162 header.addColumn (TRANS("Description"), TableModel::descCol, 300, 100, 500, TableHeaderComponent::notSortable);
164 table.setHeaderHeight (22);
165 table.setRowHeight (20);
166 table.setModel (tableModel.get());
167 table.setMultipleSelectionEnabled (true);
168 addAndMakeVisible (table);
170 addAndMakeVisible (optionsButton);
171 optionsButton.onClick = [this]
173 createOptionsMenu().showMenuAsync (PopupMenu::Options()
174 .withDeletionCheck (*this)
175 .withTargetComponent (optionsButton));
178 optionsButton.setTriggeredOnMouseDown (true);
180 setSize (400, 600);
181 list.addChangeListener (this);
182 updateList();
183 table.getHeader().reSortTable();
185 PluginDirectoryScanner::applyBlacklistingsFromDeadMansPedal (list, deadMansPedalFile);
186 deadMansPedalFile.deleteFile();
189 PluginListComponent::~PluginListComponent()
191 list.removeChangeListener (this);
194 void PluginListComponent::setOptionsButtonText (const String& newText)
196 optionsButton.setButtonText (newText);
197 resized();
200 void PluginListComponent::setScanDialogText (const String& title, const String& content)
202 dialogTitle = title;
203 dialogText = content;
206 void PluginListComponent::setNumberOfThreadsForScanning (int num)
208 numThreads = num;
211 void PluginListComponent::resized()
213 auto r = getLocalBounds().reduced (2);
215 if (optionsButton.isVisible())
217 optionsButton.setBounds (r.removeFromBottom (24));
218 optionsButton.changeWidthToFitText (24);
219 r.removeFromBottom (3);
222 table.setBounds (r);
225 void PluginListComponent::changeListenerCallback (ChangeBroadcaster*)
227 table.getHeader().reSortTable();
228 updateList();
231 void PluginListComponent::updateList()
233 table.updateContent();
234 table.repaint();
237 void PluginListComponent::removeSelectedPlugins()
239 auto selected = table.getSelectedRows();
241 for (int i = table.getNumRows(); --i >= 0;)
242 if (selected.contains (i))
243 removePluginItem (i);
246 void PluginListComponent::setTableModel (TableListBoxModel* model)
248 table.setModel (nullptr);
249 tableModel.reset (model);
250 table.setModel (tableModel.get());
252 table.getHeader().reSortTable();
253 table.updateContent();
254 table.repaint();
257 static bool canShowFolderForPlugin (KnownPluginList& list, int index)
259 return File::createFileWithoutCheckingPath (list.getTypes()[index].fileOrIdentifier).exists();
262 static void showFolderForPlugin (KnownPluginList& list, int index)
264 if (canShowFolderForPlugin (list, index))
265 File (list.getTypes()[index].fileOrIdentifier).revealToUser();
268 void PluginListComponent::removeMissingPlugins()
270 auto types = list.getTypes();
272 for (int i = types.size(); --i >= 0;)
274 auto type = types.getUnchecked (i);
276 if (! formatManager.doesPluginStillExist (type))
277 list.removeType (type);
281 void PluginListComponent::removePluginItem (int index)
283 if (index < list.getNumTypes())
284 list.removeType (list.getTypes()[index]);
285 else
286 list.removeFromBlacklist (list.getBlacklistedFiles() [index - list.getNumTypes()]);
289 PopupMenu PluginListComponent::createOptionsMenu()
291 PopupMenu menu;
292 menu.addItem (PopupMenu::Item (TRANS("Clear list"))
293 .setAction ([this] { list.clear(); }));
295 menu.addSeparator();
297 for (auto format : formatManager.getFormats())
298 if (format->canScanForPlugins())
299 menu.addItem (PopupMenu::Item ("Remove all " + format->getName() + " plug-ins")
300 .setEnabled (! list.getTypesForFormat (*format).isEmpty())
301 .setAction ([this, format]
303 for (auto& pd : list.getTypesForFormat (*format))
304 list.removeType (pd);
305 }));
307 menu.addSeparator();
309 menu.addItem (PopupMenu::Item (TRANS("Remove selected plug-in from list"))
310 .setEnabled (table.getNumSelectedRows() > 0)
311 .setAction ([this] { removeSelectedPlugins(); }));
313 menu.addItem (PopupMenu::Item (TRANS("Remove any plug-ins whose files no longer exist"))
314 .setAction ([this] { removeMissingPlugins(); }));
316 menu.addSeparator();
318 auto selectedRow = table.getSelectedRow();
320 menu.addItem (PopupMenu::Item (TRANS("Show folder containing selected plug-in"))
321 .setEnabled (canShowFolderForPlugin (list, selectedRow))
322 .setAction ([this, selectedRow] { showFolderForPlugin (list, selectedRow); }));
324 menu.addSeparator();
326 for (auto format : formatManager.getFormats())
327 if (format->canScanForPlugins())
328 menu.addItem (PopupMenu::Item ("Scan for new or updated " + format->getName() + " plug-ins")
329 .setAction ([this, format] { scanFor (*format); }));
331 return menu;
334 PopupMenu PluginListComponent::createMenuForRow (int rowNumber)
336 PopupMenu menu;
338 if (rowNumber >= 0 && rowNumber < tableModel->getNumRows())
340 menu.addItem (PopupMenu::Item (TRANS("Remove plug-in from list"))
341 .setAction ([this, rowNumber] { removePluginItem (rowNumber); }));
343 menu.addItem (PopupMenu::Item (TRANS("Show folder containing plug-in"))
344 .setEnabled (canShowFolderForPlugin (list, rowNumber))
345 .setAction ([this, rowNumber] { showFolderForPlugin (list, rowNumber); }));
348 return menu;
351 bool PluginListComponent::isInterestedInFileDrag (const StringArray& /*files*/)
353 return true;
356 void PluginListComponent::filesDropped (const StringArray& files, int, int)
358 OwnedArray<PluginDescription> typesFound;
359 list.scanAndAddDragAndDroppedFiles (formatManager, files, typesFound);
362 FileSearchPath PluginListComponent::getLastSearchPath (PropertiesFile& properties, AudioPluginFormat& format)
364 auto key = "lastPluginScanPath_" + format.getName();
366 if (properties.containsKey (key) && properties.getValue (key, {}).trim().isEmpty())
367 properties.removeValue (key);
369 return FileSearchPath (properties.getValue (key, format.getDefaultLocationsToSearch().toString()));
372 void PluginListComponent::setLastSearchPath (PropertiesFile& properties, AudioPluginFormat& format,
373 const FileSearchPath& newPath)
375 auto key = "lastPluginScanPath_" + format.getName();
377 if (newPath.getNumPaths() == 0)
378 properties.removeValue (key);
379 else
380 properties.setValue (key, newPath.toString());
383 //==============================================================================
384 class PluginListComponent::Scanner : private Timer
386 public:
387 Scanner (PluginListComponent& plc, AudioPluginFormat& format, const StringArray& filesOrIdentifiers,
388 PropertiesFile* properties, bool allowPluginsWhichRequireAsynchronousInstantiation, int threads,
389 const String& title, const String& text)
390 : owner (plc),
391 formatToScan (format),
392 filesOrIdentifiersToScan (filesOrIdentifiers),
393 propertiesToUse (properties),
394 pathChooserWindow (TRANS("Select folders to scan..."), String(), MessageBoxIconType::NoIcon),
395 progressWindow (title, text, MessageBoxIconType::NoIcon),
396 numThreads (threads),
397 allowAsync (allowPluginsWhichRequireAsynchronousInstantiation)
399 const auto blacklisted = owner.list.getBlacklistedFiles();
400 initiallyBlacklistedFiles = std::set<String> (blacklisted.begin(), blacklisted.end());
402 FileSearchPath path (formatToScan.getDefaultLocationsToSearch());
404 // You need to use at least one thread when scanning plug-ins asynchronously
405 jassert (! allowAsync || (numThreads > 0));
407 // If the filesOrIdentifiersToScan argument isn't empty, we should only scan these
408 // If the path is empty, then paths aren't used for this format.
409 if (filesOrIdentifiersToScan.isEmpty() && path.getNumPaths() > 0)
411 #if ! JUCE_IOS
412 if (propertiesToUse != nullptr)
413 path = getLastSearchPath (*propertiesToUse, formatToScan);
414 #endif
416 pathList.setSize (500, 300);
417 pathList.setPath (path);
419 pathChooserWindow.addCustomComponent (&pathList);
420 pathChooserWindow.addButton (TRANS("Scan"), 1, KeyPress (KeyPress::returnKey));
421 pathChooserWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
423 pathChooserWindow.enterModalState (true,
424 ModalCallbackFunction::forComponent (startScanCallback,
425 &pathChooserWindow, this),
426 false);
428 else
430 startScan();
434 ~Scanner() override
436 if (pool != nullptr)
438 pool->removeAllJobs (true, 60000);
439 pool.reset();
443 private:
444 PluginListComponent& owner;
445 AudioPluginFormat& formatToScan;
446 StringArray filesOrIdentifiersToScan;
447 PropertiesFile* propertiesToUse;
448 std::unique_ptr<PluginDirectoryScanner> scanner;
449 AlertWindow pathChooserWindow, progressWindow;
450 FileSearchPathListComponent pathList;
451 String pluginBeingScanned;
452 double progress = 0;
453 const int numThreads;
454 bool allowAsync, timerReentrancyCheck = false;
455 std::atomic<bool> finished { false };
456 std::unique_ptr<ThreadPool> pool;
457 std::set<String> initiallyBlacklistedFiles;
459 static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner)
461 if (alert != nullptr && scanner != nullptr)
463 if (result != 0)
464 scanner->warnUserAboutStupidPaths();
465 else
466 scanner->finishedScan();
470 // Try to dissuade people from to scanning their entire C: drive, or other system folders.
471 void warnUserAboutStupidPaths()
473 for (int i = 0; i < pathList.getPath().getNumPaths(); ++i)
475 auto f = pathList.getPath()[i];
477 if (isStupidPath (f))
479 AlertWindow::showOkCancelBox (MessageBoxIconType::WarningIcon,
480 TRANS("Plugin Scanning"),
481 TRANS("If you choose to scan folders that contain non-plugin files, "
482 "then scanning may take a long time, and can cause crashes when "
483 "attempting to load unsuitable files.")
484 + newLine
485 + TRANS ("Are you sure you want to scan the folder \"XYZ\"?")
486 .replace ("XYZ", f.getFullPathName()),
487 TRANS ("Scan"),
488 String(),
489 nullptr,
490 ModalCallbackFunction::create (warnAboutStupidPathsCallback, this));
491 return;
495 startScan();
498 static bool isStupidPath (const File& f)
500 Array<File> roots;
501 File::findFileSystemRoots (roots);
503 if (roots.contains (f))
504 return true;
506 File::SpecialLocationType pathsThatWouldBeStupidToScan[]
507 = { File::globalApplicationsDirectory,
508 File::userHomeDirectory,
509 File::userDocumentsDirectory,
510 File::userDesktopDirectory,
511 File::tempDirectory,
512 File::userMusicDirectory,
513 File::userMoviesDirectory,
514 File::userPicturesDirectory };
516 for (auto location : pathsThatWouldBeStupidToScan)
518 auto sillyFolder = File::getSpecialLocation (location);
520 if (f == sillyFolder || sillyFolder.isAChildOf (f))
521 return true;
524 return false;
527 static void warnAboutStupidPathsCallback (int result, Scanner* scanner)
529 if (result != 0)
530 scanner->startScan();
531 else
532 scanner->finishedScan();
535 void startScan()
537 pathChooserWindow.setVisible (false);
539 scanner.reset (new PluginDirectoryScanner (owner.list, formatToScan, pathList.getPath(),
540 true, owner.deadMansPedalFile, allowAsync));
542 if (! filesOrIdentifiersToScan.isEmpty())
544 scanner->setFilesOrIdentifiersToScan (filesOrIdentifiersToScan);
546 else if (propertiesToUse != nullptr)
548 setLastSearchPath (*propertiesToUse, formatToScan, pathList.getPath());
549 propertiesToUse->saveIfNeeded();
552 progressWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
553 progressWindow.addProgressBarComponent (progress);
554 progressWindow.enterModalState();
556 if (numThreads > 0)
558 pool.reset (new ThreadPool (numThreads));
560 for (int i = numThreads; --i >= 0;)
561 pool->addJob (new ScanJob (*this), true);
564 startTimer (20);
567 void finishedScan()
569 const auto blacklisted = owner.list.getBlacklistedFiles();
570 std::set<String> allBlacklistedFiles (blacklisted.begin(), blacklisted.end());
572 std::vector<String> newBlacklistedFiles;
573 std::set_difference (allBlacklistedFiles.begin(), allBlacklistedFiles.end(),
574 initiallyBlacklistedFiles.begin(), initiallyBlacklistedFiles.end(),
575 std::back_inserter (newBlacklistedFiles));
577 owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles() : StringArray(),
578 newBlacklistedFiles);
581 void timerCallback() override
583 if (timerReentrancyCheck)
584 return;
586 progress = scanner->getProgress();
588 if (pool == nullptr)
590 const ScopedValueSetter<bool> setter (timerReentrancyCheck, true);
592 if (doNextScan())
593 startTimer (20);
596 if (! progressWindow.isCurrentlyModal())
597 finished = true;
599 if (finished)
600 finishedScan();
601 else
602 progressWindow.setMessage (TRANS("Testing") + ":\n\n" + pluginBeingScanned);
605 bool doNextScan()
607 if (scanner->scanNextFile (true, pluginBeingScanned))
608 return true;
610 finished = true;
611 return false;
614 struct ScanJob : public ThreadPoolJob
616 ScanJob (Scanner& s) : ThreadPoolJob ("pluginscan"), scanner (s) {}
618 JobStatus runJob()
620 while (scanner.doNextScan() && ! shouldExit())
623 return jobHasFinished;
626 Scanner& scanner;
628 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
631 JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Scanner)
634 void PluginListComponent::scanFor (AudioPluginFormat& format)
636 scanFor (format, StringArray());
639 void PluginListComponent::scanFor (AudioPluginFormat& format, const StringArray& filesOrIdentifiersToScan)
641 currentScanner.reset (new Scanner (*this, format, filesOrIdentifiersToScan, propertiesToUse, allowAsync, numThreads,
642 dialogTitle.isNotEmpty() ? dialogTitle : TRANS("Scanning for plug-ins..."),
643 dialogText.isNotEmpty() ? dialogText : TRANS("Searching for all possible plug-in files...")));
646 bool PluginListComponent::isScanning() const noexcept
648 return currentScanner != nullptr;
651 void PluginListComponent::scanFinished (const StringArray& failedFiles,
652 const std::vector<String>& newBlacklistedFiles)
654 StringArray warnings;
656 const auto addWarningText = [&warnings] (const auto& range, const auto& prefix)
658 if (range.size() == 0)
659 return;
661 StringArray names;
663 for (auto& f : range)
664 names.add (File::createFileWithoutCheckingPath (f).getFileName());
666 warnings.add (prefix + ":\n\n" + names.joinIntoString (", "));
669 addWarningText (newBlacklistedFiles, TRANS ("The following files encountered fatal errors during validation"));
670 addWarningText (failedFiles, TRANS ("The following files appeared to be plugin files, but failed to load correctly"));
672 currentScanner.reset(); // mustn't delete this before using the failed files array
674 if (! warnings.isEmpty())
675 AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon,
676 TRANS("Scan complete"),
677 warnings.joinIntoString ("\n\n"));
680 } // namespace juce