git repos / SagoImageBrowser

src/mainwindow.cpp

browsing at commit = 4db3041aa5124ab367c9f70be0c3a9ed62a38294

raw · blame · history

#include "mainwindow.h"
#include "imagemodel.h"
#include "imageviewwidget.h"
#include "preferencesdialog.h"
#include "thumbnailcache.h"
#include "exifreader.h"
#include "fsdirmodel.h"
#include "pathcompletermodel.h"
#include "thumbnaildelegate.h"

#include <QFile>
#include <QTreeView>
#include <QTreeView>
#include <QListView>
#include <QLabel>
#include <QStackedWidget>
#include <QDir>
#include <QListWidget>
#include <QStandardPaths>
#include <QImageReader>
#include <QHeaderView>
#include <QScrollBar>
#include <QTimer>
#include <QEvent>
#include <QKeyEvent>
#include <QCloseEvent>
#include <QItemSelectionModel>
#include <QDockWidget>
#include <QMenuBar>
#include <QMenu>
#include <QAction>
#include <QSettings>
#include <QTableWidget>
#include <QDialog>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QDialogButtonBox>
#include <QPlainTextEdit>
#include <QMessageBox>
#include <QLineEdit>
#include <QPushButton>
#include <QCompleter>
#include <iostream>

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

namespace
{
ThumbnailCache::Size thumbnailSizeFromString(const QString &s)
{
    return (s == "large") ? ThumbnailCache::Size::Large
                          : ThumbnailCache::Size::Normal;
}
} // namespace

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setupUi();
    setupMenuBar();
    setupConnections();
    loadPreferences();
}

