diff --git a/CMakeLists.txt b/CMakeLists.txt index a7fdec4..b332a92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,8 @@ qt_add_executable(SagoImageBrowser src/mainwindow.h src/imagemodel.cpp src/imagemodel.h + src/fsdirmodel.cpp + src/fsdirmodel.h src/thumbnailworker.cpp src/thumbnailworker.h src/thumbnailcache.cpp diff --git a/src/fsdirmodel.cpp b/src/fsdirmodel.cpp new file mode 100644 index 0000000..a9babb5 --- /dev/null +++ b/src/fsdirmodel.cpp @@ -0,0 +1,283 @@ +#include "fsdirmodel.h" + +#include +#include + +#include +#include +#include + +namespace fs = std::filesystem; + +// --------------------------------------------------------------------------- +// Static helpers +// --------------------------------------------------------------------------- + +QList FsDirModel::listSubdirs(Node *parentNode) +{ + QList result; + try { + std::vector> entries; // {nativePath, filename} + + for (const fs::directory_entry &entry : + fs::directory_iterator(parentNode->nativePath.toStdString(), + fs::directory_options::skip_permission_denied)) + { + std::error_code ec; + const std::string fname = entry.path().filename().native(); + if (fname.empty() || fname[0] == '.') + continue; + if (!entry.is_directory(ec)) + continue; + entries.push_back({entry.path().native(), fname}); + } + + std::sort(entries.begin(), entries.end(), + [](const auto &a, const auto &b) { return a.second < b.second; }); + + result.reserve(static_cast(entries.size())); + for (const auto &[npath, fname] : entries) { + auto *child = new Node; + child->nativePath = QByteArray::fromStdString(npath); + child->displayName = QString::fromLocal8Bit(fname.c_str(), + static_cast(fname.size())); + child->parent = parentNode; + result.append(child); + } + } catch (const fs::filesystem_error &) {} + + return result; +} + +void FsDirModel::destroyChildren(Node *node) +{ + for (Node *child : node->children) { + destroyChildren(child); + delete child; + } + node->children.clear(); + node->populated = false; +} + +// --------------------------------------------------------------------------- +// Construction / destruction +// --------------------------------------------------------------------------- + +FsDirModel::FsDirModel(QObject *parent) + : QAbstractItemModel(parent) +{ + m_virtualRoot.populated = true; // virtual root is always "populated" + + QFileIconProvider iconProvider; + m_folderIcon = iconProvider.icon(QFileIconProvider::Folder); +} + +FsDirModel::~FsDirModel() +{ + destroyChildren(&m_virtualRoot); +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +void FsDirModel::setRootPath(const QString &path) +{ + setRootPath(QFile::encodeName(path)); +} + +void FsDirModel::setRootPath(const QByteArray &nativePath) +{ + beginResetModel(); + destroyChildren(&m_virtualRoot); + + if (!nativePath.isEmpty()) { + auto *node = new Node; + node->nativePath = nativePath; + // Display name = last path component (empty for "/", use "/" in that case) + std::string fname = fs::path(nativePath.toStdString()).filename().native(); + node->displayName = fname.empty() + ? QFile::decodeName(nativePath) + : QString::fromLocal8Bit(fname.c_str(), static_cast(fname.size())); + node->parent = &m_virtualRoot; + m_virtualRoot.children.append(node); + } + + endResetModel(); +} + +QModelIndex FsDirModel::rootIndex() const +{ + if (m_virtualRoot.children.isEmpty()) + return {}; + return createIndex(0, 0, m_virtualRoot.children.first()); +} + +QByteArray FsDirModel::filePath(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + return nodeForIndex(index)->nativePath; +} + +QModelIndex FsDirModel::indexForPath(const QByteArray &nativePath) +{ + if (nativePath.isEmpty() || m_virtualRoot.children.isEmpty()) + return {}; + + const fs::path target(nativePath.toStdString()); + const fs::path root(m_virtualRoot.children.first()->nativePath.toStdString()); + + // Collect the ancestry chain from root down to target + std::vector chain; + fs::path cur = target; + while (cur != root) { + chain.push_back(cur); + fs::path up = cur.parent_path(); + if (up == cur) + return {}; // reached filesystem root without finding model root + cur = up; + } + // chain is [target, …, root_child]; reverse so we walk root→target + std::reverse(chain.begin(), chain.end()); + + // Start at the root path node + Node *node = m_virtualRoot.children.first(); + QModelIndex idx = createIndex(0, 0, node); + + for (const fs::path &step : chain) { + // Ensure this node's children are loaded + if (!node->populated) { + QList children = listSubdirs(node); + if (!children.isEmpty()) { + beginInsertRows(idx, 0, children.size() - 1); + node->children = children; + node->populated = true; + endInsertRows(); + } else { + node->populated = true; + } + } + + // Find the child whose nativePath matches step + const std::string stepStr = step.native(); + bool found = false; + for (int i = 0; i < node->children.size(); ++i) { + if (node->children[i]->nativePath.toStdString() == stepStr) { + node = node->children[i]; + idx = createIndex(i, 0, node); + found = true; + break; + } + } + if (!found) + return {}; + } + + return idx; +} + +// --------------------------------------------------------------------------- +// QAbstractItemModel interface +// --------------------------------------------------------------------------- + +FsDirModel::Node *FsDirModel::nodeForIndex(const QModelIndex &index) const +{ + if (!index.isValid()) + return const_cast(&m_virtualRoot); + return static_cast(index.internalPointer()); +} + +QModelIndex FsDirModel::indexForNode(Node *node) const +{ + if (!node || node == &m_virtualRoot) + return {}; + Node *parentNode = node->parent; + int row = parentNode->children.indexOf(node); + if (row < 0) + return {}; + return createIndex(row, 0, node); +} + +QModelIndex FsDirModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column != 0) + return {}; + + Node *parentNode = nodeForIndex(parent); + if (row < 0 || row >= parentNode->children.size()) + return {}; + + return createIndex(row, 0, parentNode->children[row]); +} + +QModelIndex FsDirModel::parent(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + Node *node = static_cast(index.internalPointer()); + Node *parentNode = node->parent; + + if (!parentNode || parentNode == &m_virtualRoot) + return {}; + + return indexForNode(parentNode); +} + +int FsDirModel::rowCount(const QModelIndex &parent) const +{ + return nodeForIndex(parent)->children.size(); +} + +int FsDirModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +QVariant FsDirModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + const Node *node = static_cast(index.internalPointer()); + + switch (role) { + case Qt::DisplayRole: + return node->displayName; + case Qt::DecorationRole: + return m_folderIcon; + default: + return {}; + } +} + +bool FsDirModel::hasChildren(const QModelIndex &parent) const +{ + const Node *node = nodeForIndex(parent); + if (!node->populated) + return true; // optimistic: show expand arrow until we actually look + return !node->children.isEmpty(); +} + +bool FsDirModel::canFetchMore(const QModelIndex &parent) const +{ + return !nodeForIndex(parent)->populated; +} + +void FsDirModel::fetchMore(const QModelIndex &parent) +{ + Node *node = nodeForIndex(parent); + if (node->populated) + return; + + QList children = listSubdirs(node); + node->populated = true; + + if (children.isEmpty()) + return; + + beginInsertRows(parent, 0, children.size() - 1); + node->children = children; + endInsertRows(); +} diff --git a/src/fsdirmodel.h b/src/fsdirmodel.h new file mode 100644 index 0000000..4c1ff85 --- /dev/null +++ b/src/fsdirmodel.h @@ -0,0 +1,70 @@ +#pragma once + +#include +#include +#include +#include + +// A directory-tree model that uses std::filesystem for enumeration so that +// directory entries with non-UTF-8 filenames (e.g. Latin-1 encoded names on +// a UTF-8 locale) are listed correctly instead of being silently dropped or +// mangled by QDir / QFileSystemModel. +// +// The model is lazily populated: child directories are enumerated only when +// the view first expands a node (via canFetchMore / fetchMore). +class FsDirModel : public QAbstractItemModel +{ + Q_OBJECT + +public: + explicit FsDirModel(QObject *parent = nullptr); + ~FsDirModel() override; + + // Set the directory shown as the tree root. Call rootIndex() afterwards + // and pass the result to QTreeView::setRootIndex() so the root node itself + // is hidden and only its children are displayed as top-level items. + void setRootPath(const QByteArray &nativePath); + void setRootPath(const QString &path); // convenience: converts via QFile::encodeName + + // Model index of the root-path node (the one whose *children* are shown + // as top-level items when passed to QTreeView::setRootIndex). + QModelIndex rootIndex() const; + + // Raw native OS byte path for the node at index. + QByteArray filePath(const QModelIndex &index) const; + + // Walk the tree to find (and lazily populate) the node for nativePath. + // Returns an invalid index if the path is not under the current root. + QModelIndex indexForPath(const QByteArray &nativePath); + + // QAbstractItemModel interface + QModelIndex index(int row, int column, + const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool hasChildren(const QModelIndex &parent = QModelIndex()) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + +private: + struct Node { + QByteArray nativePath; + QString displayName; + Node *parent = nullptr; + QList children; + bool populated = false; + }; + + // Synchronously populate node->children from the filesystem. + // Only call this inside fetchMore (with proper begin/endInsertRows around it). + static QList listSubdirs(Node *parent); + + Node *nodeForIndex(const QModelIndex &index) const; + QModelIndex indexForNode(Node *node) const; + static void destroyChildren(Node *node); + + Node m_virtualRoot; // hidden root; always has exactly 0 or 1 child + QIcon m_folderIcon; +}; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 110812f..6559d9d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -4,9 +4,10 @@ #include "preferencesdialog.h" #include "thumbnailcache.h" #include "exifreader.h" +#include "fsdirmodel.h" #include -#include +#include #include #include #include @@ -54,9 +55,9 @@ MainWindow::MainWindow(QWidget *parent) void MainWindow::setupUi() { - // Folder model (left pane) - m_dirModel = new QFileSystemModel(this); - m_dirModel->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + // Folder model (left pane) — backed by std::filesystem so directories with + // non-UTF-8 filenames are listed correctly. + m_dirModel = new FsDirModel(this); m_dirModel->setRootPath(QDir::homePath()); // Root chooser list @@ -74,7 +75,7 @@ void MainWindow::setupUi() m_treeView = new QTreeView; m_treeView->setModel(m_dirModel); - m_treeView->setRootIndex(m_dirModel->index(QDir::homePath())); + m_treeView->setRootIndex(m_dirModel->rootIndex()); m_treeView->header()->hide(); m_treeView->setColumnHidden(1, true); m_treeView->setColumnHidden(2, true); @@ -172,10 +173,7 @@ void MainWindow::navigateToFolder(const QByteArray &path) { m_previousFolderPath = m_imageModel->currentDirectory(); m_imageModel->setDirectory(path); - // The tree view uses QFileSystemModel which works with QString; convert for it. - // If the path contains non-UTF-8 bytes the index lookup may fail — that is - // acceptable (the tree view simply won't highlight the node). - QModelIndex dirIdx = m_dirModel->index(QFile::decodeName(path)); + QModelIndex dirIdx = m_dirModel->indexForPath(path); if (dirIdx.isValid()) m_treeView->setCurrentIndex(dirIdx); QTimer::singleShot(0, this, [this]() { @@ -240,13 +238,13 @@ void MainWindow::setupConnections() QString rootPath = current->data(Qt::UserRole).toString(); m_dirModel->setRootPath(rootPath); - m_treeView->setRootIndex(m_dirModel->index(rootPath)); + m_treeView->setRootIndex(m_dirModel->rootIndex()); }); connect(m_treeView, &QTreeView::clicked, this, [this](const QModelIndex &index) { - QString path = m_dirModel->filePath(index); + QByteArray path = m_dirModel->filePath(index); navigateToFolder(path); }); diff --git a/src/mainwindow.h b/src/mainwindow.h index 444b448..52b4511 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -9,7 +9,7 @@ class QLabel; class QStackedWidget; class QMenuBar; class QDockWidget; -class QFileSystemModel; +class FsDirModel; class QListWidget; class QTableWidget; class ImageModel; @@ -47,7 +47,7 @@ private: bool eventFilter(QObject *obj, QEvent *event) override; void closeEvent(QCloseEvent *event) override; - QFileSystemModel *m_dirModel; + FsDirModel *m_dirModel; ImageModel *m_imageModel; QTreeView *m_treeView;