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 ¤tPathBox, &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