From a400d3ebe08ca74d448d9de4f416b186868b3464 Mon Sep 17 00:00:00 2001 From: attila Date: Wed, 5 Apr 2023 07:52:18 +0200 Subject: [PATCH] FileTreeComponent: Use incremental updates after refresh to avoid losing UI state Previously when using the FileBrowserComponent in TreeView mode, a refresh would delete all items and rebuild the UI based on new directory scan data, losing the openness state in the process. With this commit only changes are applied to the current TreeView. --- .../filebrowser/juce_FileTreeComponent.cpp | 443 ++++++++++++------ .../filebrowser/juce_FileTreeComponent.h | 3 + 2 files changed, 303 insertions(+), 143 deletions(-) diff --git a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp index 69eb78a6be3f..c7eb328e24c6 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp +++ b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp @@ -29,42 +29,30 @@ namespace juce //============================================================================== class FileListTreeItem : public TreeViewItem, private TimeSliceClient, - private AsyncUpdater, - private ChangeListener + private AsyncUpdater { public: FileListTreeItem (FileTreeComponent& treeComp, - DirectoryContentsList* parentContents, - int indexInContents, const File& f, TimeSliceThread& t) : file (f), owner (treeComp), - parentContentsList (parentContents), - indexInContentsList (indexInContents), - subContentsList (nullptr, false), thread (t) { - DirectoryContentsList::FileInfo fileInfo; + } - if (parentContents != nullptr - && parentContents->getFileInfo (indexInContents, fileInfo)) - { - fileSize = File::descriptionOfSizeInBytes (fileInfo.fileSize); - modTime = fileInfo.modificationTime.formatted ("%d %b '%y %H:%M"); - isDirectory = fileInfo.isDirectory; - } - else - { - isDirectory = true; - } + 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(); - removeSubContentsList(); } //============================================================================== @@ -76,88 +64,7 @@ class FileListTreeItem : public TreeViewItem, void itemOpennessChanged (bool isNowOpen) override { - if (isNowOpen) - { - clearSubItems(); - - isDirectory = file.isDirectory(); - - if (isDirectory) - { - if (subContentsList == nullptr && parentContentsList != nullptr) - { - auto l = new DirectoryContentsList (parentContentsList->getFilter(), thread); - - l->setDirectory (file, - parentContentsList->isFindingDirectories(), - parentContentsList->isFindingFiles()); - - setSubContentsList (l, true); - } - - changeListenerCallback (nullptr); - } - } - } - - void removeSubContentsList() - { - if (subContentsList != nullptr) - { - subContentsList->removeChangeListener (this); - subContentsList.reset(); - } - } - - void setSubContentsList (DirectoryContentsList* newList, const bool canDeleteList) - { - removeSubContentsList(); - - subContentsList = OptionalScopedPointer (newList, canDeleteList); - newList->addChangeListener (this); - } - - void selectFile (const File& target) - { - if (file == target) - { - setSelected (true, true); - return; - } - - if (subContentsList != nullptr && subContentsList->isStillLoading()) - { - pendingFileSelection.emplace (*this, target); - return; - } - - pendingFileSelection.reset(); - - if (! target.isAChildOf (file)) - return; - - setOpen (true); - - for (int i = 0; i < getNumSubItems(); ++i) - if (auto* f = dynamic_cast (getSubItem (i))) - f->selectFile (target); - } - - void changeListenerCallback (ChangeBroadcaster*) override - { - rebuildItemsFromContentList(); - } - - void rebuildItemsFromContentList() - { - clearSubItems(); - - if (isOpen() && subContentsList != nullptr) - { - for (int i = 0; i < subContentsList->getNumFiles(); ++i) - addSubItem (new FileListTreeItem (owner, subContentsList, i, - subContentsList->getFile(i), thread)); - } + NullCheckedInvocation::invoke (onOpennessChanged, file, isNowOpen); } void paintItem (Graphics& g, int width, int height) override @@ -176,7 +83,7 @@ class FileListTreeItem : public TreeViewItem, file, file.getFileName(), &icon, fileSize, modTime, isDirectory, isSelected(), - indexInContentsList, owner); + getIndexInParent(), owner); } String getAccessibilityName() override @@ -213,40 +120,11 @@ class FileListTreeItem : public TreeViewItem, } const File file; + std::function onOpennessChanged; private: - class PendingFileSelection : private Timer - { - public: - PendingFileSelection (FileListTreeItem& item, const File& f) - : owner (item), fileToSelect (f) - { - startTimer (10); - } - - ~PendingFileSelection() override - { - stopTimer(); - } - - private: - void timerCallback() override - { - // Take a copy of the file here, in case this PendingFileSelection - // object is destroyed during the call to selectFile. - owner.selectFile (File { fileToSelect }); - } - - FileListTreeItem& owner; - File fileToSelect; - }; - - Optional pendingFileSelection; FileTreeComponent& owner; - DirectoryContentsList* parentContentsList; - int indexInContentsList; - OptionalScopedPointer subContentsList; - bool isDirectory; + bool isDirectory = false; TimeSliceThread& thread; CriticalSection iconUpdate; Image icon; @@ -282,11 +160,297 @@ class FileListTreeItem : public TreeViewItem, 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 (nullptr, 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; +}; + +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 + { + static int compareElements (TreeViewItem* first, TreeViewItem* second) + { + auto* item1 = dynamic_cast (first); + auto* item2 = dynamic_cast (second); + + if (item1 == nullptr || item2 == nullptr) + return 0; + + if (item1->file < item2->file) + return -1; + + if (item1->file > item2->file) + return 1; + + return 0; + } + }; + + 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(); } @@ -298,13 +462,7 @@ FileTreeComponent::~FileTreeComponent() void FileTreeComponent::refresh() { - deleteRootItem(); - - auto root = new FileListTreeItem (*this, nullptr, 0, directoryContentsList.getDirectory(), - directoryContentsList.getTimeSliceThread()); - - root->setSubContentsList (&directoryContentsList, false); - setRootItem (root); + controller->refresh(); } //============================================================================== @@ -333,8 +491,7 @@ void FileTreeComponent::setDragAndDropDescription (const String& description) void FileTreeComponent::setSelectedFile (const File& target) { - if (auto* t = dynamic_cast (getRootItem())) - t->selectFile (target); + controller->selectFile (target); } void FileTreeComponent::setItemHeight (int newHeight) diff --git a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.h b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.h index b021bc758293..192ce88f684c 100644 --- a/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.h +++ b/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.h @@ -99,6 +99,9 @@ class JUCE_API FileTreeComponent : public TreeView, String dragAndDropDescription; int itemHeight; + class Controller; + std::unique_ptr controller; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileTreeComponent) };