/* ============================================================================== This file is part of the JUCE framework. Copyright (c) Raw Material Software Limited JUCE is an open source framework subject to commercial or open source licensing. By downloading, installing, or using the JUCE framework, or combining the JUCE framework with any other source code, object code, content or any other copyrightable work, you agree to the terms of the JUCE End User Licence Agreement, and all incorporated terms including the JUCE Privacy Policy and the JUCE Website Terms of Service, as applicable, which will bind you. If you do not agree to the terms of these agreements, we will not license the JUCE framework to you, and you must discontinue the installation or download process and cease use of the JUCE framework. JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/ JUCE Privacy Policy: https://juce.com/juce-privacy-policy JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/ Or: You may also use this code under the terms of the AGPLv3: https://www.gnu.org/licenses/agpl-3.0.en.html THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR 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 final : 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 final : 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 final : 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 final : 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