diff --git a/CMakeLists.txt b/CMakeLists.txt index eb016dd..78ebf17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,15 +7,15 @@ set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_add_executable(ImageBrowser - main.cpp - mainwindow.cpp - mainwindow.h - imagemodel.cpp - imagemodel.h - thumbnailworker.cpp - thumbnailworker.h - imageviewwidget.cpp - imageviewwidget.h + src/main.cpp + src/mainwindow.cpp + src/mainwindow.h + src/imagemodel.cpp + src/imagemodel.h + src/thumbnailworker.cpp + src/thumbnailworker.h + src/imageviewwidget.cpp + src/imageviewwidget.h ) target_link_libraries(ImageBrowser PRIVATE Qt6::Widgets) \ No newline at end of file diff --git a/imagemodel.cpp b/imagemodel.cpp deleted file mode 100644 index 3288ae0..0000000 --- a/imagemodel.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "imagemodel.h" -#include "thumbnailworker.h" - -#include -#include -#include -#include - -ImageModel::ImageModel(QObject *parent) - : QAbstractListModel(parent) -{ - m_threadPool.setMaxThreadCount(QThread::idealThreadCount()); - - m_placeholder = QPixmap(128, 128); - m_placeholder.fill(Qt::lightGray); - - QFileIconProvider iconProvider; - m_folderIcon = iconProvider.icon(QFileIconProvider::Folder); -} - -ImageModel::~ImageModel() -{ - m_cancelFlag = true; - m_threadPool.waitForDone(); -} - -void ImageModel::setDirectory(const QString &path) -{ - m_cancelFlag = true; - m_threadPool.waitForDone(); - m_cancelFlag = false; - - beginResetModel(); - m_items.clear(); - m_pendingRows.clear(); - m_currentDir = path; - - QDir dir(path); - - // Add subdirectories first - dir.setFilter(QDir::AllDirs | QDir::NoDot); - dir.setSorting(QDir::Name); - const auto dirs = dir.entryInfoList(); - QPixmap folderPixmap = m_folderIcon.pixmap(128, 128); - for (const QFileInfo &d : dirs) - { - QString name = d.fileName(); // preserves ".." for the parent entry - m_items.append({d.absoluteFilePath(), name, folderPixmap, true, true}); - } - - // Then add image files - QStringList filters; - - const auto formats = QImageReader::supportedImageFormats(); - for (const QByteArray &fmt : formats) - filters << "*." + fmt; - - dir.setNameFilters(filters); - dir.setFilter(QDir::Files); - dir.setSorting(QDir::Name); - - const auto files = dir.entryInfoList(); - - for (const QFileInfo &file : files) - { - m_items.append({file.absoluteFilePath(), file.fileName(), m_placeholder, false, false}); - } - - endResetModel(); -} - -void ImageModel::requestThumbnails(int firstRow, int lastRow) -{ - if (m_items.isEmpty()) - return; - - firstRow = std::max(0, firstRow); - lastRow = std::min(lastRow, static_cast(m_items.size()) - 1); - - for (int row = firstRow; row <= lastRow; ++row) - { - if (!m_items[row].loaded && !m_items[row].isFolder) - queueRow(row); - } - - // Optional: preload nearby rows - int buffer = 20; - for (int row = firstRow - buffer; row < firstRow; ++row) - if (row >= 0 && !m_items[row].loaded && !m_items[row].isFolder) - queueRow(row); - - for (int row = lastRow + 1; row <= lastRow + buffer; ++row) - if (row < m_items.size() && !m_items[row].loaded && !m_items[row].isFolder) - queueRow(row); -} - -void ImageModel::queueRow(int row) -{ - if (m_pendingRows.contains(row)) - return; - - m_pendingRows.insert(row); - - auto *worker = new ThumbnailWorker( - m_items[row].path, - row, - &m_cancelFlag); - - connect(worker, &ThumbnailWorker::finished, - this, &ImageModel::thumbnailReady, - Qt::QueuedConnection); - - m_threadPool.start(worker); -} - -QString ImageModel::filePath(const QModelIndex &index) const -{ - if (!index.isValid()) - return {}; - - return m_items[index.row()].path; -} - -bool ImageModel::isFolder(const QModelIndex &index) const -{ - if (!index.isValid() || index.row() >= m_items.size()) - return false; - return m_items[index.row()].isFolder; -} - -QString ImageModel::currentDirectory() const -{ - return m_currentDir; -} - -int ImageModel::rowCount(const QModelIndex &) const -{ - return m_items.size(); -} - -QVariant ImageModel::data(const QModelIndex &index, int role) const -{ - if (!index.isValid()) - return {}; - - const Item &item = m_items[index.row()]; - - if (role == Qt::DecorationRole) - return item.thumbnail; - - if (role == Qt::DisplayRole) - return item.displayName; - - return {}; -} - -void ImageModel::thumbnailReady(int row, const QImage &image) -{ - if (m_cancelFlag) - return; - - if (row < 0 || row >= m_items.size()) - return; - - m_items[row].thumbnail = QPixmap::fromImage(image); - m_items[row].loaded = true; - m_pendingRows.remove(row); - - QModelIndex idx = index(row); - emit dataChanged(idx, idx, {Qt::DecorationRole}); -} diff --git a/imagemodel.h b/imagemodel.h deleted file mode 100644 index 7586159..0000000 --- a/imagemodel.h +++ /dev/null @@ -1,53 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -class ThumbnailWorker; - -class ImageModel : public QAbstractListModel -{ - Q_OBJECT - -public: - explicit ImageModel(QObject *parent = nullptr); - ~ImageModel(); - - void setDirectory(const QString &path); - QString filePath(const QModelIndex &index) const; - bool isFolder(const QModelIndex &index) const; - QString currentDirectory() const; - - void requestThumbnails(int firstRow, int lastRow); - - int rowCount(const QModelIndex &parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const override; - - void thumbnailReady(int row, const QImage &image); - -private: - struct Item - { - QString path; - QString displayName; - QPixmap thumbnail; - bool loaded = false; - bool isFolder = false; - }; - - void queueRow(int row); - - QVector m_items; - QThreadPool m_threadPool; - QPixmap m_placeholder; - - QSet m_pendingRows; - std::atomic_bool m_cancelFlag{false}; - QIcon m_folderIcon; - QString m_currentDir; -}; diff --git a/imageviewwidget.cpp b/imageviewwidget.cpp deleted file mode 100644 index f09ed6a..0000000 --- a/imageviewwidget.cpp +++ /dev/null @@ -1,237 +0,0 @@ -#include "imageviewwidget.h" - -#include -#include -#include -#include -#include - -ImageViewWidget::ImageViewWidget(QWidget *parent) - : QWidget(parent) -{ - setFocusPolicy(Qt::StrongFocus); - setStyleSheet("background-color: black;"); -} - -void ImageViewWidget::setImage(const QString &path) -{ - QImageReader reader(path); - reader.setAutoTransform(true); - QImage image = reader.read(); - - if (!image.isNull()) - m_pixmap = QPixmap::fromImage(image); - else - m_pixmap = QPixmap(); - - m_zoomMode = FitToScreen; - m_zoomFactor = 1.0; - m_offset = QPoint(0, 0); - update(); -} - -void ImageViewWidget::paintEvent(QPaintEvent *) -{ - if (m_pixmap.isNull()) - return; - - QPainter painter(this); - 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) - { - 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::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); - } -} diff --git a/imageviewwidget.h b/imageviewwidget.h deleted file mode 100644 index bc0da7b..0000000 --- a/imageviewwidget.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class ImageViewWidget : public QWidget -{ - Q_OBJECT - -public: - enum ZoomMode { FitToScreen, OriginalSize, CustomZoom }; - - explicit ImageViewWidget(QWidget *parent = nullptr); - - void setImage(const QString &path); - void zoomIn(); - void zoomOut(); - void zoomOriginal(); - void zoomFitToScreen(); - -signals: - void closeRequested(); - void nextRequested(); - void previousRequested(); - -protected: - void paintEvent(QPaintEvent *event) override; - void keyPressEvent(QKeyEvent *event) override; - void mousePressEvent(QMouseEvent *event) override; - void mouseMoveEvent(QMouseEvent *event) override; - void mouseReleaseEvent(QMouseEvent *event) override; - -private: - void clampOffset(int imgW, int imgH); - - QPixmap m_pixmap; - ZoomMode m_zoomMode = FitToScreen; - double m_zoomFactor = 1.0; - - // Panning state - QPoint m_offset; // current pan offset (pixels) - bool m_dragging = false; - QPoint m_dragStart; - QPoint m_offsetAtDragStart; -}; diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 6ce4d29..0000000 --- a/main.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include -#include "mainwindow.h" - -int main(int argc, char *argv[]) -{ - QApplication app(argc, argv); - - MainWindow w; - w.resize(1200, 700); - w.show(); - - return app.exec(); -} diff --git a/mainwindow.cpp b/mainwindow.cpp deleted file mode 100644 index aa686c6..0000000 --- a/mainwindow.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include "mainwindow.h" -#include "imagemodel.h" -#include "imageviewwidget.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent) -{ - setupUi(); - setupConnections(); -} - -void MainWindow::setupUi() -{ - // Folder model (left pane) - m_dirModel = new QFileSystemModel(this); - m_dirModel->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); - m_dirModel->setRootPath(QDir::homePath()); - - // Root chooser combo box - m_rootCombo = new QComboBox; - m_rootCombo->addItem("Home", QDir::homePath()); - m_rootCombo->addItem("Pictures", QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); - m_rootCombo->addItem("File System", QDir::rootPath()); - - m_treeView = new QTreeView; - m_treeView->setModel(m_dirModel); - m_treeView->setRootIndex(m_dirModel->index(QDir::homePath())); - m_treeView->header()->hide(); - m_treeView->setColumnHidden(1, true); - m_treeView->setColumnHidden(2, true); - m_treeView->setColumnHidden(3, true); - - // Left pane: combo + tree inside a container widget - QWidget *leftPane = new QWidget; - QVBoxLayout *leftLayout = new QVBoxLayout(leftPane); - leftLayout->setContentsMargins(0, 0, 0, 0); - leftLayout->addWidget(m_rootCombo); - leftLayout->addWidget(m_treeView); - - // Thumbnail model (center pane) - m_imageModel = new ImageModel(this); - - m_listView = new QListView; - m_listView->setModel(m_imageModel); - m_listView->setViewMode(QListView::IconMode); - m_listView->setIconSize(QSize(128, 128)); - m_listView->setGridSize(QSize(148, 168)); - m_listView->setResizeMode(QListView::Adjust); - m_listView->setSelectionMode(QAbstractItemView::SingleSelection); - m_listView->setSpacing(4); - m_listView->setWordWrap(true); - m_listView->setTextElideMode(Qt::ElideRight); - - // Preview pane (right) - m_previewLabel = new QLabel; - m_previewLabel->setAlignment(Qt::AlignCenter); - m_previewLabel->setMinimumWidth(300); - - // Layout - QSplitter *rightSplitter = new QSplitter(Qt::Horizontal); - rightSplitter->addWidget(m_listView); - rightSplitter->addWidget(m_previewLabel); - rightSplitter->setStretchFactor(0, 3); - rightSplitter->setStretchFactor(1, 2); - - QSplitter *mainSplitter = new QSplitter(Qt::Horizontal); - mainSplitter->addWidget(leftPane); - mainSplitter->addWidget(rightSplitter); - mainSplitter->setStretchFactor(1, 1); - - m_browserPage = mainSplitter; - - // Single image view - m_imageView = new ImageViewWidget; - - // Stack to switch between browser and single image - m_stack = new QStackedWidget; - m_stack->addWidget(m_browserPage); - m_stack->addWidget(m_imageView); - - setCentralWidget(m_stack); -} - -void MainWindow::setupConnections() -{ - connect(m_rootCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - [this](int index) { - QString rootPath = m_rootCombo->itemData(index).toString(); - m_dirModel->setRootPath(rootPath); - m_treeView->setRootIndex(m_dirModel->index(rootPath)); - }); - - connect(m_treeView, &QTreeView::clicked, this, - [this](const QModelIndex &index) - { - QString path = m_dirModel->filePath(index); - m_imageModel->setDirectory(path); - // Defer thumbnail load so the view has time to lay out items - QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); - }); - - connect(m_listView, &QListView::clicked, this, - [this](const QModelIndex &index) { showPreview(index); }); - - connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, - this, [this](const QModelIndex ¤t) { showPreview(current); }); - - connect(m_listView, &QListView::doubleClicked, this, - [this](const QModelIndex &index) { - if (m_imageModel->isFolder(index)) { - QString path = m_imageModel->filePath(index); - m_imageModel->setDirectory(path); - // Update tree view selection to match - QModelIndex dirIdx = m_dirModel->index(path); - if (dirIdx.isValid()) - m_treeView->setCurrentIndex(dirIdx); - QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); - } else { - enterSingleImageMode(index); - } - }); - - connect(m_imageView, &ImageViewWidget::closeRequested, - this, &MainWindow::leaveSingleImageMode); - - connect(m_imageView, &ImageViewWidget::nextRequested, this, [this]() { - QModelIndex cur = m_listView->currentIndex(); - int next = cur.isValid() ? cur.row() + 1 : 0; - while (next < m_imageModel->rowCount() && m_imageModel->isFolder(m_imageModel->index(next))) - ++next; - if (next < m_imageModel->rowCount()) - { - QModelIndex idx = m_imageModel->index(next); - m_listView->setCurrentIndex(idx); - enterSingleImageMode(idx); - } - }); - - connect(m_imageView, &ImageViewWidget::previousRequested, this, [this]() { - QModelIndex cur = m_listView->currentIndex(); - int prev = cur.isValid() ? cur.row() - 1 : 0; - while (prev >= 0 && m_imageModel->isFolder(m_imageModel->index(prev))) - --prev; - if (prev >= 0) - { - QModelIndex idx = m_imageModel->index(prev); - m_listView->setCurrentIndex(idx); - enterSingleImageMode(idx); - } - }); - - connect(m_listView->verticalScrollBar(), &QScrollBar::valueChanged, - this, [this]() - { loadVisibleThumbnails(); }); - - // Also catch resize — visible range changes when the viewport is resized - m_listView->viewport()->installEventFilter(this); -} - -bool MainWindow::eventFilter(QObject *obj, QEvent *event) -{ - if (obj == m_listView->viewport() && event->type() == QEvent::Resize) - QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); - - return QMainWindow::eventFilter(obj, event); -} - -void MainWindow::loadVisibleThumbnails() -{ - QModelIndex topLeft = m_listView->indexAt(QPoint(0, 0)); - QModelIndex bottomRight = m_listView->indexAt( - QPoint(m_listView->viewport()->width() - 1, - m_listView->viewport()->height() - 1)); - - int first = topLeft.isValid() ? topLeft.row() : 0; - int last = bottomRight.isValid() ? bottomRight.row() - : m_imageModel->rowCount() - 1; - - if (last < 0) - return; - - std::cout << "loadVisibleThumbnails: " << first << " - " << last << "\n"; - m_imageModel->requestThumbnails(first, last); -} - -void MainWindow::showPreview(const QModelIndex &index) -{ - if (!index.isValid()) - return; - - QString path = m_imageModel->filePath(index); - - QImageReader reader(path); - reader.setAutoTransform(true); - QImage image = reader.read(); - - m_previewLabel->setPixmap( - QPixmap::fromImage(image).scaled( - m_previewLabel->size(), - Qt::KeepAspectRatio, - Qt::SmoothTransformation)); -} - -void MainWindow::enterSingleImageMode(const QModelIndex &index) -{ - if (!index.isValid()) - return; - - QString path = m_imageModel->filePath(index); - m_imageView->setImage(path); - m_stack->setCurrentWidget(m_imageView); - m_imageView->setFocus(); -} - -void MainWindow::leaveSingleImageMode() -{ - if (isFullScreen()) - { - if (m_wasFullScreen) - showFullScreen(); - else - showNormal(); - } - m_stack->setCurrentWidget(m_browserPage); - m_listView->setFocus(); -} - -void MainWindow::keyPressEvent(QKeyEvent *event) -{ - if (event->key() == Qt::Key_F11 && m_stack->currentWidget() == m_imageView) - { - if (isFullScreen()) - showNormal(); - else - showFullScreen(); - event->accept(); - return; - } - - if (event->key() == Qt::Key_Return && m_stack->currentWidget() == m_browserPage) - { - QModelIndex current = m_listView->currentIndex(); - if (current.isValid()) - { - if (m_imageModel->isFolder(current)) { - QString path = m_imageModel->filePath(current); - m_imageModel->setDirectory(path); - QModelIndex dirIdx = m_dirModel->index(path); - if (dirIdx.isValid()) - m_treeView->setCurrentIndex(dirIdx); - QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); - } else { - enterSingleImageMode(current); - } - event->accept(); - return; - } - } - - QMainWindow::keyPressEvent(event); -} diff --git a/mainwindow.h b/mainwindow.h deleted file mode 100644 index 47a79d0..0000000 --- a/mainwindow.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include - -class QTreeView; -class QListView; -class QLabel; -class QStackedWidget; -class QFileSystemModel; -class QComboBox; -class ImageModel; -class ImageViewWidget; - -class MainWindow : public QMainWindow -{ - Q_OBJECT - -public: - explicit MainWindow(QWidget *parent = nullptr); - -private: - void setupUi(); - void setupConnections(); - void loadVisibleThumbnails(); - void showPreview(const QModelIndex &index); - void enterSingleImageMode(const QModelIndex &index); - void leaveSingleImageMode(); - - void keyPressEvent(QKeyEvent *event) override; - bool eventFilter(QObject *obj, QEvent *event) override; - - QFileSystemModel *m_dirModel; - ImageModel *m_imageModel; - - QTreeView *m_treeView; - QComboBox *m_rootCombo; - QListView *m_listView; - QLabel *m_previewLabel; - - QStackedWidget *m_stack; - QWidget *m_browserPage; - ImageViewWidget *m_imageView; - bool m_wasFullScreen = false; -}; diff --git a/src/imagemodel.cpp b/src/imagemodel.cpp new file mode 100644 index 0000000..3288ae0 --- /dev/null +++ b/src/imagemodel.cpp @@ -0,0 +1,171 @@ +#include "imagemodel.h" +#include "thumbnailworker.h" + +#include +#include +#include +#include + +ImageModel::ImageModel(QObject *parent) + : QAbstractListModel(parent) +{ + m_threadPool.setMaxThreadCount(QThread::idealThreadCount()); + + m_placeholder = QPixmap(128, 128); + m_placeholder.fill(Qt::lightGray); + + QFileIconProvider iconProvider; + m_folderIcon = iconProvider.icon(QFileIconProvider::Folder); +} + +ImageModel::~ImageModel() +{ + m_cancelFlag = true; + m_threadPool.waitForDone(); +} + +void ImageModel::setDirectory(const QString &path) +{ + m_cancelFlag = true; + m_threadPool.waitForDone(); + m_cancelFlag = false; + + beginResetModel(); + m_items.clear(); + m_pendingRows.clear(); + m_currentDir = path; + + QDir dir(path); + + // Add subdirectories first + dir.setFilter(QDir::AllDirs | QDir::NoDot); + dir.setSorting(QDir::Name); + const auto dirs = dir.entryInfoList(); + QPixmap folderPixmap = m_folderIcon.pixmap(128, 128); + for (const QFileInfo &d : dirs) + { + QString name = d.fileName(); // preserves ".." for the parent entry + m_items.append({d.absoluteFilePath(), name, folderPixmap, true, true}); + } + + // Then add image files + QStringList filters; + + const auto formats = QImageReader::supportedImageFormats(); + for (const QByteArray &fmt : formats) + filters << "*." + fmt; + + dir.setNameFilters(filters); + dir.setFilter(QDir::Files); + dir.setSorting(QDir::Name); + + const auto files = dir.entryInfoList(); + + for (const QFileInfo &file : files) + { + m_items.append({file.absoluteFilePath(), file.fileName(), m_placeholder, false, false}); + } + + endResetModel(); +} + +void ImageModel::requestThumbnails(int firstRow, int lastRow) +{ + if (m_items.isEmpty()) + return; + + firstRow = std::max(0, firstRow); + lastRow = std::min(lastRow, static_cast(m_items.size()) - 1); + + for (int row = firstRow; row <= lastRow; ++row) + { + if (!m_items[row].loaded && !m_items[row].isFolder) + queueRow(row); + } + + // Optional: preload nearby rows + int buffer = 20; + for (int row = firstRow - buffer; row < firstRow; ++row) + if (row >= 0 && !m_items[row].loaded && !m_items[row].isFolder) + queueRow(row); + + for (int row = lastRow + 1; row <= lastRow + buffer; ++row) + if (row < m_items.size() && !m_items[row].loaded && !m_items[row].isFolder) + queueRow(row); +} + +void ImageModel::queueRow(int row) +{ + if (m_pendingRows.contains(row)) + return; + + m_pendingRows.insert(row); + + auto *worker = new ThumbnailWorker( + m_items[row].path, + row, + &m_cancelFlag); + + connect(worker, &ThumbnailWorker::finished, + this, &ImageModel::thumbnailReady, + Qt::QueuedConnection); + + m_threadPool.start(worker); +} + +QString ImageModel::filePath(const QModelIndex &index) const +{ + if (!index.isValid()) + return {}; + + return m_items[index.row()].path; +} + +bool ImageModel::isFolder(const QModelIndex &index) const +{ + if (!index.isValid() || index.row() >= m_items.size()) + return false; + return m_items[index.row()].isFolder; +} + +QString ImageModel::currentDirectory() const +{ + return m_currentDir; +} + +int ImageModel::rowCount(const QModelIndex &) const +{ + return m_items.size(); +} + +QVariant ImageModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return {}; + + const Item &item = m_items[index.row()]; + + if (role == Qt::DecorationRole) + return item.thumbnail; + + if (role == Qt::DisplayRole) + return item.displayName; + + return {}; +} + +void ImageModel::thumbnailReady(int row, const QImage &image) +{ + if (m_cancelFlag) + return; + + if (row < 0 || row >= m_items.size()) + return; + + m_items[row].thumbnail = QPixmap::fromImage(image); + m_items[row].loaded = true; + m_pendingRows.remove(row); + + QModelIndex idx = index(row); + emit dataChanged(idx, idx, {Qt::DecorationRole}); +} diff --git a/src/imagemodel.h b/src/imagemodel.h new file mode 100644 index 0000000..7586159 --- /dev/null +++ b/src/imagemodel.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class ThumbnailWorker; + +class ImageModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit ImageModel(QObject *parent = nullptr); + ~ImageModel(); + + void setDirectory(const QString &path); + QString filePath(const QModelIndex &index) const; + bool isFolder(const QModelIndex &index) const; + QString currentDirectory() const; + + void requestThumbnails(int firstRow, int lastRow); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void thumbnailReady(int row, const QImage &image); + +private: + struct Item + { + QString path; + QString displayName; + QPixmap thumbnail; + bool loaded = false; + bool isFolder = false; + }; + + void queueRow(int row); + + QVector m_items; + QThreadPool m_threadPool; + QPixmap m_placeholder; + + QSet m_pendingRows; + std::atomic_bool m_cancelFlag{false}; + QIcon m_folderIcon; + QString m_currentDir; +}; diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp new file mode 100644 index 0000000..f09ed6a --- /dev/null +++ b/src/imageviewwidget.cpp @@ -0,0 +1,237 @@ +#include "imageviewwidget.h" + +#include +#include +#include +#include +#include + +ImageViewWidget::ImageViewWidget(QWidget *parent) + : QWidget(parent) +{ + setFocusPolicy(Qt::StrongFocus); + setStyleSheet("background-color: black;"); +} + +void ImageViewWidget::setImage(const QString &path) +{ + QImageReader reader(path); + reader.setAutoTransform(true); + QImage image = reader.read(); + + if (!image.isNull()) + m_pixmap = QPixmap::fromImage(image); + else + m_pixmap = QPixmap(); + + m_zoomMode = FitToScreen; + m_zoomFactor = 1.0; + m_offset = QPoint(0, 0); + update(); +} + +void ImageViewWidget::paintEvent(QPaintEvent *) +{ + if (m_pixmap.isNull()) + return; + + QPainter painter(this); + 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) + { + 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::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); + } +} diff --git a/src/imageviewwidget.h b/src/imageviewwidget.h new file mode 100644 index 0000000..bc0da7b --- /dev/null +++ b/src/imageviewwidget.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ImageViewWidget : public QWidget +{ + Q_OBJECT + +public: + enum ZoomMode { FitToScreen, OriginalSize, CustomZoom }; + + explicit ImageViewWidget(QWidget *parent = nullptr); + + void setImage(const QString &path); + void zoomIn(); + void zoomOut(); + void zoomOriginal(); + void zoomFitToScreen(); + +signals: + void closeRequested(); + void nextRequested(); + void previousRequested(); + +protected: + void paintEvent(QPaintEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + +private: + void clampOffset(int imgW, int imgH); + + QPixmap m_pixmap; + ZoomMode m_zoomMode = FitToScreen; + double m_zoomFactor = 1.0; + + // Panning state + QPoint m_offset; // current pan offset (pixels) + bool m_dragging = false; + QPoint m_dragStart; + QPoint m_offsetAtDragStart; +}; diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..6ce4d29 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,13 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + MainWindow w; + w.resize(1200, 700); + w.show(); + + return app.exec(); +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp new file mode 100644 index 0000000..aa686c6 --- /dev/null +++ b/src/mainwindow.cpp @@ -0,0 +1,280 @@ +#include "mainwindow.h" +#include "imagemodel.h" +#include "imageviewwidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + setupUi(); + setupConnections(); +} + +void MainWindow::setupUi() +{ + // Folder model (left pane) + m_dirModel = new QFileSystemModel(this); + m_dirModel->setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + m_dirModel->setRootPath(QDir::homePath()); + + // Root chooser combo box + m_rootCombo = new QComboBox; + m_rootCombo->addItem("Home", QDir::homePath()); + m_rootCombo->addItem("Pictures", QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); + m_rootCombo->addItem("File System", QDir::rootPath()); + + m_treeView = new QTreeView; + m_treeView->setModel(m_dirModel); + m_treeView->setRootIndex(m_dirModel->index(QDir::homePath())); + m_treeView->header()->hide(); + m_treeView->setColumnHidden(1, true); + m_treeView->setColumnHidden(2, true); + m_treeView->setColumnHidden(3, true); + + // Left pane: combo + tree inside a container widget + QWidget *leftPane = new QWidget; + QVBoxLayout *leftLayout = new QVBoxLayout(leftPane); + leftLayout->setContentsMargins(0, 0, 0, 0); + leftLayout->addWidget(m_rootCombo); + leftLayout->addWidget(m_treeView); + + // Thumbnail model (center pane) + m_imageModel = new ImageModel(this); + + m_listView = new QListView; + m_listView->setModel(m_imageModel); + m_listView->setViewMode(QListView::IconMode); + m_listView->setIconSize(QSize(128, 128)); + m_listView->setGridSize(QSize(148, 168)); + m_listView->setResizeMode(QListView::Adjust); + m_listView->setSelectionMode(QAbstractItemView::SingleSelection); + m_listView->setSpacing(4); + m_listView->setWordWrap(true); + m_listView->setTextElideMode(Qt::ElideRight); + + // Preview pane (right) + m_previewLabel = new QLabel; + m_previewLabel->setAlignment(Qt::AlignCenter); + m_previewLabel->setMinimumWidth(300); + + // Layout + QSplitter *rightSplitter = new QSplitter(Qt::Horizontal); + rightSplitter->addWidget(m_listView); + rightSplitter->addWidget(m_previewLabel); + rightSplitter->setStretchFactor(0, 3); + rightSplitter->setStretchFactor(1, 2); + + QSplitter *mainSplitter = new QSplitter(Qt::Horizontal); + mainSplitter->addWidget(leftPane); + mainSplitter->addWidget(rightSplitter); + mainSplitter->setStretchFactor(1, 1); + + m_browserPage = mainSplitter; + + // Single image view + m_imageView = new ImageViewWidget; + + // Stack to switch between browser and single image + m_stack = new QStackedWidget; + m_stack->addWidget(m_browserPage); + m_stack->addWidget(m_imageView); + + setCentralWidget(m_stack); +} + +void MainWindow::setupConnections() +{ + connect(m_rootCombo, QOverload::of(&QComboBox::currentIndexChanged), this, + [this](int index) { + QString rootPath = m_rootCombo->itemData(index).toString(); + m_dirModel->setRootPath(rootPath); + m_treeView->setRootIndex(m_dirModel->index(rootPath)); + }); + + connect(m_treeView, &QTreeView::clicked, this, + [this](const QModelIndex &index) + { + QString path = m_dirModel->filePath(index); + m_imageModel->setDirectory(path); + // Defer thumbnail load so the view has time to lay out items + QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); + }); + + connect(m_listView, &QListView::clicked, this, + [this](const QModelIndex &index) { showPreview(index); }); + + connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged, + this, [this](const QModelIndex ¤t) { showPreview(current); }); + + connect(m_listView, &QListView::doubleClicked, this, + [this](const QModelIndex &index) { + if (m_imageModel->isFolder(index)) { + QString path = m_imageModel->filePath(index); + m_imageModel->setDirectory(path); + // Update tree view selection to match + QModelIndex dirIdx = m_dirModel->index(path); + if (dirIdx.isValid()) + m_treeView->setCurrentIndex(dirIdx); + QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); + } else { + enterSingleImageMode(index); + } + }); + + connect(m_imageView, &ImageViewWidget::closeRequested, + this, &MainWindow::leaveSingleImageMode); + + connect(m_imageView, &ImageViewWidget::nextRequested, this, [this]() { + QModelIndex cur = m_listView->currentIndex(); + int next = cur.isValid() ? cur.row() + 1 : 0; + while (next < m_imageModel->rowCount() && m_imageModel->isFolder(m_imageModel->index(next))) + ++next; + if (next < m_imageModel->rowCount()) + { + QModelIndex idx = m_imageModel->index(next); + m_listView->setCurrentIndex(idx); + enterSingleImageMode(idx); + } + }); + + connect(m_imageView, &ImageViewWidget::previousRequested, this, [this]() { + QModelIndex cur = m_listView->currentIndex(); + int prev = cur.isValid() ? cur.row() - 1 : 0; + while (prev >= 0 && m_imageModel->isFolder(m_imageModel->index(prev))) + --prev; + if (prev >= 0) + { + QModelIndex idx = m_imageModel->index(prev); + m_listView->setCurrentIndex(idx); + enterSingleImageMode(idx); + } + }); + + connect(m_listView->verticalScrollBar(), &QScrollBar::valueChanged, + this, [this]() + { loadVisibleThumbnails(); }); + + // Also catch resize — visible range changes when the viewport is resized + m_listView->viewport()->installEventFilter(this); +} + +bool MainWindow::eventFilter(QObject *obj, QEvent *event) +{ + if (obj == m_listView->viewport() && event->type() == QEvent::Resize) + QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); + + return QMainWindow::eventFilter(obj, event); +} + +void MainWindow::loadVisibleThumbnails() +{ + QModelIndex topLeft = m_listView->indexAt(QPoint(0, 0)); + QModelIndex bottomRight = m_listView->indexAt( + QPoint(m_listView->viewport()->width() - 1, + m_listView->viewport()->height() - 1)); + + int first = topLeft.isValid() ? topLeft.row() : 0; + int last = bottomRight.isValid() ? bottomRight.row() + : m_imageModel->rowCount() - 1; + + if (last < 0) + return; + + std::cout << "loadVisibleThumbnails: " << first << " - " << last << "\n"; + m_imageModel->requestThumbnails(first, last); +} + +void MainWindow::showPreview(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + QString path = m_imageModel->filePath(index); + + QImageReader reader(path); + reader.setAutoTransform(true); + QImage image = reader.read(); + + m_previewLabel->setPixmap( + QPixmap::fromImage(image).scaled( + m_previewLabel->size(), + Qt::KeepAspectRatio, + Qt::SmoothTransformation)); +} + +void MainWindow::enterSingleImageMode(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + QString path = m_imageModel->filePath(index); + m_imageView->setImage(path); + m_stack->setCurrentWidget(m_imageView); + m_imageView->setFocus(); +} + +void MainWindow::leaveSingleImageMode() +{ + if (isFullScreen()) + { + if (m_wasFullScreen) + showFullScreen(); + else + showNormal(); + } + m_stack->setCurrentWidget(m_browserPage); + m_listView->setFocus(); +} + +void MainWindow::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_F11 && m_stack->currentWidget() == m_imageView) + { + if (isFullScreen()) + showNormal(); + else + showFullScreen(); + event->accept(); + return; + } + + if (event->key() == Qt::Key_Return && m_stack->currentWidget() == m_browserPage) + { + QModelIndex current = m_listView->currentIndex(); + if (current.isValid()) + { + if (m_imageModel->isFolder(current)) { + QString path = m_imageModel->filePath(current); + m_imageModel->setDirectory(path); + QModelIndex dirIdx = m_dirModel->index(path); + if (dirIdx.isValid()) + m_treeView->setCurrentIndex(dirIdx); + QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails); + } else { + enterSingleImageMode(current); + } + event->accept(); + return; + } + } + + QMainWindow::keyPressEvent(event); +} diff --git a/src/mainwindow.h b/src/mainwindow.h new file mode 100644 index 0000000..47a79d0 --- /dev/null +++ b/src/mainwindow.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +class QTreeView; +class QListView; +class QLabel; +class QStackedWidget; +class QFileSystemModel; +class QComboBox; +class ImageModel; +class ImageViewWidget; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + +private: + void setupUi(); + void setupConnections(); + void loadVisibleThumbnails(); + void showPreview(const QModelIndex &index); + void enterSingleImageMode(const QModelIndex &index); + void leaveSingleImageMode(); + + void keyPressEvent(QKeyEvent *event) override; + bool eventFilter(QObject *obj, QEvent *event) override; + + QFileSystemModel *m_dirModel; + ImageModel *m_imageModel; + + QTreeView *m_treeView; + QComboBox *m_rootCombo; + QListView *m_listView; + QLabel *m_previewLabel; + + QStackedWidget *m_stack; + QWidget *m_browserPage; + ImageViewWidget *m_imageView; + bool m_wasFullScreen = false; +}; diff --git a/src/thumbnailworker.cpp b/src/thumbnailworker.cpp new file mode 100644 index 0000000..9b094ba --- /dev/null +++ b/src/thumbnailworker.cpp @@ -0,0 +1,44 @@ +#include "thumbnailworker.h" +#include +#include + +ThumbnailWorker::ThumbnailWorker(const QString &path, + int row, + std::atomic_bool *cancelFlag) + : m_path(path), + m_row(row), + m_cancelFlag(cancelFlag) +{ + setAutoDelete(true); +} + +void ThumbnailWorker::run() +{ + if (*m_cancelFlag) + return; + + QImageReader reader(m_path); + reader.setAutoTransform(true); + + QImage image = reader.read(); + + if (*m_cancelFlag) + return; + + if (!image.isNull()) + { + // Scale to fit within 128x128 keeping aspect ratio + QImage scaled = image.scaled(128, 128, + Qt::KeepAspectRatio, + Qt::SmoothTransformation); + // Center on a 128x128 canvas with transparent/white fill + QImage thumb(128, 128, QImage::Format_ARGB32_Premultiplied); + thumb.fill(Qt::transparent); + int x = (128 - scaled.width()) / 2; + int y = (128 - scaled.height()) / 2; + QPainter painter(&thumb); + painter.drawImage(x, y, scaled); + painter.end(); + emit finished(m_row, thumb); + } +} diff --git a/src/thumbnailworker.h b/src/thumbnailworker.h new file mode 100644 index 0000000..5cdb7cd --- /dev/null +++ b/src/thumbnailworker.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include + +class ThumbnailWorker : public QObject, public QRunnable +{ + Q_OBJECT + +public: + ThumbnailWorker(const QString &path, + int row, + std::atomic_bool *cancelFlag); + + void run() override; + +signals: + void finished(int row, const QImage &image); + +private: + QString m_path; + int m_row; + std::atomic_bool *m_cancelFlag; +}; diff --git a/thumbnailworker.cpp b/thumbnailworker.cpp deleted file mode 100644 index 9b094ba..0000000 --- a/thumbnailworker.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include "thumbnailworker.h" -#include -#include - -ThumbnailWorker::ThumbnailWorker(const QString &path, - int row, - std::atomic_bool *cancelFlag) - : m_path(path), - m_row(row), - m_cancelFlag(cancelFlag) -{ - setAutoDelete(true); -} - -void ThumbnailWorker::run() -{ - if (*m_cancelFlag) - return; - - QImageReader reader(m_path); - reader.setAutoTransform(true); - - QImage image = reader.read(); - - if (*m_cancelFlag) - return; - - if (!image.isNull()) - { - // Scale to fit within 128x128 keeping aspect ratio - QImage scaled = image.scaled(128, 128, - Qt::KeepAspectRatio, - Qt::SmoothTransformation); - // Center on a 128x128 canvas with transparent/white fill - QImage thumb(128, 128, QImage::Format_ARGB32_Premultiplied); - thumb.fill(Qt::transparent); - int x = (128 - scaled.width()) / 2; - int y = (128 - scaled.height()) / 2; - QPainter painter(&thumb); - painter.drawImage(x, y, scaled); - painter.end(); - emit finished(m_row, thumb); - } -} diff --git a/thumbnailworker.h b/thumbnailworker.h deleted file mode 100644 index 5cdb7cd..0000000 --- a/thumbnailworker.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class ThumbnailWorker : public QObject, public QRunnable -{ - Q_OBJECT - -public: - ThumbnailWorker(const QString &path, - int row, - std::atomic_bool *cancelFlag); - - void run() override; - -signals: - void finished(int row, const QImage &image); - -private: - QString m_path; - int m_row; - std::atomic_bool *m_cancelFlag; -};