From aac8d4e68b2d1b94fa6893f5dcf38af1553b74b2 Mon Sep 17 00:00:00 2001 From: Alexopus Date: Wed, 12 Nov 2025 21:45:01 +0100 Subject: [PATCH] Add file tree view --- CMakeLists.txt | 4 + src/fileview/fileview.cpp | 233 +++++++++++++++++++++++++-- src/fileview/fileview.h | 17 ++ src/fileview/fileview.ui | 156 +++++++++++++++--- src/fileview/fileviewlist.cpp | 8 +- src/fileview/fileviewtree.cpp | 205 ++++++++++++++++++++++++ src/fileview/fileviewtree.h | 78 +++++++++ src/fileview/fileviewtreeitem.h | 52 ++++++ src/fileview/fileviewtreemodel.cpp | 247 +++++++++++++++++++++++++++++ src/fileview/fileviewtreemodel.h | 72 +++++++++ 10 files changed, 1036 insertions(+), 36 deletions(-) create mode 100644 src/fileview/fileviewtree.cpp create mode 100644 src/fileview/fileviewtree.h create mode 100644 src/fileview/fileviewtreeitem.h create mode 100644 src/fileview/fileviewtreemodel.cpp create mode 100644 src/fileview/fileviewtreemodel.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9db5a974a..4da7792c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -823,6 +823,8 @@ set(SOURCES src/fileview/fileview.cpp src/fileview/fileviewlist.cpp + src/fileview/fileviewtree.cpp + src/fileview/fileviewtreemodel.cpp src/device/devicemanager.cpp src/device/devicelister.cpp @@ -1112,6 +1114,8 @@ set(HEADERS src/fileview/fileview.h src/fileview/fileviewlist.h + src/fileview/fileviewtree.h + src/fileview/fileviewtreemodel.h src/device/devicemanager.h src/device/devicelister.h diff --git a/src/fileview/fileview.cpp b/src/fileview/fileview.cpp index 2de566317..b73b8a799 100644 --- a/src/fileview/fileview.cpp +++ b/src/fileview/fileview.cpp @@ -29,13 +29,17 @@ #include #include #include -#include +#include #include #include #include #include +#include +#include #include +#include "constants/appearancesettings.h" +#include "constants/filefilterconstants.h" #include "includes/shared_ptr.h" #include "core/deletefiles.h" #include "core/filesystemmusicstorage.h" @@ -45,10 +49,11 @@ #include "dialogs/deleteconfirmationdialog.h" #include "fileview.h" #include "fileviewlist.h" +#include "fileviewtree.h" +#include "fileviewtreemodel.h" +#include "fileviewtreeitem.h" #include "ui_fileview.h" #include "organize/organizeerrordialog.h" -#include "constants/appearancesettings.h" -#include "constants/filefilterconstants.h" using std::make_unique; using namespace Qt::Literals::StringLiterals; @@ -57,9 +62,12 @@ FileView::FileView(QWidget *parent) : QWidget(parent), ui_(new Ui_FileView), model_(nullptr), + tree_model_(nullptr), undo_stack_(new QUndoStack(this)), task_manager_(nullptr), - storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) { + storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)), + tree_view_active_(false), + view_mode_spacer_(nullptr) { ui_->setupUi(this); @@ -68,12 +76,14 @@ FileView::FileView(QWidget *parent) ui_->forward->setIcon(IconLoader::Load(u"go-next"_s)); ui_->home->setIcon(IconLoader::Load(u"go-home"_s)); ui_->up->setIcon(IconLoader::Load(u"go-up"_s)); + ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s)); QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo); QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo); QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome); QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp); QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath); + QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode); QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled); QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled); @@ -87,6 +97,22 @@ FileView::FileView(QWidget *parent) QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete); QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags); + // Connect tree view signals + QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist); + QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection); + QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection); + QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice); + QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete); + QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags); + QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated); + QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick); + + // Setup tree root management buttons + ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s)); + ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s)); + QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked); + QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked); + QString filter = QLatin1String(kFileFilter); filter_list_ << filter.split(u' '); @@ -109,6 +135,19 @@ void FileView::ReloadSettings() { ui_->forward->setIconSize(QSize(iconsize, iconsize)); ui_->home->setIconSize(QSize(iconsize, iconsize)); ui_->up->setIconSize(QSize(iconsize, iconsize)); + ui_->toggle_view->setIconSize(QSize(iconsize, iconsize)); + ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize)); + ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize)); + + // Load tree root paths setting + Settings file_settings; + file_settings.beginGroup(u"FileView"_s); + tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList(); + tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool(); + file_settings.endGroup(); + + // Set initial view mode + UpdateViewModeUI(); } @@ -180,24 +219,46 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) { } void FileView::ItemActivated(const QModelIndex &idx) { - if (model_->isDir(idx)) + // Only handle activation for list view (not tree view) + if (!tree_view_active_ && model_->isDir(idx)) { ChangeFilePath(model_->filePath(idx)); + } } void FileView::ItemDoubleClick(const QModelIndex &idx) { - if (model_->isDir(idx)) { - return; + QString file_path; + bool is_file = false; + + // Handle tree view with virtual roots + if (tree_view_active_ && tree_model_) { + QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type); + if (type_var.isValid()) { + FileViewTreeItem::Type item_type = type_var.value(); + // Only handle files, ignore directories and virtual roots + if (item_type == FileViewTreeItem::Type::File) { + file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString(); + is_file = true; + } + } + } + // Handle list view with filesystem model + else if (!tree_view_active_ && model_) { + if (!model_->isDir(idx)) { + file_path = model_->filePath(idx); + is_file = true; + } } - QString file_path = model_->filePath(idx); + // Add file to playlist if it's a valid file + if (is_file && !file_path.isEmpty()) { + MimeData *mimedata = new MimeData; + mimedata->from_doubleclick_ = true; + mimedata->setUrls(QList() << QUrl::fromLocalFile(file_path)); + mimedata->name_for_new_playlist_ = file_path; - MimeData *mimedata = new MimeData; - mimedata->from_doubleclick_ = true; - mimedata->setUrls(QList() << QUrl::fromLocalFile(file_path)); - mimedata->name_for_new_playlist_ = file_path; - - Q_EMIT AddToPlaylist(mimedata); + Q_EMIT AddToPlaylist(mimedata); + } } @@ -272,12 +333,156 @@ void FileView::showEvent(QShowEvent *e) { model_->setNameFilterDisables(false); ui_->list->setModel(model_); + + // Create tree model + tree_model_ = new FileViewTreeModel(this); + tree_model_->SetNameFilters(filter_list_); + + SetupTreeView(); + ChangeFilePathWithoutUndo(QDir::homePath()); if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_); } +void FileView::SetupTreeView() { + + // Use the new tree model with virtual roots + ui_->tree->setModel(tree_model_); + + // Set the root paths in the model + tree_model_->SetRootPaths(tree_root_paths_); + + // No need to set root index - the model handles virtual roots + +} + +void FileView::ToggleViewMode() { + + tree_view_active_ = !tree_view_active_; + UpdateViewModeUI(); + + // Save the preference + Settings s; + s.beginGroup(u"FileView"_s); + s.setValue(u"tree_view_active"_s, tree_view_active_); + s.endGroup(); + +} + +void FileView::UpdateViewModeUI() { + + if (tree_view_active_) { + ui_->view_stack->setCurrentWidget(ui_->tree_page); + // Hide navigation controls in tree view mode + ui_->back->setVisible(false); + ui_->forward->setVisible(false); + ui_->up->setVisible(false); + ui_->home->setVisible(false); + ui_->path->setVisible(false); + // Show tree root management buttons + ui_->add_tree_root->setVisible(true); + ui_->remove_tree_root->setVisible(true); + // Insert spacer in tree view if not already present + if (!view_mode_spacer_) { + view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_); + } + } + else { + ui_->view_stack->setCurrentWidget(ui_->list_page); + // Show navigation controls in list view mode + ui_->back->setVisible(true); + ui_->forward->setVisible(true); + ui_->up->setVisible(true); + ui_->home->setVisible(true); + ui_->path->setVisible(true); + // Hide tree root management buttons in list view + ui_->add_tree_root->setVisible(false); + ui_->remove_tree_root->setVisible(false); + // Remove spacer in list view + if (view_mode_spacer_) { + ui_->horizontalLayout->removeItem(view_mode_spacer_); + delete view_mode_spacer_; + view_mode_spacer_ = nullptr; + } + } + +} + +void FileView::AddTreeRootPath(const QString &path) { + + if (!tree_root_paths_.contains(path)) { + tree_root_paths_.append(path); + SaveTreeRootPaths(); + + // Refresh the tree view to show the new root + if (tree_model_) { + SetupTreeView(); + } + } + +} + +void FileView::RemoveTreeRootPath(const QString &path) { + + tree_root_paths_.removeAll(path); + SaveTreeRootPaths(); + + // Refresh the tree view + if (tree_model_) { + SetupTreeView(); + } + +} + +void FileView::SaveTreeRootPaths() { + + Settings s; + s.beginGroup(u"FileView"_s); + s.setValue(u"tree_root_paths"_s, tree_root_paths_); + s.endGroup(); + +} + +void FileView::AddRootButtonClicked() { + + const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!dir.isEmpty()) { + AddTreeRootPath(dir); + } + +} + +void FileView::RemoveRootButtonClicked() { + + // Get currently selected item in tree + QModelIndex current = ui_->tree->currentIndex(); + if (!current.isValid()) return; + + QString path; + + // Get the file path from the appropriate model + if (tree_model_) { + path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString(); + } + + if (path.isEmpty()) return; + + const QString clean_path = QDir::cleanPath(path); + + // Check if this path or any parent is a configured root + for (const QString &root : std::as_const(tree_root_paths_)) { + const QString clean_root = QDir::cleanPath(root); + if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) { + RemoveTreeRootPath(root); + return; + } + } + +} + void FileView::keyPressEvent(QKeyEvent *e) { switch (e->key()) { diff --git a/src/fileview/fileview.h b/src/fileview/fileview.h index 81beabd7c..ef6627cd4 100644 --- a/src/fileview/fileview.h +++ b/src/fileview/fileview.h @@ -40,10 +40,12 @@ class QFileIconProvider; class QUndoStack; class QKeyEvent; class QShowEvent; +class QSpacerItem; class MusicStorage; class TaskManager; class Ui_FileView; +class FileViewTreeModel; class FileView : public QWidget { Q_OBJECT @@ -76,12 +78,22 @@ class FileView : public QWidget { void ChangeFilePath(const QString &new_path); void ItemActivated(const QModelIndex &idx); void ItemDoubleClick(const QModelIndex &idx); + void ToggleViewMode(); void Delete(const QStringList &filenames); void DeleteFinished(const SongList &songs_with_errors); + public Q_SLOTS: + void AddTreeRootPath(const QString &path); + void RemoveTreeRootPath(const QString &path); + private: void ChangeFilePathWithoutUndo(const QString &new_path); + void SetupTreeView(); + void SaveTreeRootPaths(); + void AddRootButtonClicked(); + void RemoveRootButtonClicked(); + void UpdateViewModeUI(); private: class UndoCommand : public QUndoCommand { @@ -110,16 +122,21 @@ class FileView : public QWidget { Ui_FileView *ui_; QFileSystemModel *model_; + FileViewTreeModel *tree_model_; QUndoStack *undo_stack_; SharedPtr task_manager_; SharedPtr storage_; QString lazy_set_path_; + QStringList tree_root_paths_; QStringList filter_list_; ScopedPtr file_icon_provider_; + + bool tree_view_active_; + QSpacerItem *view_mode_spacer_; }; #endif // FILEVIEW_H diff --git a/src/fileview/fileview.ui b/src/fileview/fileview.ui index 62dfc3c71..45748b27f 100644 --- a/src/fileview/fileview.ui +++ b/src/fileview/fileview.ui @@ -95,28 +95,143 @@ + + + + Add root directory + + + + 22 + 22 + + + + true + + + + + + + Remove selected root directory + + + + 22 + 22 + + + + true + + + + + + + + 22 + 22 + + + + true + + + Toggle between list and tree view + + + - - - true - - - QAbstractItemView::DragOnly - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - - 16 - 16 - + + + 0 + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + QAbstractItemView::DragOnly + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + 16 + 16 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + QAbstractItemView::DragOnly + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + 16 + 16 + + + + + + @@ -127,6 +242,11 @@ QListView
fileview/fileviewlist.h
+ + FileViewTree + QTreeView +
fileview/fileviewtree.h
+
diff --git a/src/fileview/fileviewlist.cpp b/src/fileview/fileviewlist.cpp index ee7f368b5..2ebed6cfc 100644 --- a/src/fileview/fileviewlist.cpp +++ b/src/fileview/fileviewlist.cpp @@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const { const QStringList filenames = FilenamesFromSelection(); - // if just one folder selected - use its path as the new playlist's name + // If just one folder selected - use its path as the new playlist's name if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) { if (filenames.first().length() > 20) { mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName(); @@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const { mimedata->name_for_new_playlist_ = filenames.first(); } } - // otherwise, use the current root path + // Otherwise, use the current root path else { QString path = qobject_cast(model())->rootPath(); if (path.length() > 20) { @@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) { case Qt::XButton2: Q_EMIT Forward(); break; - // enqueue to playlist with middleClick + // Enqueue to playlist with middleClick case Qt::MiddleButton:{ QListView::mousePressEvent(e); - // we need to update the menu selection + // We need to update the menu selection menu_selection_ = selectionModel()->selection(); MimeData *mimedata = new MimeData; diff --git a/src/fileview/fileviewtree.cpp b/src/fileview/fileviewtree.cpp new file mode 100644 index 000000000..2a078d97c --- /dev/null +++ b/src/fileview/fileviewtree.cpp @@ -0,0 +1,205 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/iconloader.h" +#include "core/mimedata.h" +#include "utilities/filemanagerutils.h" +#include "fileviewtree.h" +#include "fileviewtreemodel.h" + +using namespace Qt::Literals::StringLiterals; + +FileViewTree::FileViewTree(QWidget *parent) + : QTreeView(parent), + menu_(new QMenu(this)) { + + menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot); + menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot); + menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot); + menu_->addSeparator(); + menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot); + menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot); + menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot); + menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot); + + menu_->addSeparator(); + menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot); + menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser); + + setAttribute(Qt::WA_MacShowFocusRect, false); + setHeaderHidden(true); + setUniformRowHeights(true); + +} + +void FileViewTree::contextMenuEvent(QContextMenuEvent *e) { + + menu_selection_ = selectionModel()->selection(); + + menu_->popup(e->globalPos()); + e->accept(); + +} + +QStringList FileViewTree::FilenamesFromSelection() const { + + QStringList filenames; + const QModelIndexList indexes = menu_selection_.indexes(); + + FileViewTreeModel *tree_model = qobject_cast(model()); + if (tree_model) { + for (const QModelIndex &index : indexes) { + if (index.column() == 0) { + QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString(); + if (!path.isEmpty()) { + filenames << path; + } + } + } + } + + QCollator collator; + collator.setNumericMode(true); + std::sort(filenames.begin(), filenames.end(), collator); + + return filenames; + +} + +QList FileViewTree::UrlListFromSelection() const { + + QList urls; + const QStringList filenames = FilenamesFromSelection(); + urls.reserve(filenames.count()); + for (const QString &filename : std::as_const(filenames)) { + urls << QUrl::fromLocalFile(filename); + } + + return urls; + +} + +MimeData *FileViewTree::MimeDataFromSelection() const { + + MimeData *mimedata = new MimeData; + mimedata->setUrls(UrlListFromSelection()); + + const QStringList filenames = FilenamesFromSelection(); + + // if just one folder selected - use its path as the new playlist's name + if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) { + if (filenames.first().length() > 20) { + mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName(); + } + else { + mimedata->name_for_new_playlist_ = filenames.first(); + } + } + // otherwise, use "Files" as default + else { + mimedata->name_for_new_playlist_ = tr("Files"); + } + + return mimedata; + +} + +void FileViewTree::LoadSlot() { + + MimeData *mimedata = MimeDataFromSelection(); + mimedata->clear_first_ = true; + Q_EMIT AddToPlaylist(mimedata); + +} + +void FileViewTree::AddToPlaylistSlot() { + Q_EMIT AddToPlaylist(MimeDataFromSelection()); +} + +void FileViewTree::OpenInNewPlaylistSlot() { + + MimeData *mimedata = MimeDataFromSelection(); + mimedata->open_in_new_playlist_ = true; + Q_EMIT AddToPlaylist(mimedata); + +} + +void FileViewTree::CopyToCollectionSlot() { + Q_EMIT CopyToCollection(UrlListFromSelection()); +} + +void FileViewTree::MoveToCollectionSlot() { + Q_EMIT MoveToCollection(UrlListFromSelection()); +} + +void FileViewTree::CopyToDeviceSlot() { + Q_EMIT CopyToDevice(UrlListFromSelection()); +} + +void FileViewTree::DeleteSlot() { + Q_EMIT Delete(FilenamesFromSelection()); +} + +void FileViewTree::EditTagsSlot() { + Q_EMIT EditTags(UrlListFromSelection()); +} + +void FileViewTree::mousePressEvent(QMouseEvent *e) { + + switch (e->button()) { + // Enqueue to playlist with middleClick + case Qt::MiddleButton:{ + QTreeView::mousePressEvent(e); + + // We need to update the menu selection + QItemSelectionModel *selection_model = selectionModel(); + if (!selection_model) { + e->ignore(); + return; + } + menu_selection_ = selection_model->selection(); + + MimeData *mimedata = new MimeData; + mimedata->setUrls(UrlListFromSelection()); + mimedata->enqueue_now_ = true; + Q_EMIT AddToPlaylist(mimedata); + break; + } + default: + QTreeView::mousePressEvent(e); + break; + } + +} + +void FileViewTree::ShowInBrowser() { + Utilities::OpenInFileBrowser(UrlListFromSelection()); +} diff --git a/src/fileview/fileviewtree.h b/src/fileview/fileviewtree.h new file mode 100644 index 000000000..2fc1b9e13 --- /dev/null +++ b/src/fileview/fileviewtree.h @@ -0,0 +1,78 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef FILEVIEWTREE_H +#define FILEVIEWTREE_H + +#include +#include +#include +#include +#include +#include + +class QWidget; +class QMimeData; +class QMenu; +class QMouseEvent; +class QContextMenuEvent; + +class MimeData; + +class FileViewTree : public QTreeView { + Q_OBJECT + + public: + explicit FileViewTree(QWidget *parent = nullptr); + + void mousePressEvent(QMouseEvent *e) override; + + Q_SIGNALS: + void AddToPlaylist(QMimeData *data); + void CopyToCollection(const QList &urls); + void MoveToCollection(const QList &urls); + void CopyToDevice(const QList &urls); + void Delete(const QStringList &filenames); + void EditTags(const QList &urls); + + protected: + void contextMenuEvent(QContextMenuEvent *e) override; + + private: + QStringList FilenamesFromSelection() const; + QList UrlListFromSelection() const; + MimeData *MimeDataFromSelection() const; + + private Q_SLOTS: + void LoadSlot(); + void AddToPlaylistSlot(); + void OpenInNewPlaylistSlot(); + void CopyToCollectionSlot(); + void MoveToCollectionSlot(); + void CopyToDeviceSlot(); + void DeleteSlot(); + void EditTagsSlot(); + void ShowInBrowser(); + + private: + QMenu *menu_; + QItemSelection menu_selection_; +}; + +#endif // FILEVIEWTREE_H diff --git a/src/fileview/fileviewtreeitem.h b/src/fileview/fileviewtreeitem.h new file mode 100644 index 000000000..e24268af6 --- /dev/null +++ b/src/fileview/fileviewtreeitem.h @@ -0,0 +1,52 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef FILEVIEWTREEITEM_H +#define FILEVIEWTREEITEM_H + +#include "config.h" + +#include + +#include "core/simpletreeitem.h" + +class FileViewTreeItem : public SimpleTreeItem { + public: + enum class Type { + Root, // Hidden root + VirtualRoot, // User-configured root paths + Directory, // File system directory + File // File system file + }; + + explicit FileViewTreeItem(SimpleTreeModel *_model) : SimpleTreeItem(_model), type(Type::Root), lazy_loaded(false) {} + explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem(_parent), type(_type), lazy_loaded(false) {} + + Type type; + QString file_path; // Absolute file system path + QFileInfo file_info; // Cached file info + bool lazy_loaded; // Whether children have been loaded + + private: + Q_DISABLE_COPY(FileViewTreeItem) +}; + +Q_DECLARE_METATYPE(FileViewTreeItem::Type) + +#endif // FILEVIEWTREEITEM_H diff --git a/src/fileview/fileviewtreemodel.cpp b/src/fileview/fileviewtreemodel.cpp new file mode 100644 index 000000000..0261f2b46 --- /dev/null +++ b/src/fileview/fileviewtreemodel.cpp @@ -0,0 +1,247 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/simpletreemodel.h" +#include "core/logging.h" +#include "fileviewtreemodel.h" +#include "fileviewtreeitem.h" + +using namespace Qt::Literals::StringLiterals; + +FileViewTreeModel::FileViewTreeModel(QObject *parent) + : SimpleTreeModel(new FileViewTreeItem(this), parent), + icon_provider_(new QFileIconProvider()) { +} + +FileViewTreeModel::~FileViewTreeModel() { + delete root_; + delete icon_provider_; +} + +Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const { + + const FileViewTreeItem *item = IndexToItem(idx); + if (!item) return Qt::NoItemFlags; + + switch (item->type) { + case FileViewTreeItem::Type::VirtualRoot: + case FileViewTreeItem::Type::Directory: + case FileViewTreeItem::Type::File: + return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled; + case FileViewTreeItem::Type::Root: + default: + return Qt::ItemIsEnabled; + } + +} + +QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const { + + if (!idx.isValid()) return QVariant(); + + const FileViewTreeItem *item = IndexToItem(idx); + if (!item) return QVariant(); + + switch (role) { + case Qt::DisplayRole: + if (item->type == FileViewTreeItem::Type::VirtualRoot) { + return item->display_text.isEmpty() ? item->file_path : item->display_text; + } + return item->file_info.fileName(); + + case Qt::DecorationRole: + return GetIcon(item); + + case Role_Type: + return QVariant::fromValue(item->type); + + case Role_FilePath: + return item->file_path; + + case Role_FileName: + return item->file_info.fileName(); + + default: + return QVariant(); + } + +} + +bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const { + + const FileViewTreeItem *item = IndexToItem(parent); + if (!item) return false; + + // Root and VirtualRoot always have children (or can have them) + if (item->type == FileViewTreeItem::Type::Root) return true; + if (item->type == FileViewTreeItem::Type::VirtualRoot) return true; + + // Directories can have children + if (item->type == FileViewTreeItem::Type::Directory) { + return true; + } + + // Files don't have children + return false; + +} + +bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const { + + const FileViewTreeItem *item = IndexToItem(parent); + if (!item) return false; + + // Can fetch more if not yet lazy loaded + return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory); + +} + +void FileViewTreeModel::fetchMore(const QModelIndex &parent) { + + FileViewTreeItem *item = IndexToItem(parent); + if (!item || item->lazy_loaded) return; + + LazyLoad(item); + +} + +void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) { + + if (item->lazy_loaded) return; + + QDir dir(item->file_path); + if (!dir.exists()) { + item->lazy_loaded = true; + return; + } + + // Apply name filters + QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot; + if (!name_filters_.isEmpty()) { + dir.setNameFilters(name_filters_); + } + + QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst); + + if (!entries.isEmpty()) { + BeginInsert(item, 0, static_cast(entries.count()) - 1); + + for (const QFileInfo &entry : entries) { + FileViewTreeItem *child = new FileViewTreeItem( + entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File, + item + ); + child->file_path = entry.absoluteFilePath(); + child->file_info = entry; + child->lazy_loaded = false; + child->display_text = entry.fileName(); + } + + EndInsert(); + } + + item->lazy_loaded = true; + +} + +QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const { + + if (!item) return QIcon(); + + switch (item->type) { + case FileViewTreeItem::Type::VirtualRoot: + case FileViewTreeItem::Type::Directory: + return icon_provider_->icon(QFileIconProvider::Folder); + case FileViewTreeItem::Type::File: + return icon_provider_->icon(item->file_info); + default: + return QIcon(); + } + +} + +QStringList FileViewTreeModel::mimeTypes() const { + return QStringList() << u"text/uri-list"_s; +} + +QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const { + + if (indexes.isEmpty()) return nullptr; + + QList urls; + for (const QModelIndex &idx : indexes) { + const FileViewTreeItem *item = IndexToItem(idx); + if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) { + urls << QUrl::fromLocalFile(item->file_path); + } + } + + if (urls.isEmpty()) return nullptr; + + QMimeData *data = new QMimeData(); + data->setUrls(urls); + return data; + +} + +void FileViewTreeModel::SetRootPaths(const QStringList &paths) { + + Reset(); + + for (const QString &path : paths) { + QFileInfo info(path); + if (!info.exists() || !info.isDir()) continue; + + FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_); + virtual_root->file_path = info.absoluteFilePath(); + virtual_root->file_info = info; + virtual_root->display_text = info.absoluteFilePath(); + virtual_root->lazy_loaded = false; + } + +} + +void FileViewTreeModel::SetNameFilters(const QStringList &filters) { + name_filters_ = filters; +} + +void FileViewTreeModel::Reset() { + + beginResetModel(); + + // Clear children without notifications since we're in a reset + qDeleteAll(root_->children); + root_->children.clear(); + + endResetModel(); + +} diff --git a/src/fileview/fileviewtreemodel.h b/src/fileview/fileviewtreemodel.h new file mode 100644 index 000000000..80a5f8f62 --- /dev/null +++ b/src/fileview/fileviewtreemodel.h @@ -0,0 +1,72 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef FILEVIEWTREEMODEL_H +#define FILEVIEWTREEMODEL_H + +#include "config.h" + +#include +#include +#include +#include + +#include "core/simpletreemodel.h" +#include "fileviewtreeitem.h" + +class QFileIconProvider; +class QMimeData; + +class FileViewTreeModel : public SimpleTreeModel { + Q_OBJECT + + public: + explicit FileViewTreeModel(QObject *parent = nullptr); + ~FileViewTreeModel() override; + + enum Role { + Role_Type = Qt::UserRole + 1, + Role_FilePath, + Role_FileName, + RoleCount + }; + + // QAbstractItemModel + Qt::ItemFlags flags(const QModelIndex &idx) const override; + QVariant data(const QModelIndex &idx, const int role) const override; + bool hasChildren(const QModelIndex &parent) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + + void SetRootPaths(const QStringList &paths); + void SetNameFilters(const QStringList &filters); + + private: + void Reset(); + void LazyLoad(FileViewTreeItem *item); + QIcon GetIcon(const FileViewTreeItem *item) const; + + private: + QFileIconProvider *icon_provider_; + QStringList name_filters_; +}; + +#endif // FILEVIEWTREEMODEL_H