Skip to content

Conversation

@Halvance
Copy link

@Halvance Halvance commented Dec 17, 2025

Hotkey System:

  • Added HotkeysPage preference dialog for configuring all shortcuts
  • Support for customizing File, Edit, View, Panel, Mouse, and Room operations
  • Named hotkey configuration with defaults (e.g., Ctrl+O for Open)
  • Integration with MainWindow for applying hotkeys dynamically
  • Global shortcut registration system

Features:

  • 50+ configurable keyboard shortcuts
  • Organized by category (File, Edit, View, Panels, Modes, Rooms)
  • Empty defaults for advanced shortcuts (user can customize)
  • Immediate application of hotkey changes
  • Persistent storage in configuration

Configuration Infrastructure:

  • Added Hotkeys configuration struct with NamedConfig fields
  • All hotkeys stored with descriptive names (e.g., "HOTKEY_FILE_OPEN")
  • Read/write support for hotkey persistence
  • Change callbacks for live updates

UI Integration:

  • Hotkeys page added to preferences dialog
  • Icon integration (hotkeys.png from PR-A)
  • Tab order and accessibility support

This enables users to fully customize MMapper's keyboard shortcuts according to their preferences and workflow.

Summary by Sourcery

Introduce a configurable hotkey system, new communications and visibility tooling, and extended canvas/clock settings, wiring them through configuration, main window, and preferences UI.

New Features:

  • Add a centralized hotkeys configuration with named entries for file, edit, view, panel, mouse mode, and room operations.
  • Expose a Hotkeys preferences page that lets users view, edit, clear, and reset keyboard shortcuts, applying changes immediately.
  • Add a Communications panel driven by a CommsManager/CommsWidget, including optional auto-saving of its log on exit.
  • Introduce a Visibility Filter toolbar/panel to control visibility of infomark classes and connections on the map.
  • Support selectable texture sets, seasonal textures, and background images (with fit/opacity/focus options) for the canvas.
  • Add configurable GMCP clock broadcasting and interval to synchronize time with the client.

Enhancements:

  • Register all main window actions for global shortcuts and route shortcut configuration through the new hotkey system instead of hard-coded key sequences.
  • Extend canvas configuration with layer transparency, radial transparency toggle, and per-marker visibility control with change callbacks.
  • Refine default UI theme colors, including a darker default background color for the canvas.
  • Wire configuration dialog to emit dedicated signals for graphics, texture, hotkey, and communications settings, updating dependent components live.

Build:

  • Register new communications, visibility filter, and hotkeys UI/manager sources in the CMake build.

Hotkey System:
- Added HotkeysPage preference dialog for configuring all shortcuts
- Support for customizing File, Edit, View, Panel, Mouse, and Room operations
- Named hotkey configuration with defaults (e.g., Ctrl+O for Open)
- Integration with MainWindow for applying hotkeys dynamically
- Global shortcut registration system

Features:
- 50+ configurable keyboard shortcuts
- Organized by category (File, Edit, View, Panels, Modes, Rooms)
- Empty defaults for advanced shortcuts (user can customize)
- Immediate application of hotkey changes
- Persistent storage in configuration

Configuration Infrastructure:
- Added Hotkeys configuration struct with NamedConfig fields
- All hotkeys stored with descriptive names (e.g., "HOTKEY_FILE_OPEN")
- Read/write support for hotkey persistence
- Change callbacks for live updates

UI Integration:
- Hotkeys page added to preferences dialog
- Icon integration (hotkeys.png from PR-A)
- Tab order and accessibility support

This enables users to fully customize MMapper's keyboard shortcuts
according to their preferences and workflow.
@sourcery-ai
Copy link

sourcery-ai bot commented Dec 17, 2025

Reviewer's Guide

Introduces a configurable hotkey system wired through configuration, preferences UI, and MainWindow so that most map, panel, and room operations can have dynamic, persisted shortcuts, while also adding related canvas visibility/texture options and a new communications panel with configurable settings.

Sequence diagram for applying updated hotkeys from preferences

