diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index 1f8b39e..aa06e7e 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp @@ -6,6 +6,35 @@ #include #include #include +#include + +// --- ImageLoadWorker --- + +ImageLoadWorker::ImageLoadWorker(const QString &path, QObject *parent) + : QThread(parent), m_path(path) +{ +} + +void ImageLoadWorker::run() +{ + QImageReader reader(m_path); + reader.setAutoTransform(true); + reader.setDecideFormatFromContent(true); + reader.setAllocationLimit(0); + QImage image = reader.read(); + + QPixmap pixmap; + if (!image.isNull()) { + if (image.format() != QImage::Format_RGB32 + && image.format() != QImage::Format_ARGB32 + && image.format() != QImage::Format_ARGB32_Premultiplied) + image = image.convertToFormat(QImage::Format_ARGB32); + pixmap = QPixmap::fromImage(image); + } + emit imageLoaded(m_path, pixmap); +} + +// --- ImageViewWidget --- ImageViewWidget::ImageViewWidget(QWidget *parent) : QWidget(parent) @@ -15,29 +44,93 @@ ImageViewWidget::ImageViewWidget(QWidget *parent) setBackgroundColor("black"); } -void ImageViewWidget::setImage(const QString &path) +QPixmap ImageViewWidget::loadImageFromDisk(const QString &path) { QImageReader reader(path); reader.setAutoTransform(true); reader.setDecideFormatFromContent(true); - // Disable the allocation limit so very large images can be viewed. - // The default 128 MB limit rejects high-resolution photos. reader.setAllocationLimit(0); QImage image = reader.read(); if (!image.isNull()) { - // Ensure the image is in a high-quality format - if (image.format() != QImage::Format_RGB32 && image.format() != QImage::Format_ARGB32 && image.format() != QImage::Format_ARGB32_Premultiplied) + if (image.format() != QImage::Format_RGB32 + && image.format() != QImage::Format_ARGB32 + && image.format() != QImage::Format_ARGB32_Premultiplied) image = image.convertToFormat(QImage::Format_ARGB32); - m_pixmap = QPixmap::fromImage(image); + return QPixmap::fromImage(image); + } + return QPixmap(); +} + +void ImageViewWidget::setImage(const QString &path) +{ + m_currentPath = path; + + // Check cache first + if (m_cache.contains(path)) { + m_pixmap = m_cache[path]; + } else { + m_pixmap = loadImageFromDisk(path); + m_cache[path] = m_pixmap; } - else - m_pixmap = QPixmap(); m_zoomMode = FitToScreen; m_zoomFactor = 1.0; m_offset = QPoint(0, 0); update(); + + // Trigger prefetch of neighbors if they have been set + if (!m_neighborPaths.isEmpty()) { + // Evict entries that are no longer in the neighbor window + QSet keep(m_neighborPaths.begin(), m_neighborPaths.end()); + keep.insert(m_currentPath); + for (auto it = m_cache.begin(); it != m_cache.end(); ) { + if (!keep.contains(it.key())) + it = m_cache.erase(it); + else + ++it; + } + + // Prefetch neighbors that are not yet cached + for (const QString &neighbor : m_neighborPaths) { + prefetchImage(neighbor); + } + } +} + +void ImageViewWidget::prefetchImage(const QString &path) +{ + if (m_cache.contains(path) || m_pendingLoads.contains(path)) + return; + + m_pendingLoads.insert(path); + auto *worker = new ImageLoadWorker(path, this); + connect(worker, &ImageLoadWorker::imageLoaded, this, &ImageViewWidget::onImageLoaded); + connect(worker, &QThread::finished, worker, &QObject::deleteLater); + worker->start(); +} + +void ImageViewWidget::onImageLoaded(const QString &path, const QPixmap &pixmap) +{ + m_pendingLoads.remove(path); + + // Only cache if path is still relevant (current or a neighbor) + QSet keep(m_neighborPaths.begin(), m_neighborPaths.end()); + keep.insert(m_currentPath); + if (keep.contains(path)) { + m_cache[path] = pixmap; + } +} + +void ImageViewWidget::setNeighborPaths(const QStringList &paths) +{ + m_neighborPaths = paths; +} + +void ImageViewWidget::clearCache() +{ + m_cache.clear(); + m_neighborPaths.clear(); } void ImageViewWidget::setBackgroundColor(const QString &color) diff --git a/src/imageviewwidget.h b/src/imageviewwidget.h index 201d498..b7ebca4 100644 --- a/src/imageviewwidget.h +++ b/src/imageviewwidget.h @@ -5,12 +5,31 @@ #include #include #include +#include +#include +#include + +class ImageLoadWorker : public QThread +{ + Q_OBJECT +public: + explicit ImageLoadWorker(const QString &path, QObject *parent = nullptr); + void run() override; +signals: + void imageLoaded(const QString &path, const QPixmap &pixmap); +private: + QString m_path; +}; class ImageViewWidget : public QWidget { Q_OBJECT public: + // Number of images to read ahead and keep behind the current image + static constexpr int CacheReadAhead = 2; + static constexpr int CacheKeepBehind = 2; + enum ZoomMode { FitToScreen, OriginalSize, CustomZoom }; explicit ImageViewWidget(QWidget *parent = nullptr); @@ -22,6 +41,10 @@ public: void zoomOriginal(); void zoomFitToScreen(); + void prefetchImage(const QString &path); + void setNeighborPaths(const QStringList &paths); + void clearCache(); + signals: void closeRequested(); void nextRequested(); @@ -37,11 +60,19 @@ protected: private: void clampOffset(int imgW, int imgH); + QPixmap loadImageFromDisk(const QString &path); + void onImageLoaded(const QString &path, const QPixmap &pixmap); QPixmap m_pixmap; ZoomMode m_zoomMode = FitToScreen; double m_zoomFactor = 1.0; QString m_backgroundColor = "black"; + QString m_currentPath; + + // Image cache: path -> pixmap + QMap m_cache; + QStringList m_neighborPaths; // ordered list of paths around current image + QSet m_pendingLoads; // paths currently being loaded in background // Panning state QPoint m_offset; // current pan offset (pixels) diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index e5f1520..3c558ce 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -289,6 +289,7 @@ void MainWindow::enterSingleImageMode(const QModelIndex &index) m_imageView->setBackgroundColor(m_backgroundColorPreference); QString path = m_imageModel->filePath(index); + m_imageView->setNeighborPaths(computeNeighborPaths(index)); m_imageView->setImage(path); m_stack->setCurrentWidget(m_imageView); m_imageView->setFocus(); @@ -305,6 +306,7 @@ void MainWindow::leaveSingleImageMode() m_imageViewWasFullScreen = true; setFullScreenMode(false); } + m_imageView->clearCache(); m_stack->setCurrentWidget(m_browserPage); m_menuBar->show(); if (m_folderDockWasVisible) @@ -314,6 +316,34 @@ void MainWindow::leaveSingleImageMode() m_listView->setFocus(); } +QStringList MainWindow::computeNeighborPaths(const QModelIndex &index) const +{ + QStringList paths; + int row = index.row(); + + // Collect up to CacheKeepBehind non-folder images before current + int count = 0; + for (int i = row - 1; i >= 0 && count < ImageViewWidget::CacheKeepBehind; --i) { + QModelIndex idx = m_imageModel->index(i); + if (!m_imageModel->isFolder(idx)) { + paths.prepend(m_imageModel->filePath(idx)); + ++count; + } + } + + // Collect up to CacheReadAhead non-folder images after current + count = 0; + for (int i = row + 1; i < m_imageModel->rowCount() && count < ImageViewWidget::CacheReadAhead; ++i) { + QModelIndex idx = m_imageModel->index(i); + if (!m_imageModel->isFolder(idx)) { + paths.append(m_imageModel->filePath(idx)); + ++count; + } + } + + return paths; +} + void MainWindow::setFullScreenMode(bool fullScreen) { if (fullScreen == isFullScreen()) @@ -459,9 +489,4 @@ void MainWindow::resetLayoutToDefault() // Show both docks m_folderDock->show(); m_previewDock->show(); - - // Reset window to normal state - /*if (isMaximized() || isFullScreen()) { - showNormal(); - }*/ } diff --git a/src/mainwindow.h b/src/mainwindow.h index 0dbbcf0..e981315 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -36,6 +36,7 @@ private: void showPreview(const QModelIndex &index); void enterSingleImageMode(const QModelIndex &index); void leaveSingleImageMode(); + QStringList computeNeighborPaths(const QModelIndex &index) const; void keyPressEvent(QKeyEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;