mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-09 23:34:20 +00:00
690 lines
21 KiB
C++
690 lines
21 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
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 <typename T>
|
|
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 <size_t position, typename... Ts>
|
|
constexpr int threeWayCompareImpl (const std::tuple<Ts...>& a, const std::tuple<Ts...>& b)
|
|
{
|
|
if constexpr (position == sizeof... (Ts))
|
|
{
|
|
ignoreUnused (a, b);
|
|
return 0;
|
|
}
|
|
else
|
|
{
|
|
const auto head = threeWayCompare (std::get<position> (a), std::get<position> (b));
|
|
|
|
if (head != 0)
|
|
return head;
|
|
|
|
return threeWayCompareImpl<position + 1> (a, b);
|
|
}
|
|
}
|
|
|
|
template <typename... Ts>
|
|
constexpr int threeWayCompare (const std::tuple<Ts...>& a, const std::tuple<Ts...>& 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<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 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<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;
|
|
};
|
|
|
|
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 <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
|
|
{
|
|
// 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<FileListTreeItem*> (first);
|
|
auto* item2 = dynamic_cast<FileListTreeItem*> (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<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();
|
|
}
|
|
}
|
|
|
|
#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<String>& 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<FileEntry> 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<FileEntry> 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<FileEntry> 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
|