git repos / SagoImageBrowser

commit 83f0e0bc

Poul Sander · 2026-05-30 17:59
83f0e0bcc90b033a792e4778bdfc55553554e21f patch · browse files
parent 4d6b74767ab112b57f120df8dfee2b405817b5e1

Make shotcut keys adjustable

Changed files

M CMakeLists.txt before
M src/imageviewwidget.cpp before
M src/mainwindow.cpp before
M src/preferencesdialog.cpp before
M src/preferencesdialog.h before
A src/shortcutmanager.cpp
A src/shortcutmanager.h
diff --git a/CMakeLists.txt b/CMakeLists.txt index 4cf8380..a75c564 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -24,6 +24,8 @@ qt_add_executable(SagoImageBrowser
src/imageviewwidget.h
src/preferencesdialog.cpp
src/preferencesdialog.h
+ src/shortcutmanager.cpp
+ src/shortcutmanager.h
src/exifreader.cpp
src/exifreader.h
src/pathcompletermodel.cpp
diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index 5e83fab..af5925d 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp
@@ -1,4 +1,5 @@
#include "imageviewwidget.h"
+#include "shortcutmanager.h"
#include <QPainter>
#include <QKeyEvent>
@@ -284,20 +285,41 @@ void ImageViewWidget::paintEvent(QPaintEvent *)
// Help overlay — centered, toggled with F1
if (m_showHelpOverlay) {
- static const QPair<QString, QString> shortcuts[] = {
- {QStringLiteral("F1"), QStringLiteral("Show/hide keyboard shortcuts")},
- {QStringLiteral("Esc / Enter"), QStringLiteral("Close viewer")},
- {QStringLiteral("PgDn / PgUp"), QStringLiteral("Next / previous image")},
- {QStringLiteral("\u2190 \u2192 \u2191 \u2193"),
- QStringLiteral("Pan image")},
- {QStringLiteral("/"), QStringLiteral("Zoom to original size")},
- {QStringLiteral("*"), QStringLiteral("Zoom to fit screen")},
- {QStringLiteral("+ / \u2212"), QStringLiteral("Zoom in / out")},
- {QStringLiteral("I"), QStringLiteral("Toggle EXIF info")},
- {QStringLiteral("E"), QStringLiteral("Edit caption")},
- {QStringLiteral("F11"), QStringLiteral("Toggle fullscreen")},
- };
- const int count = static_cast<int>(std::size(shortcuts));
+ const ShortcutManager &sc = ShortcutManager::instance();
+
+ // Build the shortcut table dynamically from current bindings.
+ // Some actions are grouped together for display (e.g. pan directions).
+ struct HelpEntry { QString key; QString desc; };
+ QList<HelpEntry> entries;
+
+ entries.append({sc.displayKey(ShortcutManager::ToggleHelp),
+ sc.description(ShortcutManager::ToggleHelp)});
+ entries.append({sc.displayKey(ShortcutManager::CloseViewer)
+ + " / " + sc.displayKey(ShortcutManager::CloseViewerAlt),
+ QStringLiteral("Close viewer")});
+ entries.append({sc.displayKey(ShortcutManager::NextImage)
+ + " / " + sc.displayKey(ShortcutManager::PrevImage),
+ QStringLiteral("Next / previous image")});
+ entries.append({sc.displayKey(ShortcutManager::PanLeft)
+ + " " + sc.displayKey(ShortcutManager::PanRight)
+ + " " + sc.displayKey(ShortcutManager::PanUp)
+ + " " + sc.displayKey(ShortcutManager::PanDown),
+ QStringLiteral("Pan image")});
+ entries.append({sc.displayKey(ShortcutManager::ZoomOriginal),
+ sc.description(ShortcutManager::ZoomOriginal)});
+ entries.append({sc.displayKey(ShortcutManager::ZoomFit),
+ sc.description(ShortcutManager::ZoomFit)});
+ entries.append({sc.displayKey(ShortcutManager::ZoomIn)
+ + " / " + sc.displayKey(ShortcutManager::ZoomOut),
+ QStringLiteral("Zoom in / out")});
+ entries.append({sc.displayKey(ShortcutManager::ToggleExif),
+ sc.description(ShortcutManager::ToggleExif)});
+ entries.append({sc.displayKey(ShortcutManager::EditCaption),
+ sc.description(ShortcutManager::EditCaption)});
+ entries.append({sc.displayKey(ShortcutManager::ToggleFullscreen),
+ sc.description(ShortcutManager::ToggleFullscreen)});
+
+ const int count = entries.size();
const int padding = 16;
const int lineH = 22;
@@ -310,9 +332,9 @@ void ImageViewWidget::paintEvent(QPaintEvent *)
int keyColW = 0;
int descColW = 0;
- for (const auto &s : shortcuts) {
- keyColW = qMax(keyColW, fm.horizontalAdvance(s.first));
- descColW = qMax(descColW, fm.horizontalAdvance(s.second));
+ for (const auto &e : entries) {
+ keyColW = qMax(keyColW, fm.horizontalAdvance(e.key));
+ descColW = qMax(descColW, fm.horizontalAdvance(e.desc));
}
const int boxW = padding + keyColW + colGap + descColW + padding;
@@ -331,13 +353,13 @@ void ImageViewWidget::paintEvent(QPaintEvent *)
painter.setPen(QColor(180, 180, 255));
painter.drawText(QRect(boxX + padding, textY - fm.ascent(), keyColW, lineH),
Qt::AlignRight | Qt::AlignVCenter,
- shortcuts[i].first);
+ entries[i].key);
// Description column — left-aligned, white
painter.setPen(Qt::white);
painter.drawText(QRect(boxX + padding + keyColW + colGap,
textY - fm.ascent(), descColW, lineH),
Qt::AlignLeft | Qt::AlignVCenter,
- shortcuts[i].second);
+ entries[i].desc);
}
painter.restore();
}
@@ -345,7 +367,9 @@ void ImageViewWidget::paintEvent(QPaintEvent *)
void ImageViewWidget::keyPressEvent(QKeyEvent *event)
{
- if (event->key() == Qt::Key_F1)
+ const ShortcutManager &sc = ShortcutManager::instance();
+
+ if (sc.matches(event, ShortcutManager::ToggleHelp))
{
m_showHelpOverlay = !m_showHelpOverlay;
update();
@@ -353,21 +377,22 @@ void ImageViewWidget::keyPressEvent(QKeyEvent *event)
return;
}
- if (event->key() == Qt::Key_Escape || event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
+ if (sc.matches(event, ShortcutManager::CloseViewer)
+ || sc.matches(event, ShortcutManager::CloseViewerAlt))
{
emit closeRequested();
event->accept();
return;
}
- if (event->key() == Qt::Key_PageDown)
+ if (sc.matches(event, ShortcutManager::NextImage))
{
emit nextRequested();
event->accept();
return;
}
- if (event->key() == Qt::Key_PageUp)
+ if (sc.matches(event, ShortcutManager::PrevImage))
{
emit previousRequested();
event->accept();
@@ -376,28 +401,28 @@ void ImageViewWidget::keyPressEvent(QKeyEvent *event)
// Arrow keys for panning
constexpr int panStep = 50;
- if (event->key() == Qt::Key_Left)
+ if (sc.matches(event, ShortcutManager::PanLeft))
{
m_offset.rx() += panStep;
update();
event->accept();
return;
}
- if (event->key() == Qt::Key_Right)
+ if (sc.matches(event, ShortcutManager::PanRight))
{
m_offset.rx() -= panStep;
update();
event->accept();
return;
}
- if (event->key() == Qt::Key_Up)
+ if (sc.matches(event, ShortcutManager::PanUp))
{
m_offset.ry() += panStep;
update();
event->accept();
return;
}
- if (event->key() == Qt::Key_Down)
+ if (sc.matches(event, ShortcutManager::PanDown))
{
m_offset.ry() -= panStep;
update();
@@ -405,42 +430,42 @@ void ImageViewWidget::keyPressEvent(QKeyEvent *event)
return;
}
- if (event->key() == Qt::Key_Slash)
+ if (sc.matches(event, ShortcutManager::ZoomOriginal))
{
zoomOriginal();
event->accept();
return;
}
- if (event->key() == Qt::Key_Asterisk)
+ if (sc.matches(event, ShortcutManager::ZoomFit))
{
zoomFitToScreen();
event->accept();
return;
}
- if (event->key() == Qt::Key_Plus)
+ if (sc.matches(event, ShortcutManager::ZoomIn))
{
zoomIn();
event->accept();
return;
}
- if (event->key() == Qt::Key_Minus)
+ if (sc.matches(event, ShortcutManager::ZoomOut))
{
zoomOut();
event->accept();
return;
}
- if (event->key() == Qt::Key_I)
+ if (sc.matches(event, ShortcutManager::ToggleExif))
{
toggleExifOverlay();
event->accept();
return;
}
- if (event->key() == Qt::Key_E)
+ if (sc.matches(event, ShortcutManager::EditCaption))
{
emit editCaptionRequested();
event->accept();
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index be58306..6a442c4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp
@@ -2,6 +2,7 @@
#include "imagemodel.h"
#include "imageviewwidget.h"
#include "preferencesdialog.h"
+#include "shortcutmanager.h"
#include "thumbnailcache.h"
#include "exifreader.h"
#include "fsdirmodel.h"
@@ -587,14 +588,18 @@ void MainWindow::keyPressEvent(QKeyEvent *event)
// 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.
- if (event->key() == Qt::Key_F11 && m_stack->currentWidget() == m_imageView)
+ const ShortcutManager &sc = ShortcutManager::instance();
+
+ if (sc.matches(event, ShortcutManager::ToggleFullscreen)
+ && m_stack->currentWidget() == m_imageView)
{
setFullScreenMode(!isFullScreen());
event->accept();
return;
}
- if (event->key() == Qt::Key_Return && m_stack->currentWidget() == m_browserPage)
+ if (sc.matches(event, ShortcutManager::ActivateEntry)
+ && m_stack->currentWidget() == m_browserPage)
{
QModelIndex current = m_listView->currentIndex();
if (current.isValid())
@@ -652,6 +657,7 @@ void MainWindow::loadPreferences()
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"))
@@ -665,6 +671,7 @@ void MainWindow::savePreferences()
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());
}
@@ -677,6 +684,15 @@ void MainWindow::onPreferencesTriggered()
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;
@@ -697,6 +713,13 @@ void MainWindow::onPreferencesTriggered()
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();
}
diff --git a/src/preferencesdialog.cpp b/src/preferencesdialog.cpp index 2b1c3ef..e87d3b6 100644 --- a/src/preferencesdialog.cpp +++ b/src/preferencesdialog.cpp
@@ -1,4 +1,5 @@
#include "preferencesdialog.h"
+#include "shortcutmanager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
@@ -8,17 +9,49 @@
#include <QCheckBox>
#include <QPushButton>
#include <QGroupBox>
+#include <QTabWidget>
+#include <QTableWidget>
+#include <QKeySequenceEdit>
+#include <QHeaderView>
PreferencesDialog::PreferencesDialog(QWidget *parent)
: QDialog(parent)
{
setWindowTitle("Preferences");
setModal(true);
- setMinimumWidth(400);
+ setMinimumWidth(500);
QVBoxLayout *mainLayout = new QVBoxLayout(this);
- QGroupBox *imageGroup = new QGroupBox("Image Viewer", this);
+ QTabWidget *tabs = new QTabWidget(this);
+ setupGeneralTab(tabs);
+ setupShortcutsTab(tabs);
+ mainLayout->addWidget(tabs);
+
+ QHBoxLayout *buttonLayout = new QHBoxLayout;
+ QPushButton *okButton = new QPushButton("OK");
+ QPushButton *cancelButton = new QPushButton("Cancel");
+
+ buttonLayout->addStretch();
+ buttonLayout->addWidget(okButton);
+ buttonLayout->addWidget(cancelButton);
+
+ mainLayout->addLayout(buttonLayout);
+
+ connect(okButton, &QPushButton::clicked, this, [this]() {
+ if (m_resetLayoutCheck->isChecked())
+ emit resetLayoutRequested();
+ accept();
+ });
+ connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+}
+
+void PreferencesDialog::setupGeneralTab(QTabWidget *tabs)
+{
+ QWidget *page = new QWidget;
+ QVBoxLayout *pageLayout = new QVBoxLayout(page);
+
+ QGroupBox *imageGroup = new QGroupBox("Image Viewer", page);
QVBoxLayout *groupLayout = new QVBoxLayout(imageGroup);
QHBoxLayout *bgColorLayout = new QHBoxLayout;
@@ -54,36 +87,192 @@ PreferencesDialog::PreferencesDialog(QWidget *parent)
thumbSizeLayout->addStretch();
groupLayout->addLayout(thumbSizeLayout);
- mainLayout->addWidget(imageGroup);
+ pageLayout->addWidget(imageGroup);
- QGroupBox *layoutGroup = new QGroupBox("Layout", this);
+ QGroupBox *layoutGroup = new QGroupBox("Layout", page);
QVBoxLayout *layoutGroupLayout = new QVBoxLayout(layoutGroup);
m_lockDockingCheck = new QCheckBox("Lock docking (prevent accidental moves)");
layoutGroupLayout->addWidget(m_lockDockingCheck);
-
+
m_resetLayoutCheck = new QCheckBox("Reset layout to default when closing the menu");
layoutGroupLayout->addWidget(m_resetLayoutCheck);
-
- mainLayout->addWidget(layoutGroup);
- mainLayout->addStretch();
+ pageLayout->addWidget(layoutGroup);
+ pageLayout->addStretch();
- QHBoxLayout *buttonLayout = new QHBoxLayout;
- QPushButton *okButton = new QPushButton("OK");
- QPushButton *cancelButton = new QPushButton("Cancel");
+ tabs->addTab(page, "General");
+}
- buttonLayout->addStretch();
- buttonLayout->addWidget(okButton);
- buttonLayout->addWidget(cancelButton);
+void PreferencesDialog::setupShortcutsTab(QTabWidget *tabs)
+{
+ QWidget *page = new QWidget;
+ QVBoxLayout *pageLayout = new QVBoxLayout(page);
- mainLayout->addLayout(buttonLayout);
+ // Count configurable actions to size the table (plus 2 section header rows)
+ int configurableCount = 0;
+ for (int i = 0; i < ShortcutManager::ActionCount; ++i) {
+ if (ShortcutManager::actionCategory(static_cast<ShortcutManager::Action>(i))
+ != ShortcutManager::CatNotConfigurable)
+ ++configurableCount;
+ }
+ // +2 for section headers (Image Viewer, Browser)
+ const int totalRows = configurableCount + 2;
- connect(okButton, &QPushButton::clicked, this, [this]() {
- if (m_resetLayoutCheck->isChecked())
- emit resetLayoutRequested();
- accept();
+ m_shortcutsTable = new QTableWidget(totalRows, 2, page);
+ m_shortcutsTable->setHorizontalHeaderLabels({"Action", "Key"});
+ m_shortcutsTable->horizontalHeader()->setStretchLastSection(true);
+ m_shortcutsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
+ m_shortcutsTable->verticalHeader()->hide();
+ m_shortcutsTable->setSelectionMode(QAbstractItemView::SingleSelection);
+
+ m_rowToAction.clear();
+ const ShortcutManager &mgr = ShortcutManager::instance();
+ int row = 0;
+
+ auto addSectionHeader = [&](const QString &title) {
+ auto *item = new QTableWidgetItem(title);
+ item->setFlags(Qt::NoItemFlags);
+ QFont f = item->font();
+ f.setBold(true);
+ item->setFont(f);
+ item->setBackground(palette().alternateBase());
+ m_shortcutsTable->setItem(row, 0, item);
+ auto *emptyItem = new QTableWidgetItem();
+ emptyItem->setFlags(Qt::NoItemFlags);
+ emptyItem->setBackground(palette().alternateBase());
+ m_shortcutsTable->setItem(row, 1, emptyItem);
+ m_rowToAction.append(-1); // -1 = section header
+ ++row;
+ };
+
+ auto addAction = [&](ShortcutManager::Action action) {
+ auto *descItem = new QTableWidgetItem(mgr.description(action));
+ descItem->setFlags(descItem->flags() & ~Qt::ItemIsEditable);
+ m_shortcutsTable->setItem(row, 0, descItem);
+
+ auto *edit = new QKeySequenceEdit(mgr.shortcut(action));
+ edit->setMaximumSequenceLength(1);
+ connect(edit, &QKeySequenceEdit::keySequenceChanged, this, [this]() {
+ highlightConflicts();
+ });
+ m_shortcutsTable->setCellWidget(row, 1, edit);
+ m_rowToAction.append(static_cast<int>(action));
+ ++row;
+ };
+
+ // Image Viewer section
+ addSectionHeader("Image Viewer");
+ for (int i = 0; i < ShortcutManager::ActionCount; ++i) {
+ auto action = static_cast<ShortcutManager::Action>(i);
+ if (ShortcutManager::actionCategory(action) == ShortcutManager::CatImageViewer)
+ addAction(action);
+ }
+
+ // Browser section
+ addSectionHeader("Browser");
+ for (int i = 0; i < ShortcutManager::ActionCount; ++i) {
+ auto action = static_cast<ShortcutManager::Action>(i);
+ if (ShortcutManager::actionCategory(action) == ShortcutManager::CatBrowser)
+ addAction(action);
+ }
+
+ pageLayout->addWidget(m_shortcutsTable);
+
+ QHBoxLayout *btnLayout = new QHBoxLayout;
+ QPushButton *resetBtn = new QPushButton("Reset to Defaults");
+ btnLayout->addStretch();
+ btnLayout->addWidget(resetBtn);
+ pageLayout->addLayout(btnLayout);
+
+ connect(resetBtn, &QPushButton::clicked, this, [this]() {
+ for (int r = 0; r < m_rowToAction.size(); ++r) {
+ int actionIdx = m_rowToAction[r];
+ if (actionIdx < 0)
+ continue;
+ auto *edit = qobject_cast<QKeySequenceEdit *>(
+ m_shortcutsTable->cellWidget(r, 1));
+ if (edit)
+ edit->setKeySequence(ShortcutManager::defaultShortcut(
+ static_cast<ShortcutManager::Action>(actionIdx)));
+ }
+ highlightConflicts();
});
- connect(cancelButton, &QPushButton::clicked, this, &QDialog::reject);
+
+ tabs->addTab(page, "Shortcuts");
+}
+
+void PreferencesDialog::highlightConflicts()
+{
+ // Group sequences by category, then detect conflicts within each category.
+ // key: (category, sequenceString) → list of rows
+ QMap<QPair<int,QString>, QList<int>> catSeqToRows;
+
+ for (int r = 0; r < m_rowToAction.size(); ++r) {
+ int actionIdx = m_rowToAction[r];
+ if (actionIdx < 0)
+ continue;
+ auto *edit = qobject_cast<QKeySequenceEdit *>(
+ m_shortcutsTable->cellWidget(r, 1));
+ if (!edit)
+ continue;
+ QString s = edit->keySequence().toString();
+ if (!s.isEmpty()) {
+ int cat = ShortcutManager::actionCategory(
+ static_cast<ShortcutManager::Action>(actionIdx));
+ catSeqToRows[{cat, s}].append(r);
+ }
+ }
+
+ // Reset all backgrounds
+ for (int r = 0; r < m_rowToAction.size(); ++r) {
+ if (m_rowToAction[r] < 0)
+ continue;
+ QTableWidgetItem *item = m_shortcutsTable->item(r, 0);
+ if (item)
+ item->setBackground(QBrush());
+ }
+
+ // Highlight conflicts (only within same category)
+ for (auto it = catSeqToRows.cbegin(); it != catSeqToRows.cend(); ++it) {
+ if (it.value().size() > 1) {
+ for (int r : it.value()) {
+ QTableWidgetItem *item = m_shortcutsTable->item(r, 0);
+ if (item)
+ item->setBackground(QColor(255, 100, 100, 80));
+ }
+ }
+ }
+}
+
+QMap<int, QKeySequence> PreferencesDialog::getShortcuts() const
+{
+ QMap<int, QKeySequence> map;
+ for (int r = 0; r < m_rowToAction.size(); ++r) {
+ int actionIdx = m_rowToAction[r];
+ if (actionIdx < 0)
+ continue;
+ auto *edit = qobject_cast<QKeySequenceEdit *>(
+ m_shortcutsTable->cellWidget(r, 1));
+ if (edit)
+ map[actionIdx] = edit->keySequence();
+ }
+ return map;
+}
+
+void PreferencesDialog::setShortcuts(const QMap<int, QKeySequence> &map)
+{
+ for (int r = 0; r < m_rowToAction.size(); ++r) {
+ int actionIdx = m_rowToAction[r];
+ if (actionIdx < 0)
+ continue;
+ if (!map.contains(actionIdx))
+ continue;
+ auto *edit = qobject_cast<QKeySequenceEdit *>(
+ m_shortcutsTable->cellWidget(r, 1));
+ if (edit)
+ edit->setKeySequence(map[actionIdx]);
+ }
+ highlightConflicts();
}
QString PreferencesDialog::getBackgroundColor() const
diff --git a/src/preferencesdialog.h b/src/preferencesdialog.h index f4dae5d..5d5117f 100644 --- a/src/preferencesdialog.h +++ b/src/preferencesdialog.h
@@ -1,10 +1,14 @@
#pragma once
#include <QDialog>
+#include <QKeySequence>
+#include <QMap>
class QComboBox;
class QCheckBox;
class QSpinBox;
+class QTabWidget;
+class QTableWidget;
class PreferencesDialog : public QDialog
{
@@ -28,13 +32,22 @@ public:
bool getResetLayout() const;
void setResetLayout(bool reset);
+ QMap<int, QKeySequence> getShortcuts() const;
+ void setShortcuts(const QMap<int, QKeySequence> &map);
+
signals:
void resetLayoutRequested();
private:
+ void setupGeneralTab(QTabWidget *tabs);
+ void setupShortcutsTab(QTabWidget *tabs);
+ void highlightConflicts();
+
QComboBox *m_backgroundColorCombo;
QSpinBox *m_cacheSizeSpinBox;
QComboBox *m_thumbnailSizeCombo;
QCheckBox *m_lockDockingCheck;
QCheckBox *m_resetLayoutCheck;
+ QTableWidget *m_shortcutsTable = nullptr;
+ QList<int> m_rowToAction; // maps table row → ShortcutManager::Action
};
diff --git a/src/shortcutmanager.cpp b/src/shortcutmanager.cpp new file mode 100644 index 0000000..6f98427 --- /dev/null +++ b/src/shortcutmanager.cpp
@@ -0,0 +1,153 @@
+#include "shortcutmanager.h"
+
+#include <QKeyEvent>
+#include <QSettings>
+
+namespace {
+
+struct ActionInfo {
+ const char *name; // settings key
+ const char *description; // human-readable for help overlay / prefs table
+ QKeySequence defaultKey;
+ ShortcutManager::Category category;
+};
+
+// Order must match ShortcutManager::Action enum
+static const ActionInfo kDefaults[ShortcutManager::ActionCount] = {
+ {"ToggleHelp", "Show/hide keyboard shortcuts", QKeySequence(Qt::Key_F1), ShortcutManager::CatNotConfigurable},
+ {"CloseViewer", "Close viewer", QKeySequence(Qt::Key_Escape), ShortcutManager::CatImageViewer},
+ {"CloseViewerAlt", "Close viewer (alt)", QKeySequence(Qt::Key_Return), ShortcutManager::CatImageViewer},
+ {"NextImage", "Next image", QKeySequence(Qt::Key_PageDown), ShortcutManager::CatImageViewer},
+ {"PrevImage", "Previous image", QKeySequence(Qt::Key_PageUp), ShortcutManager::CatImageViewer},
+ {"PanLeft", "Pan left", QKeySequence(Qt::Key_Left), ShortcutManager::CatImageViewer},
+ {"PanRight", "Pan right", QKeySequence(Qt::Key_Right), ShortcutManager::CatImageViewer},
+ {"PanUp", "Pan up", QKeySequence(Qt::Key_Up), ShortcutManager::CatImageViewer},
+ {"PanDown", "Pan down", QKeySequence(Qt::Key_Down), ShortcutManager::CatImageViewer},
+ {"ZoomOriginal", "Zoom to original size", QKeySequence(Qt::Key_Slash), ShortcutManager::CatImageViewer},
+ {"ZoomFit", "Zoom to fit screen", QKeySequence(Qt::Key_Asterisk), ShortcutManager::CatImageViewer},
+ {"ZoomIn", "Zoom in", QKeySequence(Qt::Key_Plus), ShortcutManager::CatImageViewer},
+ {"ZoomOut", "Zoom out", QKeySequence(Qt::Key_Minus), ShortcutManager::CatImageViewer},
+ {"ToggleExif", "Toggle EXIF info", QKeySequence(Qt::Key_I), ShortcutManager::CatImageViewer},
+ {"EditCaption", "Edit caption", QKeySequence(Qt::Key_E), ShortcutManager::CatImageViewer},
+ {"ToggleFullscreen","Toggle fullscreen", QKeySequence(Qt::Key_F11), ShortcutManager::CatImageViewer},
+ {"ActivateEntry", "Open folder / image", QKeySequence(Qt::Key_Return), ShortcutManager::CatBrowser},
+};
+
+} // namespace
+
+ShortcutManager::ShortcutManager()
+{
+ resetToDefaults();
+}
+
+ShortcutManager &ShortcutManager::instance()
+{
+ static ShortcutManager mgr;
+ return mgr;
+}
+
+QKeySequence ShortcutManager::shortcut(Action action) const
+{
+ if (action < 0 || action >= ActionCount)
+ return {};
+ return m_bindings[action].key;
+}
+
+QString ShortcutManager::displayKey(Action action) const
+{
+ if (action < 0 || action >= ActionCount)
+ return {};
+ return m_bindings[action].key.toString(QKeySequence::NativeText);
+}
+
+QString ShortcutManager::description(Action action) const
+{
+ return actionDescription(action);
+}
+
+void ShortcutManager::setShortcut(Action action, const QKeySequence &seq)
+{
+ if (action >= 0 && action < ActionCount)
+ m_bindings[action].key = seq;
+}
+
+void ShortcutManager::resetToDefaults()
+{
+ for (int i = 0; i < ActionCount; ++i)
+ m_bindings[i].key = kDefaults[i].defaultKey;
+}
+
+void ShortcutManager::load(QSettings &settings)
+{
+ settings.beginGroup("shortcuts");
+ for (int i = 0; i < ActionCount; ++i) {
+ if (kDefaults[i].category == CatNotConfigurable) {
+ m_bindings[i].key = kDefaults[i].defaultKey;
+ continue;
+ }
+ const QString key = QString::fromLatin1(kDefaults[i].name);
+ if (settings.contains(key))
+ m_bindings[i].key = QKeySequence(settings.value(key).toString());
+ else
+ m_bindings[i].key = kDefaults[i].defaultKey;
+ }
+ settings.endGroup();
+}
+
+void ShortcutManager::save(QSettings &settings) const
+{
+ settings.beginGroup("shortcuts");
+ for (int i = 0; i < ActionCount; ++i) {
+ if (kDefaults[i].category == CatNotConfigurable)
+ continue;
+ const QString key = QString::fromLatin1(kDefaults[i].name);
+ if (m_bindings[i].key == kDefaults[i].defaultKey)
+ settings.remove(key); // only store non-defaults
+ else
+ settings.setValue(key, m_bindings[i].key.toString());
+ }
+ settings.endGroup();
+}
+
+QKeySequence ShortcutManager::defaultShortcut(Action action)
+{
+ if (action < 0 || action >= ActionCount)
+ return {};
+ return kDefaults[action].defaultKey;
+}
+
+QString ShortcutManager::actionName(Action action)
+{
+ if (action < 0 || action >= ActionCount)
+ return {};
+ return QString::fromLatin1(kDefaults[action].name);
+}
+
+QString ShortcutManager::actionDescription(Action action)
+{
+ if (action < 0 || action >= ActionCount)
+ return {};
+ return QString::fromLatin1(kDefaults[action].description);
+}
+
+ShortcutManager::Category ShortcutManager::actionCategory(Action action)
+{
+ if (action < 0 || action >= ActionCount)
+ return CatNotConfigurable;
+ return kDefaults[action].category;
+}
+
+bool ShortcutManager::matches(const QKeyEvent *event, Action action) const
+{
+ if (action < 0 || action >= ActionCount)
+ return false;
+ const QKeySequence &seq = m_bindings[action].key;
+ if (seq.isEmpty())
+ return false;
+ // Compare the first key in the sequence against the event's key + modifiers.
+ int eventKey = event->key();
+ Qt::KeyboardModifiers mods = event->modifiers() & ~Qt::KeypadModifier;
+ if (mods != Qt::NoModifier)
+ eventKey |= mods;
+ return seq[0] == QKeyCombination::fromCombined(eventKey);
+}
diff --git a/src/shortcutmanager.h b/src/shortcutmanager.h new file mode 100644 index 0000000..2a741e8 --- /dev/null +++ b/src/shortcutmanager.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include <QKeySequence>
+#include <QString>
+#include <array>
+
+class QSettings;
+class QKeyEvent;
+
+class ShortcutManager
+{
+public:
+ enum Action {
+ ToggleHelp,
+ CloseViewer,
+ CloseViewerAlt,
+ NextImage,
+ PrevImage,
+ PanLeft,
+ PanRight,
+ PanUp,
+ PanDown,
+ ZoomOriginal,
+ ZoomFit,
+ ZoomIn,
+ ZoomOut,
+ ToggleExif,
+ EditCaption,
+ ToggleFullscreen,
+ ActivateEntry,
+ ActionCount
+ };
+
+ enum Category {
+ CatBrowser,
+ CatImageViewer,
+ CatNotConfigurable
+ };
+
+ static ShortcutManager &instance();
+
+ QKeySequence shortcut(Action action) const;
+ QString displayKey(Action action) const;
+ QString description(Action action) const;
+
+ void setShortcut(Action action, const QKeySequence &seq);
+ void resetToDefaults();
+
+ void load(QSettings &settings);
+ void save(QSettings &settings) const;
+
+ static QKeySequence defaultShortcut(Action action);
+ static QString actionName(Action action);
+ static QString actionDescription(Action action);
+ static Category actionCategory(Action action);
+
+ /// Returns true if the key event matches the configured shortcut for the action.
+ bool matches(const QKeyEvent *event, Action action) const;
+
+private:
+ ShortcutManager();
+
+ struct Binding {
+ QKeySequence key;
+ };
+
+ std::array<Binding, ActionCount> m_bindings;
+};