sequenceDiagram
    actor User
    participant MainWindow
    participant ConfigDialog
    participant HotkeysPage
    participant Configuration

    User->>MainWindow: Open Preferences (Edit -> Preferences)
    MainWindow->>MainWindow: slot_onPreferences()
    MainWindow->>ConfigDialog: create if needed
    MainWindow->>ConfigDialog: show()
    ConfigDialog-->>HotkeysPage: sig_loadConfig
    HotkeysPage->>Configuration: loadSettings() from getConfig().hotkeys
    Configuration-->>HotkeysPage: current hotkey values

    User->>HotkeysPage: Edit one or more hotkey fields
    HotkeysPage->>HotkeysPage: QKeySequenceEdit.editingFinished
    HotkeysPage->>Configuration: saveSettings() to setConfig().hotkeys
    HotkeysPage-->>ConfigDialog: sig_hotkeysChanged
    ConfigDialog-->>MainWindow: sig_hotkeysChanged
    MainWindow->>MainWindow: applyHotkeys()
    MainWindow->>Configuration: read current hotkeys
    Configuration-->>MainWindow: NamedConfig values
    MainWindow->>MainWindow: set QAction shortcuts

    User->>MainWindow: Use updated shortcut (e.g. Ctrl+O)
    MainWindow->>MainWindow: QAction triggered
    MainWindow->>MainWindow: connected slot executed (e.g. slot_open())
Loading

Updated class diagram for configuration hotkeys, canvas visibility, and comms settings

