1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
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.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 class PluginListComponent::TableModel  : public TableListBoxModel
30 {
31 public:
TableModel(PluginListComponent & c,KnownPluginList & l)32     TableModel (PluginListComponent& c, KnownPluginList& l)  : owner (c), list (l) {}
33 
getNumRows()34     int getNumRows() override
35     {
36         return list.getNumTypes() + list.getBlacklistedFiles().size();
37     }
38 
paintRowBackground(Graphics & g,int,int,int,bool rowIsSelected)39     void paintRowBackground (Graphics& g, int /*rowNumber*/, int /*width*/, int /*height*/, bool rowIsSelected) override
40     {
41         const auto defaultColour = owner.findColour (ListBox::backgroundColourId);
42         const auto c = rowIsSelected ? defaultColour.interpolatedWith (owner.findColour (ListBox::textColourId), 0.5f)
43                                      : defaultColour;
44 
45         g.fillAll (c);
46     }
47 
48     enum
49     {
50         nameCol = 1,
51         typeCol = 2,
52         categoryCol = 3,
53         manufacturerCol = 4,
54         descCol = 5
55     };
56 
paintCell(Graphics & g,int row,int columnId,int width,int height,bool)57     void paintCell (Graphics& g, int row, int columnId, int width, int height, bool /*rowIsSelected*/) override
58     {
59         String text;
60         bool isBlacklisted = row >= list.getNumTypes();
61 
62         if (isBlacklisted)
63         {
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");
68         }
69         else
70         {
71             auto desc = list.getTypes()[row];
72 
73             switch (columnId)
74             {
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;
80 
81                 default: jassertfalse; break;
82             }
83         }
84 
85         if (text.isNotEmpty())
86         {
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);
93         }
94     }
95 
cellClicked(int rowNumber,int columnId,const juce::MouseEvent & e)96     void cellClicked (int rowNumber, int columnId, const juce::MouseEvent& e) override
97     {
98         TableListBoxModel::cellClicked (rowNumber, columnId, e);
99 
100         if (rowNumber >= 0 && rowNumber < getNumRows() && e.mods.isPopupMenu())
101             owner.createMenuForRow (rowNumber).showMenuAsync (PopupMenu::Options().withDeletionCheck (owner));
102     }
103 
deleteKeyPressed(int)104     void deleteKeyPressed (int) override
105     {
106         owner.removeSelectedPlugins();
107     }
108 
sortOrderChanged(int newSortColumnId,bool isForwards)109     void sortOrderChanged (int newSortColumnId, bool isForwards) override
110     {
111         switch (newSortColumnId)
112         {
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;
118 
119             default: jassertfalse; break;
120         }
121     }
122 
getPluginDescription(const PluginDescription & desc)123     static String getPluginDescription (const PluginDescription& desc)
124     {
125         StringArray items;
126 
127         if (desc.descriptiveName != desc.name)
128             items.add (desc.descriptiveName);
129 
130         items.add (desc.version);
131 
132         items.removeEmptyStrings();
133         return items.joinIntoString (" - ");
134     }
135 
136     PluginListComponent& owner;
137     KnownPluginList& list;
138 
139     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (TableModel)
140 };
141 
142 //==============================================================================
PluginListComponent(AudioPluginFormatManager & manager,KnownPluginList & listToEdit,const File & deadMansPedal,PropertiesFile * const props,bool allowPluginsWhichRequireAsynchronousInstantiation)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)
153 {
154     tableModel.reset (new TableModel (*this, listToEdit));
155 
156     TableHeaderComponent& header = table.getHeader();
157 
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);
163 
164     table.setHeaderHeight (22);
165     table.setRowHeight (20);
166     table.setModel (tableModel.get());
167     table.setMultipleSelectionEnabled (true);
168     addAndMakeVisible (table);
169 
170     addAndMakeVisible (optionsButton);
171     optionsButton.onClick = [this]
172     {
173         createOptionsMenu().showMenuAsync (PopupMenu::Options()
174                                               .withDeletionCheck (*this)
175                                               .withTargetComponent (optionsButton));
176     };
177 
178     optionsButton.setTriggeredOnMouseDown (true);
179 
180     setSize (400, 600);
181     list.addChangeListener (this);
182     updateList();
183     table.getHeader().reSortTable();
184 
185     PluginDirectoryScanner::applyBlacklistingsFromDeadMansPedal (list, deadMansPedalFile);
186     deadMansPedalFile.deleteFile();
187 }
188 
~PluginListComponent()189 PluginListComponent::~PluginListComponent()
190 {
191     list.removeChangeListener (this);
192 }
193 
setOptionsButtonText(const String & newText)194 void PluginListComponent::setOptionsButtonText (const String& newText)
195 {
196     optionsButton.setButtonText (newText);
197     resized();
198 }
199 
setScanDialogText(const String & title,const String & content)200 void PluginListComponent::setScanDialogText (const String& title, const String& content)
201 {
202     dialogTitle = title;
203     dialogText = content;
204 }
205 
setNumberOfThreadsForScanning(int num)206 void PluginListComponent::setNumberOfThreadsForScanning (int num)
207 {
208     numThreads = num;
209 }
210 
resized()211 void PluginListComponent::resized()
212 {
213     auto r = getLocalBounds().reduced (2);
214 
215     if (optionsButton.isVisible())
216     {
217         optionsButton.setBounds (r.removeFromBottom (24));
218         optionsButton.changeWidthToFitText (24);
219         r.removeFromBottom (3);
220     }
221 
222     table.setBounds (r);
223 }
224 
changeListenerCallback(ChangeBroadcaster *)225 void PluginListComponent::changeListenerCallback (ChangeBroadcaster*)
226 {
227     table.getHeader().reSortTable();
228     updateList();
229 }
230 
updateList()231 void PluginListComponent::updateList()
232 {
233     table.updateContent();
234     table.repaint();
235 }
236 
removeSelectedPlugins()237 void PluginListComponent::removeSelectedPlugins()
238 {
239     auto selected = table.getSelectedRows();
240 
241     for (int i = table.getNumRows(); --i >= 0;)
242         if (selected.contains (i))
243             removePluginItem (i);
244 }
245 
setTableModel(TableListBoxModel * model)246 void PluginListComponent::setTableModel (TableListBoxModel* model)
247 {
248     table.setModel (nullptr);
249     tableModel.reset (model);
250     table.setModel (tableModel.get());
251 
252     table.getHeader().reSortTable();
253     table.updateContent();
254     table.repaint();
255 }
256 
canShowFolderForPlugin(KnownPluginList & list,int index)257 static bool canShowFolderForPlugin (KnownPluginList& list, int index)
258 {
259     return File::createFileWithoutCheckingPath (list.getTypes()[index].fileOrIdentifier).exists();
260 }
261 
showFolderForPlugin(KnownPluginList & list,int index)262 static void showFolderForPlugin (KnownPluginList& list, int index)
263 {
264     if (canShowFolderForPlugin (list, index))
265         File (list.getTypes()[index].fileOrIdentifier).getParentDirectory().startAsProcess();
266 }
267 
removeMissingPlugins()268 void PluginListComponent::removeMissingPlugins()
269 {
270     auto types = list.getTypes();
271 
272     for (int i = types.size(); --i >= 0;)
273     {
274         auto type = types.getUnchecked (i);
275 
276         if (! formatManager.doesPluginStillExist (type))
277             list.removeType (type);
278     }
279 }
280 
removePluginItem(int index)281 void PluginListComponent::removePluginItem (int index)
282 {
283     if (index < list.getNumTypes())
284         list.removeType (list.getTypes()[index]);
285     else
286         list.removeFromBlacklist (list.getBlacklistedFiles() [index - list.getNumTypes()]);
287 }
288 
createOptionsMenu()289 PopupMenu PluginListComponent::createOptionsMenu()
290 {
291     PopupMenu menu;
292     menu.addItem (PopupMenu::Item (TRANS("Clear list"))
293                     .setAction ([this] { list.clear(); }));
294 
295     menu.addSeparator();
296 
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]
302                                         {
303                                             for (auto& pd : list.getTypesForFormat (*format))
304                                                 list.removeType (pd);
305                                         }));
306 
307     menu.addSeparator();
308 
309     menu.addItem (PopupMenu::Item (TRANS("Remove selected plug-in from list"))
310                     .setEnabled (table.getNumSelectedRows() > 0)
311                     .setAction ([this] { removeSelectedPlugins(); }));
312 
313     menu.addItem (PopupMenu::Item (TRANS("Remove any plug-ins whose files no longer exist"))
314                     .setAction ([this] { removeMissingPlugins(); }));
315 
316     menu.addSeparator();
317 
318     auto selectedRow = table.getSelectedRow();
319 
320     menu.addItem (PopupMenu::Item (TRANS("Show folder containing selected plug-in"))
321                     .setEnabled (canShowFolderForPlugin (list, selectedRow))
322                     .setAction ([this, selectedRow] { showFolderForPlugin (list, selectedRow); }));
323 
324     menu.addSeparator();
325 
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); }));
330 
331     return menu;
332 }
333 
createMenuForRow(int rowNumber)334 PopupMenu PluginListComponent::createMenuForRow (int rowNumber)
335 {
336     PopupMenu menu;
337 
338     if (rowNumber >= 0 && rowNumber < tableModel->getNumRows())
339     {
340         menu.addItem (PopupMenu::Item (TRANS("Remove plug-in from list"))
341                         .setAction ([this, rowNumber] { removePluginItem (rowNumber); }));
342 
343         menu.addItem (PopupMenu::Item (TRANS("Show folder containing plug-in"))
344                         .setEnabled (canShowFolderForPlugin (list, rowNumber))
345                         .setAction ([this, rowNumber] { showFolderForPlugin (list, rowNumber); }));
346     }
347 
348     return menu;
349 }
350 
isInterestedInFileDrag(const StringArray &)351 bool PluginListComponent::isInterestedInFileDrag (const StringArray& /*files*/)
352 {
353     return true;
354 }
355 
filesDropped(const StringArray & files,int,int)356 void PluginListComponent::filesDropped (const StringArray& files, int, int)
357 {
358     OwnedArray<PluginDescription> typesFound;
359     list.scanAndAddDragAndDroppedFiles (formatManager, files, typesFound);
360 }
361 
getLastSearchPath(PropertiesFile & properties,AudioPluginFormat & format)362 FileSearchPath PluginListComponent::getLastSearchPath (PropertiesFile& properties, AudioPluginFormat& format)
363 {
364     auto key = "lastPluginScanPath_" + format.getName();
365 
366     if (properties.containsKey (key) && properties.getValue (key, {}).trim().isEmpty())
367         properties.removeValue (key);
368 
369     return FileSearchPath (properties.getValue (key, format.getDefaultLocationsToSearch().toString()));
370 }
371 
setLastSearchPath(PropertiesFile & properties,AudioPluginFormat & format,const FileSearchPath & newPath)372 void PluginListComponent::setLastSearchPath (PropertiesFile& properties, AudioPluginFormat& format,
373                                              const FileSearchPath& newPath)
374 {
375     auto key = "lastPluginScanPath_" + format.getName();
376 
377     if (newPath.getNumPaths() == 0)
378         properties.removeValue (key);
379     else
380         properties.setValue (key, newPath.toString());
381 }
382 
383 //==============================================================================
384 class PluginListComponent::Scanner    : private Timer
385 {
386 public:
Scanner(PluginListComponent & plc,AudioPluginFormat & format,const StringArray & filesOrIdentifiers,PropertiesFile * properties,bool allowPluginsWhichRequireAsynchronousInstantiation,int threads,const String & title,const String & text)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), formatToScan (format), filesOrIdentifiersToScan (filesOrIdentifiers), propertiesToUse (properties),
391           pathChooserWindow (TRANS("Select folders to scan..."), String(), AlertWindow::NoIcon),
392           progressWindow (title, text, AlertWindow::NoIcon),
393           numThreads (threads), allowAsync (allowPluginsWhichRequireAsynchronousInstantiation)
394     {
395         FileSearchPath path (formatToScan.getDefaultLocationsToSearch());
396 
397         // You need to use at least one thread when scanning plug-ins asynchronously
398         jassert (! allowAsync || (numThreads > 0));
399 
400         // If the filesOrIdentifiersToScan argument isn't empty, we should only scan these
401         // If the path is empty, then paths aren't used for this format.
402         if (filesOrIdentifiersToScan.isEmpty() && path.getNumPaths() > 0)
403         {
404            #if ! JUCE_IOS
405             if (propertiesToUse != nullptr)
406                 path = getLastSearchPath (*propertiesToUse, formatToScan);
407            #endif
408 
409             pathList.setSize (500, 300);
410             pathList.setPath (path);
411 
412             pathChooserWindow.addCustomComponent (&pathList);
413             pathChooserWindow.addButton (TRANS("Scan"),   1, KeyPress (KeyPress::returnKey));
414             pathChooserWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
415 
416             pathChooserWindow.enterModalState (true,
417                                                ModalCallbackFunction::forComponent (startScanCallback,
418                                                                                     &pathChooserWindow, this),
419                                                false);
420         }
421         else
422         {
423             startScan();
424         }
425     }
426 
~Scanner()427     ~Scanner() override
428     {
429         if (pool != nullptr)
430         {
431             pool->removeAllJobs (true, 60000);
432             pool.reset();
433         }
434     }
435 
436 private:
437     PluginListComponent& owner;
438     AudioPluginFormat& formatToScan;
439     StringArray filesOrIdentifiersToScan;
440     PropertiesFile* propertiesToUse;
441     std::unique_ptr<PluginDirectoryScanner> scanner;
442     AlertWindow pathChooserWindow, progressWindow;
443     FileSearchPathListComponent pathList;
444     String pluginBeingScanned;
445     double progress = 0;
446     int numThreads;
447     bool allowAsync, finished = false, timerReentrancyCheck = false;
448     std::unique_ptr<ThreadPool> pool;
449 
startScanCallback(int result,AlertWindow * alert,Scanner * scanner)450     static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner)
451     {
452         if (alert != nullptr && scanner != nullptr)
453         {
454             if (result != 0)
455                 scanner->warnUserAboutStupidPaths();
456             else
457                 scanner->finishedScan();
458         }
459     }
460 
461     // Try to dissuade people from to scanning their entire C: drive, or other system folders.
warnUserAboutStupidPaths()462     void warnUserAboutStupidPaths()
463     {
464         for (int i = 0; i < pathList.getPath().getNumPaths(); ++i)
465         {
466             auto f = pathList.getPath()[i];
467 
468             if (isStupidPath (f))
469             {
470                 AlertWindow::showOkCancelBox (AlertWindow::WarningIcon,
471                                               TRANS("Plugin Scanning"),
472                                               TRANS("If you choose to scan folders that contain non-plugin files, "
473                                                     "then scanning may take a long time, and can cause crashes when "
474                                                     "attempting to load unsuitable files.")
475                                                 + newLine
476                                                 + TRANS ("Are you sure you want to scan the folder \"XYZ\"?")
477                                                    .replace ("XYZ", f.getFullPathName()),
478                                               TRANS ("Scan"),
479                                               String(),
480                                               nullptr,
481                                               ModalCallbackFunction::create (warnAboutStupidPathsCallback, this));
482                 return;
483             }
484         }
485 
486         startScan();
487     }
488 
isStupidPath(const File & f)489     static bool isStupidPath (const File& f)
490     {
491         Array<File> roots;
492         File::findFileSystemRoots (roots);
493 
494         if (roots.contains (f))
495             return true;
496 
497         File::SpecialLocationType pathsThatWouldBeStupidToScan[]
498             = { File::globalApplicationsDirectory,
499                 File::userHomeDirectory,
500                 File::userDocumentsDirectory,
501                 File::userDesktopDirectory,
502                 File::tempDirectory,
503                 File::userMusicDirectory,
504                 File::userMoviesDirectory,
505                 File::userPicturesDirectory };
506 
507         for (auto location : pathsThatWouldBeStupidToScan)
508         {
509             auto sillyFolder = File::getSpecialLocation (location);
510 
511             if (f == sillyFolder || sillyFolder.isAChildOf (f))
512                 return true;
513         }
514 
515         return false;
516     }
517 
warnAboutStupidPathsCallback(int result,Scanner * scanner)518     static void warnAboutStupidPathsCallback (int result, Scanner* scanner)
519     {
520         if (result != 0)
521             scanner->startScan();
522         else
523             scanner->finishedScan();
524     }
525 
startScan()526     void startScan()
527     {
528         pathChooserWindow.setVisible (false);
529 
530         scanner.reset (new PluginDirectoryScanner (owner.list, formatToScan, pathList.getPath(),
531                                                    true, owner.deadMansPedalFile, allowAsync));
532 
533         if (! filesOrIdentifiersToScan.isEmpty())
534         {
535             scanner->setFilesOrIdentifiersToScan (filesOrIdentifiersToScan);
536         }
537         else if (propertiesToUse != nullptr)
538         {
539             setLastSearchPath (*propertiesToUse, formatToScan, pathList.getPath());
540             propertiesToUse->saveIfNeeded();
541         }
542 
543         progressWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
544         progressWindow.addProgressBarComponent (progress);
545         progressWindow.enterModalState();
546 
547         if (numThreads > 0)
548         {
549             pool.reset (new ThreadPool (numThreads));
550 
551             for (int i = numThreads; --i >= 0;)
552                 pool->addJob (new ScanJob (*this), true);
553         }
554 
555         startTimer (20);
556     }
557 
finishedScan()558     void finishedScan()
559     {
560         owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles()
561                                                : StringArray());
562     }
563 
timerCallback()564     void timerCallback() override
565     {
566         if (timerReentrancyCheck)
567             return;
568 
569         if (pool == nullptr)
570         {
571             const ScopedValueSetter<bool> setter (timerReentrancyCheck, true);
572 
573             if (doNextScan())
574                 startTimer (20);
575         }
576 
577         if (! progressWindow.isCurrentlyModal())
578             finished = true;
579 
580         if (finished)
581             finishedScan();
582         else
583             progressWindow.setMessage (TRANS("Testing") + ":\n\n" + pluginBeingScanned);
584     }
585 
doNextScan()586     bool doNextScan()
587     {
588         if (scanner->scanNextFile (true, pluginBeingScanned))
589         {
590             progress = scanner->getProgress();
591             return true;
592         }
593 
594         finished = true;
595         return false;
596     }
597 
598     struct ScanJob  : public ThreadPoolJob
599     {
ScanJobjuce::PluginListComponent::Scanner::ScanJob600         ScanJob (Scanner& s)  : ThreadPoolJob ("pluginscan"), scanner (s) {}
601 
runJobjuce::PluginListComponent::Scanner::ScanJob602         JobStatus runJob()
603         {
604             while (scanner.doNextScan() && ! shouldExit())
605             {}
606 
607             return jobHasFinished;
608         }
609 
610         Scanner& scanner;
611 
612         JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
613     };
614 
615     JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Scanner)
616 };
617 
scanFor(AudioPluginFormat & format)618 void PluginListComponent::scanFor (AudioPluginFormat& format)
619 {
620     scanFor (format, StringArray());
621 }
622 
scanFor(AudioPluginFormat & format,const StringArray & filesOrIdentifiersToScan)623 void PluginListComponent::scanFor (AudioPluginFormat& format, const StringArray& filesOrIdentifiersToScan)
624 {
625     currentScanner.reset (new Scanner (*this, format, filesOrIdentifiersToScan, propertiesToUse, allowAsync, numThreads,
626                                        dialogTitle.isNotEmpty() ? dialogTitle : TRANS("Scanning for plug-ins..."),
627                                        dialogText.isNotEmpty()  ? dialogText  : TRANS("Searching for all possible plug-in files...")));
628 }
629 
isScanning() const630 bool PluginListComponent::isScanning() const noexcept
631 {
632     return currentScanner != nullptr;
633 }
634 
scanFinished(const StringArray & failedFiles)635 void PluginListComponent::scanFinished (const StringArray& failedFiles)
636 {
637     StringArray shortNames;
638 
639     for (auto& f : failedFiles)
640         shortNames.add (File::createFileWithoutCheckingPath (f).getFileName());
641 
642     currentScanner.reset(); // mustn't delete this before using the failed files array
643 
644     if (shortNames.size() > 0)
645         AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon,
646                                           TRANS("Scan complete"),
647                                           TRANS("Note that the following files appeared to be plugin files, but failed to load correctly")
648                                             + ":\n\n"
649                                             + shortNames.joinIntoString (", "));
650 }
651 
652 } // namespace juce
653