git repos / SagoImageBrowser

blame: src/imagemodel.cpp

normal view · raw

14cf904f Poul Sander 2026-06-22 18:18 1
/*
14cf904f Poul Sander 2026-06-22 18:18 2
MIT License
14cf904f Poul Sander 2026-06-22 18:18 3
14cf904f Poul Sander 2026-06-22 18:18 4
Copyright (c) 2026 Poul Sander
14cf904f Poul Sander 2026-06-22 18:18 5
14cf904f Poul Sander 2026-06-22 18:18 6
Permission is hereby granted, free of charge, to any person obtaining a copy
14cf904f Poul Sander 2026-06-22 18:18 7
of this software and associated documentation files (the "Software"), to deal
14cf904f Poul Sander 2026-06-22 18:18 8
in the Software without restriction, including without limitation the rights
14cf904f Poul Sander 2026-06-22 18:18 9
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14cf904f Poul Sander 2026-06-22 18:18 10
copies of the Software, and to permit persons to whom the Software is
14cf904f Poul Sander 2026-06-22 18:18 11
furnished to do so, subject to the following conditions:
14cf904f Poul Sander 2026-06-22 18:18 12
14cf904f Poul Sander 2026-06-22 18:18 13
The above copyright notice and this permission notice shall be included in all
14cf904f Poul Sander 2026-06-22 18:18 14
copies or substantial portions of the Software.
14cf904f Poul Sander 2026-06-22 18:18 15
14cf904f Poul Sander 2026-06-22 18:18 16
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14cf904f Poul Sander 2026-06-22 18:18 17
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14cf904f Poul Sander 2026-06-22 18:18 18
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14cf904f Poul Sander 2026-06-22 18:18 19
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14cf904f Poul Sander 2026-06-22 18:18 20
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
14cf904f Poul Sander 2026-06-22 18:18 21
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
14cf904f Poul Sander 2026-06-22 18:18 22
SOFTWARE.
14cf904f Poul Sander 2026-06-22 18:18 23
14cf904f Poul Sander 2026-06-22 18:18 24
*/
14cf904f Poul Sander 2026-06-22 18:18 25
032aa1e1 Poul Sander 2026-02-19 18:55 26
#include "imagemodel.h"
cec3b5da Poul Sander 2026-02-19 19:17 27
#include "thumbnailworker.h"
032aa1e1 Poul Sander 2026-02-19 18:55 28
032aa1e1 Poul Sander 2026-02-19 18:55 29
#include <QImageReader>
09130ab9 Poul Sander 2026-02-22 22:19 30
#include <QFileIconProvider>
032aa1e1 Poul Sander 2026-02-19 18:55 31
80a9926d Poul Sander 2026-05-19 20:28 32
#include <algorithm>
80a9926d Poul Sander 2026-05-19 20:28 33
#include <filesystem>
80a9926d Poul Sander 2026-05-19 20:28 34
#include <vector>
80a9926d Poul Sander 2026-05-19 20:28 35
032aa1e1 Poul Sander 2026-02-19 18:55 36
ImageModel::ImageModel(QObject *parent)
032aa1e1 Poul Sander 2026-02-19 18:55 37
    : QAbstractListModel(parent)