void MainWindow::setupUi()
{
    // Folder model (left pane) — backed by std::filesystem so directories with
    // non-UTF-8 filenames are listed correctly.
    m_dirModel = new FsDirModel(this);
    m_dirModel->setRootPath(QDir::homePath());

    // Root chooser list
    m_rootList = new QListWidget;
    m_rootList->setSelectionMode(QAbstractItemView::SingleSelection);

    auto addRootItem = [this](const QString &label, const QString &path) {
        QListWidgetItem *item = new QListWidgetItem(label, m_rootList);
        item->setData(Qt::UserRole, path);
    };
    addRootItem("Home", QDir::homePath());
    addRootItem("Pictures", QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
    addRootItem("File System", QDir::rootPath());
    m_rootList->setCurrentRow(0);

    m_treeView = new QTreeView;
    m_treeView->setModel(m_dirModel);
    m_treeView->setRootIndex(m_dirModel->rootIndex());
    m_treeView->header()->hide();
    m_treeView->setColumnHidden(1, true);
    m_treeView->setColumnHidden(2, true);
    m_treeView->setColumnHidden(3, true);

    // 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);
    m_listView->setItemDelegate(new ThumbnailDelegate(this));

    // Preview pane (right)
    m_previewLabel = new QLabel;
    m_previewLabel->setAlignment(Qt::AlignCenter);
    m_previewLabel->setMinimumWidth(200);

    // Single image view
    m_imageView = new ImageViewWidget;

    // Path bar: editable absolute path with ajax-style folder completion and a Go button
    m_pathEdit = new QLineEdit;
    m_goButton = new QPushButton(tr("Go"));
    m_pathCompleterModel = new PathCompleterModel(this);
    m_pathCompleter = new QCompleter(m_pathCompleterModel, this);
    m_pathCompleter->setCaseSensitivity(Qt::CaseSensitive);
    m_pathCompleter->setCompletionMode(QCompleter::PopupCompletion);
    m_pathCompleter->setFilterMode(Qt::MatchStartsWith);
    m_pathEdit->setCompleter(m_pathCompleter);

    auto *pathRow = new QHBoxLayout;
    pathRow->setContentsMargins(4, 4, 4, 0);
    pathRow->addWidget(m_pathEdit, 1);
    pathRow->addWidget(m_goButton);

    auto *browserPage = new QWidget;
    auto *vbox = new QVBoxLayout(browserPage);
    vbox->setContentsMargins(0, 0, 0, 0);
    vbox->setSpacing(0);
    vbox->addLayout(pathRow);
    vbox->addWidget(m_listView, 1);

    // Stack to switch between browser and single image
    m_stack = new QStackedWidget;
    m_stack->addWidget(browserPage);
    m_stack->addWidget(m_imageView);
    m_browserPage = browserPage;

    setCentralWidget(m_stack);

    updatePathField(QFile::encodeName(QDir::homePath()));

    // Dock widgets — can be dragged, floated and stacked by the user
    m_rootDock = new QDockWidget(tr("Roots"), this);
    m_rootDock->setObjectName("RootDock");
    m_rootDock->setWidget(m_rootList);
    addDockWidget(Qt::LeftDockWidgetArea, m_rootDock);

    m_folderDock = new QDockWidget(tr("Folders"), this);
    m_folderDock->setObjectName("FolderDock");
    m_folderDock->setWidget(m_treeView);
    addDockWidget(Qt::LeftDockWidgetArea, m_folderDock);
    splitDockWidget(m_rootDock, m_folderDock, Qt::Vertical);

    m_previewDock = new QDockWidget(tr("Preview"), this);
    m_previewDock->setObjectName("PreviewDock");
    m_previewDock->setWidget(m_previewLabel);
    addDockWidget(Qt::RightDockWidgetArea, m_previewDock);

    // EXIF info dock — placed below the preview dock by default
    m_exifTable = new QTableWidget(0, 2);
    m_exifTable->setHorizontalHeaderLabels({tr("Field"), tr("Value")});
    m_exifTable->horizontalHeader()->setStretchLastSection(true);
    m_exifTable->verticalHeader()->hide();
    m_exifTable->setEditTriggers(QAbstractItemView::NoEditTriggers);
    m_exifTable->setSelectionMode(QAbstractItemView::NoSelection);
    m_exifTable->setShowGrid(false);
    m_exifTable->setAlternatingRowColors(true);
    m_exifTable->setMinimumWidth(200);

    m_exifDock = new QDockWidget(tr("EXIF Info"), this);
    m_exifDock->setObjectName("ExifDock");
    m_exifDock->setWidget(m_exifTable);
    addDockWidget(Qt::RightDockWidgetArea, m_exifDock);
    splitDockWidget(m_previewDock, m_exifDock, Qt::Vertical);
}

void MainWindow::setupMenuBar()
{
    m_menuBar = menuBar();
    m_menuBar->setStyleSheet(
        "QMenuBar { padding: 1px 2px; }"
        "QMenuBar::item { padding: 2px 6px; }"
    );
    QMenu *viewMenu = m_menuBar->addMenu("&View");
    viewMenu->addAction(m_rootDock->toggleViewAction());
    viewMenu->addAction(m_folderDock->toggleViewAction());
    viewMenu->addAction(m_previewDock->toggleViewAction());
    viewMenu->addAction(m_exifDock->toggleViewAction());

    QMenu *editMenu = m_menuBar->addMenu("&Edit");
    QAction *preferencesAction = editMenu->addAction("&Preferences");
    connect(preferencesAction, &QAction::triggered, this, &MainWindow::onPreferencesTriggered);
}

void MainWindow::navigateToFolder(const QString &path)
{
    navigateToFolder(QFile::encodeName(path));
}

void MainWindow::navigateToFolder(const QByteArray &path)
{
    m_previousFolderPath = m_imageModel->currentDirectory();
    m_imageModel->setDirectory(path);
    QModelIndex dirIdx = m_dirModel->indexForPath(path);
    if (dirIdx.isValid())
        m_treeView->setCurrentIndex(dirIdx);
    updatePathField(path);
    QTimer::singleShot(0, this, [this]() {
        selectPreviousFolderIfExists();
        loadVisibleThumbnails();
    });
}

void MainWindow::updatePathField(const QByteArray &path)
{
    if (!m_pathEdit)
        return;
    const QString text = QFile::decodeName(path);
    if (m_pathEdit->text() != text)
        m_pathEdit->setText(text);
}

void MainWindow::onPathEntered()
{
    const QString text = m_pathEdit->text().trimmed();
    if (text.isEmpty())
        return;
    openPath(text);
}

void MainWindow::openPath(const QString &path)
{
    QFileInfo info(path);
    if (!info.exists())
        return;

    if (info.isDir()) {
        navigateToFolder(QFile::encodeName(info.absoluteFilePath()));
    } else if (info.isFile()) {
        navigateToFolder(QFile::encodeName(info.absolutePath()));

        // ImageModel::setDirectory is synchronous, so items are ready now.
        // Find the file in the model and open it.
        QByteArray target = QFile::encodeName(info.absoluteFilePath());
        for (int i = 0; i < m_imageModel->rowCount(); ++i) {
            QModelIndex idx = m_imageModel->index(i);
            if (m_imageModel->filePath(idx) == target) {
                m_listView->setCurrentIndex(idx);
                m_listView->scrollTo(idx);
                showPreview(idx);
                if (!m_imageModel->isFolder(idx))
                    enterSingleImageMode(idx);
                break;
            }
        }
    }
}

void MainWindow::selectPreviousFolderIfExists()
{
    if (m_previousFolderPath.isEmpty())
        return;

    // Find the previous folder in the current directory's contents
    for (int i = 0; i < m_imageModel->rowCount(); ++i) {
        QModelIndex idx = m_imageModel->index(i);
        if (m_imageModel->isFolder(idx)) {
            QByteArray folderPath = m_imageModel->filePath(idx);
            if (folderPath == m_previousFolderPath) {
                m_listView->setCurrentIndex(idx);
                m_listView->scrollTo(idx);
                return;
            }
        }
    }
}

void MainWindow::setupConnections()
{
    connect(m_pathEdit, &QLineEdit::returnPressed,
            this, &MainWindow::onPathEntered);
    connect(m_goButton, &QPushButton::clicked,
            this, &MainWindow::onPathEntered);
    connect(m_pathEdit, &QLineEdit::textEdited,
            m_pathCompleterModel, &PathCompleterModel::setPrefix);

    connect(m_rootList, &QListWidget::currentItemChanged, this,
            [this](QListWidgetItem *current, QListWidgetItem *) {
                if (current == nullptr)
                    return;

                QString rootPath = current->data(Qt::UserRole).toString();
                m_dirModel->setRootPath(rootPath);
                m_treeView->setRootIndex(m_dirModel->rootIndex());
            });

    connect(m_treeView, &QTreeView::clicked, this,
            [this](const QModelIndex &index)
            {
                QByteArray path = m_dirModel->filePath(index);
                navigateToFolder(path);
            });

    connect(m_listView, &QListView::clicked, this,
            [this](const QModelIndex &index) { showPreview(index); });

    connect(m_listView->selectionModel(), &QItemSelectionModel::currentChanged,
            this, [this](const QModelIndex &current) { showPreview(current); });

    connect(m_listView, &QListView::doubleClicked, this,
            [this](const QModelIndex &index) {
                if (m_imageModel->isFolder(index)) {
                    QByteArray path = m_imageModel->filePath(index);
                    navigateToFolder(path);
                } else {
                    enterSingleImageMode(index);
                }
            });

    connect(m_imageView, &ImageViewWidget::closeRequested,
            this, &MainWindow::leaveSingleImageMode);
    connect(m_imageView, &ImageViewWidget::editCaptionRequested,
        this, &MainWindow::onEditCaption);
    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;

    QByteArray path = m_imageModel->filePath(index);

    int fd = ::open(path.constData(), O_RDONLY | O_CLOEXEC);
    if (fd >= 0) {
        QFile file;
        if (file.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
            QImageReader reader(&file);
            reader.setAutoTransform(true);
            QSize fullSize = reader.size();
            if (fullSize.isValid()) {
                QSize target = fullSize.scaled(m_previewLabel->size(), Qt::KeepAspectRatio);
                reader.setScaledSize(target);
            }
            QImage image = reader.read();
            m_previewLabel->setPixmap(
                QPixmap::fromImage(image).scaled(
                    m_previewLabel->size(),
                    Qt::KeepAspectRatio,
                    Qt::SmoothTransformation));
        }
    }

    updateExifInfo(path);
}

void MainWindow::updateExifInfo(const QByteArray &path)
{
    // Check if this is a folder by attempting stat
    struct ::stat st{};
    if (::stat(path.constData(), &st) != 0 || S_ISDIR(st.st_mode)) {
        m_exifTable->setRowCount(0);
        return;
    }
    const ExifData data = ExifReader::read(path);
    const auto fields = data.toList();
    m_exifTable->setRowCount(fields.size());
    for (int i = 0; i < fields.size(); ++i) {
        m_exifTable->setItem(i, 0, new QTableWidgetItem(fields[i].first));
        m_exifTable->setItem(i, 1, new QTableWidgetItem(fields[i].second));
    }
    m_exifTable->resizeColumnToContents(0);
}

void MainWindow::enterSingleImageMode(const QModelIndex &index)
{
    if (!index.isValid())
        return;

    // Only save state and hide chrome when transitioning from browse mode
    if (m_stack->currentWidget() != m_imageView) {
        m_rootDockWasVisible = m_rootDock->isVisible();
        m_folderDockWasVisible = m_folderDock->isVisible();
        m_previewDockWasVisible = m_previewDock->isVisible();
        m_exifDockWasVisible = m_exifDock->isVisible();
        m_rootDock->hide();
        m_folderDock->hide();
        m_previewDock->hide();
        m_exifDock->hide();
        m_menuBar->hide();
    }

    m_imageView->setBackgroundColor(m_backgroundColorPreference);
    QByteArray path = m_imageModel->filePath(index);
    m_imageView->setNeighborPaths(computeNeighborPaths(index));
    m_imageView->setImage(path);
    m_imageView->setExifData(ExifReader::read(path));
    updateExifInfo(path);
    m_stack->setCurrentWidget(m_imageView);
    m_imageView->setFocus();

    if (m_imageViewWasFullScreen) {
        setFullScreenMode(true);
        m_imageViewWasFullScreen = false;
    }
}

void MainWindow::onEditCaption()
{
    const QByteArray path = m_imageView->currentPath();
    if (path.isEmpty())
        return;

    if (::access(path.constData(), W_OK) != 0) {
        QMessageBox::warning(this, tr("Cannot Edit Caption"),
            tr("The file is write-protected and cannot be edited.\n\n%1")
                .arg(QString::fromLocal8Bit(path)));
        return;
    }

    ExifData data = ExifReader::read(path);

    QDialog dlg(this);
    dlg.setWindowTitle(tr("Edit Caption"));
    auto *layout = new QVBoxLayout(&dlg);
    layout->addWidget(new QLabel(tr("Caption-Abstract:"), &dlg));
    auto *edit = new QPlainTextEdit(&dlg);
    edit->setPlainText(data.captionAbstract);
    edit->setMinimumWidth(400);
    layout->addWidget(edit);
    auto *buttons = new QDialogButtonBox(
        QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg);
    layout->addWidget(buttons);
    connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept);
    connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject);

    if (dlg.exec() != QDialog::Accepted)
        return;

    const QString newCaption = edit->toPlainText();
    if (ExifReader::saveCaption(path, newCaption, data)) {
        ExifData updated = ExifReader::read(path);
        m_imageView->setExifData(updated);
        updateExifInfo(path);
    }
}

