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.

624 lines
18KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  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 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. namespace juce
  20. {
  21. FileBrowserComponent::FileBrowserComponent (int flags_,
  22. const File& initialFileOrDirectory,
  23. const FileFilter* fileFilter_,
  24. FilePreviewComponent* previewComp_)
  25. : FileFilter ({}),
  26. fileFilter (fileFilter_),
  27. flags (flags_),
  28. previewComp (previewComp_),
  29. currentPathBox ("path"),
  30. fileLabel ("f", TRANS ("file:")),
  31. thread ("JUCE FileBrowser"),
  32. wasProcessActive (true)
  33. {
  34. // You need to specify one or other of the open/save flags..
  35. jassert ((flags & (saveMode | openMode)) != 0);
  36. jassert ((flags & (saveMode | openMode)) != (saveMode | openMode));
  37. // You need to specify at least one of these flags..
  38. jassert ((flags & (canSelectFiles | canSelectDirectories)) != 0);
  39. String filename;
  40. if (initialFileOrDirectory == File())
  41. {
  42. currentRoot = File::getCurrentWorkingDirectory();
  43. }
  44. else if (initialFileOrDirectory.isDirectory())
  45. {
  46. currentRoot = initialFileOrDirectory;
  47. }
  48. else
  49. {
  50. chosenFiles.add (initialFileOrDirectory);
  51. currentRoot = initialFileOrDirectory.getParentDirectory();
  52. filename = initialFileOrDirectory.getFileName();
  53. }
  54. fileList.reset (new DirectoryContentsList (this, thread));
  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.addListener (this);
  82. filenameBox.setReadOnly ((flags & (filenameBoxIsReadOnly | canSelectMultipleItems)) != 0);
  83. addAndMakeVisible (fileLabel);
  84. fileLabel.attachToComponent (&filenameBox, true);
  85. goUpButton.reset (getLookAndFeel().createFileBrowserGoUpButton());
  86. addAndMakeVisible (goUpButton.get());
  87. goUpButton->onClick = [this] { goUp(); };
  88. goUpButton->setTooltip (TRANS ("Go up to parent directory"));
  89. if (previewComp != nullptr)
  90. addAndMakeVisible (previewComp);
  91. lookAndFeelChanged();
  92. setRoot (currentRoot);
  93. thread.startThread (4);
  94. startTimer (2000);
  95. }
  96. FileBrowserComponent::~FileBrowserComponent()
  97. {
  98. fileListComponent.reset();
  99. fileList.reset();
  100. thread.stopThread (10000);
  101. }
  102. //==============================================================================
  103. void FileBrowserComponent::addListener (FileBrowserListener* const newListener)
  104. {
  105. listeners.add (newListener);
  106. }
  107. void FileBrowserComponent::removeListener (FileBrowserListener* const listener)
  108. {
  109. listeners.remove (listener);
  110. }
  111. //==============================================================================
  112. bool FileBrowserComponent::isSaveMode() const noexcept
  113. {
  114. return (flags & saveMode) != 0;
  115. }
  116. int FileBrowserComponent::getNumSelectedFiles() const noexcept
  117. {
  118. if (chosenFiles.isEmpty() && currentFileIsValid())
  119. return 1;
  120. return chosenFiles.size();
  121. }
  122. File FileBrowserComponent::getSelectedFile (int index) const noexcept
  123. {
  124. if ((flags & canSelectDirectories) != 0 && filenameBox.getText().isEmpty())
  125. return currentRoot;
  126. if (! filenameBox.isReadOnly())
  127. return currentRoot.getChildFile (filenameBox.getText());
  128. return chosenFiles[index];
  129. }
  130. bool FileBrowserComponent::currentFileIsValid() const
  131. {
  132. auto f = getSelectedFile (0);
  133. if (isSaveMode())
  134. return (flags & canSelectDirectories) != 0 || ! f.isDirectory();
  135. return f.exists();
  136. }
  137. File FileBrowserComponent::getHighlightedFile() const noexcept
  138. {
  139. return fileListComponent->getSelectedFile (0);
  140. }
  141. void FileBrowserComponent::deselectAllFiles()
  142. {
  143. fileListComponent->deselectAllFiles();
  144. }
  145. //==============================================================================
  146. bool FileBrowserComponent::isFileSuitable (const File& file) const
  147. {
  148. return (flags & canSelectFiles) != 0
  149. && (fileFilter == nullptr || fileFilter->isFileSuitable (file));
  150. }
  151. bool FileBrowserComponent::isDirectorySuitable (const File&) const
  152. {
  153. return true;
  154. }
  155. bool FileBrowserComponent::isFileOrDirSuitable (const File& f) const
  156. {
  157. if (f.isDirectory())
  158. return (flags & canSelectDirectories) != 0
  159. && (fileFilter == nullptr || fileFilter->isDirectorySuitable (f));
  160. return (flags & canSelectFiles) != 0 && f.exists()
  161. && (fileFilter == nullptr || fileFilter->isFileSuitable (f));
  162. }
  163. //==============================================================================
  164. const File& FileBrowserComponent::getRoot() const
  165. {
  166. return currentRoot;
  167. }
  168. void FileBrowserComponent::setRoot (const File& newRootDirectory)
  169. {
  170. bool callListeners = false;
  171. if (currentRoot != newRootDirectory)
  172. {
  173. callListeners = true;
  174. fileListComponent->scrollToTop();
  175. String path (newRootDirectory.getFullPathName());
  176. if (path.isEmpty())
  177. path = File::getSeparatorString();
  178. StringArray rootNames, rootPaths;
  179. getRoots (rootNames, rootPaths);
  180. if (! rootPaths.contains (path, true))
  181. {
  182. bool alreadyListed = false;
  183. for (int i = currentPathBox.getNumItems(); --i >= 0;)
  184. {
  185. if (currentPathBox.getItemText (i).equalsIgnoreCase (path))
  186. {
  187. alreadyListed = true;
  188. break;
  189. }
  190. }
  191. if (! alreadyListed)
  192. currentPathBox.addItem (path, currentPathBox.getNumItems() + 2);
  193. }
  194. }
  195. currentRoot = newRootDirectory;
  196. fileList->setDirectory (currentRoot, true, true);
  197. if (auto* tree = dynamic_cast<FileTreeComponent*> (fileListComponent.get()))
  198. tree->refresh();
  199. auto currentRootName = currentRoot.getFullPathName();
  200. if (currentRootName.isEmpty())
  201. currentRootName = File::getSeparatorString();
  202. currentPathBox.setText (currentRootName, dontSendNotification);
  203. goUpButton->setEnabled (currentRoot.getParentDirectory().isDirectory()
  204. && currentRoot.getParentDirectory() != currentRoot);
  205. if (callListeners)
  206. {
  207. Component::BailOutChecker checker (this);
  208. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.browserRootChanged (currentRoot); });
  209. }
  210. }
  211. void FileBrowserComponent::setFileName (const String& newName)
  212. {
  213. filenameBox.setText (newName, true);
  214. fileListComponent->setSelectedFile (currentRoot.getChildFile (newName));
  215. }
  216. void FileBrowserComponent::resetRecentPaths()
  217. {
  218. currentPathBox.clear();
  219. StringArray rootNames, rootPaths;
  220. getRoots (rootNames, rootPaths);
  221. for (int i = 0; i < rootNames.size(); ++i)
  222. {
  223. if (rootNames[i].isEmpty())
  224. currentPathBox.addSeparator();
  225. else
  226. currentPathBox.addItem (rootNames[i], i + 1);
  227. }
  228. currentPathBox.addSeparator();
  229. }
  230. void FileBrowserComponent::goUp()
  231. {
  232. setRoot (getRoot().getParentDirectory());
  233. }
  234. void FileBrowserComponent::refresh()
  235. {
  236. fileList->refresh();
  237. }
  238. void FileBrowserComponent::setFileFilter (const FileFilter* const newFileFilter)
  239. {
  240. if (fileFilter != newFileFilter)
  241. {
  242. fileFilter = newFileFilter;
  243. refresh();
  244. }
  245. }
  246. String FileBrowserComponent::getActionVerb() const
  247. {
  248. return isSaveMode() ? ((flags & canSelectDirectories) != 0 ? TRANS("Choose")
  249. : TRANS("Save"))
  250. : TRANS("Open");
  251. }
  252. void FileBrowserComponent::setFilenameBoxLabel (const String& name)
  253. {
  254. fileLabel.setText (name, dontSendNotification);
  255. }
  256. FilePreviewComponent* FileBrowserComponent::getPreviewComponent() const noexcept
  257. {
  258. return previewComp;
  259. }
  260. DirectoryContentsDisplayComponent* FileBrowserComponent::getDisplayComponent() const noexcept
  261. {
  262. return fileListComponent.get();
  263. }
  264. //==============================================================================
  265. void FileBrowserComponent::resized()
  266. {
  267. getLookAndFeel()
  268. .layoutFileBrowserComponent (*this, fileListComponent.get(), previewComp,
  269. &currentPathBox, &filenameBox, goUpButton.get());
  270. }
  271. //==============================================================================
  272. void FileBrowserComponent::lookAndFeelChanged()
  273. {
  274. currentPathBox.setColour (ComboBox::backgroundColourId, findColour (currentPathBoxBackgroundColourId));
  275. currentPathBox.setColour (ComboBox::textColourId, findColour (currentPathBoxTextColourId));
  276. currentPathBox.setColour (ComboBox::arrowColourId, findColour (currentPathBoxArrowColourId));
  277. filenameBox.setColour (TextEditor::backgroundColourId, findColour (filenameBoxBackgroundColourId));
  278. filenameBox.setColour (TextEditor::textColourId, findColour (filenameBoxTextColourId));
  279. }
  280. //==============================================================================
  281. void FileBrowserComponent::sendListenerChangeMessage()
  282. {
  283. Component::BailOutChecker checker (this);
  284. if (previewComp != nullptr)
  285. previewComp->selectedFileChanged (getSelectedFile (0));
  286. // You shouldn't delete the browser when the file gets changed!
  287. jassert (! checker.shouldBailOut());
  288. listeners.callChecked (checker, [] (FileBrowserListener& l) { l.selectionChanged(); });
  289. }
  290. void FileBrowserComponent::selectionChanged()
  291. {
  292. StringArray newFilenames;
  293. bool resetChosenFiles = true;
  294. for (int i = 0; i < fileListComponent->getNumSelectedFiles(); ++i)
  295. {
  296. const File f (fileListComponent->getSelectedFile (i));
  297. if (isFileOrDirSuitable (f))
  298. {
  299. if (resetChosenFiles)
  300. {
  301. chosenFiles.clear();
  302. resetChosenFiles = false;
  303. }
  304. chosenFiles.add (f);
  305. newFilenames.add (f.getRelativePathFrom (getRoot()));
  306. }
  307. }
  308. if (newFilenames.size() > 0)
  309. filenameBox.setText (newFilenames.joinIntoString (", "), false);
  310. sendListenerChangeMessage();
  311. }
  312. void FileBrowserComponent::fileClicked (const File& f, const MouseEvent& e)
  313. {
  314. Component::BailOutChecker checker (this);
  315. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileClicked (f, e); });
  316. }
  317. void FileBrowserComponent::fileDoubleClicked (const File& f)
  318. {
  319. if (f.isDirectory())
  320. {
  321. setRoot (f);
  322. if ((flags & canSelectDirectories) != 0 && (flags & doNotClearFileNameOnRootChange) == 0)
  323. filenameBox.setText ({});
  324. }
  325. else
  326. {
  327. Component::BailOutChecker checker (this);
  328. listeners.callChecked (checker, [&] (FileBrowserListener& l) { l.fileDoubleClicked (f); });
  329. }
  330. }
  331. void FileBrowserComponent::browserRootChanged (const File&) {}
  332. bool FileBrowserComponent::keyPressed (const KeyPress& key)
  333. {
  334. #if JUCE_LINUX || JUCE_WINDOWS
  335. if (key.getModifiers().isCommandDown()
  336. && (key.getKeyCode() == 'H' || key.getKeyCode() == 'h'))
  337. {
  338. fileList->setIgnoresHiddenFiles (! fileList->ignoresHiddenFiles());
  339. fileList->refresh();
  340. return true;
  341. }
  342. #endif
  343. ignoreUnused (key);
  344. return false;
  345. }
  346. //==============================================================================
  347. void FileBrowserComponent::textEditorTextChanged (TextEditor&)
  348. {
  349. sendListenerChangeMessage();
  350. }
  351. void FileBrowserComponent::textEditorReturnKeyPressed (TextEditor&)
  352. {
  353. if (filenameBox.getText().containsChar (File::getSeparatorChar()))
  354. {
  355. auto f = currentRoot.getChildFile (filenameBox.getText());
  356. if (f.isDirectory())
  357. {
  358. setRoot (f);
  359. chosenFiles.clear();
  360. if ((flags & doNotClearFileNameOnRootChange) == 0)
  361. filenameBox.setText ({});
  362. }
  363. else
  364. {
  365. setRoot (f.getParentDirectory());
  366. chosenFiles.clear();
  367. chosenFiles.add (f);
  368. filenameBox.setText (f.getFileName());
  369. }
  370. }
  371. else
  372. {
  373. fileDoubleClicked (getSelectedFile (0));
  374. }
  375. }
  376. void FileBrowserComponent::textEditorEscapeKeyPressed (TextEditor&)
  377. {
  378. }
  379. void FileBrowserComponent::textEditorFocusLost (TextEditor&)
  380. {
  381. if (! isSaveMode())
  382. selectionChanged();
  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. Array<File> volumes;
  462. File vol ("/Volumes");
  463. vol.findChildFiles (volumes, File::findDirectories, false);
  464. for (int i = 0; i < volumes.size(); ++i)
  465. {
  466. const File& volume = volumes.getReference(i);
  467. if (volume.isDirectory() && ! volume.getFileName().startsWithChar ('.'))
  468. {
  469. rootPaths.add (volume.getFullPathName());
  470. rootNames.add (volume.getFileName());
  471. }
  472. }
  473. #else
  474. rootPaths.add ("/");
  475. rootNames.add ("/");
  476. rootPaths.add (File::getSpecialLocation (File::userHomeDirectory).getFullPathName());
  477. rootNames.add (TRANS("Home folder"));
  478. rootPaths.add (File::getSpecialLocation (File::userDesktopDirectory).getFullPathName());
  479. rootNames.add (TRANS("Desktop"));
  480. #endif
  481. }
  482. void FileBrowserComponent::getRoots (StringArray& rootNames, StringArray& rootPaths)
  483. {
  484. getDefaultRoots (rootNames, rootPaths);
  485. }
  486. void FileBrowserComponent::timerCallback()
  487. {
  488. const bool isProcessActive = Process::isForegroundProcess();
  489. if (wasProcessActive != isProcessActive)
  490. {
  491. wasProcessActive = isProcessActive;
  492. if (isProcessActive && fileList != nullptr)
  493. refresh();
  494. }
  495. }
  496. } // namespace juce