/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { template int threeWayCompare (const T& a, const T& b) { if (a < b) return -1; if (b < a) return 1; return 0; } int threeWayCompare (const String& a, const String& b); int threeWayCompare (const String& a, const String& b) { return a.compare (b); } struct ReverseCompareString { String value; }; int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b); int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b) { return b.value.compare (a.value); } template constexpr int threeWayCompareImpl (const std::tuple& a, const std::tuple& b) { if constexpr (position == sizeof... (Ts)) { ignoreUnused (a, b); return 0; } else { const auto head = threeWayCompare (std::get (a), std::get (b)); if (head != 0) return head; return threeWayCompareImpl (a, b); } } template constexpr int threeWayCompare (const std::tuple& a, const std::tuple& b) { return threeWayCompareImpl<0> (a, b); } //============================================================================== class FileListTreeItem : public TreeViewItem, private TimeSliceClient, private AsyncUpdater { public: FileListTreeItem (FileTreeComponent& treeComp, const File& f, TimeSliceThread& t) : file (f), owner (treeComp), thread (t) { } void update (const DirectoryContentsList::FileInfo& fileInfo) { fileSize = File::descriptionOfSizeInBytes (fileInfo.fileSize); modTime = fileInfo.modificationTime.formatted ("%d %b '%y %H:%M"); isDirectory = fileInfo.isDirectory; repaintItem(); } ~FileListTreeItem() override { thread.removeTimeSliceClient (this); clearSubItems(); } //============================================================================== bool mightContainSubItems() override { return isDirectory; } String getUniqueName() const override { return file.getFullPathName(); } int getItemHeight() const override { return owner.getItemHeight(); } var getDragSourceDescription() override { return owner.getDragAndDropDescription(); } void itemOpennessChanged (bool isNowOpen) override { NullCheckedInvocation::invoke (onOpennessChanged, file, isNowOpen); } void paintItem (Graphics& g, int width, int height) override { ScopedLock lock (iconUpdate); if (file != File()) { updateIcon (true); if (icon.isNull()) thread.addTimeSliceClient (this); } owner.getLookAndFeel().drawFileBrowserRow (g, width, height, file, file.getFileName(), &icon, fileSize, modTime, isDirectory, isSelected(), getIndexInParent(), owner); } String getAccessibilityName() override { return file.getFileName(); } void itemClicked (const MouseEvent& e) override { owner.sendMouseClickMessage (file, e); } void itemDoubleClicked (const MouseEvent& e) override { TreeViewItem::itemDoubleClicked (e); owner.sendDoubleClickMessage (file); } void itemSelectionChanged (bool) override { owner.sendSelectionChangeMessage(); } int useTimeSlice() override { updateIcon (false); return -1; } void handleAsyncUpdate() override { owner.repaint(); } const File file; std::function onOpennessChanged; private: FileTreeComponent& owner; bool isDirectory = false; TimeSliceThread& thread; CriticalSection iconUpdate; Image icon; String fileSize, modTime; void updateIcon (const bool onlyUpdateIfCached) { if (icon.isNull()) { auto hashCode = (file.getFullPathName() + "_iconCacheSalt").hashCode(); auto im = ImageCache::getFromHashCode (hashCode); if (im.isNull() && ! onlyUpdateIfCached) { im = detail::WindowingHelpers::createIconForFile (file); if (im.isValid()) ImageCache::addImageToCache (im, hashCode); } if (im.isValid()) { { ScopedLock lock (iconUpdate); icon = im; } triggerAsyncUpdate(); } } } JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileListTreeItem) }; class DirectoryScanner : private ChangeListener { public: struct Listener { virtual ~Listener() = default; virtual void rootChanged() = 0; virtual void directoryChanged (const DirectoryContentsList&) = 0; }; DirectoryScanner (DirectoryContentsList& rootIn, Listener& listenerIn) : root (rootIn), listener (listenerIn) { root.addChangeListener (this); } ~DirectoryScanner() override { root.removeChangeListener (this); } void refresh() { root.refresh(); } void open (const File& f) { auto& contentsList = [&]() -> auto& { if (auto it = contentsLists.find (f); it != contentsLists.end()) return it->second; auto insertion = contentsLists.emplace (std::piecewise_construct, std::forward_as_tuple (f), std::forward_as_tuple (root.getFilter(), root.getTimeSliceThread())); return insertion.first->second; }(); contentsList.addChangeListener (this); contentsList.setDirectory (f, true, true); contentsList.refresh(); } void close (const File& f) { if (auto it = contentsLists.find (f); it != contentsLists.end()) contentsLists.erase (it); } File getRootDirectory() const { return root.getDirectory(); } bool isStillLoading() const { return std::any_of (contentsLists.begin(), contentsLists.end(), [] (const auto& it) { return it.second.isStillLoading(); }); } private: void changeListenerCallback (ChangeBroadcaster* source) override { auto* sourceList = static_cast (source); if (sourceList == &root) { if (std::exchange (lastDirectory, root.getDirectory()) != root.getDirectory()) { contentsLists.clear(); listener.rootChanged(); } else { for (auto& contentsList : contentsLists) contentsList.second.refresh(); } } listener.directoryChanged (*sourceList); } DirectoryContentsList& root; Listener& listener; File lastDirectory; std::map contentsLists; }; struct FileEntry { String path; bool isDirectory; int compareWindows (const FileEntry& other) const { const auto toTuple = [] (const auto& x) { return std::tuple (! x.isDirectory, x.path.toLowerCase()); }; return threeWayCompare (toTuple (*this), toTuple (other)); } int compareLinux (const FileEntry& other) const { const auto toTuple = [] (const auto& x) { return std::tuple (x.path.toUpperCase(), ReverseCompareString { x.path }); }; return threeWayCompare (toTuple (*this), toTuple (other)); } int compareDefault (const FileEntry& other) const { return threeWayCompare (path.toLowerCase(), other.path.toLowerCase()); } }; class OSDependentFileComparisonRules { public: explicit OSDependentFileComparisonRules (SystemStats::OperatingSystemType systemTypeIn) : systemType (systemTypeIn) {} int compare (const FileEntry& first, const FileEntry& second) const { if ((systemType & SystemStats::OperatingSystemType::Windows) != 0) return first.compareWindows (second); if ((systemType & SystemStats::OperatingSystemType::Linux) != 0) return first.compareLinux (second); return first.compareDefault (second); } bool operator() (const FileEntry& first, const FileEntry& second) const { return compare (first, second) < 0; } private: SystemStats::OperatingSystemType systemType; }; class FileTreeComponent::Controller : private DirectoryScanner::Listener { public: explicit Controller (FileTreeComponent& ownerIn) : owner (ownerIn), scanner (owner.directoryContentsList, *this) { refresh(); } ~Controller() override { owner.deleteRootItem(); } void refresh() { scanner.refresh(); } void selectFile (const File& target) { pendingFileSelection.emplace (target); tryResolvePendingFileSelection(); } private: template static void forEachItemRecursive (TreeViewItem* item, ItemCallback&& cb) { if (item == nullptr) return; if (auto* fileListItem = dynamic_cast (item)) cb (fileListItem); for (int i = 0; i < item->getNumSubItems(); ++i) forEachItemRecursive (item->getSubItem (i), cb); } //============================================================================== void rootChanged() override { owner.deleteRootItem(); treeItemForFile.clear(); owner.setRootItem (createNewItem (scanner.getRootDirectory()).release()); } void directoryChanged (const DirectoryContentsList& contentsList) override { auto* parentItem = [&]() -> FileListTreeItem* { if (auto it = treeItemForFile.find (contentsList.getDirectory()); it != treeItemForFile.end()) return it->second; return nullptr; }(); if (parentItem == nullptr) { jassertfalse; return; } for (int i = 0; i < contentsList.getNumFiles(); ++i) { auto file = contentsList.getFile (i); DirectoryContentsList::FileInfo fileInfo; contentsList.getFileInfo (i, fileInfo); auto* item = [&] { if (auto it = treeItemForFile.find (file); it != treeItemForFile.end()) return it->second; auto* newItem = createNewItem (file).release(); parentItem->addSubItem (newItem); return newItem; }(); if (item->isOpen() && fileInfo.isDirectory) scanner.open (item->file); item->update (fileInfo); } if (contentsList.isStillLoading()) return; std::set allFiles; for (int i = 0; i < contentsList.getNumFiles(); ++i) allFiles.insert (contentsList.getFile (i)); for (int i = 0; i < parentItem->getNumSubItems();) { auto* fileItem = dynamic_cast (parentItem->getSubItem (i)); if (fileItem != nullptr && allFiles.count (fileItem->file) == 0) { forEachItemRecursive (parentItem->getSubItem (i), [this] (auto* item) { scanner.close (item->file); treeItemForFile.erase (item->file); }); parentItem->removeSubItem (i); } else { ++i; } } struct Comparator { // The different OSes compare and order files in different ways. This function aims // to match these different rules of comparison to mimic other FileBrowserComponent // view modes where we don't need to order the results, and can just rely on the // ordering of the list provided by the OS. static int compareElements (TreeViewItem* first, TreeViewItem* second) { auto* item1 = dynamic_cast (first); auto* item2 = dynamic_cast (second); if (item1 == nullptr || item2 == nullptr) return 0; static const OSDependentFileComparisonRules comparisonRules { SystemStats::getOperatingSystemType() }; return comparisonRules.compare ({ item1->file.getFullPathName(), item1->file.isDirectory() }, { item2->file.getFullPathName(), item2->file.isDirectory() }); } }; static Comparator comparator; parentItem->sortSubItems (comparator); tryResolvePendingFileSelection(); } std::unique_ptr createNewItem (const File& file) { auto newItem = std::make_unique (owner, file, owner.directoryContentsList.getTimeSliceThread()); newItem->onOpennessChanged = [this, itemPtr = newItem.get()] (const auto& f, auto isOpen) { if (isOpen) { scanner.open (f); } else { forEachItemRecursive (itemPtr, [this] (auto* item) { scanner.close (item->file); }); } }; treeItemForFile[file] = newItem.get(); return newItem; } void tryResolvePendingFileSelection() { if (! pendingFileSelection.has_value()) return; if (auto item = treeItemForFile.find (*pendingFileSelection); item != treeItemForFile.end()) { item->second->setSelected (true, true); pendingFileSelection.reset(); return; } if (owner.directoryContentsList.isStillLoading() || scanner.isStillLoading()) return; owner.clearSelectedItems(); } FileTreeComponent& owner; std::map treeItemForFile; DirectoryScanner scanner; std::optional pendingFileSelection; }; //============================================================================== FileTreeComponent::FileTreeComponent (DirectoryContentsList& listToShow) : DirectoryContentsDisplayComponent (listToShow), itemHeight (22) { controller = std::make_unique (*this); setRootItemVisible (false); refresh(); } FileTreeComponent::~FileTreeComponent() { deleteRootItem(); } void FileTreeComponent::refresh() { controller->refresh(); } //============================================================================== File FileTreeComponent::getSelectedFile (const int index) const { if (auto* item = dynamic_cast (getSelectedItem (index))) return item->file; return {}; } void FileTreeComponent::deselectAllFiles() { clearSelectedItems(); } void FileTreeComponent::scrollToTop() { getViewport()->getVerticalScrollBar().setCurrentRangeStart (0); } void FileTreeComponent::setDragAndDropDescription (const String& description) { dragAndDropDescription = description; } void FileTreeComponent::setSelectedFile (const File& target) { controller->selectFile (target); } void FileTreeComponent::setItemHeight (int newHeight) { if (itemHeight != newHeight) { itemHeight = newHeight; if (auto* root = getRootItem()) root->treeHasChanged(); } } #if JUCE_UNIT_TESTS class FileTreeComponentTests : public UnitTest { public: //============================================================================== FileTreeComponentTests() : UnitTest ("FileTreeComponentTests", UnitTestCategories::gui) {} void runTest() override { const auto checkOrder = [] (const auto& orderedFiles, const std::vector& expected) { return std::equal (orderedFiles.begin(), orderedFiles.end(), expected.begin(), expected.end(), [] (const auto& entry, const auto& expectedPath) { return entry.path == expectedPath; }); }; const auto doSort = [] (const auto platform, auto& range) { std::sort (range.begin(), range.end(), OSDependentFileComparisonRules { platform }); }; beginTest ("Test Linux filename ordering"); { std::vector filesToOrder { { "_test", false }, { "Atest", false }, { "atest", false } }; doSort (SystemStats::OperatingSystemType::Linux, filesToOrder); expect (checkOrder (filesToOrder, { "atest", "Atest", "_test" })); } beginTest ("Test Windows filename ordering"); { std::vector filesToOrder { { "cmake_install.cmake", false }, { "CMakeFiles", true }, { "JUCEConfig.cmake", false }, { "tools", true }, { "cmakefiles.cmake", false } }; doSort (SystemStats::OperatingSystemType::Windows, filesToOrder); expect (checkOrder (filesToOrder, { "CMakeFiles", "tools", "cmake_install.cmake", "cmakefiles.cmake", "JUCEConfig.cmake" })); } beginTest ("Test MacOS filename ordering"); { std::vector filesToOrder { { "cmake_install.cmake", false }, { "CMakeFiles", true }, { "tools", true }, { "JUCEConfig.cmake", false } }; doSort (SystemStats::OperatingSystemType::MacOSX, filesToOrder); expect (checkOrder (filesToOrder, { "cmake_install.cmake", "CMakeFiles", "JUCEConfig.cmake", "tools" })); } } }; static FileTreeComponentTests fileTreeComponentTests; #endif } // namespace juce