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 
FileBrowserComponent(int flags_,const File & initialFileOrDirectory,const FileFilter * fileFilter_,FilePreviewComponent * previewComp_)29 FileBrowserComponent::FileBrowserComponent (int flags_,
30                                             const File& initialFileOrDirectory,
31                                             const FileFilter* fileFilter_,
32                                             FilePreviewComponent* previewComp_)
33    : FileFilter ({}),
34      fileFilter (fileFilter_),
35      flags (flags_),
36      previewComp (previewComp_),
37      currentPathBox ("path"),
38      fileLabel ("f", TRANS ("file:")),
39      thread ("JUCE FileBrowser"),
40      wasProcessActive (true)
41 {
42     // You need to specify one or other of the open/save flags..
43     jassert ((flags & (saveMode | openMode)) != 0);
44     jassert ((flags & (saveMode | openMode)) != (saveMode | openMode));
45 
46     // You need to specify at least one of these flags..
47     jassert ((flags & (canSelectFiles | canSelectDirectories)) != 0);
48 
49     String filename;
50 
51     if (initialFileOrDirectory == File())
52     {
53         currentRoot = File::getCurrentWorkingDirectory();
54     }
55     else if (initialFileOrDirectory.isDirectory())
56     {
57         currentRoot = initialFileOrDirectory;
58     }
59     else
60     {
61         chosenFiles.add (initialFileOrDirectory);
62         currentRoot = initialFileOrDirectory.getParentDirectory();
63         filename = initialFileOrDirectory.getFileName();
64     }
65 
66     fileList.reset (new DirectoryContentsList (this, thread));
67     fileList->setDirectory (currentRoot, true, true);
68 
69     if ((flags & useTreeView) != 0)
70     {
71         auto tree = new FileTreeComponent (*fileList);
72         fileListComponent.reset (tree);
73 
74         if ((flags & canSelectMultipleItems) != 0)
75             tree->setMultiSelectEnabled (true);
76 
77         addAndMakeVisible (tree);
78     }
79     else
80     {
81         auto list = new FileListComponent (*fileList);
82         fileListComponent.reset (list);
83         list->setOutlineThickness (1);
84 
85         if ((flags & canSelectMultipleItems) != 0)
86             list->setMultipleSelectionEnabled (true);
87 
88         addAndMakeVisible (list);
89     }
90 
91     fileListComponent->addListener (this);
92 
93     addAndMakeVisible (currentPathBox);
94     currentPathBox.setEditableText (true);
95     resetRecentPaths();
__anon5a351f860102null96     currentPathBox.onChange = [this] { updateSelectedPath(); };
97 
98     addAndMakeVisible (filenameBox);
99     filenameBox.setMultiLine (false);
100     filenameBox.setSelectAllWhenFocused (true);
101     filenameBox.setText (filename, false);
__anon5a351f860202null102     filenameBox.onTextChange = [this] { sendListenerChangeMessage(); };
__anon5a351f860302null103     filenameBox.onReturnKey  = [this] { changeFilename(); };
104     filenameBox.onFocusLost  = [this]
__anon5a351f860402null105     {
106         if (! isSaveMode())
107             selectionChanged();
108     };
109 
110     filenameBox.setReadOnly ((flags & (filenameBoxIsReadOnly | canSelectMultipleItems)) != 0);
111 
112     addAndMakeVisible (fileLabel);
113     fileLabel.attachToComponent (&filenameBox, true);
114 
115     if (previewComp != nullptr)
116         addAndMakeVisible (previewComp);
117 
118     lookAndFeelChanged();
119 
120     setRoot (currentRoot);
121 
122     if (filename.isNotEmpty())
123         setFileName (filename);
124 
125     thread.startThread (4);
126 
127     startTimer (2000);
128 }
129 
~FileBrowserComponent()130 FileBrowserComponent::~FileBrowserComponent()
131 {
132     fileListComponent.reset();
133     fileList.reset();
134     thread.stopThread (10000);
135 }
136 
137 //==============================================================================
addListener(FileBrowserListener * const newListener)138 void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
139 {
140     listeners.add (newListener);
141 }
142 
removeListener(FileBrowserListener * const listener)143 void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
144 {
145     listeners.remove (listener);
146 }
147 
148 //==============================================================================
isSaveMode() const149 bool FileBrowserComponent::isSaveMode() const noexcept
150 {
151     return (flags & saveMode) != 0;
152 }
153 
getNumSelectedFiles() const154 int FileBrowserComponent::getNumSelectedFiles() const noexcept
155 {
156     if (chosenFiles.isEmpty() && currentFileIsValid())
157         return 1;
158 
159     return chosenFiles.size();
160 }
161 
getSelectedFile(int index) const162 File FileBrowserComponent::getSelectedFile (int index) const noexcept
163 {
164     if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
165         return currentRoot;
166 
167     if (! filenameBox.isReadOnly())
168         return currentRoot.getChildFile (filenameBox.getText());
169 
170     return chosenFiles[index];
171 }
172 
currentFileIsValid() const173 bool FileBrowserComponent::currentFileIsValid() const
174 {
175     auto f = getSelectedFile (0);
176 
177     if ((flags & canSelectDirectories) == 0 && f.isDirectory())
178         return false;
179 
180     return isSaveMode() || f.exists();
181 }
182 
getHighlightedFile() const183 File FileBrowserComponent::getHighlightedFile() const noexcept
184 {
185     return fileListComponent->getSelectedFile (0);
186 }
187 
deselectAllFiles()188 void FileBrowserComponent::deselectAllFiles()
189 {
190     fileListComponent->deselectAllFiles();
191 }
192 
193 //==============================================================================
isFileSuitable(const File & file) const194 bool FileBrowserComponent::isFileSuitable (const File& file) const
195 {
196     return (flags & canSelectFiles) != 0
197              && (fileFilter == nullptr || fileFilter->isFileSuitable (file));
198 }
199 
isDirectorySuitable(const File &) const200 bool FileBrowserComponent::isDirectorySuitable (const File&) const
201 {
202     return true;
203 }
204 
isFileOrDirSuitable(const File & f) const205 bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
206 {
207     if (f.isDirectory())
208         return (flags & canSelectDirectories) != 0
209                  && (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
210 
211     return (flags & canSelectFiles) != 0 && f.exists()
212              && (fileFilter == nullptr || fileFilter->isFileSuitable (f));
213 }
214 
215 //==============================================================================
getRoot() const216 const File& FileBrowserComponent::getRoot() const
217 {
218     return currentRoot;
219 }
220 
setRoot(const File & newRootDirectory)221 void FileBrowserComponent::setRoot (const File& newRootDirectory)
222 {
223     bool callListeners = false;
224 
225     if (currentRoot != newRootDirectory)
226     {
227         callListeners = true;
228         fileListComponent->scrollToTop();
229 
230         String path (newRootDirectory.getFullPathName());
231 
232         if (path.isEmpty())
233             path = File::getSeparatorString();
234 
235         StringArray rootNames, rootPaths;
236         getRoots (rootNames, rootPaths);
237 
238         if (! rootPaths.contains (path, true))
239         {
240             bool alreadyListed = false;
241 
242             for (int i = currentPathBox.getNumItems(); --i >= 0;)
243             {
244                 if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
245                 {
246                     alreadyListed = true;
247                     break;
248                 }
249             }
250 
251             if (! alreadyListed)
252                 currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
253         }
254     }
255 
256     currentRoot = newRootDirectory;
257     fileList->setDirectory (currentRoot, true, true);
258 
259     if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
260         tree->refresh();
261 
262     auto currentRootName = currentRoot.getFullPathName();
263 
264     if (currentRootName.isEmpty())
265         currentRootName = File::getSeparatorString();
266 
267     currentPathBox.setText (currentRootName, dontSendNotification);
268 
269     goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
270                              && currentRoot.getParentDirectory() != currentRoot);
271 
272     if (callListeners)
273     {
274         Component::BailOutChecker checker (this);
275         listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
276     }
277 }
278 
setFileName(const String & newName)279 void FileBrowserComponent::setFileName (const String& newName)
280 {
281     filenameBox.setText (newName, true);
282 
283     fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
284 }
285 
resetRecentPaths()286 void FileBrowserComponent::resetRecentPaths()
287 {
288     currentPathBox.clear();
289 
290     StringArray rootNames, rootPaths;
291     getRoots (rootNames, rootPaths);
292 
293     for (int i = 0; i < rootNames.size(); ++i)
294     {
295         if (rootNames[i].isEmpty())
296             currentPathBox.addSeparator();
297         else
298             currentPathBox.addItem (rootNames[i], i + 1);
299     }
300 
301     currentPathBox.addSeparator();
302 }
303 
goUp()304 void FileBrowserComponent::goUp()
305 {
306     setRoot (getRoot().getParentDirectory());
307 }
308 
refresh()309 void FileBrowserComponent::refresh()
310 {
311     fileList->refresh();
312 }
313 
setFileFilter(const FileFilter * const newFileFilter)314 void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
315 {
316     if (fileFilter != newFileFilter)
317     {
318         fileFilter = newFileFilter;
319         refresh();
320     }
321 }
322 
getActionVerb() const323 String FileBrowserComponent::getActionVerb() const
324 {
325     return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS("Choose")
326                                                                : TRANS("Save"))
327                         : TRANS("Open");
328 }
329 
setFilenameBoxLabel(const String & name)330 void FileBrowserComponent::setFilenameBoxLabel (const String& name)
331 {
332     fileLabel.setText (name, dontSendNotification);
333 }
334 
getPreviewComponent() const335 FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
336 {
337     return previewComp;
338 }
339 
getDisplayComponent() const340 DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
341 {
342     return fileListComponent.get();
343 }
344 
345 //==============================================================================
resized()346 void FileBrowserComponent::resized()
347 {
348     getLookAndFeel()
349         .layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
350                                      &currentPathBox, &filenameBox, goUpButton.get());
351 }
352 
353 //==============================================================================
lookAndFeelChanged()354 void FileBrowserComponent::lookAndFeelChanged()
355 {
356     goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
357 
358     if (auto* buttonPtr = goUpButton.get())
359     {
360         addAndMakeVisible (*buttonPtr);
361         buttonPtr->onClick = [this] { goUp(); };
362         buttonPtr->setTooltip (TRANS ("Go up to parent directory"));
363     }
364 
365     currentPathBox.setColour (ComboBox::backgroundColourId,    findColour (currentPathBoxBackgroundColourId));
366     currentPathBox.setColour (ComboBox::textColourId,          findColour (currentPathBoxTextColourId));
367     currentPathBox.setColour (ComboBox::arrowColourId,         findColour (currentPathBoxArrowColourId));
368 
369     filenameBox.setColour (TextEditor::backgroundColourId,     findColour (filenameBoxBackgroundColourId));
370     filenameBox.applyColourToAllText (findColour (filenameBoxTextColourId));
371 
372     resized();
373     repaint();
374 }
375 
376 //==============================================================================
sendListenerChangeMessage()377 void FileBrowserComponent::sendListenerChangeMessage()
378 {
379     Component::BailOutChecker checker (this);
380 
381     if (previewComp != nullptr)
382         previewComp->selectedFileChanged (getSelectedFile (0));
383 
384     // You shouldn't delete the browser when the file gets changed!
385     jassert (! checker.shouldBailOut());
386 
387     listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
388 }
389 
selectionChanged()390 void FileBrowserComponent::selectionChanged()
391 {
392     StringArray newFilenames;
393     bool resetChosenFiles = true;
394 
395     for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
396     {
397         const File f (fileListComponent->getSelectedFile (i));
398 
399         if (isFileOrDirSuitable (f))
400         {
401             if (resetChosenFiles)
402             {
403                 chosenFiles.clear();
404                 resetChosenFiles = false;
405             }
406 
407             chosenFiles.add (f);
408             newFilenames.add (f.getRelativePathFrom (getRoot()));
409         }
410     }
411 
412     if (newFilenames.size() > 0)
413         filenameBox.setText (newFilenames.joinIntoString (", "), false);
414 
415     sendListenerChangeMessage();
416 }
417 
fileClicked(const File & f,const MouseEvent & e)418 void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
419 {
420     Component::BailOutChecker checker (this);
421     listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
422 }
423 
fileDoubleClicked(const File & f)424 void FileBrowserComponent::fileDoubleClicked (const File& f)
425 {
426     if (f.isDirectory())
427     {
428         setRoot (f);
429 
430         if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
431             filenameBox.setText ({});
432     }
433     else
434     {
435         Component::BailOutChecker checker (this);
436         listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
437     }
438 }
439 
browserRootChanged(const File &)440 void FileBrowserComponent::browserRootChanged (const File&) {}
441 
keyPressed(const KeyPress & key)442 bool FileBrowserComponent::keyPressed (const KeyPress& key)
443 {
444    #if JUCE_LINUX || JUCE_WINDOWS
445     if (key.getModifiers().isCommandDown()
446          && (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
447     {
448         fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
449         fileList->refresh();
450         return true;
451     }
452    #endif
453 
454     ignoreUnused (key);
455     return false;
456 }
457 
458 //==============================================================================
changeFilename()459 void FileBrowserComponent::changeFilename()
460 {
461     if (filenameBox.getText().containsChar (File::getSeparatorChar()))
462     {
463         auto f = currentRoot.getChildFile (filenameBox.getText());
464 
465         if (f.isDirectory())
466         {
467             setRoot (f);
468             chosenFiles.clear();
469 
470             if ((flags & doNotClearFileNameOnRootChange) == 0)
471                 filenameBox.setText ({});
472         }
473         else
474         {
475             setRoot (f.getParentDirectory());
476             chosenFiles.clear();
477             chosenFiles.add (f);
478             filenameBox.setText (f.getFileName());
479         }
480     }
481     else
482     {
483         fileDoubleClicked (getSelectedFile (0));
484     }
485 }
486 
487 //==============================================================================
updateSelectedPath()488 void FileBrowserComponent::updateSelectedPath()
489 {
490     auto newText = currentPathBox.getText().trim().unquoted();
491 
492     if (newText.isNotEmpty())
493     {
494         auto index = currentPathBox.getSelectedId() - 1;
495 
496         StringArray rootNames, rootPaths;
497         getRoots (rootNames, rootPaths);
498 
499         if (rootPaths[index].isNotEmpty())
500         {
501             setRoot (File (rootPaths[index]));
502         }
503         else
504         {
505             File f (newText);
506 
507             for (;;)
508             {
509                 if (f.isDirectory())
510                 {
511                     setRoot (f);
512                     break;
513                 }
514 
515                 if (f.getParentDirectory() == f)
516                     break;
517 
518                 f = f.getParentDirectory();
519             }
520         }
521     }
522 }
523 
getDefaultRoots(StringArray & rootNames,StringArray & rootPaths)524 void FileBrowserComponent::getDefaultRoots (StringArray& rootNames, StringArray& rootPaths)
525 {
526    #if JUCE_WINDOWS
527     Array<File> roots;
528     File::findFileSystemRoots (roots);
529     rootPaths.clear();
530 
531     for (int i = 0; i < roots.size(); ++i)
532     {
533         const File& drive = roots.getReference(i);
534 
535         String name (drive.getFullPathName());
536         rootPaths.add (name);
537 
538         if (drive.isOnHardDisk())
539         {
540             String volume (drive.getVolumeLabel());
541 
542             if (volume.isEmpty())
543                 volume = TRANS("Hard Drive");
544 
545             name << " [" << volume << ']';
546         }
547         else if (drive.isOnCDRomDrive())
548         {
549             name << " [" << TRANS("CD/DVD drive") << ']';
550         }
551 
552         rootNames.add (name);
553     }
554 
555     rootPaths.add ({});
556     rootNames.add ({});
557 
558     rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
559     rootNames.add (TRANS("Documents"));
560     rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
561     rootNames.add (TRANS("Music"));
562     rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
563     rootNames.add (TRANS("Pictures"));
564     rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
565     rootNames.add (TRANS("Desktop"));
566 
567    #elif JUCE_MAC
568     rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
569     rootNames.add (TRANS("Home folder"));
570     rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
571     rootNames.add (TRANS("Documents"));
572     rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
573     rootNames.add (TRANS("Music"));
574     rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
575     rootNames.add (TRANS("Pictures"));
576     rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
577     rootNames.add (TRANS("Desktop"));
578 
579     rootPaths.add ({});
580     rootNames.add ({});
581 
582     for (auto& volume : File ("/Volumes").findChildFiles (File::findDirectories, false))
583     {
584         if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
585         {
586             rootPaths.add (volume.getFullPathName());
587             rootNames.add (volume.getFileName());
588         }
589     }
590 
591    #else
592     rootPaths.add ("/");
593     rootNames.add ("/");
594     rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
595     rootNames.add (TRANS("Home folder"));
596     rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
597     rootNames.add (TRANS("Desktop"));
598    #endif
599 }
600 
getRoots(StringArray & rootNames,StringArray & rootPaths)601 void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
602 {
603     getDefaultRoots (rootNames, rootPaths);
604 }
605 
timerCallback()606 void FileBrowserComponent::timerCallback()
607 {
608     const bool isProcessActive = Process::isForegroundProcess();
609 
610     if (wasProcessActive != isProcessActive)
611     {
612         wasProcessActive = isProcessActive;
613 
614         if (isProcessActive && fileList != nullptr)
615             refresh();
616     }
617 }
618 
619 } // namespace juce
620