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.

682 lines
21KB

  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. template <typename T>
  21. int threeWayCompare (const T& a, const T& b)
  22. {
  23. if (a < b) return -1;
  24. if (b < a) return 1;
  25. return 0;
  26. }
  27. int threeWayCompare (const String& a, const String& b);
  28. int threeWayCompare (const String& a, const String& b)
  29. {
  30. return a.compare (b);
  31. }
  32. struct ReverseCompareString
  33. {
  34. String value;
  35. };
  36. int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b);
  37. int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b)
  38. {
  39. return b.value.compare (a.value);
  40. }
  41. template <size_t position, typename... Ts>
  42. constexpr int threeWayCompareImpl (const std::tuple<Ts...>& a, const std::tuple<Ts...>& b)
  43. {
  44. if constexpr (position == sizeof... (Ts))
  45. {
  46. ignoreUnused (a, b);
  47. return 0;
  48. }
  49. else
  50. {
  51. const auto head = threeWayCompare (std::get<position> (a), std::get<position> (b));
  52. if (head != 0)
  53. return head;
  54. return threeWayCompareImpl<position + 1> (a, b);
  55. }
  56. }
  57. template <typename... Ts>
  58. constexpr int threeWayCompare (const std::tuple<Ts...>& a, const std::tuple<Ts...>& b)
  59. {
  60. return threeWayCompareImpl<0> (a, b);
  61. }
  62. //==============================================================================
  63. class FileListTreeItem final : public TreeViewItem,
  64. private TimeSliceClient,
  65. private AsyncUpdater
  66. {
  67. public:
  68. FileListTreeItem (FileTreeComponent& treeComp,
  69. const File& f,
  70. TimeSliceThread& t)
  71. : file (f),
  72. owner (treeComp),
  73. thread (t)
  74. {
  75. }
  76. void update (const DirectoryContentsList::FileInfo& fileInfo)
  77. {
  78. fileSize = File::descriptionOfSizeInBytes (fileInfo.fileSize);
  79. modTime = fileInfo.modificationTime.formatted ("%d %b '%y %H:%M");
  80. isDirectory = fileInfo.isDirectory;
  81. repaintItem();
  82. }
  83. ~FileListTreeItem() override
  84. {
  85. thread.removeTimeSliceClient (this);
  86. clearSubItems();
  87. }
  88. //==============================================================================
  89. bool mightContainSubItems() override { return isDirectory; }
  90. String getUniqueName() const override { return file.getFullPathName(); }
  91. int getItemHeight() const override { return owner.getItemHeight(); }
  92. var getDragSourceDescription() override { return owner.getDragAndDropDescription(); }
  93. void itemOpennessChanged (bool isNowOpen) override
  94. {
  95. NullCheckedInvocation::invoke (onOpennessChanged, file, isNowOpen);
  96. }
  97. void paintItem (Graphics& g, int width, int height) override
  98. {
  99. ScopedLock lock (iconUpdate);
  100. if (file != File())
  101. {
  102. updateIcon (true);
  103. if (icon.isNull())
  104. thread.addTimeSliceClient (this);
  105. }
  106. owner.getLookAndFeel().drawFileBrowserRow (g, width, height,
  107. file, file.getFileName(),
  108. &icon, fileSize, modTime,
  109. isDirectory, isSelected(),
  110. getIndexInParent(), owner);
  111. }
  112. String getAccessibilityName() override
  113. {
  114. return file.getFileName();
  115. }
  116. void itemClicked (const MouseEvent& e) override
  117. {
  118. owner.sendMouseClickMessage (file, e);
  119. }
  120. void itemDoubleClicked (const MouseEvent& e) override
  121. {
  122. TreeViewItem::itemDoubleClicked (e);
  123. owner.sendDoubleClickMessage (file);
  124. }
  125. void itemSelectionChanged (bool) override
  126. {
  127. owner.sendSelectionChangeMessage();
  128. }
  129. int useTimeSlice() override
  130. {
  131. updateIcon (false);
  132. return -1;
  133. }
  134. void handleAsyncUpdate() override
  135. {
  136. owner.repaint();
  137. }
  138. const File file;
  139. std::function<void (const File&, bool)> onOpennessChanged;
  140. private:
  141. FileTreeComponent& owner;
  142. bool isDirectory = false;
  143. TimeSliceThread& thread;
  144. CriticalSection iconUpdate;
  145. Image icon;
  146. String fileSize, modTime;
  147. void updateIcon (const bool onlyUpdateIfCached)
  148. {
  149. if (icon.isNull())
  150. {
  151. auto hashCode = (file.getFullPathName() + "_iconCacheSalt").hashCode();
  152. auto im = ImageCache::getFromHashCode (hashCode);
  153. if (im.isNull() && ! onlyUpdateIfCached)
  154. {
  155. im = detail::WindowingHelpers::createIconForFile (file);
  156. if (im.isValid())
  157. ImageCache::addImageToCache (im, hashCode);
  158. }
  159. if (im.isValid())
  160. {
  161. {
  162. ScopedLock lock (iconUpdate);
  163. icon = im;
  164. }
  165. triggerAsyncUpdate();
  166. }
  167. }
  168. }
  169. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileListTreeItem)
  170. };
  171. class DirectoryScanner final : private ChangeListener
  172. {
  173. public:
  174. struct Listener
  175. {
  176. virtual ~Listener() = default;
  177. virtual void rootChanged() = 0;
  178. virtual void directoryChanged (const DirectoryContentsList&) = 0;
  179. };
  180. DirectoryScanner (DirectoryContentsList& rootIn, Listener& listenerIn)
  181. : root (rootIn), listener (listenerIn)
  182. {
  183. root.addChangeListener (this);
  184. }
  185. ~DirectoryScanner() override
  186. {
  187. root.removeChangeListener (this);
  188. }
  189. void refresh()
  190. {
  191. root.refresh();
  192. }
  193. void open (const File& f)
  194. {
  195. auto& contentsList = [&]() -> auto&
  196. {
  197. if (auto it = contentsLists.find (f); it != contentsLists.end())
  198. return it->second;
  199. auto insertion = contentsLists.emplace (std::piecewise_construct,
  200. std::forward_as_tuple (f),
  201. std::forward_as_tuple (root.getFilter(),
  202. root.getTimeSliceThread()));
  203. return insertion.first->second;
  204. }();
  205. contentsList.addChangeListener (this);
  206. contentsList.setDirectory (f, true, true);
  207. contentsList.refresh();
  208. }
  209. void close (const File& f)
  210. {
  211. if (auto it = contentsLists.find (f); it != contentsLists.end())
  212. contentsLists.erase (it);
  213. }
  214. File getRootDirectory() const
  215. {
  216. return root.getDirectory();
  217. }
  218. bool isStillLoading() const
  219. {
  220. return std::any_of (contentsLists.begin(),
  221. contentsLists.end(),
  222. [] (const auto& it)
  223. {
  224. return it.second.isStillLoading();
  225. });
  226. }
  227. private:
  228. void changeListenerCallback (ChangeBroadcaster* source) override
  229. {
  230. auto* sourceList = static_cast<DirectoryContentsList*> (source);
  231. if (sourceList == &root)
  232. {
  233. if (std::exchange (lastDirectory, root.getDirectory()) != root.getDirectory())
  234. {
  235. contentsLists.clear();
  236. listener.rootChanged();
  237. }
  238. else
  239. {
  240. for (auto& contentsList : contentsLists)
  241. contentsList.second.refresh();
  242. }
  243. }
  244. listener.directoryChanged (*sourceList);
  245. }
  246. DirectoryContentsList& root;
  247. Listener& listener;
  248. File lastDirectory;
  249. std::map<File, DirectoryContentsList> contentsLists;
  250. };
  251. struct FileEntry
  252. {
  253. String path;
  254. bool isDirectory;
  255. int compareWindows (const FileEntry& other) const
  256. {
  257. const auto toTuple = [] (const auto& x) { return std::tuple (! x.isDirectory, x.path.toLowerCase()); };
  258. return threeWayCompare (toTuple (*this), toTuple (other));
  259. }
  260. int compareLinux (const FileEntry& other) const
  261. {
  262. const auto toTuple = [] (const auto& x) { return std::tuple (x.path.toUpperCase(), ReverseCompareString { x.path }); };
  263. return threeWayCompare (toTuple (*this), toTuple (other));
  264. }
  265. int compareDefault (const FileEntry& other) const
  266. {
  267. return threeWayCompare (path.toLowerCase(), other.path.toLowerCase());
  268. }
  269. };
  270. class OSDependentFileComparisonRules
  271. {
  272. public:
  273. explicit OSDependentFileComparisonRules (SystemStats::OperatingSystemType systemTypeIn)
  274. : systemType (systemTypeIn)
  275. {}
  276. int compare (const FileEntry& first, const FileEntry& second) const
  277. {
  278. if ((systemType & SystemStats::OperatingSystemType::Windows) != 0)
  279. return first.compareWindows (second);
  280. if ((systemType & SystemStats::OperatingSystemType::Linux) != 0)
  281. return first.compareLinux (second);
  282. return first.compareDefault (second);
  283. }
  284. bool operator() (const FileEntry& first, const FileEntry& second) const
  285. {
  286. return compare (first, second) < 0;
  287. }
  288. private:
  289. SystemStats::OperatingSystemType systemType;
  290. };
  291. class FileTreeComponent::Controller final : private DirectoryScanner::Listener
  292. {
  293. public:
  294. explicit Controller (FileTreeComponent& ownerIn)
  295. : owner (ownerIn),
  296. scanner (owner.directoryContentsList, *this)
  297. {
  298. refresh();
  299. }
  300. ~Controller() override
  301. {
  302. owner.deleteRootItem();
  303. }
  304. void refresh()
  305. {
  306. scanner.refresh();
  307. }
  308. void selectFile (const File& target)
  309. {
  310. pendingFileSelection.emplace (target);
  311. tryResolvePendingFileSelection();
  312. }
  313. private:
  314. template <typename ItemCallback>
  315. static void forEachItemRecursive (TreeViewItem* item, ItemCallback&& cb)
  316. {
  317. if (item == nullptr)
  318. return;
  319. if (auto* fileListItem = dynamic_cast<FileListTreeItem*> (item))
  320. cb (fileListItem);
  321. for (int i = 0; i < item->getNumSubItems(); ++i)
  322. forEachItemRecursive (item->getSubItem (i), cb);
  323. }
  324. //==============================================================================
  325. void rootChanged() override
  326. {
  327. owner.deleteRootItem();
  328. treeItemForFile.clear();
  329. owner.setRootItem (createNewItem (scanner.getRootDirectory()).release());
  330. }
  331. void directoryChanged (const DirectoryContentsList& contentsList) override
  332. {
  333. auto* parentItem = [&]() -> FileListTreeItem*
  334. {
  335. if (auto it = treeItemForFile.find (contentsList.getDirectory()); it != treeItemForFile.end())
  336. return it->second;
  337. return nullptr;
  338. }();
  339. if (parentItem == nullptr)
  340. {
  341. jassertfalse;
  342. return;
  343. }
  344. for (int i = 0; i < contentsList.getNumFiles(); ++i)
  345. {
  346. auto file = contentsList.getFile (i);
  347. DirectoryContentsList::FileInfo fileInfo;
  348. contentsList.getFileInfo (i, fileInfo);
  349. auto* item = [&]
  350. {
  351. if (auto it = treeItemForFile.find (file); it != treeItemForFile.end())
  352. return it->second;
  353. auto* newItem = createNewItem (file).release();
  354. parentItem->addSubItem (newItem);
  355. return newItem;
  356. }();
  357. if (item->isOpen() && fileInfo.isDirectory)
  358. scanner.open (item->file);
  359. item->update (fileInfo);
  360. }
  361. if (contentsList.isStillLoading())
  362. return;
  363. std::set<File> allFiles;
  364. for (int i = 0; i < contentsList.getNumFiles(); ++i)
  365. allFiles.insert (contentsList.getFile (i));
  366. for (int i = 0; i < parentItem->getNumSubItems();)
  367. {
  368. auto* fileItem = dynamic_cast<FileListTreeItem*> (parentItem->getSubItem (i));
  369. if (fileItem != nullptr && allFiles.count (fileItem->file) == 0)
  370. {
  371. forEachItemRecursive (parentItem->getSubItem (i),
  372. [this] (auto* item)
  373. {
  374. scanner.close (item->file);
  375. treeItemForFile.erase (item->file);
  376. });
  377. parentItem->removeSubItem (i);
  378. }
  379. else
  380. {
  381. ++i;
  382. }
  383. }
  384. struct Comparator
  385. {
  386. // The different OSes compare and order files in different ways. This function aims
  387. // to match these different rules of comparison to mimic other FileBrowserComponent
  388. // view modes where we don't need to order the results, and can just rely on the
  389. // ordering of the list provided by the OS.
  390. static int compareElements (TreeViewItem* first, TreeViewItem* second)
  391. {
  392. auto* item1 = dynamic_cast<FileListTreeItem*> (first);
  393. auto* item2 = dynamic_cast<FileListTreeItem*> (second);
  394. if (item1 == nullptr || item2 == nullptr)
  395. return 0;
  396. static const OSDependentFileComparisonRules comparisonRules { SystemStats::getOperatingSystemType() };
  397. return comparisonRules.compare ({ item1->file.getFullPathName(), item1->file.isDirectory() },
  398. { item2->file.getFullPathName(), item2->file.isDirectory() });
  399. }
  400. };
  401. static Comparator comparator;
  402. parentItem->sortSubItems (comparator);
  403. tryResolvePendingFileSelection();
  404. }
  405. std::unique_ptr<FileListTreeItem> createNewItem (const File& file)
  406. {
  407. auto newItem = std::make_unique<FileListTreeItem> (owner,
  408. file,
  409. owner.directoryContentsList.getTimeSliceThread());
  410. newItem->onOpennessChanged = [this, itemPtr = newItem.get()] (const auto& f, auto isOpen)
  411. {
  412. if (isOpen)
  413. {
  414. scanner.open (f);
  415. }
  416. else
  417. {
  418. forEachItemRecursive (itemPtr,
  419. [this] (auto* item)
  420. {
  421. scanner.close (item->file);
  422. });
  423. }
  424. };
  425. treeItemForFile[file] = newItem.get();
  426. return newItem;
  427. }
  428. void tryResolvePendingFileSelection()
  429. {
  430. if (! pendingFileSelection.has_value())
  431. return;
  432. if (auto item = treeItemForFile.find (*pendingFileSelection); item != treeItemForFile.end())
  433. {
  434. item->second->setSelected (true, true);
  435. pendingFileSelection.reset();
  436. return;
  437. }
  438. if (owner.directoryContentsList.isStillLoading() || scanner.isStillLoading())
  439. return;
  440. owner.clearSelectedItems();
  441. }
  442. FileTreeComponent& owner;
  443. std::map<File, FileListTreeItem*> treeItemForFile;
  444. DirectoryScanner scanner;
  445. std::optional<File> pendingFileSelection;
  446. };
  447. //==============================================================================
  448. FileTreeComponent::FileTreeComponent (DirectoryContentsList& listToShow)
  449. : DirectoryContentsDisplayComponent (listToShow),
  450. itemHeight (22)
  451. {
  452. controller = std::make_unique<Controller> (*this);
  453. setRootItemVisible (false);
  454. refresh();
  455. }
  456. FileTreeComponent::~FileTreeComponent()
  457. {
  458. deleteRootItem();
  459. }
  460. void FileTreeComponent::refresh()
  461. {
  462. controller->refresh();
  463. }
  464. //==============================================================================
  465. File FileTreeComponent::getSelectedFile (const int index) const
  466. {
  467. if (auto* item = dynamic_cast<const FileListTreeItem*> (getSelectedItem (index)))
  468. return item->file;
  469. return {};
  470. }
  471. void FileTreeComponent::deselectAllFiles()
  472. {
  473. clearSelectedItems();
  474. }
  475. void FileTreeComponent::scrollToTop()
  476. {
  477. getViewport()->getVerticalScrollBar().setCurrentRangeStart (0);
  478. }
  479. void FileTreeComponent::setDragAndDropDescription (const String& description)
  480. {
  481. dragAndDropDescription = description;
  482. }
  483. void FileTreeComponent::setSelectedFile (const File& target)
  484. {
  485. controller->selectFile (target);
  486. }
  487. void FileTreeComponent::setItemHeight (int newHeight)
  488. {
  489. if (itemHeight != newHeight)
  490. {
  491. itemHeight = newHeight;
  492. if (auto* root = getRootItem())
  493. root->treeHasChanged();
  494. }
  495. }
  496. #if JUCE_UNIT_TESTS
  497. class FileTreeComponentTests final : public UnitTest
  498. {
  499. public:
  500. //==============================================================================
  501. FileTreeComponentTests() : UnitTest ("FileTreeComponentTests", UnitTestCategories::gui) {}
  502. void runTest() override
  503. {
  504. const auto checkOrder = [] (const auto& orderedFiles, const std::vector<String>& expected)
  505. {
  506. return std::equal (orderedFiles.begin(), orderedFiles.end(),
  507. expected.begin(), expected.end(),
  508. [] (const auto& entry, const auto& expectedPath) { return entry.path == expectedPath; });
  509. };
  510. const auto doSort = [] (const auto platform, auto& range)
  511. {
  512. std::sort (range.begin(), range.end(), OSDependentFileComparisonRules { platform });
  513. };
  514. beginTest ("Test Linux filename ordering");
  515. {
  516. std::vector<FileEntry> filesToOrder { { "_test", false },
  517. { "Atest", false },
  518. { "atest", false } };
  519. doSort (SystemStats::OperatingSystemType::Linux, filesToOrder);
  520. expect (checkOrder (filesToOrder, { "atest", "Atest", "_test" }));
  521. }
  522. beginTest ("Test Windows filename ordering");
  523. {
  524. std::vector<FileEntry> filesToOrder { { "cmake_install.cmake", false },
  525. { "CMakeFiles", true },
  526. { "JUCEConfig.cmake", false },
  527. { "tools", true },
  528. { "cmakefiles.cmake", false } };
  529. doSort (SystemStats::OperatingSystemType::Windows, filesToOrder);
  530. expect (checkOrder (filesToOrder, { "CMakeFiles",
  531. "tools",
  532. "cmake_install.cmake",
  533. "cmakefiles.cmake",
  534. "JUCEConfig.cmake" }));
  535. }
  536. beginTest ("Test MacOS filename ordering");
  537. {
  538. std::vector<FileEntry> filesToOrder { { "cmake_install.cmake", false },
  539. { "CMakeFiles", true },
  540. { "tools", true },
  541. { "JUCEConfig.cmake", false } };
  542. doSort (SystemStats::OperatingSystemType::MacOSX, filesToOrder);
  543. expect (checkOrder (filesToOrder, { "cmake_install.cmake",
  544. "CMakeFiles",
  545. "JUCEConfig.cmake",
  546. "tools" }));
  547. }
  548. }
  549. };
  550. static FileTreeComponentTests fileTreeComponentTests;
  551. #endif
  552. } // namespace juce