void MainWindow::leaveSingleImageMode()
{
    if (isFullScreen()) {
        m_imageViewWasFullScreen = true;
        setFullScreenMode(false);
    }
    m_imageView->clearCache();
    m_stack->setCurrentWidget(m_browserPage);
    m_menuBar->show();
    if (m_rootDockWasVisible)
        m_rootDock->show();
    if (m_folderDockWasVisible)
        m_folderDock->show();
    if (m_previewDockWasVisible)
        m_previewDock->show();
    if (m_exifDockWasVisible)
        m_exifDock->show();
    m_listView->setFocus();
}

QList<QByteArray> MainWindow::computeNeighborPaths(const QModelIndex &index) const
{
    QList<QByteArray> paths;
    int row = index.row();

    // Collect up to CacheKeepBehind non-folder images before current
    int count = 0;
    for (int i = row - 1; i >= 0 && count < ImageViewWidget::CacheKeepBehind; --i) {
        QModelIndex idx = m_imageModel->index(i);
        if (!m_imageModel->isFolder(idx)) {
            paths.prepend(m_imageModel->filePath(idx));
            ++count;
        }
    }

    // Collect up to CacheReadAhead non-folder images after current
    count = 0;
    for (int i = row + 1; i < m_imageModel->rowCount() && count < ImageViewWidget::CacheReadAhead; ++i) {
        QModelIndex idx = m_imageModel->index(i);
        if (!m_imageModel->isFolder(idx)) {
            paths.append(m_imageModel->filePath(idx));
            ++count;
        }
    }

    return paths;
}

