The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

625 lines
19KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. namespace juce
  19. {
  20. FileBrowserComponent::FileBrowserComponent (int flags_,
  21. const File& initialFileOrDirectory,
  22. const FileFilter* fileFilter_,
  23. FilePreviewComponent* previewComp_)
  24. : FileFilter ({}),
  25. fileFilter (fileFilter_),
  26. flags (flags_),
  27. previewComp (previewComp_),
  28. currentPathBox ("path"),
  29. fileLabel ("f", TRANS ("file:")),
  30. thread ("JUCE FileBrowser"),
  31. wasProcessActive (true)
  32. {
  33. // You need to specify one or other of the open/save flags..
  34. jassert ((flags & (saveMode | openMode)) != 0);
  35. jassert ((flags & (saveMode | openMode)) != (saveMode | openMode));
  36. // You need to specify at least one of these flags..
  37. jassert ((flags & (canSelectFiles | canSelectDirectories)) != 0);
  38. String filename;
  39. if (initialFileOrDirectory == File())
  40. {
  41. currentRoot = File::getCurrentWorkingDirectory();
  42. }
  43. else if (initialFileOrDirectory.isDirectory())
  44. {
  45. currentRoot = initialFileOrDirectory;
  46. }
  47. else
  48. {
  49. chosenFiles.add (initialFileOrDirectory);
  50. currentRoot = initialFileOrDirectory.getParentDirectory();
  51. filename = initialFileOrDirectory.getFileName();
  52. }
  53. // The thread must be started before the DirectoryContentsList attempts to scan any file
  54. thread.startThread (Thread::Priority::low);
  55. fileList.reset (new DirectoryContentsList (this, thread));
  56. fileList->setDirectory (currentRoot, true, true);
  57. if ((flags & useTreeView) != 0)
  58. {
  59. auto tree = new FileTreeComponent (*fileList);
  60. fileListComponent.reset (tree);
  61. if ((flags & canSelectMultipleItems) != 0)
  62. tree->setMultiSelectEnabled (true);
  63. addAndMakeVisible (tree);
  64. }
  65. else
  66. {
  67. auto list = new FileListComponent (*fileList);
  68. fileListComponent.reset (list);
  69. list->setOutlineThickness (1);
  70. if ((flags & canSelectMultipleItems) != 0)
  71. list->setMultipleSelectionEnabled (true);
  72. addAndMakeVisible (list);
  73. }
  74. fileListComponent->addListener (this);
  75. addAndMakeVisible (currentPathBox);
  76. currentPathBox.setEditableText (true);
  77. resetRecentPaths();
  78. currentPathBox.onChange = [this] { updateSelectedPath(); };
  79. addAndMakeVisible (filenameBox);
  80. filenameBox.setMultiLine (false);
  81. filenameBox.setSelectAllWhenFocused (true);
  82. filenameBox.setText (filename, false);
  83. filenameBox.onTextChange = [this] { sendListenerChangeMessage(); };
  84. filenameBox.onReturnKey = [this] { changeFilename(); };
  85. filenameBox.onFocusLost = [this]
  86. {
  87. if (! isSaveMode())
  88. selectionChanged();
  89. };
  90. filenameBox.setReadOnly ((flags & (filenameBoxIsReadOnly | canSelectMultipleItems)) != 0);
  91. addAndMakeVisible (fileLabel);
  92. fileLabel.attachToComponent (&filenameBox, true);
  93. if (previewComp != nullptr)
  94. addAndMakeVisible (previewComp);
  95. lookAndFeelChanged();
  96. setRoot (currentRoot);
  97. if (filename.isNotEmpty())
  98. setFileName (filename);
  99. startTimer (2000);
  100. }
  101. FileBrowserComponent::~FileBrowserComponent()
  102. {
  103. fileListComponent.reset();
  104. fileList.reset();
  105. thread.stopThread (10000);
  106. }
  107. //==============================================================================
  108. void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
  109. {
  110. listeners.add (newListener);
  111. }
  112. void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
  113. {
  114. listeners.remove (listener);
  115. }
  116. //==============================================================================
  117. bool FileBrowserComponent::isSaveMode() const noexcept
  118. {
  119. return (flags & saveMode) != 0;
  120. }
  121. int FileBrowserComponent::getNumSelectedFiles() const noexcept
  122. {
  123. if (chosenFiles.isEmpty() && currentFileIsValid())
  124. return 1;
  125. return chosenFiles.size();
  126. }
  127. File FileBrowserComponent::getSelectedFile (int index) const noexcept
  128. {
  129. if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
  130. return currentRoot;
  131. if (! filenameBox.isReadOnly())
  132. return currentRoot.getChildFile (filenameBox.getText());
  133. return chosenFiles[index];
  134. }
  135. bool FileBrowserComponent::currentFileIsValid() const
  136. {
  137. auto f = getSelectedFile (0);
  138. if ((flags & canSelectDirectories) == 0 && f.isDirectory())
  139. return false;
  140. return isSaveMode() || f.exists();
  141. }
  142. File FileBrowserComponent::getHighlightedFile() const noexcept
  143. {
  144. return fileListComponent->getSelectedFile (0);
  145. }
  146. void FileBrowserComponent::deselectAllFiles()
  147. {
  148. fileListComponent->deselectAllFiles();
  149. }
  150. //==============================================================================
  151. bool FileBrowserComponent::isFileSuitable (const File& file) const
  152. {
  153. return (flags & canSelectFiles) != 0
  154. && (fileFilter == nullptr || fileFilter->isFileSuitable (file));
  155. }
  156. bool FileBrowserComponent::isDirectorySuitable (const File&) const
  157. {
  158. return true;
  159. }
  160. bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
  161. {
  162. if (f.isDirectory())
  163. return (flags & canSelectDirectories) != 0
  164. && (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
  165. return (flags & canSelectFiles) != 0 && f.exists()
  166. && (fileFilter == nullptr || fileFilter->isFileSuitable (f));
  167. }
  168. //==============================================================================
  169. const File& FileBrowserComponent::getRoot() const
  170. {
  171. return currentRoot;
  172. }
  173. void FileBrowserComponent::setRoot (const File& newRootDirectory)
  174. {
  175. bool callListeners = false;
  176. if (currentRoot != newRootDirectory)
  177. {
  178. callListeners = true;
  179. fileListComponent->scrollToTop();
  180. String path (newRootDirectory.getFullPathName());
  181. if (path.isEmpty())
  182. path = File::getSeparatorString();
  183. StringArray rootNames, rootPaths;
  184. getRoots (rootNames, rootPaths);
  185. if (! rootPaths.contains (path, true))
  186. {
  187. bool alreadyListed = false;
  188. for (int i = currentPathBox.getNumItems(); --i >= 0;)
  189. {
  190. if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
  191. {
  192. alreadyListed = true;
  193. break;
  194. }
  195. }
  196. if (! alreadyListed)
  197. currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
  198. }
  199. }
  200. currentRoot = newRootDirectory;
  201. fileList->setDirectory (currentRoot, true, true);
  202. if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
  203. tree->refresh();
  204. auto currentRootName = currentRoot.getFullPathName();
  205. if (currentRootName.isEmpty())
  206. currentRootName = File::getSeparatorString();
  207. currentPathBox.setText (currentRootName, dontSendNotification);
  208. goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
  209. && currentRoot.getParentDirectory() != currentRoot);
  210. if (callListeners)
  211. {
  212. Component::BailOutChecker checker (this);
  213. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
  214. }
  215. }
  216. void FileBrowserComponent::setFileName (const String& newName)
  217. {
  218. filenameBox.setText (newName, true);
  219. fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
  220. }
  221. void FileBrowserComponent::resetRecentPaths()
  222. {
  223. currentPathBox.clear();
  224. StringArray rootNames, rootPaths;
  225. getRoots (rootNames, rootPaths);
  226. for (int i = 0; i < rootNames.size(); ++i)
  227. {
  228. if (rootNames[i].isEmpty())
  229. currentPathBox.addSeparator();
  230. else
  231. currentPathBox.addItem (rootNames[i], i + 1);
  232. }
  233. currentPathBox.addSeparator();
  234. }
  235. void FileBrowserComponent::goUp()
  236. {
  237. setRoot (getRoot().getParentDirectory());
  238. }
  239. void FileBrowserComponent::refresh()
  240. {
  241. fileList->refresh();
  242. }
  243. void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
  244. {
  245. if (fileFilter != newFileFilter)
  246. {
  247. fileFilter = newFileFilter;
  248. refresh();
  249. }
  250. }
  251. String FileBrowserComponent::getActionVerb() const
  252. {
  253. return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS ("Choose")
  254. : TRANS ("Save"))
  255. : TRANS ("Open");
  256. }
  257. void FileBrowserComponent::setFilenameBoxLabel (const String& name)
  258. {
  259. fileLabel.setText (name, dontSendNotification);
  260. }
  261. FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
  262. {
  263. return previewComp;
  264. }
  265. DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
  266. {
  267. return fileListComponent.get();
  268. }
  269. //==============================================================================
  270. void FileBrowserComponent::resized()
  271. {
  272. getLookAndFeel()
  273. .layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
  274. &currentPathBox, &filenameBox, goUpButton.get());
  275. }
  276. //==============================================================================
  277. void FileBrowserComponent::lookAndFeelChanged()
  278. {
  279. goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
  280. if (auto* buttonPtr = goUpButton.get())
  281. {
  282. addAndMakeVisible (*buttonPtr);
  283. buttonPtr->onClick = [this] { goUp(); };
  284. buttonPtr->setTooltip (TRANS ("Go up to parent directory"));
  285. }
  286. currentPathBox.setColour (ComboBox::backgroundColourId, findColour (currentPathBoxBackgroundColourId));
  287. currentPathBox.setColour (ComboBox::textColourId, findColour (currentPathBoxTextColourId));
  288. currentPathBox.setColour (ComboBox::arrowColourId, findColour (currentPathBoxArrowColourId));
  289. filenameBox.setColour (TextEditor::backgroundColourId, findColour (filenameBoxBackgroundColourId));
  290. filenameBox.applyColourToAllText (findColour (filenameBoxTextColourId));
  291. resized();
  292. }
  293. //==============================================================================
  294. void FileBrowserComponent::sendListenerChangeMessage()
  295. {
  296. Component::BailOutChecker checker (this);
  297. if (previewComp != nullptr)
  298. previewComp->selectedFileChanged (getSelectedFile (0));
  299. // You shouldn't delete the browser when the file gets changed!
  300. jassert (! checker.shouldBailOut());
  301. listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
  302. }
  303. void FileBrowserComponent::selectionChanged()
  304. {
  305. StringArray newFilenames;
  306. bool resetChosenFiles = true;
  307. for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
  308. {
  309. const File f (fileListComponent->getSelectedFile (i));
  310. if (isFileOrDirSuitable (f))
  311. {
  312. if (resetChosenFiles)
  313. {
  314. chosenFiles.clear();
  315. resetChosenFiles = false;
  316. }
  317. chosenFiles.add (f);
  318. newFilenames.add (f.getRelativePathFrom (getRoot()));
  319. }
  320. }
  321. if (newFilenames.size() > 0)
  322. filenameBox.setText (newFilenames.joinIntoString (", "), false);
  323. sendListenerChangeMessage();
  324. }
  325. void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
  326. {
  327. Component::BailOutChecker checker (this);
  328. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
  329. }
  330. void FileBrowserComponent::fileDoubleClicked (const File& f)
  331. {
  332. if (f.isDirectory())
  333. {
  334. setRoot (f);
  335. if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
  336. filenameBox.setText ({});
  337. }
  338. else
  339. {
  340. Component::BailOutChecker checker (this);
  341. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
  342. }
  343. }
  344. void FileBrowserComponent::browserRootChanged (const File&) {}
  345. bool FileBrowserComponent::keyPressed ([[maybe_unused]] const KeyPress& key)
  346. {
  347. #if JUCE_LINUX || JUCE_BSD || JUCE_WINDOWS
  348. if (key.getModifiers().isCommandDown()
  349. && (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
  350. {
  351. fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
  352. fileList->refresh();
  353. return true;
  354. }
  355. #endif
  356. return false;
  357. }
  358. //==============================================================================
  359. void FileBrowserComponent::changeFilename()
  360. {
  361. if (filenameBox.getText().containsChar (File::getSeparatorChar()))
  362. {
  363. auto f = currentRoot.getChildFile (filenameBox.getText());
  364. if (f.isDirectory())
  365. {
  366. setRoot (f);
  367. chosenFiles.clear();
  368. if ((flags & doNotClearFileNameOnRootChange) == 0)
  369. filenameBox.setText ({});
  370. }
  371. else
  372. {
  373. setRoot (f.getParentDirectory());
  374. chosenFiles.clear();
  375. chosenFiles.add (f);
  376. filenameBox.setText (f.getFileName());
  377. }
  378. }
  379. else
  380. {
  381. fileDoubleClicked (getSelectedFile (0));
  382. }
  383. }
  384. //==============================================================================
  385. void FileBrowserComponent::updateSelectedPath()
  386. {
  387. auto newText = currentPathBox.getText().trim().unquoted();
  388. if (newText.isNotEmpty())
  389. {
  390. auto index = currentPathBox.getSelectedId() - 1;
  391. StringArray rootNames, rootPaths;
  392. getRoots (rootNames, rootPaths);
  393. if (rootPaths[index].isNotEmpty())
  394. {
  395. setRoot (File (rootPaths[index]));
  396. }
  397. else
  398. {
  399. File f (newText);
  400. for (;;)
  401. {
  402. if (f.isDirectory())
  403. {
  404. setRoot (f);
  405. break;
  406. }
  407. if (f.getParentDirectory() == f)
  408. break;
  409. f = f.getParentDirectory();
  410. }
  411. }
  412. }
  413. }
  414. void FileBrowserComponent::getDefaultRoots (StringArray& rootNames, StringArray& rootPaths)
  415. {
  416. #if JUCE_WINDOWS
  417. Array<File> roots;
  418. File::findFileSystemRoots (roots);
  419. rootPaths.clear();
  420. for (int i = 0; i < roots.size(); ++i)
  421. {
  422. const File& drive = roots.getReference (i);
  423. String name (drive.getFullPathName());
  424. rootPaths.add (name);
  425. if (drive.isOnHardDisk())
  426. {
  427. String volume (drive.getVolumeLabel());
  428. if (volume.isEmpty())
  429. volume = TRANS ("Hard Drive");
  430. name << " [" << volume << ']';
  431. }
  432. else if (drive.isOnCDRomDrive())
  433. {
  434. name << " [" << TRANS ("CD/DVD drive") << ']';
  435. }
  436. rootNames.add (name);
  437. }
  438. rootPaths.add ({});
  439. rootNames.add ({});
  440. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  441. rootNames.add (TRANS ("Documents"));
  442. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  443. rootNames.add (TRANS ("Music"));
  444. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  445. rootNames.add (TRANS ("Pictures"));
  446. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  447. rootNames.add (TRANS ("Desktop"));
  448. #elif JUCE_MAC
  449. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  450. rootNames.add (TRANS ("Home folder"));
  451. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  452. rootNames.add (TRANS ("Documents"));
  453. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  454. rootNames.add (TRANS ("Music"));
  455. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  456. rootNames.add (TRANS ("Pictures"));
  457. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  458. rootNames.add (TRANS ("Desktop"));
  459. rootPaths.add ({});
  460. rootNames.add ({});
  461. for (auto& volume : File ("/Volumes").findChildFiles (File::findDirectories, false))
  462. {
  463. if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
  464. {
  465. rootPaths.add (volume.getFullPathName());
  466. rootNames.add (volume.getFileName());
  467. }
  468. }
  469. #else
  470. rootPaths.add ("/");
  471. rootNames.add ("/");
  472. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  473. rootNames.add (TRANS ("Home folder"));
  474. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  475. rootNames.add (TRANS ("Desktop"));
  476. #endif
  477. }
  478. void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
  479. {
  480. getDefaultRoots (rootNames, rootPaths);
  481. }
  482. void FileBrowserComponent::timerCallback()
  483. {
  484. const auto isProcessActive = detail::WindowingHelpers::isForegroundOrEmbeddedProcess (this);
  485. if (wasProcessActive != isProcessActive)
  486. {
  487. wasProcessActive = isProcessActive;
  488. if (isProcessActive && fileList != nullptr)
  489. refresh();
  490. }
  491. }
  492. //==============================================================================
  493. std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler()
  494. {
  495. return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
  496. }
  497. } // namespace juce