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 #include @@ -284,20 +285,41 @@ void ImageViewWidget::paintEvent(QPaintEvent *) // Help overlay — centered, toggled with F1 if (m_showHelpOverlay) { - static const QPair 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(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 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 scMap; + for (int i = 0; i < ShortcutManager::ActionCount; ++i) + scMap[i] = ShortcutManager::instance().shortcut( + static_cast(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 scMap = dialog.getShortcuts(); + for (auto it = scMap.cbegin(); it != scMap.cend(); ++it) + ShortcutManager::instance().setShortcut( + static_cast(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 #include @@ -8,17 +9,49 @@ #include #include #include +#include +#include +#include +#include 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(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(action)); + ++row; + }; + + // Image Viewer section + addSectionHeader("Image Viewer"); + for (int i = 0; i < ShortcutManager::ActionCount; ++i) { + auto action = static_cast(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(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( + m_shortcutsTable->cellWidget(r, 1)); + if (edit) + edit->setKeySequence(ShortcutManager::defaultShortcut( + static_cast(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, QList> catSeqToRows; + + for (int r = 0; r < m_rowToAction.size(); ++r) { + int actionIdx = m_rowToAction[r]; + if (actionIdx < 0) + continue; + auto *edit = qobject_cast( + m_shortcutsTable->cellWidget(r, 1)); + if (!edit) + continue; + QString s = edit->keySequence().toString(); + if (!s.isEmpty()) { + int cat = ShortcutManager::actionCategory( + static_cast(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 PreferencesDialog::getShortcuts() const +{ + QMap map; + for (int r = 0; r < m_rowToAction.size(); ++r) { + int actionIdx = m_rowToAction[r]; + if (actionIdx < 0) + continue; + auto *edit = qobject_cast( + m_shortcutsTable->cellWidget(r, 1)); + if (edit) + map[actionIdx] = edit->keySequence(); + } + return map; +} + +void PreferencesDialog::setShortcuts(const QMap &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( + 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 +#include +#include 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 getShortcuts() const; + void setShortcuts(const QMap &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 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 +#include + +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 +#include +#include + +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 m_bindings; +};