void MainWindow::setFullScreenMode(bool fullScreen)
{
    if (fullScreen == isFullScreen())
        return;

    if (fullScreen) {
        m_wasMaximized = isMaximized();
        m_menuBar->hide();
        showFullScreen();
    } else {
        // Only restore menu bar when returning to browse mode
        if (m_stack->currentWidget() != m_imageView)
            m_menuBar->show();
        if (m_wasMaximized)
            showMaximized();
        else
            showNormal();
    }
}

void MainWindow::keyPressEvent(QKeyEvent *event)
{
    if (event->key() == Qt::Key_F11 && m_stack->currentWidget() == m_imageView)
    {
        setFullScreenMode(!isFullScreen());
        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)) {
                QByteArray path = m_imageModel->filePath(current);
                navigateToFolder(path);
            } else {
                enterSingleImageMode(current);
            }
            event->accept();
            return;
        }
    }

    QMainWindow::keyPressEvent(event);
}

void MainWindow::closeEvent(QCloseEvent *event)
{
    if (m_stack->currentWidget() == m_imageView)
    {
        leaveSingleImageMode();
        event->ignore();
        return;
    }

    savePreferences();
    QMainWindow::closeEvent(event);
}

void MainWindow::applyDockLocking(bool lock)
{
    const QDockWidget::DockWidgetFeatures movableFeatures =
        QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable;
    const QDockWidget::DockWidgetFeatures baseFeatures =
        QDockWidget::DockWidgetClosable;

    QDockWidget::DockWidgetFeatures features = lock ? baseFeatures
                                                    : (baseFeatures | movableFeatures);
    m_rootDock->setFeatures(features);
    m_folderDock->setFeatures(features);
    m_previewDock->setFeatures(features);
    m_exifDock->setFeatures(features);
}

