Audio plugin host https://kx.studio/carla
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
18KB

  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. fileList.reset (new DirectoryContentsList (this, thread));
  54. fileList->setDirectory (currentRoot, true, true);
  55. if ((flags & useTreeView) != 0)
  56. {
  57. auto tree = new FileTreeComponent (*fileList);
  58. fileListComponent.reset (tree);
  59. if ((flags & canSelectMultipleItems) != 0)
  60. tree->setMultiSelectEnabled (true);
  61. addAndMakeVisible (tree);
  62. }
  63. else
  64. {
  65. auto list = new FileListComponent (*fileList);
  66. fileListComponent.reset (list);
  67. list->setOutlineThickness (1);
  68. if ((flags & canSelectMultipleItems) != 0)
  69. list->setMultipleSelectionEnabled (true);
  70. addAndMakeVisible (list);
  71. }
  72. fileListComponent->addListener (this);
  73. addAndMakeVisible (currentPathBox);
  74. currentPathBox.setEditableText (true);
  75. resetRecentPaths();
  76. currentPathBox.onChange = [this] { updateSelectedPath(); };
  77. addAndMakeVisible (filenameBox);
  78. filenameBox.setMultiLine (false);
  79. filenameBox.setSelectAllWhenFocused (true);
  80. filenameBox.setText (filename, false);
  81. filenameBox.onTextChange = [this] { sendListenerChangeMessage(); };
  82. filenameBox.onReturnKey = [this] { changeFilename(); };
  83. filenameBox.onFocusLost = [this]
  84. {
  85. if (! isSaveMode())
  86. selectionChanged();
  87. };
  88. filenameBox.setReadOnly ((flags & (filenameBoxIsReadOnly | canSelectMultipleItems)) != 0);
  89. addAndMakeVisible (fileLabel);
  90. fileLabel.attachToComponent (&filenameBox, true);
  91. if (previewComp != nullptr)
  92. addAndMakeVisible (previewComp);
  93. lookAndFeelChanged();
  94. setRoot (currentRoot);
  95. if (filename.isNotEmpty())
  96. setFileName (filename);
  97. thread.startThread (4);
  98. startTimer (2000);
  99. }
  100. FileBrowserComponent::~FileBrowserComponent()
  101. {
  102. fileListComponent.reset();
  103. fileList.reset();
  104. thread.stopThread (10000);
  105. }
  106. //==============================================================================
  107. void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
  108. {
  109. listeners.add (newListener);
  110. }
  111. void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
  112. {
  113. listeners.remove (listener);
  114. }
  115. //==============================================================================
  116. bool FileBrowserComponent::isSaveMode() const noexcept
  117. {
  118. return (flags & saveMode) != 0;
  119. }
  120. int FileBrowserComponent::getNumSelectedFiles() const noexcept
  121. {
  122. if (chosenFiles.isEmpty() && currentFileIsValid())
  123. return 1;
  124. return chosenFiles.size();
  125. }
  126. File FileBrowserComponent::getSelectedFile (int index) const noexcept
  127. {
  128. if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
  129. return currentRoot;
  130. if (! filenameBox.isReadOnly())
  131. return currentRoot.getChildFile (filenameBox.getText());
  132. return chosenFiles[index];
  133. }
  134. bool FileBrowserComponent::currentFileIsValid() const
  135. {
  136. auto f = getSelectedFile (0);
  137. if ((flags & canSelectDirectories) == 0 && f.isDirectory())
  138. return false;
  139. return isSaveMode() || f.exists();
  140. }
  141. File FileBrowserComponent::getHighlightedFile() const noexcept
  142. {
  143. return fileListComponent->getSelectedFile (0);
  144. }
  145. void FileBrowserComponent::deselectAllFiles()
  146. {
  147. fileListComponent->deselectAllFiles();
  148. }
  149. //==============================================================================
  150. bool FileBrowserComponent::isFileSuitable (const File& file) const
  151. {
  152. return (flags & canSelectFiles) != 0
  153. && (fileFilter == nullptr || fileFilter->isFileSuitable (file));
  154. }
  155. bool FileBrowserComponent::isDirectorySuitable (const File&) const
  156. {
  157. return true;
  158. }
  159. bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
  160. {
  161. if (f.isDirectory())
  162. return (flags & canSelectDirectories) != 0
  163. && (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
  164. return (flags & canSelectFiles) != 0 && f.exists()
  165. && (fileFilter == nullptr || fileFilter->isFileSuitable (f));
  166. }
  167. //==============================================================================
  168. const File& FileBrowserComponent::getRoot() const
  169. {
  170. return currentRoot;
  171. }
  172. void FileBrowserComponent::setRoot (const File& newRootDirectory)
  173. {
  174. bool callListeners = false;
  175. if (currentRoot != newRootDirectory)
  176. {
  177. callListeners = true;
  178. fileListComponent->scrollToTop();
  179. String path (newRootDirectory.getFullPathName());
  180. if (path.isEmpty())
  181. path = File::getSeparatorString();
  182. StringArray rootNames, rootPaths;
  183. getRoots (rootNames, rootPaths);
  184. if (! rootPaths.contains (path, true))
  185. {
  186. bool alreadyListed = false;
  187. for (int i = currentPathBox.getNumItems(); --i >= 0;)
  188. {
  189. if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
  190. {
  191. alreadyListed = true;
  192. break;
  193. }
  194. }
  195. if (! alreadyListed)
  196. currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
  197. }
  198. }
  199. currentRoot = newRootDirectory;
  200. fileList->setDirectory (currentRoot, true, true);
  201. if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
  202. tree->refresh();
  203. auto currentRootName = currentRoot.getFullPathName();
  204. if (currentRootName.isEmpty())
  205. currentRootName = File::getSeparatorString();
  206. currentPathBox.setText (currentRootName, dontSendNotification);
  207. goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
  208. && currentRoot.getParentDirectory() != currentRoot);
  209. if (callListeners)
  210. {
  211. Component::BailOutChecker checker (this);
  212. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
  213. }
  214. }
  215. void FileBrowserComponent::setFileName (const String& newName)
  216. {
  217. filenameBox.setText (newName, true);
  218. fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
  219. }
  220. void FileBrowserComponent::resetRecentPaths()
  221. {
  222. currentPathBox.clear();
  223. StringArray rootNames, rootPaths;
  224. getRoots (rootNames, rootPaths);
  225. for (int i = 0; i < rootNames.size(); ++i)
  226. {
  227. if (rootNames[i].isEmpty())
  228. currentPathBox.addSeparator();
  229. else
  230. currentPathBox.addItem (rootNames[i], i + 1);
  231. }
  232. currentPathBox.addSeparator();
  233. }
  234. void FileBrowserComponent::goUp()
  235. {
  236. setRoot (getRoot().getParentDirectory());
  237. }
  238. void FileBrowserComponent::refresh()
  239. {
  240. fileList->refresh();
  241. }
  242. void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
  243. {
  244. if (fileFilter != newFileFilter)
  245. {
  246. fileFilter = newFileFilter;
  247. refresh();
  248. }
  249. }
  250. String FileBrowserComponent::getActionVerb() const
  251. {
  252. return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS("Choose")
  253. : TRANS("Save"))
  254. : TRANS("Open");
  255. }
  256. void FileBrowserComponent::setFilenameBoxLabel (const String& name)
  257. {
  258. fileLabel.setText (name, dontSendNotification);
  259. }
  260. FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
  261. {
  262. return previewComp;
  263. }
  264. DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
  265. {
  266. return fileListComponent.get();
  267. }
  268. //==============================================================================
  269. void FileBrowserComponent::resized()
  270. {
  271. getLookAndFeel()
  272. .layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
  273. &currentPathBox, &filenameBox, goUpButton.get());
  274. }
  275. //==============================================================================
  276. void FileBrowserComponent::lookAndFeelChanged()
  277. {
  278. goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
  279. if (auto* buttonPtr = goUpButton.get())
  280. {
  281. addAndMakeVisible (*buttonPtr);
  282. buttonPtr->onClick = [this] { goUp(); };
  283. buttonPtr->setTooltip (TRANS ("Go up to parent directory"));
  284. }
  285. currentPathBox.setColour (ComboBox::backgroundColourId, findColour (currentPathBoxBackgroundColourId));
  286. currentPathBox.setColour (ComboBox::textColourId, findColour (currentPathBoxTextColourId));
  287. currentPathBox.setColour (ComboBox::arrowColourId, findColour (currentPathBoxArrowColourId));
  288. filenameBox.setColour (TextEditor::backgroundColourId, findColour (filenameBoxBackgroundColourId));
  289. filenameBox.applyColourToAllText (findColour (filenameBoxTextColourId));
  290. resized();
  291. repaint();
  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 (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. ignoreUnused (key);
  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