classDiagram
    class Configuration {
        +GeneralSettings general
        +CanvasSettings canvas
        +Hotkeys hotkeys
        +CommsSettings comms
        +MumeClockSettings mumeClock
    }

    class NamedConfig~T~ {
        +T get()
        +void set(T value)
        +QString getName()
    }

    class CanvasSettings {
        +bool trilinearFiltering
        +bool softwareOpenGL
        +QString resourcesDirectory
        +TextureSetEnum textureSet
        +bool enableSeasonalTextures
        +float layerTransparency
        +bool enableRadialTransparency
        +Advanced advanced
        +VisibilityFilter visibilityFilter
        +void read(QSettings conf)
        +void write(QSettings conf)
    }

    class Advanced {
        +NamedConfig~bool~ enableHiddenLayers
        +NamedConfig~bool~ autoTilt
        +NamedConfig~bool~ printPerfStats
        +bool useBackgroundImage
        +QString backgroundImagePath
        +int backgroundFitMode
        +float backgroundOpacity
        +float backgroundFocusedScale
        +float backgroundFocusedOffsetX
        +float backgroundFocusedOffsetY
        +FixedPoint~1~ fov
        +FixedPoint~1~ verticalAngle
        +FixedPoint~1~ horizontalAngle
        +FixedPoint~1~ layerHeight
        +void registerChangeCallback(ChangeMonitor_Lifetime lifetime, ChangeMonitor_Function callback)
    }

    class VisibilityFilter {
        +NamedConfig~bool~ generic
        +NamedConfig~bool~ herb
        +NamedConfig~bool~ river
        +NamedConfig~bool~ place
        +NamedConfig~bool~ mob
        +NamedConfig~bool~ comment
        +NamedConfig~bool~ road
        +NamedConfig~bool~ object
        +NamedConfig~bool~ action
        +NamedConfig~bool~ locality
        +NamedConfig~bool~ connections
        +bool isVisible(InfomarkClassEnum markerClass)
        +void setVisible(InfomarkClassEnum markerClass, bool visible)
        +bool isConnectionsVisible()
        +void setConnectionsVisible(bool visible)
        +void showAll()
        +void hideAll()
        +void registerChangeCallback(ChangeMonitor_Lifetime lifetime, ChangeMonitor_Function callback)
    }

    class Hotkeys {
        +NamedConfig~QString~ fileOpen
        +NamedConfig~QString~ fileSave
        +NamedConfig~QString~ fileReload
        +NamedConfig~QString~ fileQuit
        +NamedConfig~QString~ editUndo
        +NamedConfig~QString~ editRedo
        +NamedConfig~QString~ editPreferences
        +NamedConfig~QString~ editPreferencesAlt
        +NamedConfig~QString~ editFindRooms
        +NamedConfig~QString~ editRoom
        +NamedConfig~QString~ viewZoomIn
        +NamedConfig~QString~ viewZoomOut
        +NamedConfig~QString~ viewZoomReset
        +NamedConfig~QString~ viewLayerUp
        +NamedConfig~QString~ viewLayerDown
        +NamedConfig~QString~ viewLayerReset
        +NamedConfig~QString~ viewRadialTransparency
        +NamedConfig~QString~ viewStatusBar
        +NamedConfig~QString~ viewScrollBars
        +NamedConfig~QString~ viewMenuBar
        +NamedConfig~QString~ viewAlwaysOnTop
        +NamedConfig~QString~ panelLog
        +NamedConfig~QString~ panelClient
        +NamedConfig~QString~ panelGroup
        +NamedConfig~QString~ panelRoom
        +NamedConfig~QString~ panelAdventure
        +NamedConfig~QString~ panelDescription
        +NamedConfig~QString~ panelComms
        +NamedConfig~QString~ modeMoveMap
        +NamedConfig~QString~ modeRaypick
        +NamedConfig~QString~ modeSelectRooms
        +NamedConfig~QString~ modeSelectMarkers
        +NamedConfig~QString~ modeSelectConnection
        +NamedConfig~QString~ modeCreateMarker
        +NamedConfig~QString~ modeCreateRoom
        +NamedConfig~QString~ modeCreateConnection
        +NamedConfig~QString~ modeCreateOnewayConnection
        +NamedConfig~QString~ roomCreate
        +NamedConfig~QString~ roomMoveUp
        +NamedConfig~QString~ roomMoveDown
        +NamedConfig~QString~ roomMergeUp
        +NamedConfig~QString~ roomMergeDown
        +NamedConfig~QString~ roomDelete
        +NamedConfig~QString~ roomConnectNeighbors
        +NamedConfig~QString~ roomMoveToSelected
        +NamedConfig~QString~ roomUpdateSelected
        +void read(QSettings conf)
        +void write(QSettings conf)
    }

    class CommsSettings {
        +NamedConfig~QColor~ tellColor
        +NamedConfig~QColor~ whisperColor
        +NamedConfig~QColor~ groupColor
        +NamedConfig~QColor~ askColor
        +NamedConfig~QColor~ sayColor
        +NamedConfig~QColor~ emoteColor
        +NamedConfig~QColor~ socialColor
        +NamedConfig~QColor~ yellColor
        +NamedConfig~QColor~ narrateColor
        +NamedConfig~QColor~ prayColor
        +NamedConfig~QColor~ shoutColor
        +NamedConfig~QColor~ singColor
        +NamedConfig~QColor~ backgroundColor
        +NamedConfig~QColor~ talkerYouColor
        +NamedConfig~QColor~ talkerPlayerColor
        +NamedConfig~QColor~ talkerNpcColor
        +NamedConfig~QColor~ talkerAllyColor
        +NamedConfig~QColor~ talkerNeutralColor
        +NamedConfig~QColor~ talkerEnemyColor
        +NamedConfig~bool~ yellAllCaps
        +NamedConfig~bool~ whisperItalic
        +NamedConfig~bool~ emoteItalic
        +NamedConfig~bool~ showTimestamps
        +NamedConfig~bool~ saveLogOnExit
        +NamedConfig~QString~ logDirectory
        +NamedConfig~bool~ muteDirectTab
        +NamedConfig~bool~ muteLocalTab
        +NamedConfig~bool~ muteGlobalTab
        +void read(QSettings conf)
        +void write(QSettings conf)
    }

    class MumeClockSettings {
        +int64_t startEpoch
        +bool display
        +NamedConfig~bool~ gmcpBroadcast
        +NamedConfig~int~ gmcpBroadcastInterval
        +MumeClockSettings()
        +void read(QSettings conf)
        +void write(QSettings conf)
    }

    class QSettings

    Configuration --> CanvasSettings : has
    Configuration --> Hotkeys : has
    Configuration --> CommsSettings : has
    Configuration --> MumeClockSettings : has

    CanvasSettings --> Advanced : has
    CanvasSettings --> VisibilityFilter : has

    Hotkeys --> QSettings : read_write
    CommsSettings --> QSettings : read_write
    CanvasSettings --> QSettings : read_write
    MumeClockSettings --> QSettings : read_write

    Hotkeys --> NamedConfig : uses
    CommsSettings --> NamedConfig : uses
    VisibilityFilter --> NamedConfig : uses
    Advanced --> NamedConfig : uses
    MumeClockSettings --> NamedConfig : uses
Loading

Updated class diagram for MainWindow hotkey wiring and communications integration

