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.

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