src/mainwindow.cpp
browsing at commit = ff6ac640c1802ab141912d1bf1bedce036acd50c
#include "mainwindow.h"
#include "imagemodel.h"
#include "imageviewwidget.h"
#include "preferencesdialog.h"
#include "shortcutmanager.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 <filesystem>
#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(tr("Home"), QDir::homePath());
addRootItem(tr("Pictures"), QStandardPaths::writableLocation(QStandardPaths::PicturesLocation));
addRootItem(tr("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_prevFolderButton = new QPushButton(tr("Prev"));
m_nextFolderButton = new QPushButton(tr("Next"));
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);
pathRow->addWidget(m_prevFolderButton);
pathRow->addWidget(m_nextFolderButton);
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(tr("&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(tr("&Edit"));
QAction *preferencesAction = editMenu->addAction(tr("&Preferences"));
connect(preferencesAction, &QAction::triggered, this, &MainWindow::onPreferencesTriggered);
}
void MainWindow::navigateToFolder(const QString &path)
{
navigateToFolder(QFile::encodeName(path));
}
QByteArray MainWindow::siblingFolder(int delta) const
{
QByteArray currentDir = m_imageModel->currentDirectory();
if (currentDir.isEmpty())
return {};
namespace fs = std::filesystem;
fs::path current(currentDir.toStdString());
fs::path parent = current.parent_path();
if (parent.empty() || parent == current)
return {};
QByteArray parentPath = QByteArray::fromStdString(parent.native());
QModelIndex parentIdx = m_dirModel->indexForPath(parentPath);
if (!parentIdx.isValid())
return {};
int count = m_dirModel->rowCount(parentIdx);
int currentRow = -1;
for (int i = 0; i < count; ++i) {
QModelIndex idx = m_dirModel->index(i, 0, parentIdx);
if (m_dirModel->filePath(idx) == currentDir) {
currentRow = i;
break;
}
}
if (currentRow < 0)
return {};
int targetRow = currentRow + delta;
if (targetRow < 0 || targetRow >= count)
return {};
QModelIndex targetIdx = m_dirModel->index(targetRow, 0, parentIdx);
return m_dirModel->filePath(targetIdx);
}
void MainWindow::navigateToPrevFolder()
{
QByteArray path = siblingFolder(-1);
if (!path.isEmpty())
navigateToFolder(path);
}
void MainWindow::navigateToNextFolder()
{
QByteArray path = siblingFolder(+1);
if (!path.isEmpty())
navigateToFolder(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_prevFolderButton, &QPushButton::clicked,
this, &MainWindow::navigateToPrevFolder);
connect(m_nextFolderButton, &QPushButton::clicked,
this, &MainWindow::navigateToNextFolder);
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 ¤t) { 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);
// The folder list and tree views consume Ctrl+Left/Ctrl+Right for their own
// item navigation, so those keys never reach MainWindow::keyPressEvent.
// Filter them here so PrevFolder/NextFolder shortcuts work while a view has focus.
m_listView->installEventFilter(this);
m_treeView->installEventFilter(this);
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
if (obj == m_listView->viewport() && event->type() == QEvent::Resize)
QTimer::singleShot(0, this, &MainWindow::loadVisibleThumbnails);
if ((obj == m_listView || obj == m_treeView)
&& event->type() == QEvent::KeyPress
&& m_stack->currentWidget() == m_browserPage)
{
const ShortcutManager &sc = ShortcutManager::instance();
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (sc.matches(keyEvent, ShortcutManager::PrevFolder)) {
navigateToPrevFolder();
return true;
}
if (sc.matches(keyEvent, ShortcutManager::NextFolder)) {
navigateToNextFolder();
return true;
}
}
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)
{
// THREADING: No race condition with F11 fullscreen toggle. All cross-thread
// signal deliveries (thumbnailReady via explicit QueuedConnection,
// onImageLoaded via AutoConnection resolving to QueuedConnection) execute on
// the main thread's event loop. The event loop processes events one at a
// time, so this handler and any signal slot can never interleave. All widget
// state (m_wasMaximized, m_stack, fullscreen flag) is only accessed from the
// main thread.
const ShortcutManager &sc = ShortcutManager::instance();
if (sc.matches(event, ShortcutManager::ToggleFullscreen)
&& m_stack->currentWidget() == m_imageView)
{
setFullScreenMode(!isFullScreen());
event->accept();
return;
}
if (sc.matches(event, ShortcutManager::ActivateEntry)
&& 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;
}
}
if (sc.matches(event, ShortcutManager::PrevFolder)
&& m_stack->currentWidget() == m_browserPage)
{
navigateToPrevFolder();
event->accept();
return;
}
if (sc.matches(event, ShortcutManager::NextFolder)
&& m_stack->currentWidget() == m_browserPage)
{
navigateToNextFolder();
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));
ShortcutManager::instance().load(settings);
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);
ShortcutManager::instance().save(settings);
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);
// Populate shortcuts tab from current bindings
{
QMap<int, QKeySequence> scMap;
for (int i = 0; i < ShortcutManager::ActionCount; ++i)
scMap[i] = ShortcutManager::instance().shortcut(
static_cast<ShortcutManager::Action>(i));
dialog.setShortcuts(scMap);
}
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();
}
// Apply shortcut changes
QMap<int, QKeySequence> scMap = dialog.getShortcuts();
for (auto it = scMap.cbegin(); it != scMap.cend(); ++it)
ShortcutManager::instance().setShortcut(
static_cast<ShortcutManager::Action>(it.key()), it.value());
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();
}