void MainWindow::loadPreferences()
{
    QSettings settings("SagoImageBrowser", "SagoImageBrowser");
    m_backgroundColorPreference = settings.value("backgroundColor", "black").toString();
    m_imageView->setBackgroundColor(m_backgroundColorPreference);
    m_lockDockingPreference = settings.value("lockDocking", false).toBool();
    applyDockLocking(m_lockDockingPreference);
    m_folderCacheSizePreference = settings.value("folderCacheSize", 4).toInt();
    m_imageModel->setCacheMaxSize(m_folderCacheSizePreference);
    m_thumbnailCacheSizePreference = settings.value("thumbnailCacheSize", "normal").toString();
    m_imageModel->setThumbnailSize(thumbnailSizeFromString(m_thumbnailCacheSizePreference));
    if (settings.contains("windowGeometry"))
        restoreGeometry(settings.value("windowGeometry").toByteArray());
    if (settings.contains("windowState"))
        restoreState(settings.value("windowState").toByteArray());
}

void MainWindow::savePreferences()
{
    QSettings settings("SagoImageBrowser", "SagoImageBrowser");
    settings.setValue("backgroundColor", m_backgroundColorPreference);
    settings.setValue("lockDocking", m_lockDockingPreference);
    settings.setValue("folderCacheSize", m_folderCacheSizePreference);
    settings.setValue("thumbnailCacheSize", m_thumbnailCacheSizePreference);
    settings.setValue("windowGeometry", saveGeometry());
    settings.setValue("windowState", saveState());
}

void MainWindow::onPreferencesTriggered()
{
    PreferencesDialog dialog(this);
    dialog.setBackgroundColor(m_backgroundColorPreference);
    dialog.setLockDocking(m_lockDockingPreference);
    dialog.setCacheSize(m_folderCacheSizePreference);
    dialog.setThumbnailCacheSize(m_thumbnailCacheSizePreference);

    bool shouldResetLayout = false;
    connect(&dialog, &PreferencesDialog::resetLayoutRequested, this, [&shouldResetLayout]() {
        shouldResetLayout = true;
    });

    if (dialog.exec() == QDialog::Accepted)
    {
        m_backgroundColorPreference = dialog.getBackgroundColor();
        m_imageView->setBackgroundColor(m_backgroundColorPreference);
        m_lockDockingPreference = dialog.getLockDocking();
        applyDockLocking(m_lockDockingPreference);
        m_folderCacheSizePreference = dialog.getCacheSize();
        m_imageModel->setCacheMaxSize(m_folderCacheSizePreference);
        QString newThumbSize = dialog.getThumbnailCacheSize();
        if (newThumbSize != m_thumbnailCacheSizePreference)
        {
            m_thumbnailCacheSizePreference = newThumbSize;
            m_imageModel->setThumbnailSize(thumbnailSizeFromString(m_thumbnailCacheSizePreference));
            loadVisibleThumbnails();
        }
        savePreferences();
    }
    
    // Apply layout reset immediately after dialog closes if requested
    if (shouldResetLayout) {
        resetLayoutToDefault();
    }
}

void MainWindow::resetLayoutToDefault()
{
    QSettings settings("SagoImageBrowser", "SagoImageBrowser");
    settings.remove("windowGeometry");
    settings.remove("windowState");
    
    // Hide and detach docks completely
    m_rootDock->hide();
    m_folderDock->hide();
    m_previewDock->hide();
    
    m_rootDock->setFloating(false);
    m_folderDock->setFloating(false);
    m_previewDock->setFloating(false);
    
    removeDockWidget(m_rootDock);
    removeDockWidget(m_folderDock);
    removeDockWidget(m_previewDock);
    
    // Re-add in default positions
    addDockWidget(Qt::LeftDockWidgetArea, m_rootDock);
    addDockWidget(Qt::LeftDockWidgetArea, m_folderDock);
    addDockWidget(Qt::RightDockWidgetArea, m_previewDock);
    splitDockWidget(m_rootDock, m_folderDock, Qt::Vertical);
    
    // Show all docks
    m_rootDock->show();
    m_folderDock->show();
    m_previewDock->show();
}