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
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
23 ==============================================================================
29 class PluginListComponent::TableModel
: public TableListBoxModel
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
)
57 void paintCell (Graphics
& g
, int row
, int columnId
, int width
, int height
, bool /*rowIsSelected*/) override
60 bool isBlacklisted
= row
>= list
.getNumTypes();
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");
71 auto desc
= list
.getTypes()[row
];
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;
119 default: jassertfalse
; break;
123 static String
getPluginDescription (const PluginDescription
& desc
)
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
),
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);
181 list
.addChangeListener (this);
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
);
200 void PluginListComponent::setScanDialogText (const String
& title
, const String
& content
)
203 dialogText
= content
;
206 void PluginListComponent::setNumberOfThreadsForScanning (int 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);
225 void PluginListComponent::changeListenerCallback (ChangeBroadcaster
*)
227 table
.getHeader().reSortTable();
231 void PluginListComponent::updateList()
233 table
.updateContent();
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();
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
]);
286 list
.removeFromBlacklist (list
.getBlacklistedFiles() [index
- list
.getNumTypes()]);
289 PopupMenu
PluginListComponent::createOptionsMenu()
292 menu
.addItem (PopupMenu::Item (TRANS("Clear list"))
293 .setAction ([this] { list
.clear(); }));
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
);
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(); }));
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
); }));
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
); }));
334 PopupMenu
PluginListComponent::createMenuForRow (int rowNumber
)
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
); }));
351 bool PluginListComponent::isInterestedInFileDrag (const StringArray
& /*files*/)
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
);
380 properties
.setValue (key
, newPath
.toString());
383 //==============================================================================
384 class PluginListComponent::Scanner
: private Timer
387 Scanner (PluginListComponent
& plc
, AudioPluginFormat
& format
, const StringArray
& filesOrIdentifiers
,
388 PropertiesFile
* properties
, bool allowPluginsWhichRequireAsynchronousInstantiation
, int threads
,
389 const String
& title
, const String
& text
)
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)
412 if (propertiesToUse
!= nullptr)
413 path
= getLastSearchPath (*propertiesToUse
, formatToScan
);
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),
438 pool
->removeAllJobs (true, 60000);
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
;
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)
464 scanner
->warnUserAboutStupidPaths();
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.")
485 + TRANS ("Are you sure you want to scan the folder \"XYZ\"?")
486 .replace ("XYZ", f
.getFullPathName()),
490 ModalCallbackFunction::create (warnAboutStupidPathsCallback
, this));
498 static bool isStupidPath (const File
& f
)
501 File::findFileSystemRoots (roots
);
503 if (roots
.contains (f
))
506 File::SpecialLocationType pathsThatWouldBeStupidToScan
[]
507 = { File::globalApplicationsDirectory
,
508 File::userHomeDirectory
,
509 File::userDocumentsDirectory
,
510 File::userDesktopDirectory
,
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
))
527 static void warnAboutStupidPathsCallback (int result
, Scanner
* scanner
)
530 scanner
->startScan();
532 scanner
->finishedScan();
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();
558 pool
.reset (new ThreadPool (numThreads
));
560 for (int i
= numThreads
; --i
>= 0;)
561 pool
->addJob (new ScanJob (*this), true);
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
)
586 progress
= scanner
->getProgress();
590 const ScopedValueSetter
<bool> setter (timerReentrancyCheck
, true);
596 if (! progressWindow
.isCurrentlyModal())
602 progressWindow
.setMessage (TRANS("Testing") + ":\n\n" + pluginBeingScanned
);
607 if (scanner
->scanNextFile (true, pluginBeingScanned
))
614 struct ScanJob
: public ThreadPoolJob
616 ScanJob (Scanner
& s
) : ThreadPoolJob ("pluginscan"), scanner (s
) {}
620 while (scanner
.doNextScan() && ! shouldExit())
623 return jobHasFinished
;
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)
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"));