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.

617 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2020 - 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 6 End-User License
  8. Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
  9. End User License Agreement: www.juce.com/juce-6-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. addAndMakeVisible (goUpButton.get());
  95. goUpButton->onClick = [this] { goUp(); };
  96. goUpButton->setTooltip (TRANS ("Go up to parent directory"));
  97. setRoot (currentRoot);
  98. if (filename.isNotEmpty())
  99. setFileName (filename);
  100. thread.startThread (4);
  101. startTimer (2000);
  102. }
  103. FileBrowserComponent::~FileBrowserComponent()
  104. {
  105. fileListComponent.reset();
  106. fileList.reset();
  107. thread.stopThread (10000);
  108. }
  109. //==============================================================================
  110. void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
  111. {
  112. listeners.add (newListener);
  113. }
  114. void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
  115. {
  116. listeners.remove (listener);
  117. }
  118. //==============================================================================
  119. bool FileBrowserComponent::isSaveMode() const noexcept
  120. {
  121. return (flags & saveMode) != 0;
  122. }
  123. int FileBrowserComponent::getNumSelectedFiles() const noexcept
  124. {
  125. if (chosenFiles.isEmpty() && currentFileIsValid())
  126. return 1;
  127. return chosenFiles.size();
  128. }
  129. File FileBrowserComponent::getSelectedFile (int index) const noexcept
  130. {
  131. if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
  132. return currentRoot;
  133. if (! filenameBox.isReadOnly())
  134. return currentRoot.getChildFile (filenameBox.getText());
  135. return chosenFiles[index];
  136. }
  137. bool FileBrowserComponent::currentFileIsValid() const
  138. {
  139. auto f = getSelectedFile (0);
  140. if ((flags & canSelectDirectories) == 0 && f.isDirectory())
  141. return false;
  142. return isSaveMode() || f.exists();
  143. }
  144. File FileBrowserComponent::getHighlightedFile() const noexcept
  145. {
  146. return fileListComponent->getSelectedFile (0);
  147. }
  148. void FileBrowserComponent::deselectAllFiles()
  149. {
  150. fileListComponent->deselectAllFiles();
  151. }
  152. //==============================================================================
  153. bool FileBrowserComponent::isFileSuitable (const File& file) const
  154. {
  155. return (flags & canSelectFiles) != 0
  156. && (fileFilter == nullptr || fileFilter->isFileSuitable (file));
  157. }
  158. bool FileBrowserComponent::isDirectorySuitable (const File&) const
  159. {
  160. return true;
  161. }
  162. bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
  163. {
  164. if (f.isDirectory())
  165. return (flags & canSelectDirectories) != 0
  166. && (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
  167. return (flags & canSelectFiles) != 0 && f.exists()
  168. && (fileFilter == nullptr || fileFilter->isFileSuitable (f));
  169. }
  170. //==============================================================================
  171. const File& FileBrowserComponent::getRoot() const
  172. {
  173. return currentRoot;
  174. }
  175. void FileBrowserComponent::setRoot (const File& newRootDirectory)
  176. {
  177. bool callListeners = false;
  178. if (currentRoot != newRootDirectory)
  179. {
  180. callListeners = true;
  181. fileListComponent->scrollToTop();
  182. String path (newRootDirectory.getFullPathName());
  183. if (path.isEmpty())
  184. path = File::getSeparatorString();
  185. StringArray rootNames, rootPaths;
  186. getRoots (rootNames, rootPaths);
  187. if (! rootPaths.contains (path, true))
  188. {
  189. bool alreadyListed = false;
  190. for (int i = currentPathBox.getNumItems(); --i >= 0;)
  191. {
  192. if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
  193. {
  194. alreadyListed = true;
  195. break;
  196. }
  197. }
  198. if (! alreadyListed)
  199. currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
  200. }
  201. }
  202. currentRoot = newRootDirectory;
  203. fileList->setDirectory (currentRoot, true, true);
  204. if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
  205. tree->refresh();
  206. auto currentRootName = currentRoot.getFullPathName();
  207. if (currentRootName.isEmpty())
  208. currentRootName = File::getSeparatorString();
  209. currentPathBox.setText (currentRootName, dontSendNotification);
  210. goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
  211. && currentRoot.getParentDirectory() != currentRoot);
  212. if (callListeners)
  213. {
  214. Component::BailOutChecker checker (this);
  215. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
  216. }
  217. }
  218. void FileBrowserComponent::setFileName (const String& newName)
  219. {
  220. filenameBox.setText (newName, true);
  221. fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
  222. }
  223. void FileBrowserComponent::resetRecentPaths()
  224. {
  225. currentPathBox.clear();
  226. StringArray rootNames, rootPaths;
  227. getRoots (rootNames, rootPaths);
  228. for (int i = 0; i < rootNames.size(); ++i)
  229. {
  230. if (rootNames[i].isEmpty())
  231. currentPathBox.addSeparator();
  232. else
  233. currentPathBox.addItem (rootNames[i], i + 1);
  234. }
  235. currentPathBox.addSeparator();
  236. }
  237. void FileBrowserComponent::goUp()
  238. {
  239. setRoot (getRoot().getParentDirectory());
  240. }
  241. void FileBrowserComponent::refresh()
  242. {
  243. fileList->refresh();
  244. }
  245. void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
  246. {
  247. if (fileFilter != newFileFilter)
  248. {
  249. fileFilter = newFileFilter;
  250. refresh();
  251. }
  252. }
  253. String FileBrowserComponent::getActionVerb() const
  254. {
  255. return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS("Choose")
  256. : TRANS("Save"))
  257. : TRANS("Open");
  258. }
  259. void FileBrowserComponent::setFilenameBoxLabel (const String& name)
  260. {
  261. fileLabel.setText (name, dontSendNotification);
  262. }
  263. FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
  264. {
  265. return previewComp;
  266. }
  267. DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
  268. {
  269. return fileListComponent.get();
  270. }
  271. //==============================================================================
  272. void FileBrowserComponent::resized()
  273. {
  274. getLookAndFeel()
  275. .layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
  276. &currentPathBox, &filenameBox, goUpButton.get());
  277. }
  278. //==============================================================================
  279. void FileBrowserComponent::lookAndFeelChanged()
  280. {
  281. goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
  282. currentPathBox.setColour (ComboBox::backgroundColourId, findColour (currentPathBoxBackgroundColourId));
  283. currentPathBox.setColour (ComboBox::textColourId, findColour (currentPathBoxTextColourId));
  284. currentPathBox.setColour (ComboBox::arrowColourId, findColour (currentPathBoxArrowColourId));
  285. filenameBox.setColour (TextEditor::backgroundColourId, findColour (filenameBoxBackgroundColourId));
  286. filenameBox.setColour (TextEditor::textColourId, findColour (filenameBoxTextColourId));
  287. resized();
  288. repaint();
  289. }
  290. //==============================================================================
  291. void FileBrowserComponent::sendListenerChangeMessage()
  292. {
  293. Component::BailOutChecker checker (this);
  294. if (previewComp != nullptr)
  295. previewComp->selectedFileChanged (getSelectedFile (0));
  296. // You shouldn't delete the browser when the file gets changed!
  297. jassert (! checker.shouldBailOut());
  298. listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
  299. }
  300. void FileBrowserComponent::selectionChanged()
  301. {
  302. StringArray newFilenames;
  303. bool resetChosenFiles = true;
  304. for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
  305. {
  306. const File f (fileListComponent->getSelectedFile (i));
  307. if (isFileOrDirSuitable (f))
  308. {
  309. if (resetChosenFiles)
  310. {
  311. chosenFiles.clear();
  312. resetChosenFiles = false;
  313. }
  314. chosenFiles.add (f);
  315. newFilenames.add (f.getRelativePathFrom (getRoot()));
  316. }
  317. }
  318. if (newFilenames.size() > 0)
  319. filenameBox.setText (newFilenames.joinIntoString (", "), false);
  320. sendListenerChangeMessage();
  321. }
  322. void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
  323. {
  324. Component::BailOutChecker checker (this);
  325. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
  326. }
  327. void FileBrowserComponent::fileDoubleClicked (const File& f)
  328. {
  329. if (f.isDirectory())
  330. {
  331. setRoot (f);
  332. if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
  333. filenameBox.setText ({});
  334. }
  335. else
  336. {
  337. Component::BailOutChecker checker (this);
  338. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
  339. }
  340. }
  341. void FileBrowserComponent::browserRootChanged (const File&) {}
  342. bool FileBrowserComponent::keyPressed (const KeyPress& key)
  343. {
  344. #if JUCE_LINUX || JUCE_WINDOWS
  345. if (key.getModifiers().isCommandDown()
  346. && (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
  347. {
  348. fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
  349. fileList->refresh();
  350. return true;
  351. }
  352. #endif
  353. ignoreUnused (key);
  354. return false;
  355. }
  356. //==============================================================================
  357. void FileBrowserComponent::changeFilename()
  358. {
  359. if (filenameBox.getText().containsChar (File::getSeparatorChar()))
  360. {
  361. auto f = currentRoot.getChildFile (filenameBox.getText());
  362. if (f.isDirectory())
  363. {
  364. setRoot (f);
  365. chosenFiles.clear();
  366. if ((flags & doNotClearFileNameOnRootChange) == 0)
  367. filenameBox.setText ({});
  368. }
  369. else
  370. {
  371. setRoot (f.getParentDirectory());
  372. chosenFiles.clear();
  373. chosenFiles.add (f);
  374. filenameBox.setText (f.getFileName());
  375. }
  376. }
  377. else
  378. {
  379. fileDoubleClicked (getSelectedFile (0));
  380. }
  381. }
  382. //==============================================================================
  383. void FileBrowserComponent::updateSelectedPath()
  384. {
  385. auto newText = currentPathBox.getText().trim().unquoted();
  386. if (newText.isNotEmpty())
  387. {
  388. auto index = currentPathBox.getSelectedId() - 1;
  389. StringArray rootNames, rootPaths;
  390. getRoots (rootNames, rootPaths);
  391. if (rootPaths[index].isNotEmpty())
  392. {
  393. setRoot (File (rootPaths[index]));
  394. }
  395. else
  396. {
  397. File f (newText);
  398. for (;;)
  399. {
  400. if (f.isDirectory())
  401. {
  402. setRoot (f);
  403. break;
  404. }
  405. if (f.getParentDirectory() == f)
  406. break;
  407. f = f.getParentDirectory();
  408. }
  409. }
  410. }
  411. }
  412. void FileBrowserComponent::getDefaultRoots (StringArray& rootNames, StringArray& rootPaths)
  413. {
  414. #if JUCE_WINDOWS
  415. Array<File> roots;
  416. File::findFileSystemRoots (roots);
  417. rootPaths.clear();
  418. for (int i = 0; i < roots.size(); ++i)
  419. {
  420. const File& drive = roots.getReference(i);
  421. String name (drive.getFullPathName());
  422. rootPaths.add (name);
  423. if (drive.isOnHardDisk())
  424. {
  425. String volume (drive.getVolumeLabel());
  426. if (volume.isEmpty())
  427. volume = TRANS("Hard Drive");
  428. name << " [" << volume << ']';
  429. }
  430. else if (drive.isOnCDRomDrive())
  431. {
  432. name << " [" << TRANS("CD/DVD drive") << ']';
  433. }
  434. rootNames.add (name);
  435. }
  436. rootPaths.add ({});
  437. rootNames.add ({});
  438. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  439. rootNames.add (TRANS("Documents"));
  440. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  441. rootNames.add (TRANS("Music"));
  442. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  443. rootNames.add (TRANS("Pictures"));
  444. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  445. rootNames.add (TRANS("Desktop"));
  446. #elif JUCE_MAC
  447. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  448. rootNames.add (TRANS("Home folder"));
  449. rootPaths.add (File::getSpecialLocation (File::userDocumentsDirectory).getFullPathName());
  450. rootNames.add (TRANS("Documents"));
  451. rootPaths.add (File::getSpecialLocation (File::userMusicDirectory).getFullPathName());
  452. rootNames.add (TRANS("Music"));
  453. rootPaths.add (File::getSpecialLocation (File::userPicturesDirectory).getFullPathName());
  454. rootNames.add (TRANS("Pictures"));
  455. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  456. rootNames.add (TRANS("Desktop"));
  457. rootPaths.add ({});
  458. rootNames.add ({});
  459. for (auto& volume : File ("/Volumes").findChildFiles (File::findDirectories, false))
  460. {
  461. if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
  462. {
  463. rootPaths.add (volume.getFullPathName());
  464. rootNames.add (volume.getFileName());
  465. }
  466. }
  467. #else
  468. rootPaths.add ("/");
  469. rootNames.add ("/");
  470. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  471. rootNames.add (TRANS("Home folder"));
  472. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  473. rootNames.add (TRANS("Desktop"));
  474. #endif
  475. }
  476. void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
  477. {
  478. getDefaultRoots (rootNames, rootPaths);
  479. }
  480. void FileBrowserComponent::timerCallback()
  481. {
  482. const bool isProcessActive = Process::isForegroundProcess();
  483. if (wasProcessActive != isProcessActive)
  484. {
  485. wasProcessActive = isProcessActive;
  486. if (isProcessActive && fileList != nullptr)
  487. refresh();
  488. }
  489. }
  490. } // namespace juce