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.

626 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. repaint();
  293. }
  294. //==============================================================================
  295. void FileBrowserComponent::sendListenerChangeMessage()
  296. {
  297. Component::BailOutChecker checker (this);
  298. if (previewComp != nullptr)
  299. previewComp->selectedFileChanged (getSelectedFile (0));
  300. // You shouldn't delete the browser when the file gets changed!
  301. jassert (! checker.shouldBailOut());
  302. listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
  303. }
  304. void FileBrowserComponent::selectionChanged()
  305. {
  306. StringArray newFilenames;
  307. bool resetChosenFiles = true;
  308. for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
  309. {
  310. const File f (fileListComponent->getSelectedFile (i));
  311. if (isFileOrDirSuitable (f))
  312. {
  313. if (resetChosenFiles)
  314. {
  315. chosenFiles.clear();
  316. resetChosenFiles = false;
  317. }
  318. chosenFiles.add (f);
  319. newFilenames.add (f.getRelativePathFrom (getRoot()));
  320. }
  321. }
  322. if (newFilenames.size() > 0)
  323. filenameBox.setText (newFilenames.joinIntoString (", "), false);
  324. sendListenerChangeMessage();
  325. }
  326. void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
  327. {
  328. Component::BailOutChecker checker (this);
  329. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
  330. }
  331. void FileBrowserComponent::fileDoubleClicked (const File& f)
  332. {
  333. if (f.isDirectory())
  334. {
  335. setRoot (f);
  336. if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
  337. filenameBox.setText ({});
  338. }
  339. else
  340. {
  341. Component::BailOutChecker checker (this);
  342. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
  343. }
  344. }
  345. void FileBrowserComponent::browserRootChanged (const File&) {}
  346. bool FileBrowserComponent::keyPressed ([[maybe_unused]] const KeyPress& key)
  347. {
  348. #if JUCE_LINUX || JUCE_BSD || JUCE_WINDOWS
  349. if (key.getModifiers().isCommandDown()
  350. && (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
  351. {
  352. fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
  353. fileList->refresh();
  354. return true;
  355. }
  356. #endif
  357. return false;
  358. }
  359. //==============================================================================
  360. void FileBrowserComponent::changeFilename()
  361. {
  362. if (filenameBox.getText().containsChar (File::getSeparatorChar()))
  363. {
  364. auto f = currentRoot.getChildFile (filenameBox.getText());
  365. if (f.isDirectory())
  366. {
  367. setRoot (f);
  368. chosenFiles.clear();
  369. if ((flags & doNotClearFileNameOnRootChange) == 0)
  370. filenameBox.setText ({});
  371. }
  372. else
  373. {
  374. setRoot (f.getParentDirectory());
  375. chosenFiles.clear();
  376. chosenFiles.add (f);
  377. filenameBox.setText (f.getFileName());
  378. }
  379. }
  380. else
  381. {
  382. fileDoubleClicked (getSelectedFile (0));
  383. }
  384. }
  385. //==============================================================================
  386. void FileBrowserComponent::updateSelectedPath()
  387. {
  388. auto newText = currentPathBox.getText().trim().unquoted();
  389. if (newText.isNotEmpty())
  390. {
  391. auto index = currentPathBox.getSelectedId() - 1;
  392. StringArray rootNames, rootPaths;
  393. getRoots (rootNames, rootPaths);
  394. if (rootPaths[index].isNotEmpty())
  395. {
  396. setRoot (File (rootPaths[index]));
  397. }
  398. else
  399. {
  400. File f (newText);
  401. for (;;)
  402. {
  403. if (f.isDirectory())
  404. {
  405. setRoot (f);
  406. break;
  407. }
  408. if (f.getParentDirectory() == f)
  409. break;
  410. f = f.getParentDirectory();
  411. }
  412. }
  413. }
  414. }
  415. void FileBrowserComponent::getDefaultRoots (StringArray& rootNames, StringArray& rootPaths)
  416. {
  417. #if JUCE_WINDOWS
  418. Array<File> roots;
  419. File::findFileSystemRoots (roots);
  420. rootPaths.clear();
  421. for (int i = 0; i < roots.size(); ++i)
  422. {
  423. const File& drive = roots.getReference(i);
  424. String name (drive.getFullPathName());
  425. rootPaths.add (name);
  426. if (drive.isOnHardDisk())
  427. {
  428. String volume (drive.getVolumeLabel());
  429. if (volume.isEmpty())
  430. volume = TRANS("Hard Drive");
  431. name << " [" << volume << ']';
  432. }
  433. else if (drive.isOnCDRomDrive())
  434. {
  435. name << " [" << TRANS("CD/DVD drive") << ']';
  436. }
  437. rootNames.add (name);
  438. }
  439. rootPaths.add ({});
  440. rootNames.add ({});
  441. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  442. rootNames.add (TRANS("Documents"));
  443. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  444. rootNames.add (TRANS("Music"));
  445. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  446. rootNames.add (TRANS("Pictures"));
  447. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  448. rootNames.add (TRANS("Desktop"));
  449. #elif JUCE_MAC
  450. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  451. rootNames.add (TRANS("Home folder"));
  452. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  453. rootNames.add (TRANS("Documents"));
  454. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  455. rootNames.add (TRANS("Music"));
  456. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  457. rootNames.add (TRANS("Pictures"));
  458. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  459. rootNames.add (TRANS("Desktop"));
  460. rootPaths.add ({});
  461. rootNames.add ({});
  462. for (auto& volume : File ("/Volumes").findChildFiles (File::findDirectories, false))
  463. {
  464. if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
  465. {
  466. rootPaths.add (volume.getFullPathName());
  467. rootNames.add (volume.getFileName());
  468. }
  469. }
  470. #else
  471. rootPaths.add ("/");
  472. rootNames.add ("/");
  473. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  474. rootNames.add (TRANS("Home folder"));
  475. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  476. rootNames.add (TRANS("Desktop"));
  477. #endif
  478. }
  479. void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
  480. {
  481. getDefaultRoots (rootNames, rootPaths);
  482. }
  483. void FileBrowserComponent::timerCallback()
  484. {
  485. const auto isProcessActive = isForegroundOrEmbeddedProcess (this);
  486. if (wasProcessActive != isProcessActive)
  487. {
  488. wasProcessActive = isProcessActive;
  489. if (isProcessActive && fileList != nullptr)
  490. refresh();
  491. }
  492. }
  493. //==============================================================================
  494. std::unique_ptr<AccessibilityHandler> FileBrowserComponent::createAccessibilityHandler()
  495. {
  496. return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
  497. }
  498. } // namespace juce