#include "imageviewwidget.h" #include #include #include #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) { setFocusPolicy(Qt::StrongFocus); setAutoFillBackground(true); setBackgroundColor("black"); } QPixmap ImageViewWidget::loadImageFromDisk(const QString &path) { QImageReader reader(path); reader.setAutoTransform(true); reader.setDecideFormatFromContent(true); reader.setAllocationLimit(0); QImage image = reader.read(); 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); 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; } 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) { m_backgroundColor = color; QPalette pal = palette(); pal.setColor(QPalette::Window, QColor(color)); setPalette(pal); update(); } void ImageViewWidget::paintEvent(QPaintEvent *) { if (m_pixmap.isNull()) return; QPainter painter(this); painter.setRenderHint(QPainter::SmoothPixmapTransform, true); painter.setRenderHint(QPainter::Antialiasing, true); QPixmap drawn; switch (m_zoomMode) { case FitToScreen: drawn = m_pixmap.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); break; case OriginalSize: drawn = m_pixmap; break; case CustomZoom: { QSize target(static_cast(m_pixmap.width() * m_zoomFactor), static_cast(m_pixmap.height() * m_zoomFactor)); drawn = m_pixmap.scaled(target, Qt::KeepAspectRatio, Qt::SmoothTransformation); break; } } clampOffset(drawn.width(), drawn.height()); int x = (width() - drawn.width()) / 2 + m_offset.x(); int y = (height() - drawn.height()) / 2 + m_offset.y(); painter.drawPixmap(x, y, drawn); } void ImageViewWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { emit closeRequested(); event->accept(); return; } if (event->key() == Qt::Key_PageDown) { emit nextRequested(); event->accept(); return; } if (event->key() == Qt::Key_PageUp) { emit previousRequested(); event->accept(); return; } // Arrow keys for panning constexpr int panStep = 50; if (event->key() == Qt::Key_Left) { m_offset.rx() += panStep; update(); event->accept(); return; } if (event->key() == Qt::Key_Right) { m_offset.rx() -= panStep; update(); event->accept(); return; } if (event->key() == Qt::Key_Up) { m_offset.ry() += panStep; update(); event->accept(); return; } if (event->key() == Qt::Key_Down) { m_offset.ry() -= panStep; update(); event->accept(); return; } if (event->key() == Qt::Key_Slash) { zoomOriginal(); event->accept(); return; } if (event->key() == Qt::Key_Asterisk) { zoomFitToScreen(); event->accept(); return; } if (event->key() == Qt::Key_Plus) { zoomIn(); event->accept(); return; } if (event->key() == Qt::Key_Minus) { zoomOut(); event->accept(); return; } QWidget::keyPressEvent(event); } void ImageViewWidget::zoomIn() { if (m_zoomMode == FitToScreen) { // Calculate the current effective scale so zooming feels continuous double sx = static_cast(width()) / m_pixmap.width(); double sy = static_cast(height()) / m_pixmap.height(); m_zoomFactor = std::min(sx, sy); } m_zoomFactor *= 1.25; m_zoomMode = CustomZoom; update(); } void ImageViewWidget::zoomOut() { if (m_zoomMode == FitToScreen) { double sx = static_cast(width()) / m_pixmap.width(); double sy = static_cast(height()) / m_pixmap.height(); m_zoomFactor = std::min(sx, sy); } m_zoomFactor /= 1.25; if (m_zoomFactor < 0.01) m_zoomFactor = 0.01; m_zoomMode = CustomZoom; update(); } void ImageViewWidget::zoomOriginal() { m_zoomMode = OriginalSize; m_zoomFactor = 1.0; update(); } void ImageViewWidget::zoomFitToScreen() { m_zoomMode = FitToScreen; m_offset = QPoint(0, 0); update(); } void ImageViewWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { m_dragging = true; m_dragStart = event->pos(); m_offsetAtDragStart = m_offset; setCursor(Qt::ClosedHandCursor); event->accept(); } } void ImageViewWidget::mouseMoveEvent(QMouseEvent *event) { if (m_dragging) { m_offset = m_offsetAtDragStart + (event->pos() - m_dragStart); update(); event->accept(); } } void ImageViewWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton && m_dragging) { m_dragging = false; setCursor(Qt::ArrowCursor); event->accept(); } } void ImageViewWidget::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { emit closeRequested(); event->accept(); } } void ImageViewWidget::clampOffset(int imgW, int imgH) { // Only allow panning when the drawn image exceeds the widget int overflowX = imgW - width(); int overflowY = imgH - height(); if (overflowX <= 0) m_offset.rx() = 0; else { int limit = overflowX / 2; m_offset.rx() = std::clamp(m_offset.x(), -limit, limit); } if (overflowY <= 0) m_offset.ry() = 0; else { int limit = overflowY / 2; m_offset.ry() = std::clamp(m_offset.y(), -limit, limit); } }