classDiagram
    class MainWindow {
        +QDockWidget* m_dockDialogLog
        +QDockWidget* m_dockDialogClient
        +QDockWidget* m_dockDialogGroup
        +QDockWidget* m_dockDialogRoom
        +QDockWidget* m_dockDialogAdventure
        +QDockWidget* m_dockDialogDescription
        +QDockWidget* m_dockDialogComms
        +QDockWidget* m_dockDialogVisibleMarkers
        +unique_ptr~GameObserver~ m_gameObserver
        +AutoLogger* m_logger
        +CommsManager* m_commsManager
        +CommsWidget* m_commsWidget
        +VisibilityFilterWidget* m_visibilityFilterWidget
        +MumeClock* m_mumeClock
        +QAction* openAct
        +QAction* saveAct
        +QAction* reloadAct
        +QAction* exitAct
        +QAction* m_undoAction
        +QAction* m_redoAction
        +QAction* preferencesAct
        +QAction* findRoomsAct
        +QAction* editRoomSelectionAct
        +QAction* zoomInAct
        +QAction* zoomOutAct
        +QAction* zoomResetAct
        +QAction* layerUpAct
        +QAction* layerDownAct
        +QAction* layerResetAct
        +QAction* radialTransparencyAct
        +QAction* showStatusBarAct
        +QAction* showScrollBarsAct
        +QAction* showMenuBarAct
        +QAction* alwaysOnTopAct
        +QAction* saveLogAct
        +QAction* saveCommsLogAct
        +void createActions()
        +void setupMenuBar()
        +void setupToolBars()
        +void setupStatusBar()
        +void wireConnections()
        +void applyHotkeys()
        +void registerGlobalShortcuts()
        +void slot_onPreferences()
        +void slot_alwaysOnTop()
        +void slot_setRadialTransparency()
        +bool eventFilter(QObject* obj, QEvent* event)
        +void closeEvent(QCloseEvent* event)
    }

    class ConfigDialog {
        +signal sig_graphicsSettingsChanged()
        +signal sig_textureSettingsChanged()
        +signal sig_groupSettingsChanged()
        +signal sig_hotkeysChanged()
        +signal sig_commsSettingsChanged()
        +signal sig_loadConfig()
    }

    class HotkeysPage {
        +QKeySequenceEdit* m_fileOpen
        +QKeySequenceEdit* m_fileSave
        +QKeySequenceEdit* m_fileReload
        +QKeySequenceEdit* m_fileQuit
        +QKeySequenceEdit* m_editUndo
        +QKeySequenceEdit* m_editRedo
        +QKeySequenceEdit* m_editPreferences
        +QKeySequenceEdit* m_editPreferencesAlt
        +QKeySequenceEdit* m_editFindRooms
        +QKeySequenceEdit* m_editRoom
        +QKeySequenceEdit* m_viewZoomIn
        +QKeySequenceEdit* m_viewZoomOut
        +QKeySequenceEdit* m_viewZoomReset
        +QKeySequenceEdit* m_viewLayerUp
        +QKeySequenceEdit* m_viewLayerDown
        +QKeySequenceEdit* m_viewLayerReset
        +QKeySequenceEdit* m_viewRadialTransparency
        +QKeySequenceEdit* m_viewStatusBar
        +QKeySequenceEdit* m_viewScrollBars
        +QKeySequenceEdit* m_viewMenuBar
        +QKeySequenceEdit* m_viewAlwaysOnTop
        +QKeySequenceEdit* m_panelLog
        +QKeySequenceEdit* m_panelClient
        +QKeySequenceEdit* m_panelGroup
        +QKeySequenceEdit* m_panelRoom
        +QKeySequenceEdit* m_panelAdventure
        +QKeySequenceEdit* m_panelComms
        +QKeySequenceEdit* m_panelDescription
        +QKeySequenceEdit* m_modeMoveMap
        +QKeySequenceEdit* m_modeRaypick
        +QKeySequenceEdit* m_modeSelectRooms
        +QKeySequenceEdit* m_modeSelectMarkers
        +QKeySequenceEdit* m_modeSelectConnection
        +QKeySequenceEdit* m_modeCreateMarker
        +QKeySequenceEdit* m_modeCreateRoom
        +QKeySequenceEdit* m_modeCreateConnection
        +QKeySequenceEdit* m_modeCreateOnewayConnection
        +QKeySequenceEdit* m_roomCreate
        +QKeySequenceEdit* m_roomMoveUp
        +QKeySequenceEdit* m_roomMoveDown
        +QKeySequenceEdit* m_roomMergeUp
        +QKeySequenceEdit* m_roomMergeDown
        +QKeySequenceEdit* m_roomDelete
        +QKeySequenceEdit* m_roomConnectNeighbors
        +QKeySequenceEdit* m_roomMoveToSelected
        +QKeySequenceEdit* m_roomUpdateSelected
        +QPushButton* m_resetButton
        +HotkeysPage(QWidget* parent)
        +void slot_loadConfig()
        +signal sig_hotkeysChanged()
        -void setupUI()
        -void connectSignals()
        -void loadSettings()
        -void saveSettings()
        -void resetToDefaults()
        -QWidget* createHotkeyRow(QString label, QKeySequenceEdit** editor, QPushButton** clearBtn)
        -void clearShortcut(QKeySequenceEdit* editor)
    }

    class CommsManager {
        +signal sig_log(QString message)
        +void slot_parseGmcpInput(GmcpMessage gmcp)
        +void slot_parseRawGameText(QString text)
    }

    class CommsWidget {
        +CommsWidget(CommsManager& commsManager, AutoLogger* logger, QWidget* parent)
        +void slot_saveLog()
        +void slot_saveLogOnExit()
        +void slot_loadSettings()
    }

    class VisibilityFilterWidget {
        +signal sig_visibilityChanged()
        +signal sig_connectionsVisibilityChanged()
    }

    class GameObserver {
        +signal sig2_sentToUserGmcp(GmcpMessage gmcp)
        +signal sig2_rawGameText(QString text)
    }

    class MapWindow {
        +MapCanvas* getCanvas()
        +void slot_graphicsSettingsChanged()
    }

    class MapCanvas {
        +void infomarksChanged()
        +void slot_onSeasonChanged()
        +void slot_reloadTextures()
    }

    class MumeClock {
        +signal sig_log(QString message)
        +signal sig_seasonChanged()
        +MumeMoment getMumeMoment()
    }

    class AutoLogger {
        +void slot_saveLogOnExit()
    }

    MainWindow --> ConfigDialog : creates
    ConfigDialog --> HotkeysPage : contains

    HotkeysPage --> Configuration : reads_writes_hotkeys

    ConfigDialog ..> MainWindow : sig_hotkeysChanged --> applyHotkeys
    HotkeysPage ..> ConfigDialog : sig_hotkeysChanged

    MainWindow --> CommsManager : owns
    MainWindow --> CommsWidget : owns
    MainWindow --> VisibilityFilterWidget : owns

    GameObserver ..> CommsManager : sig2_sentToUserGmcp --> slot_parseGmcpInput
    GameObserver ..> CommsManager : sig2_rawGameText --> slot_parseRawGameText

    CommsManager ..> MainWindow : sig_log --> slot_log

    MainWindow ..> CommsWidget : saveCommsLogAct --> slot_saveLog
    MainWindow ..> CommsWidget : closeEvent --> slot_saveLogOnExit
    ConfigDialog ..> CommsWidget : sig_commsSettingsChanged --> slot_loadSettings

    VisibilityFilterWidget ..> MapCanvas : sig_visibilityChanged --> infomarksChanged
    VisibilityFilterWidget ..> MapCanvas : sig_connectionsVisibilityChanged --> update

    MumeClock ..> MapCanvas : sig_seasonChanged --> slot_onSeasonChanged
    MainWindow ..> MumeClock : construct_with_startEpoch
    MumeClock ..> MainWindow : sig_log --> slot_log
