1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00
JUCE/modules/juce_gui_basics/filebrowser/juce_FileTreeComponent.cpp
attila a400d3ebe0 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.
2023-04-19 08:54:50 +02:00

508 lines
14 KiB
C++

/*
==============================================================================
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
{
//==============================================================================
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<void (const File&, bool)> 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 (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<DirectoryContentsList*> (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<File, DirectoryContentsList> 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 <typename ItemCallback>
static void forEachItemRecursive (TreeViewItem* item, ItemCallback&& cb)
{
if (item == nullptr)
return;
if (auto* fileListItem = dynamic_cast<FileListTreeItem*> (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<File> 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<FileListTreeItem*> (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<FileListTreeItem*> (first);
auto* item2 = dynamic_cast<FileListTreeItem*> (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<FileListTreeItem> createNewItem (const File& file)
{
auto newItem = std::make_unique<FileListTreeItem> (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<File, FileListTreeItem*> treeItemForFile;
DirectoryScanner scanner;
std::optional<File> pendingFileSelection;
};
//==============================================================================
FileTreeComponent::FileTreeComponent (DirectoryContentsList& listToShow)
: DirectoryContentsDisplayComponent (listToShow),
itemHeight (22)
{
controller = std::make_unique<Controller> (*this);
setRootItemVisible (false);
refresh();
}
FileTreeComponent::~FileTreeComponent()
{
deleteRootItem();
}
void FileTreeComponent::refresh()
{
controller->refresh();
}
//==============================================================================
File FileTreeComponent::getSelectedFile (const int index) const
{
if (auto* item = dynamic_cast<const FileListTreeItem*> (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();
}
}
} // namespace juce