032aa1e1 Poul Sander 2026-02-19 18:55 38
{
459bad0f Poul Sander 2026-05-17 14:11 39
    // Limit 1 thread for thumbnail loading to avoid excessive disk I/O on harddrives. SSDs are so fast the extra threads don't help much anyway.
459bad0f Poul Sander 2026-05-17 14:11 40
    m_threadPool.setMaxThreadCount(1);  //QThread::idealThreadCount()
cec3b5da Poul Sander 2026-02-19 19:17 41
cec3b5da Poul Sander 2026-02-19 19:17 42
    m_placeholder = QPixmap(128, 128);
cec3b5da Poul Sander 2026-02-19 19:17 43
    m_placeholder.fill(Qt::lightGray);
09130ab9 Poul Sander 2026-02-22 22:19 44
09130ab9 Poul Sander 2026-02-22 22:19 45
    QFileIconProvider iconProvider;
09130ab9 Poul Sander 2026-02-22 22:19 46
    m_folderIcon = iconProvider.icon(QFileIconProvider::Folder);
cec3b5da Poul Sander 2026-02-19 19:17 47
}
cec3b5da Poul Sander 2026-02-19 19:17 48
cec3b5da Poul Sander 2026-02-19 19:17 49
ImageModel::~ImageModel()
cec3b5da Poul Sander 2026-02-19 19:17 50
{
6fcd6c90 Poul Sander 2026-02-19 21:12 51
    m_cancelFlag = true;
cec3b5da Poul Sander 2026-02-19 19:17 52
    m_threadPool.waitForDone();
032aa1e1 Poul Sander 2026-02-19 18:55 53
}
032aa1e1 Poul Sander 2026-02-19 18:55 54
459bad0f Poul Sander 2026-05-17 14:11 55
void ImageModel::setCacheMaxSize(int n)
459bad0f Poul Sander 2026-05-17 14:11 56
{
459bad0f Poul Sander 2026-05-17 14:11 57
    m_cacheMaxSize = std::max(0, n);
459bad0f Poul Sander 2026-05-17 14:11 58
    while (m_cacheOrder.size() > m_cacheMaxSize)
459bad0f Poul Sander 2026-05-17 14:11 59
        m_folderThumbnailCache.remove(m_cacheOrder.takeFirst());
459bad0f Poul Sander 2026-05-17 14:11 60
}
459bad0f Poul Sander 2026-05-17 14:11 61
ac476ca8 Poul Sander 2026-05-18 16:47 62
void ImageModel::setThumbnailSize(ThumbnailCache::Size size)
ac476ca8 Poul Sander 2026-05-18 16:47 63
{
ac476ca8 Poul Sander 2026-05-18 16:47 64
    if (size == m_thumbnailSize)
ac476ca8 Poul Sander 2026-05-18 16:47 65
        return;
ac476ca8 Poul Sander 2026-05-18 16:47 66
ac476ca8 Poul Sander 2026-05-18 16:47 67
    m_thumbnailSize = size;
ac476ca8 Poul Sander 2026-05-18 16:47 68
ac476ca8 Poul Sander 2026-05-18 16:47 69
    // Cancel any in-flight workers (they were started with the previous size).
ac476ca8 Poul Sander 2026-05-18 16:47 70
    m_cancelFlag = true;
ac476ca8 Poul Sander 2026-05-18 16:47 71
    m_threadPool.waitForDone();
ac476ca8 Poul Sander 2026-05-18 16:47 72
    m_cancelFlag = false;
ac476ca8 Poul Sander 2026-05-18 16:47 73
ac476ca8 Poul Sander 2026-05-18 16:47 74
    m_pendingRows.clear();
ac476ca8 Poul Sander 2026-05-18 16:47 75
ac476ca8 Poul Sander 2026-05-18 16:47 76
    // The folder cache is keyed by directory and size-agnostic; drop it so the
ac476ca8 Poul Sander 2026-05-18 16:47 77
    // user actually sees the new size.
ac476ca8 Poul Sander 2026-05-18 16:47 78
    m_folderThumbnailCache.clear();
ac476ca8 Poul Sander 2026-05-18 16:47 79
    m_cacheOrder.clear();
ac476ca8 Poul Sander 2026-05-18 16:47 80
ac476ca8 Poul Sander 2026-05-18 16:47 81
    // Mark existing image entries unloaded so they get re-fetched at the new size.
ac476ca8 Poul Sander 2026-05-18 16:47 82
    if (!m_items.isEmpty())
ac476ca8 Poul Sander 2026-05-18 16:47 83
    {
ac476ca8 Poul Sander 2026-05-18 16:47 84
        for (int row = 0; row < m_items.size(); ++row)
ac476ca8 Poul Sander 2026-05-18 16:47 85
        {
ac476ca8 Poul Sander 2026-05-18 16:47 86
            Item &item = m_items[row];
ac476ca8 Poul Sander 2026-05-18 16:47 87
            if (item.isFolder)
ac476ca8 Poul Sander 2026-05-18 16:47 88
                continue;
ac476ca8 Poul Sander 2026-05-18 16:47 89
            item.thumbnail = m_placeholder;
ac476ca8 Poul Sander 2026-05-18 16:47 90
            item.loaded = false;
ac476ca8 Poul Sander 2026-05-18 16:47 91
        }
ac476ca8 Poul Sander 2026-05-18 16:47 92
        emit dataChanged(index(0), index(m_items.size() - 1),
ac476ca8 Poul Sander 2026-05-18 16:47 93
                         {Qt::DecorationRole});
ac476ca8 Poul Sander 2026-05-18 16:47 94
    }
ac476ca8 Poul Sander 2026-05-18 16:47 95
}
ac476ca8 Poul Sander 2026-05-18 16:47 96
032aa1e1 Poul Sander 2026-02-19 18:55 97
void ImageModel::setDirectory(const QString &path)
032aa1e1 Poul Sander 2026-02-19 18:55 98
{
80a9926d Poul Sander 2026-05-19 20:28 99
    setDirectory(QFile::encodeName(path));
80a9926d Poul Sander 2026-05-19 20:28 100
}
80a9926d Poul Sander 2026-05-19 20:28 101
80a9926d Poul Sander 2026-05-19 20:28 102
void ImageModel::setDirectory(const QByteArray &path)
80a9926d Poul Sander 2026-05-19 20:28 103
{
6fcd6c90 Poul Sander 2026-02-19 21:12 104
    m_cancelFlag = true;
6fcd6c90 Poul Sander 2026-02-19 21:12 105
    m_threadPool.waitForDone();
6fcd6c90 Poul Sander 2026-02-19 21:12 106
    m_cancelFlag = false;
6fcd6c90 Poul Sander 2026-02-19 21:12 107
459bad0f Poul Sander 2026-05-17 14:11 108
    // Save current folder's thumbnails to cache before discarding them
459bad0f Poul Sander 2026-05-17 14:11 109
    if (!m_currentDir.isEmpty()) {
459bad0f Poul Sander 2026-05-17 14:11 110
        ThumbnailMap map;
459bad0f Poul Sander 2026-05-17 14:11 111
        for (const Item &item : m_items)
459bad0f Poul Sander 2026-05-17 14:11 112
            if (item.loaded && !item.isFolder)
459bad0f Poul Sander 2026-05-17 14:11 113
                map[item.path] = item.thumbnail;
459bad0f Poul Sander 2026-05-17 14:11 114
        if (!map.isEmpty()) {
459bad0f Poul Sander 2026-05-17 14:11 115
            if (!m_cacheOrder.contains(m_currentDir)) {
459bad0f Poul Sander 2026-05-17 14:11 116
                m_cacheOrder.append(m_currentDir);
459bad0f Poul Sander 2026-05-17 14:11 117
                if (m_cacheOrder.size() > m_cacheMaxSize)
459bad0f Poul Sander 2026-05-17 14:11 118
                    m_folderThumbnailCache.remove(m_cacheOrder.takeFirst());
459bad0f Poul Sander 2026-05-17 14:11 119
            }
459bad0f Poul Sander 2026-05-17 14:11 120
            m_folderThumbnailCache[m_currentDir] = std::move(map);
459bad0f Poul Sander 2026-05-17 14:11 121
        }
459bad0f Poul Sander 2026-05-17 14:11 122
    }
459bad0f Poul Sander 2026-05-17 14:11 123
032aa1e1 Poul Sander 2026-02-19 18:55 124
    beginResetModel();
032aa1e1 Poul Sander 2026-02-19 18:55 125
    m_items.clear();
6fcd6c90 Poul Sander 2026-02-19 21:12 126
    m_pendingRows.clear();
09130ab9 Poul Sander 2026-02-22 22:19 127
    m_currentDir = path;
032aa1e1 Poul Sander 2026-02-19 18:55 128
80a9926d Poul Sander 2026-05-19 20:28 129
    // Build set of supported image extensions (lower-case, without dot)
80a9926d Poul Sander 2026-05-19 20:28 130
    QSet<QByteArray> imageExts;
80a9926d Poul Sander 2026-05-19 20:28 131
    for (const QByteArray &fmt : QImageReader::supportedImageFormats())
80a9926d Poul Sander 2026-05-19 20:28 132
        imageExts.insert(fmt.toLower());
80a9926d Poul Sander 2026-05-19 20:28 133
80a9926d Poul Sander 2026-05-19 20:28 134
    // Use std::filesystem::directory_iterator so that directory entries with
80a9926d Poul Sander 2026-05-19 20:28 135
    // non-UTF-8 filenames (e.g. Latin-1 encoded names) are not silently
80a9926d Poul Sander 2026-05-19 20:28 136
    // skipped or mangled as they would be with QDir::entryInfoList().
80a9926d Poul Sander 2026-05-19 20:28 137
    namespace fs = std::filesystem;
80a9926d Poul Sander 2026-05-19 20:28 138
80a9926d Poul Sander 2026-05-19 20:28 139
    std::vector<std::string> dirs;
80a9926d Poul Sander 2026-05-19 20:28 140
    std::vector<std::string> imageFiles;
80a9926d Poul Sander 2026-05-19 20:28 141
80a9926d Poul Sander 2026-05-19 20:28 142
    try {
80a9926d Poul Sander 2026-05-19 20:28 143
        fs::path dirPath(path.toStdString());
80a9926d Poul Sander 2026-05-19 20:28 144
80a9926d Poul Sander 2026-05-19 20:28 145
        // Add ".." parent entry directly with the literal display name ".."
80a9926d Poul Sander 2026-05-19 20:28 146
        // (mirrors QDir::NoDot behaviour)
80a9926d Poul Sander 2026-05-19 20:28 147
        fs::path parentPath = dirPath.parent_path();
80a9926d Poul Sander 2026-05-19 20:28 148
        if (!parentPath.empty() && parentPath != dirPath) {
80a9926d Poul Sander 2026-05-19 20:28 149
            QPixmap folderPixmap = m_folderIcon.pixmap(128, 128);
80a9926d Poul Sander 2026-05-19 20:28 150
            m_items.append({QByteArray::fromStdString(parentPath.native()),
80a9926d Poul Sander 2026-05-19 20:28 151
                            QStringLiteral(".."), folderPixmap, true, true});
80a9926d Poul Sander 2026-05-19 20:28 152
        }
09130ab9 Poul Sander 2026-02-22 22:19 153
80a9926d Poul Sander 2026-05-19 20:28 154
        for (const fs::directory_entry &entry :
80a9926d Poul Sander 2026-05-19 20:28 155
             fs::directory_iterator(dirPath,
80a9926d Poul Sander 2026-05-19 20:28 156
                 fs::directory_options::skip_permission_denied))
80a9926d Poul Sander 2026-05-19 20:28 157
        {
80a9926d Poul Sander 2026-05-19 20:28 158
            std::error_code ec;
80a9926d Poul Sander 2026-05-19 20:28 159
            const std::string fname = entry.path().filename().native();
80a9926d Poul Sander 2026-05-19 20:28 160
            if (fname.empty() || fname[0] == '.')
80a9926d Poul Sander 2026-05-19 20:28 161
                continue;   // skip hidden entries
80a9926d Poul Sander 2026-05-19 20:28 162
80a9926d Poul Sander 2026-05-19 20:28 163
            if (entry.is_directory(ec)) {
80a9926d Poul Sander 2026-05-19 20:28 164
                dirs.push_back(entry.path().native());
80a9926d Poul Sander 2026-05-19 20:28 165
            } else if (entry.is_regular_file(ec)) {
80a9926d Poul Sander 2026-05-19 20:28 166
                // Filter by image extension
80a9926d Poul Sander 2026-05-19 20:28 167
                const std::string &nativeExt = entry.path().extension().native();
80a9926d Poul Sander 2026-05-19 20:28 168
                if (nativeExt.empty())
80a9926d Poul Sander 2026-05-19 20:28 169
                    continue;
80a9926d Poul Sander 2026-05-19 20:28 170
                // extension() includes the dot; strip it and lower-case
80a9926d Poul Sander 2026-05-19 20:28 171
                QByteArray ext = QByteArray(nativeExt.c_str() + 1,
80a9926d Poul Sander 2026-05-19 20:28 172
                                           static_cast<qsizetype>(nativeExt.size()) - 1).toLower();
80a9926d Poul Sander 2026-05-19 20:28 173
                if (imageExts.contains(ext))
80a9926d Poul Sander 2026-05-19 20:28 174
                    imageFiles.push_back(entry.path().native());
80a9926d Poul Sander 2026-05-19 20:28 175
            }
80a9926d Poul Sander 2026-05-19 20:28 176
        }
80a9926d Poul Sander 2026-05-19 20:28 177
    } catch (const fs::filesystem_error &) {
80a9926d Poul Sander 2026-05-19 20:28 178
        // Directory unreadable — show empty listing
09130ab9 Poul Sander 2026-02-22 22:19 179
    }
09130ab9 Poul Sander 2026-02-22 22:19 180
80a9926d Poul Sander 2026-05-19 20:28 181
    // Sort subdirectories by filename
80a9926d Poul Sander 2026-05-19 20:28 182
    std::sort(dirs.begin(), dirs.end(),
80a9926d Poul Sander 2026-05-19 20:28 183
              [](const auto &a, const auto &b) {
80a9926d Poul Sander 2026-05-19 20:28 184
                  return fs::path(a).filename() < fs::path(b).filename();
80a9926d Poul Sander 2026-05-19 20:28 185
              });
032aa1e1 Poul Sander 2026-02-19 18:55 186
80a9926d Poul Sander 2026-05-19 20:28 187
    std::sort(imageFiles.begin(), imageFiles.end(),
80a9926d Poul Sander 2026-05-19 20:28 188
              [](const auto &a, const auto &b) {
80a9926d Poul Sander 2026-05-19 20:28 189
                  return fs::path(a).filename() < fs::path(b).filename();
80a9926d Poul Sander 2026-05-19 20:28 190
              });
032aa1e1 Poul Sander 2026-02-19 18:55 191
80a9926d Poul Sander 2026-05-19 20:28 192
    QPixmap folderPixmap = m_folderIcon.pixmap(128, 128);
80a9926d Poul Sander 2026-05-19 20:28 193
    for (const std::string &nativePath : dirs)
80a9926d Poul Sander 2026-05-19 20:28 194
    {
80a9926d Poul Sander 2026-05-19 20:28 195
        QByteArray entryPath = QByteArray::fromStdString(nativePath);
80a9926d Poul Sander 2026-05-19 20:28 196
        std::string fname = fs::path(nativePath).filename().native();
80a9926d Poul Sander 2026-05-19 20:28 197
        QString displayName = QString::fromLocal8Bit(fname.c_str(),
80a9926d Poul Sander 2026-05-19 20:28 198
                                                     static_cast<qsizetype>(fname.size()));
80a9926d Poul Sander 2026-05-19 20:28 199
        m_items.append({entryPath, displayName, folderPixmap, true, true});
80a9926d Poul Sander 2026-05-19 20:28 200
    }
cec3b5da Poul Sander 2026-02-19 19:17 201
459bad0f Poul Sander 2026-05-17 14:11 202
    // Restore cached thumbnails for this folder if available
459bad0f Poul Sander 2026-05-17 14:11 203
    const ThumbnailMap *cached = m_folderThumbnailCache.contains(path)
459bad0f Poul Sander 2026-05-17 14:11 204
        ? &m_folderThumbnailCache[path]
459bad0f Poul Sander 2026-05-17 14:11 205
        : nullptr;
459bad0f Poul Sander 2026-05-17 14:11 206
80a9926d Poul Sander 2026-05-19 20:28 207
    for (const std::string &nativePath : imageFiles)
032aa1e1 Poul Sander 2026-02-19 18:55 208
    {
80a9926d Poul Sander 2026-05-19 20:28 209
        QByteArray entryPath = QByteArray::fromStdString(nativePath);
80a9926d Poul Sander 2026-05-19 20:28 210
        std::string fname = fs::path(nativePath).filename().native();
80a9926d Poul Sander 2026-05-19 20:28 211
        QString displayName = QString::fromLocal8Bit(fname.c_str(),
80a9926d Poul Sander 2026-05-19 20:28 212
                                                     static_cast<qsizetype>(fname.size()));
80a9926d Poul Sander 2026-05-19 20:28 213
459bad0f Poul Sander 2026-05-17 14:11 214
        QPixmap thumb = m_placeholder;
459bad0f Poul Sander 2026-05-17 14:11 215
        bool loaded = false;
459bad0f Poul Sander 2026-05-17 14:11 216
        if (cached) {
80a9926d Poul Sander 2026-05-19 20:28 217
            auto it = cached->find(entryPath);
459bad0f Poul Sander 2026-05-17 14:11 218
            if (it != cached->end()) {
459bad0f Poul Sander 2026-05-17 14:11 219
                thumb = it.value();
459bad0f Poul Sander 2026-05-17 14:11 220
                loaded = true;
459bad0f Poul Sander 2026-05-17 14:11 221
            }
459bad0f Poul Sander 2026-05-17 14:11 222
        }
80a9926d Poul Sander 2026-05-19 20:28 223
        m_items.append({entryPath, displayName, thumb, loaded, false});
6fcd6c90 Poul Sander 2026-02-19 21:12 224
    }
cec3b5da Poul Sander 2026-02-19 19:17 225
6fcd6c90 Poul Sander 2026-02-19 21:12 226
    endResetModel();
6fcd6c90 Poul Sander 2026-02-19 21:12 227
}
032aa1e1 Poul Sander 2026-02-19 18:55 228
6fcd6c90 Poul Sander 2026-02-19 21:12 229
void ImageModel::requestThumbnails(int firstRow, int lastRow)
6fcd6c90 Poul Sander 2026-02-19 21:12 230
{
6fcd6c90 Poul Sander 2026-02-19 21:12 231
    if (m_items.isEmpty())
6fcd6c90 Poul Sander 2026-02-19 21:12 232
        return;
032aa1e1 Poul Sander 2026-02-19 18:55 233
6fcd6c90 Poul Sander 2026-02-19 21:12 234
    firstRow = std::max(0, firstRow);
6fcd6c90 Poul Sander 2026-02-19 21:12 235
    lastRow = std::min(lastRow, static_cast<int>(m_items.size()) - 1);
cec3b5da Poul Sander 2026-02-19 19:17 236
6fcd6c90 Poul Sander 2026-02-19 21:12 237
    for (int row = firstRow; row <= lastRow; ++row)
6fcd6c90 Poul Sander 2026-02-19 21:12 238
    {
09130ab9 Poul Sander 2026-02-22 22:19 239
        if (!m_items[row].loaded && !m_items[row].isFolder)
6fcd6c90 Poul Sander 2026-02-19 21:12 240
            queueRow(row);
032aa1e1 Poul Sander 2026-02-19 18:55 241
    }
032aa1e1 Poul Sander 2026-02-19 18:55 242
6fcd6c90 Poul Sander 2026-02-19 21:12 243
    // Optional: preload nearby rows
6fcd6c90 Poul Sander 2026-02-19 21:12 244
    int buffer = 20;
6fcd6c90 Poul Sander 2026-02-19 21:12 245
    for (int row = firstRow - buffer; row < firstRow; ++row)
09130ab9 Poul Sander 2026-02-22 22:19 246
        if (row >= 0 && !m_items[row].loaded && !m_items[row].isFolder)
6fcd6c90 Poul Sander 2026-02-19 21:12 247
            queueRow(row);
6fcd6c90 Poul Sander 2026-02-19 21:12 248
6fcd6c90 Poul Sander 2026-02-19 21:12 249
    for (int row = lastRow + 1; row <= lastRow + buffer; ++row)
09130ab9 Poul Sander 2026-02-22 22:19 250
        if (row < m_items.size() && !m_items[row].loaded && !m_items[row].isFolder)
6fcd6c90 Poul Sander 2026-02-19 21:12 251
            queueRow(row);
6fcd6c90 Poul Sander 2026-02-19 21:12 252
}
6fcd6c90 Poul Sander 2026-02-19 21:12 253
48534775 Poul Sander 2026-06-08 18:57 254
void ImageModel::refreshThumbnail(const QModelIndex &index)
48534775 Poul Sander 2026-06-08 18:57 255
{
48534775 Poul Sander 2026-06-08 18:57 256
    if (!index.isValid())
48534775 Poul Sander 2026-06-08 18:57 257
        return;
48534775 Poul Sander 2026-06-08 18:57 258
48534775 Poul Sander 2026-06-08 18:57 259
    const int row = index.row();
48534775 Poul Sander 2026-06-08 18:57 260
    if (row < 0 || row >= m_items.size() || m_items[row].isFolder)
48534775 Poul Sander 2026-06-08 18:57 261
        return;
48534775 Poul Sander 2026-06-08 18:57 262
48534775 Poul Sander 2026-06-08 18:57 263
    // The on-disk thumbnail cache is keyed by source mtime, so it invalidates
48534775 Poul Sander 2026-06-08 18:57 264
    // itself once the file is rewritten. Drop the in-memory copy and re-decode.
48534775 Poul Sander 2026-06-08 18:57 265
    m_items[row].thumbnail = m_placeholder;
48534775 Poul Sander 2026-06-08 18:57 266
    m_items[row].loaded = false;
48534775 Poul Sander 2026-06-08 18:57 267
    emit dataChanged(index, index, {Qt::DecorationRole});
48534775 Poul Sander 2026-06-08 18:57 268
    queueRow(row);
48534775 Poul Sander 2026-06-08 18:57 269
}
48534775 Poul Sander 2026-06-08 18:57 270
6fcd6c90 Poul Sander 2026-02-19 21:12 271
void ImageModel::queueRow(int row)
6fcd6c90 Poul Sander 2026-02-19 21:12 272
{
6fcd6c90 Poul Sander 2026-02-19 21:12 273
    if (m_pendingRows.contains(row))
6fcd6c90 Poul Sander 2026-02-19 21:12 274
        return;
6fcd6c90 Poul Sander 2026-02-19 21:12 275
6fcd6c90 Poul Sander 2026-02-19 21:12 276
    m_pendingRows.insert(row);
6fcd6c90 Poul Sander 2026-02-19 21:12 277
6fcd6c90 Poul Sander 2026-02-19 21:12 278
    auto *worker = new ThumbnailWorker(
6fcd6c90 Poul Sander 2026-02-19 21:12 279
        m_items[row].path,
6fcd6c90 Poul Sander 2026-02-19 21:12 280
        row,
ac476ca8 Poul Sander 2026-05-18 16:47 281
        m_thumbnailSize,
6fcd6c90 Poul Sander 2026-02-19 21:12 282
        &m_cancelFlag);
6fcd6c90 Poul Sander 2026-02-19 21:12 283
4d6b7476 Poul Sander 2026-05-30 16:39 284
    // THREADING: Explicit QueuedConnection ensures thumbnailReady() is invoked
4d6b7476 Poul Sander 2026-05-30 16:39 285
    // on the main thread even though the signal is emitted from a pool thread.
4d6b7476 Poul Sander 2026-05-30 16:39 286
    // This means all m_items[] mutations happen on the main thread only.
6fcd6c90 Poul Sander 2026-02-19 21:12 287
    connect(worker, &ThumbnailWorker::finished,
6fcd6c90 Poul Sander 2026-02-19 21:12 288
            this, &ImageModel::thumbnailReady,
6fcd6c90 Poul Sander 2026-02-19 21:12 289
            Qt::QueuedConnection);
6fcd6c90 Poul Sander 2026-02-19 21:12 290
6fcd6c90 Poul Sander 2026-02-19 21:12 291
    m_threadPool.start(worker);
032aa1e1 Poul Sander 2026-02-19 18:55 292
}
032aa1e1 Poul Sander 2026-02-19 18:55 293
80a9926d Poul Sander 2026-05-19 20:28 294
QByteArray ImageModel::filePath(const QModelIndex &index) const
032aa1e1 Poul Sander 2026-02-19 18:55 295
{
032aa1e1 Poul Sander 2026-02-19 18:55 296
    if (!index.isValid())
032aa1e1 Poul Sander 2026-02-19 18:55 297
        return {};
032aa1e1 Poul Sander 2026-02-19 18:55 298
032aa1e1 Poul Sander 2026-02-19 18:55 299
    return m_items[index.row()].path;
032aa1e1 Poul Sander 2026-02-19 18:55 300
}
032aa1e1 Poul Sander 2026-02-19 18:55 301
09130ab9 Poul Sander 2026-02-22 22:19 302
bool ImageModel::isFolder(const QModelIndex &index) const
09130ab9 Poul Sander 2026-02-22 22:19 303
{
09130ab9 Poul Sander 2026-02-22 22:19 304
    if (!index.isValid() || index.row() >= m_items.size())
09130ab9 Poul Sander 2026-02-22 22:19 305
        return false;
09130ab9 Poul Sander 2026-02-22 22:19 306
    return m_items[index.row()].isFolder;
09130ab9 Poul Sander 2026-02-22 22:19 307
}
09130ab9 Poul Sander 2026-02-22 22:19 308
80a9926d Poul Sander 2026-05-19 20:28 309
QByteArray ImageModel::currentDirectory() const
09130ab9 Poul Sander 2026-02-22 22:19 310
{
09130ab9 Poul Sander 2026-02-22 22:19 311
    return m_currentDir;
09130ab9 Poul Sander 2026-02-22 22:19 312
}
09130ab9 Poul Sander 2026-02-22 22:19 313
032aa1e1 Poul Sander 2026-02-19 18:55 314
int ImageModel::rowCount(const QModelIndex &) const
032aa1e1 Poul Sander 2026-02-19 18:55 315
{
032aa1e1 Poul Sander 2026-02-19 18:55 316
    return m_items.size();
032aa1e1 Poul Sander 2026-02-19 18:55 317
}
032aa1e1 Poul Sander 2026-02-19 18:55 318
032aa1e1 Poul Sander 2026-02-19 18:55 319
QVariant ImageModel::data(const QModelIndex &index, int role) const
032aa1e1 Poul Sander 2026-02-19 18:55 320
{
032aa1e1 Poul Sander 2026-02-19 18:55 321
    if (!index.isValid())
032aa1e1 Poul Sander 2026-02-19 18:55 322
        return {};
032aa1e1 Poul Sander 2026-02-19 18:55 323
032aa1e1 Poul Sander 2026-02-19 18:55 324
    const Item &item = m_items[index.row()];
032aa1e1 Poul Sander 2026-02-19 18:55 325
032aa1e1 Poul Sander 2026-02-19 18:55 326
    if (role == Qt::DecorationRole)
032aa1e1 Poul Sander 2026-02-19 18:55 327
        return item.thumbnail;
032aa1e1 Poul Sander 2026-02-19 18:55 328
032aa1e1 Poul Sander 2026-02-19 18:55 329
    if (role == Qt::DisplayRole)
09130ab9 Poul Sander 2026-02-22 22:19 330
        return item.displayName;
032aa1e1 Poul Sander 2026-02-19 18:55 331
1dcea855 Poul Sander 2026-05-26 21:48 332
    if (role == Qt::UserRole)
1dcea855 Poul Sander 2026-05-26 21:48 333
        return item.isFolder;
1dcea855 Poul Sander 2026-05-26 21:48 334
032aa1e1 Poul Sander 2026-02-19 18:55 335
    return {};
032aa1e1 Poul Sander 2026-02-19 18:55 336
}
cec3b5da Poul Sander 2026-02-19 19:17 337
6fcd6c90 Poul Sander 2026-02-19 21:12 338
void ImageModel::thumbnailReady(int row, const QImage &image)
cec3b5da Poul Sander 2026-02-19 19:17 339
{
6fcd6c90 Poul Sander 2026-02-19 21:12 340
    if (m_cancelFlag)
6fcd6c90 Poul Sander 2026-02-19 21:12 341
        return;
6fcd6c90 Poul Sander 2026-02-19 21:12 342
cec3b5da Poul Sander 2026-02-19 19:17 343
    if (row < 0 || row >= m_items.size())
cec3b5da Poul Sander 2026-02-19 19:17 344
        return;
cec3b5da Poul Sander 2026-02-19 19:17 345
6fcd6c90 Poul Sander 2026-02-19 21:12 346
    m_items[row].thumbnail = QPixmap::fromImage(image);
cec3b5da Poul Sander 2026-02-19 19:17 347
    m_items[row].loaded = true;
6fcd6c90 Poul Sander 2026-02-19 21:12 348
    m_pendingRows.remove(row);
cec3b5da Poul Sander 2026-02-19 19:17 349
cec3b5da Poul Sander 2026-02-19 19:17 350
    QModelIndex idx = index(row);
cec3b5da Poul Sander 2026-02-19 19:17 351
    emit dataChanged(idx, idx, {Qt::DecorationRole});
cec3b5da Poul Sander 2026-02-19 19:17 352
}
1970-01-01 00:00 353