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.

510 lines
14KB

  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. //==============================================================================
  21. class FileListTreeItem : public TreeViewItem,
  22. private TimeSliceClient,
  23. private AsyncUpdater
  24. {
  25. public:
  26. FileListTreeItem (FileTreeComponent& treeComp,
  27. const File& f,
  28. TimeSliceThread& t)
  29. : file (f),
  30. owner (treeComp),
  31. thread (t)
  32. {
  33. }
  34. void update (const DirectoryContentsList::FileInfo& fileInfo)
  35. {
  36. fileSize = File::descriptionOfSizeInBytes (fileInfo.fileSize);
  37. modTime = fileInfo.modificationTime.formatted ("%d %b '%y %H:%M");
  38. isDirectory = fileInfo.isDirectory;
  39. repaintItem();
  40. }
  41. ~FileListTreeItem() override
  42. {
  43. thread.removeTimeSliceClient (this);
  44. clearSubItems();
  45. }
  46. //==============================================================================
  47. bool mightContainSubItems() override { return isDirectory; }
  48. String getUniqueName() const override { return file.getFullPathName(); }
  49. int getItemHeight() const override { return owner.getItemHeight(); }
  50. var getDragSourceDescription() override { return owner.getDragAndDropDescription(); }
  51. void itemOpennessChanged (bool isNowOpen) override
  52. {
  53. NullCheckedInvocation::invoke (onOpennessChanged, file, isNowOpen);
  54. }
  55. void paintItem (Graphics& g, int width, int height) override
  56. {
  57. ScopedLock lock (iconUpdate);
  58. if (file != File())
  59. {
  60. updateIcon (true);
  61. if (icon.isNull())
  62. thread.addTimeSliceClient (this);
  63. }
  64. owner.getLookAndFeel().drawFileBrowserRow (g, width, height,
  65. file, file.getFileName(),
  66. &icon, fileSize, modTime,
  67. isDirectory, isSelected(),
  68. getIndexInParent(), owner);
  69. }
  70. String getAccessibilityName() override
  71. {
  72. return file.getFileName();
  73. }
  74. void itemClicked (const MouseEvent& e) override
  75. {
  76. owner.sendMouseClickMessage (file, e);
  77. }
  78. void itemDoubleClicked (const MouseEvent& e) override
  79. {
  80. TreeViewItem::itemDoubleClicked (e);
  81. owner.sendDoubleClickMessage (file);
  82. }
  83. void itemSelectionChanged (bool) override
  84. {
  85. owner.sendSelectionChangeMessage();
  86. }
  87. int useTimeSlice() override
  88. {
  89. updateIcon (false);
  90. return -1;
  91. }
  92. void handleAsyncUpdate() override
  93. {
  94. owner.repaint();
  95. }
  96. const File file;
  97. std::function<void (const File&, bool)> onOpennessChanged;
  98. private:
  99. FileTreeComponent& owner;
  100. bool isDirectory = false;
  101. TimeSliceThread& thread;
  102. CriticalSection iconUpdate;
  103. Image icon;
  104. String fileSize, modTime;
  105. void updateIcon (const bool onlyUpdateIfCached)
  106. {
  107. if (icon.isNull())
  108. {
  109. auto hashCode = (file.getFullPathName() + "_iconCacheSalt").hashCode();
  110. auto im = ImageCache::getFromHashCode (hashCode);
  111. if (im.isNull() && ! onlyUpdateIfCached)
  112. {
  113. im = detail::WindowingHelpers::createIconForFile (file);
  114. if (im.isValid())
  115. ImageCache::addImageToCache (im, hashCode);
  116. }
  117. if (im.isValid())
  118. {
  119. {
  120. ScopedLock lock (iconUpdate);
  121. icon = im;
  122. }
  123. triggerAsyncUpdate();
  124. }
  125. }
  126. }
  127. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileListTreeItem)
  128. };
  129. class DirectoryScanner : private ChangeListener
  130. {
  131. public:
  132. struct Listener
  133. {
  134. virtual ~Listener() = default;
  135. virtual void rootChanged() = 0;
  136. virtual void directoryChanged (const DirectoryContentsList&) = 0;
  137. };
  138. DirectoryScanner (DirectoryContentsList& rootIn, Listener& listenerIn)
  139. : root (rootIn), listener (listenerIn)
  140. {
  141. root.addChangeListener (this);
  142. }
  143. ~DirectoryScanner() override
  144. {
  145. root.removeChangeListener (this);
  146. }
  147. void refresh()
  148. {
  149. root.refresh();
  150. }
  151. void open (const File& f)
  152. {
  153. auto& contentsList = [&]() -> auto&
  154. {
  155. if (auto it = contentsLists.find (f); it != contentsLists.end())
  156. return it->second;
  157. auto insertion = contentsLists.emplace (std::piecewise_construct,
  158. std::forward_as_tuple (f),
  159. std::forward_as_tuple (root.getFilter(),
  160. root.getTimeSliceThread()));
  161. return insertion.first->second;
  162. }();
  163. contentsList.addChangeListener (this);
  164. contentsList.setDirectory (f, true, true);
  165. contentsList.refresh();
  166. }
  167. void close (const File& f)
  168. {
  169. if (auto it = contentsLists.find (f); it != contentsLists.end())
  170. contentsLists.erase (it);
  171. }
  172. File getRootDirectory() const
  173. {
  174. return root.getDirectory();
  175. }
  176. bool isStillLoading() const
  177. {
  178. return std::any_of (contentsLists.begin(),
  179. contentsLists.end(),
  180. [] (const auto& it)
  181. {
  182. return it.second.isStillLoading();
  183. });
  184. }
  185. private:
  186. void changeListenerCallback (ChangeBroadcaster* source) override
  187. {
  188. auto* sourceList = static_cast<DirectoryContentsList*> (source);
  189. if (sourceList == &root)
  190. {
  191. if (std::exchange (lastDirectory, root.getDirectory()) != root.getDirectory())
  192. {
  193. contentsLists.clear();
  194. listener.rootChanged();
  195. }
  196. else
  197. {
  198. for (auto& contentsList : contentsLists)
  199. contentsList.second.refresh();
  200. }
  201. }
  202. listener.directoryChanged (*sourceList);
  203. }
  204. DirectoryContentsList& root;
  205. Listener& listener;
  206. File lastDirectory;
  207. std::map<File, DirectoryContentsList> contentsLists;
  208. };
  209. class FileTreeComponent::Controller : private DirectoryScanner::Listener
  210. {
  211. public:
  212. explicit Controller (FileTreeComponent& ownerIn)
  213. : owner (ownerIn),
  214. scanner (owner.directoryContentsList, *this)
  215. {
  216. refresh();
  217. }
  218. ~Controller() override
  219. {
  220. owner.deleteRootItem();
  221. }
  222. void refresh()
  223. {
  224. scanner.refresh();
  225. }
  226. void selectFile (const File& target)
  227. {
  228. pendingFileSelection.emplace (target);
  229. tryResolvePendingFileSelection();
  230. }
  231. private:
  232. template <typename ItemCallback>
  233. static void forEachItemRecursive (TreeViewItem* item, ItemCallback&& cb)
  234. {
  235. if (item == nullptr)
  236. return;
  237. if (auto* fileListItem = dynamic_cast<FileListTreeItem*> (item))
  238. cb (fileListItem);
  239. for (int i = 0; i < item->getNumSubItems(); ++i)
  240. forEachItemRecursive (item->getSubItem (i), cb);
  241. }
  242. //==============================================================================
  243. void rootChanged() override
  244. {
  245. owner.deleteRootItem();
  246. treeItemForFile.clear();
  247. owner.setRootItem (createNewItem (scanner.getRootDirectory()).release());
  248. }
  249. void directoryChanged (const DirectoryContentsList& contentsList) override
  250. {
  251. auto* parentItem = [&]() -> FileListTreeItem*
  252. {
  253. if (auto it = treeItemForFile.find (contentsList.getDirectory()); it != treeItemForFile.end())
  254. return it->second;
  255. return nullptr;
  256. }();
  257. if (parentItem == nullptr)
  258. {
  259. jassertfalse;
  260. return;
  261. }
  262. for (int i = 0; i < contentsList.getNumFiles(); ++i)
  263. {
  264. auto file = contentsList.getFile (i);
  265. DirectoryContentsList::FileInfo fileInfo;
  266. contentsList.getFileInfo (i, fileInfo);
  267. auto* item = [&]
  268. {
  269. if (auto it = treeItemForFile.find (file); it != treeItemForFile.end())
  270. return it->second;
  271. auto* newItem = createNewItem (file).release();
  272. parentItem->addSubItem (newItem);
  273. return newItem;
  274. }();
  275. if (item->isOpen() && fileInfo.isDirectory)
  276. scanner.open (item->file);
  277. item->update (fileInfo);
  278. }
  279. if (contentsList.isStillLoading())
  280. return;
  281. std::set<File> allFiles;
  282. for (int i = 0; i < contentsList.getNumFiles(); ++i)
  283. allFiles.insert (contentsList.getFile (i));
  284. for (int i = 0; i < parentItem->getNumSubItems();)
  285. {
  286. auto* fileItem = dynamic_cast<FileListTreeItem*> (parentItem->getSubItem (i));
  287. if (fileItem != nullptr && allFiles.count (fileItem->file) == 0)
  288. {
  289. forEachItemRecursive (parentItem->getSubItem (i),
  290. [this] (auto* item)
  291. {
  292. scanner.close (item->file);
  293. treeItemForFile.erase (item->file);
  294. });
  295. parentItem->removeSubItem (i);
  296. }
  297. else
  298. {
  299. ++i;
  300. }
  301. }
  302. struct Comparator
  303. {
  304. static int compareElements (TreeViewItem* first, TreeViewItem* second)
  305. {
  306. auto* item1 = dynamic_cast<FileListTreeItem*> (first);
  307. auto* item2 = dynamic_cast<FileListTreeItem*> (second);
  308. if (item1 == nullptr || item2 == nullptr)
  309. return 0;
  310. if (item1->file < item2->file)
  311. return -1;
  312. if (item1->file > item2->file)
  313. return 1;
  314. return 0;
  315. }
  316. };
  317. static Comparator comparator;
  318. parentItem->sortSubItems (comparator);
  319. tryResolvePendingFileSelection();
  320. }
  321. std::unique_ptr<FileListTreeItem> createNewItem (const File& file)
  322. {
  323. auto newItem = std::make_unique<FileListTreeItem> (owner,
  324. file,
  325. owner.directoryContentsList.getTimeSliceThread());
  326. newItem->onOpennessChanged = [this, itemPtr = newItem.get()] (const auto& f, auto isOpen)
  327. {
  328. if (isOpen)
  329. {
  330. scanner.open (f);
  331. }
  332. else
  333. {
  334. forEachItemRecursive (itemPtr,
  335. [this] (auto* item)
  336. {
  337. scanner.close (item->file);
  338. });
  339. }
  340. };
  341. treeItemForFile[file] = newItem.get();
  342. return newItem;
  343. }
  344. void tryResolvePendingFileSelection()
  345. {
  346. if (! pendingFileSelection.has_value())
  347. return;
  348. if (auto item = treeItemForFile.find (*pendingFileSelection); item != treeItemForFile.end())
  349. {
  350. item->second->setSelected (true, true);
  351. pendingFileSelection.reset();
  352. return;
  353. }
  354. if (owner.directoryContentsList.isStillLoading() || scanner.isStillLoading())
  355. return;
  356. owner.clearSelectedItems();
  357. }
  358. FileTreeComponent& owner;
  359. std::map<File, FileListTreeItem*> treeItemForFile;
  360. DirectoryScanner scanner;
  361. std::optional<File> pendingFileSelection;
  362. };
  363. //==============================================================================
  364. FileTreeComponent::FileTreeComponent (DirectoryContentsList& listToShow)
  365. : DirectoryContentsDisplayComponent (listToShow),
  366. itemHeight (22)
  367. {
  368. controller = std::make_unique<Controller> (*this);
  369. setRootItemVisible (false);
  370. refresh();
  371. }
  372. FileTreeComponent::~FileTreeComponent()
  373. {
  374. deleteRootItem();
  375. }
  376. void FileTreeComponent::refresh()
  377. {
  378. controller->refresh();
  379. }
  380. //==============================================================================
  381. File FileTreeComponent::getSelectedFile (const int index) const
  382. {
  383. if (auto* item = dynamic_cast<const FileListTreeItem*> (getSelectedItem (index)))
  384. return item->file;
  385. return {};
  386. }
  387. void FileTreeComponent::deselectAllFiles()
  388. {
  389. clearSelectedItems();
  390. }
  391. void FileTreeComponent::scrollToTop()
  392. {
  393. getViewport()->getVerticalScrollBar().setCurrentRangeStart (0);
  394. }
  395. void FileTreeComponent::setDragAndDropDescription (const String& description)
  396. {
  397. dragAndDropDescription = description;
  398. }
  399. void FileTreeComponent::setSelectedFile (const File& target)
  400. {
  401. controller->selectFile (target);
  402. }
  403. void FileTreeComponent::setItemHeight (int newHeight)
  404. {
  405. if (itemHeight != newHeight)
  406. {
  407. itemHeight = newHeight;
  408. if (auto* root = getRootItem())
  409. root->treeHasChanged();
  410. }
  411. }
  412. } // namespace juce