git repos / SagoImageBrowser

src/fsdirmodel.cpp

browsing at commit = edaf35e267126cebf2631d084097a394ad8b643b

raw · blame · history

#include "fsdirmodel.h"

#include <QFileIconProvider>

#include <algorithm>
#include <filesystem>
#include <vector>

namespace fs = std::filesystem;

// ---------------------------------------------------------------------------
// Static helpers
// ---------------------------------------------------------------------------

QList<FsDirModel::Node *> FsDirModel::listSubdirs(Node *parentNode)
{
    QList<Node *> result;
    try {
        std::vector<std::pair<std::string, std::string>> 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<qsizetype>(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<qsizetype>(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<qsizetype>(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<fs::path> 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<Node *> 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<Node *>(&m_virtualRoot);
    return static_cast<Node *>(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<Node *>(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<const Node *>(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<Node *> children = listSubdirs(node);
    node->populated = true;

    if (children.isEmpty())
        return;

    beginInsertRows(parent, 0, children.size() - 1);
    node->children = children;
    endInsertRows();
}