Loading

File-Level Changes

Change Details Files
Add persisted hotkey configuration model and wire it into QSettings I/O.
  • Define a Hotkeys struct on Configuration with NamedConfig-backed entries for file, edit, view, panel, mode, and room operations, including defaults like Ctrl+O, Ctrl+S, Ctrl+Q, and Del.
  • Introduce QSettings keys for all hotkeys and implement Configuration::Hotkeys::read/write to load and persist them.
  • Register the new Hotkeys group with the configuration callback dispatcher so changes can be observed live.
src/configuration/configuration.h
src/configuration/configuration.cpp
Apply hotkeys dynamically to actions in MainWindow and ensure shortcuts work globally.
  • Remove hard-coded QKeySequence assignments from QAction construction and instead add MainWindow::applyHotkeys to map configuration hotkey values onto all relevant QAction and dock toggle actions, including dual shortcuts for Preferences.
  • Introduce MainWindow::registerGlobalShortcuts to register all actions on the main window so shortcuts are active regardless of focus.
  • Call applyHotkeys and registerGlobalShortcuts from the MainWindow constructor and connect ConfigDialog::sig_hotkeysChanged so changes in preferences take effect immediately.
src/mainwindow/mainwindow.h
src/mainwindow/mainwindow.cpp
Expose a Hotkeys preferences page for editing shortcuts with per-action editors and reset support.
  • Add HotkeysPage widget containing grouped QKeySequenceEdit rows (with Clear buttons) for each supported operation, and a Reset to Defaults button that restores configuration defaults.
  • Wire HotkeysPage into ConfigDialog, including tab icon, list entry, sig_loadConfig hookup, and a sig_hotkeysChanged signal that is emitted on edits and reset.
  • Implement load/save logic in HotkeysPage to sync QKeySequenceEdit values with Configuration::hotkeys and emit sig_hotkeysChanged whenever settings change.
