commit 323e0142
Moved source files to "src"
Changed files
| M | CMakeLists.txt before |
| D | imagemodel.cpp before |
| D | imagemodel.h before |
| D | imageviewwidget.cpp before |
| D | imageviewwidget.h before |
| D | main.cpp before |
| D | mainwindow.cpp before |
| D | mainwindow.h before |
| A | src/imagemodel.cpp |
| A | src/imagemodel.h |
| A | src/imageviewwidget.cpp |
| A | src/imageviewwidget.h |
| A | src/main.cpp |
| A | src/mainwindow.cpp |
| A | src/mainwindow.h |
| A | src/thumbnailworker.cpp |
| A | src/thumbnailworker.h |
| D | thumbnailworker.cpp before |
| D | thumbnailworker.h before |
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 <QDir>
-#include <QImageReader>
-#include <QFileInfo>
-#include <QFileIconProvider>
-
-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<int>(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 <QAbstractListModel>
-#include <QPixmap>
-#include <QVector>
-#include <QThreadPool>
-#include <QSet>
-#include <atomic>
-#include <QIcon>
-
-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<Item> m_items;
- QThreadPool m_threadPool;
- QPixmap m_placeholder;
-
- QSet<int> 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 <QPainter>
-#include <QKeyEvent>
-#include <QMouseEvent>
-#include <QImageReader>
-#include <algorithm>
-
-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<int>(m_pixmap.width() * m_zoomFactor),
- static_cast<int>(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<double>(width()) / m_pixmap.width();
- double sy = static_cast<double>(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<double>(width()) / m_pixmap.width();
- double sy = static_cast<double>(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 <QWidget>
-#include <QImage>
-#include <QPixmap>
-#include <QString>
-#include <QPoint>
-
-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 <QApplication>
-#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 <QFileSystemModel>
-#include <QTreeView>
-#include <QListView>
-#include <QLabel>
-#include <QSplitter>
-#include <QStackedWidget>
-#include <QDir>
-#include <QComboBox>
-#include <QVBoxLayout>
-#include <QStandardPaths>
-#include <QImageReader>
-#include <QHeaderView>
-#include <QScrollBar>
-#include <QTimer>
-#include <QEvent>
-#include <QKeyEvent>
-#include <QItemSelectionModel>
-#include <iostream>
-
-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<int>::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 <QMainWindow>
-
-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 <QDir>
+#include <QImageReader>
+#include <QFileInfo>
+#include <QFileIconProvider>
+
+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<int>(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 <QAbstractListModel>
+#include <QPixmap>
+#include <QVector>
+#include <QThreadPool>
+#include <QSet>
+#include <atomic>
+#include <QIcon>
+
+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<Item> m_items;
+ QThreadPool m_threadPool;
+ QPixmap m_placeholder;
+
+ QSet<int> 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 <QPainter>
+#include <QKeyEvent>
+#include <QMouseEvent>
+#include <QImageReader>
+#include <algorithm>
+
+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<int>(m_pixmap.width() * m_zoomFactor),
+ static_cast<int>(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<double>(width()) / m_pixmap.width();
+ double sy = static_cast<double>(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<double>(width()) / m_pixmap.width();
+ double sy = static_cast<double>(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 <QWidget>
+#include <QImage>
+#include <QPixmap>
+#include <QString>
+#include <QPoint>
+
+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 <QApplication>
+#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 <QFileSystemModel>
+#include <QTreeView>
+#include <QListView>
+#include <QLabel>
+#include <QSplitter>
+#include <QStackedWidget>
+#include <QDir>
+#include <QComboBox>
+#include <QVBoxLayout>
+#include <QStandardPaths>
+#include <QImageReader>
+#include <QHeaderView>
+#include <QScrollBar>
+#include <QTimer>
+#include <QEvent>
+#include <QKeyEvent>
+#include <QItemSelectionModel>
+#include <iostream>
+
+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<int>::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 <QMainWindow>
+
+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 <QImageReader>
+#include <QPainter>
+
+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 <QRunnable>
+#include <QObject>
+#include <QImage>
+#include <atomic>
+
+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 <QImageReader>
-#include <QPainter>
-
-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 <QRunnable>
-#include <QObject>
-#include <QImage>
-#include <atomic>
-
-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;
-};