src/preferences/configdialog.h
src/preferences/configdialog.cpp
src/preferences/hotkeyspage.h
src/preferences/hotkeyspage.cpp
src/CMakeLists.txt
Extend configuration and main window with auxiliary visual and communications features that integrate with the new configuration plumbing.
  • Add canvas texture set, seasonal texture toggle, layer transparency, background image options, and an Infomark visibility filter (with QSettings keys, read/write, and change callbacks), plus a radial transparency flag and UI action to toggle it.
  • Add a CommsSettings struct (colors, styles, timestamps, logging/muting options) with QSettings integration, and hook it up via a new CommsPage and signals from ConfigDialog to CommsWidget.
  • Refactor MainWindow initialization to construct GameObserver/MumeClock earlier for MapData, add CommsManager/CommsWidget and their dock, wire MUME clock season changes into MapCanvas, add a VisibilityFilterWidget dock and connect its signals to MapCanvas updates, and ensure comms log saving on exit.
src/configuration/configuration.h
src/configuration/configuration.cpp
src/mainwindow/mainwindow.h
src/mainwindow/mainwindow.cpp
src/preferences/configdialog.h
src/preferences/configdialog.cpp
src/CMakeLists.txt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • The default hotkey values are hard‑coded both in Configuration::Hotkeys (NamedConfig defaults) and again in HotkeysPage::resetToDefaults(), which risks them getting out of sync; consider deriving the reset values directly from the NamedConfig defaults or a shared table so there is a single source of truth.
  • The HotkeysPage implementation is very repetitive (manual members, UI rows, connections, load/save code for each hotkey); you could simplify and make future additions safer by driving this from a data structure (e.g., a list of {category, label, getter/setter} descriptors) and generating the widgets, connections, and load/save logic in a loop.
  • Similarly, MainWindow::applyHotkeys() and registerGlobalShortcuts() manually enumerate each action, which is easy to overlook when adding/removing hotkeys; a shared mapping between configuration keys and actions (e.g., a static table) would reduce duplication and make it harder to forget to wire a new hotkey to both the config page and the QAction.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The default hotkey values are hard‑coded both in `Configuration::Hotkeys` (NamedConfig defaults) and again in `HotkeysPage::resetToDefaults()`, which risks them getting out of sync; consider deriving the reset values directly from the NamedConfig defaults or a shared table so there is a single source of truth.
- The `HotkeysPage` implementation is very repetitive (manual members, UI rows, connections, load/save code for each hotkey); you could simplify and make future additions safer by driving this from a data structure (e.g., a list of {category, label, getter/setter} descriptors) and generating the widgets, connections, and load/save logic in a loop.
- Similarly, `MainWindow::applyHotkeys()` and `registerGlobalShortcuts()` manually enumerate each action, which is easy to overlook when adding/removing hotkeys; a shared mapping between configuration keys and actions (e.g., a static table) would reduce duplication and make it harder to forget to wire a new hotkey to both the config page and the QAction.

## Individual Comments

### Comment 1
<location> `src/configuration/configuration.cpp:56-65` </location>
<code_context>
     }
 }

+NODISCARD TextureSetEnum intToTextureSet(int value)
+{
+    switch (value) {
+    case 0:
+        return TextureSetEnum::CLASSIC;
+    case 1:
+        return TextureSetEnum::MODERN;
+    case 2:
+        return TextureSetEnum::CUSTOM;
+    default:
+        return TextureSetEnum::MODERN; // Default to Modern
+    }
+}
+
+NODISCARD int textureSetToInt(TextureSetEnum value)
+{
+    return static_cast<int>(value);
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Encoding/decoding texture set as raw ints couples config format to enum ordinal values

The manual `int` mapping and `static_cast<int>` assume `TextureSetEnum` will always be `{CLASSIC=0, MODERN=1, CUSTOM=2}`. Any future reordering or insertion would silently break existing configs. Either persist stable string identifiers instead, or assign explicit numeric values in the enum and clearly document that these helpers depend on that mapping.

Suggested implementation:

```cpp
/**
 * NOTE: These helpers rely on TextureSetEnum having stable, explicitly-assigned
 * integer values that are part of the persisted configuration format.
 * Do not change the underlying numeric values of TextureSetEnum without
 * providing a migration path for existing configs.
 */
NODISCARD TextureSetEnum intToTextureSet(int value)
{
    switch (value) {
    case static_cast<int>(TextureSetEnum::CLASSIC):
        return TextureSetEnum::CLASSIC;
    case static_cast<int>(TextureSetEnum::MODERN):
        return TextureSetEnum::MODERN;
    case static_cast<int>(TextureSetEnum::CUSTOM):
        return TextureSetEnum::CUSTOM;
    default:
        // Default to Modern to preserve previous behaviour; also guards
        // against unknown values from older/newer configs.
        return TextureSetEnum::MODERN;
    }
}

NODISCARD int textureSetToInt(TextureSetEnum value)
{
    // This intentionally encodes the enum's explicit numeric value into config.
    return static_cast<int>(value);
}

```

To fully address the coupling concern, you should also:
1. Update the `TextureSetEnum` definition (likely in `configuration.h` or a related header) to assign explicit numeric values, e.g.:
   ```cpp
   enum class TextureSetEnum {
       CLASSIC = 0,
       MODERN  = 1,
       CUSTOM  = 2,
   };
   ```
2. Add a comment near the `TextureSetEnum` definition reiterating that its underlying values are part of the serialized configuration format and must remain stable.
3. If any other serialization/deserialization code assumes ordinal values (e.g. direct `static_cast<int>(TextureSetEnum)` without this helper), consider routing it through `textureSetToInt`/`intToTextureSet` or updating it to rely on the explicit enum values with similar documentation.
</issue_to_address>

### Comment 2
<location> `src/configuration/configuration.cpp:1364-1373` </location>
<code_context>

+Configuration::CanvasSettings::VisibilityFilter::VisibilityFilter() = default;
+
+bool Configuration::CanvasSettings::VisibilityFilter::isVisible(InfomarkClassEnum markerClass) const
+{
+    switch (markerClass) {
+    case InfomarkClassEnum::GENERIC:
+        return generic.get();
+    case InfomarkClassEnum::HERB:
+        return herb.get();
+    case InfomarkClassEnum::RIVER:
+        return river.get();
+    case InfomarkClassEnum::PLACE:
+        return place.get();
+    case InfomarkClassEnum::MOB:
+        return mob.get();
+    case InfomarkClassEnum::COMMENT:
+        return comment.get();
+    case InfomarkClassEnum::ROAD:
+        return road.get();
+    case InfomarkClassEnum::OBJECT:
+        return object.get();
+    case InfomarkClassEnum::ACTION:
+        return action.get();
+    case InfomarkClassEnum::LOCALITY:
+        return locality.get();
+    }
+    return true; // Default to visible for unknown types
+}
+
</code_context>

<issue_to_address>
**issue (bug_risk):** VisibilityFilter silently treats unknown marker classes as visible, but setVisible ignores them

`isVisible` returns `true` for any `InfomarkClassEnum` not handled in the switch, while `setVisible` (elsewhere) ignores unknown values. If new enum values are added but not wired here, they’ll be permanently visible and not controllable via the UI. Consider making both methods fail loudly for unhandled enum values (e.g., `assert(false)`, logging + conservative default) and keeping their behavior consistent.
</issue_to_address>

### Comment 3
<location> `src/preferences/hotkeyspage.cpp:48-57` </location>
<code_context>
+void HotkeysPage::setupUI()
</code_context>

<issue_to_address>
**suggestion:** HotkeysPage builds a very tall form without any scrolling support

On smaller displays or lower resolutions, this many grouped rows will likely exceed the dialog’s vertical space and get clipped. Since you already forward-declare `QScrollArea`, consider making the main content widget the scroll area’s child and using that as the page to ensure the layout remains usable on constrained heights.

Suggested implementation:

```cpp
void HotkeysPage::setupUI()
{
    auto *outerLayout = new QVBoxLayout(this);

    auto *scrollArea = new QScrollArea(this);
    scrollArea->setWidgetResizable(true);

    auto *contentWidget = new QWidget(scrollArea);
    auto *mainLayout = new QVBoxLayout(contentWidget);
    scrollArea->setWidget(contentWidget);

    outerLayout->addWidget(scrollArea);

    // File Operations Group

```

```cpp
    auto *fileGroup = new QGroupBox(tr("File Operations"));
    auto *fileLayout = new QVBoxLayout(fileGroup);

```

1. Ensure `#include <QScrollArea>` and `#include <QWidget>` are present in `hotkeyspage.cpp` (or included transitively) since we are now instantiating `QScrollArea` and `QWidget` in the implementation file.
2. If there are any direct calls elsewhere in `setupUI()` that assumed the layout on `this` was `mainLayout` (e.g., `mainLayout->addStretch()` at the end), they should continue to work without changes since `mainLayout` is still used for the content, and the scroll area is the single child of the outer layout. If there is code that specifically refers to `layout()` on `this`, it now returns `outerLayout`; adapt such usages if necessary.
</issue_to_address>

### Comment 4
<location> `src/mainwindow/mainwindow.cpp:2315-2324` </location>
<code_context>
+void MainWindow::applyHotkeys()
</code_context>

<issue_to_address>
**nitpick:** applyHotkeys() emits quite verbose qDebug output for every shortcut assignment

This per-action `qDebug()` will generate a lot of console noise whenever hotkeys are applied, particularly from the preferences dialog. If it’s only for development, consider wrapping it in a debug-only guard (e.g. `#if IS_DEBUG_BUILD` or `QT_NO_DEBUG_OUTPUT`) or replacing it with a single summary log line.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +56 to +65
NODISCARD TextureSetEnum intToTextureSet(int value)
{
switch (value) {
case 0:
return TextureSetEnum::CLASSIC;
case 1:
return TextureSetEnum::MODERN;
case 2:
return TextureSetEnum::CUSTOM;
default:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Encoding/decoding texture set as raw ints couples config format to enum ordinal values

The manual int mapping and static_cast<int> assume TextureSetEnum will always be {CLASSIC=0, MODERN=1, CUSTOM=2}. Any future reordering or insertion would silently break existing configs. Either persist stable string identifiers instead, or assign explicit numeric values in the enum and clearly document that these helpers depend on that mapping.

Suggested implementation:

/**
 * NOTE: These helpers rely on TextureSetEnum having stable, explicitly-assigned
 * integer values that are part of the persisted configuration format.
 * Do not change the underlying numeric values of TextureSetEnum without
 * providing a migration path for existing configs.
 */
NODISCARD TextureSetEnum intToTextureSet(int value)
{
    switch (value) {
    case static_cast<int>(TextureSetEnum::CLASSIC):
        return TextureSetEnum::CLASSIC;
    case static_cast<int>(TextureSetEnum::MODERN):
        return TextureSetEnum::MODERN;
    case static_cast<int>(TextureSetEnum::CUSTOM):
        return TextureSetEnum::CUSTOM;
    default:
        // Default to Modern to preserve previous behaviour; also guards
        // against unknown values from older/newer configs.
        return TextureSetEnum::MODERN;
    }
}

NODISCARD int textureSetToInt(TextureSetEnum value)
{
    // This intentionally encodes the enum's explicit numeric value into config.
    return static_cast<int>(value);
}

To fully address the coupling concern, you should also:

  1. Update the TextureSetEnum definition (likely in configuration.h or a related header) to assign explicit numeric values, e.g.:
    enum class TextureSetEnum {
        CLASSIC = 0,
        MODERN  = 1,
        CUSTOM  = 2,
    };
  2. Add a comment near the TextureSetEnum definition reiterating that its underlying values are part of the serialized configuration format and must remain stable.
  3. If any other serialization/deserialization code assumes ordinal values (e.g. direct static_cast<int>(TextureSetEnum) without this helper), consider routing it through textureSetToInt/intToTextureSet or updating it to rely on the explicit enum values with similar documentation.

Comment on lines +1364 to +1373
bool Configuration::CanvasSettings::VisibilityFilter::isVisible(InfomarkClassEnum markerClass) const
{
switch (markerClass) {
case InfomarkClassEnum::GENERIC:
return generic.get();
case InfomarkClassEnum::HERB:
return herb.get();
case InfomarkClassEnum::RIVER:
return river.get();
case InfomarkClassEnum::PLACE:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): VisibilityFilter silently treats unknown marker classes as visible, but setVisible ignores them

isVisible returns true for any InfomarkClassEnum not handled in the switch, while setVisible (elsewhere) ignores unknown values. If new enum values are added but not wired here, they’ll be permanently visible and not controllable via the UI. Consider making both methods fail loudly for unhandled enum values (e.g., assert(false), logging + conservative default) and keeping their behavior consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant