From 4eca2d2c9441cf5044b7a8e73244f9f745e89f4b Mon Sep 17 00:00:00 2001 From: Durika Date: Wed, 17 Dec 2025 15:19:01 +0100 Subject: [PATCH] Add comprehensive hotkey customization system 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. --- src/CMakeLists.txt | 10 + src/configuration/configuration.cpp | 479 +++++++++++++++++++++++++++- src/configuration/configuration.h | 162 +++++++++- src/mainwindow/mainwindow.cpp | 350 ++++++++++++++++++-- src/mainwindow/mainwindow.h | 14 + src/preferences/configdialog.cpp | 19 ++ src/preferences/configdialog.h | 3 + src/preferences/hotkeyspage.cpp | 461 ++++++++++++++++++++++++++ src/preferences/hotkeyspage.h | 98 ++++++ 9 files changed, 1560 insertions(+), 36 deletions(-) create mode 100644 src/preferences/hotkeyspage.cpp create mode 100644 src/preferences/hotkeyspage.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 585e85b69..7c4d80959 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -32,6 +32,10 @@ set(mmapper_SRCS clock/mumeclockwidget.h clock/mumemoment.cpp clock/mumemoment.h + comms/CommsManager.cpp + comms/CommsManager.h + comms/CommsWidget.cpp + comms/CommsWidget.h configuration/NamedConfig.h configuration/PasswordConfig.cpp configuration/PasswordConfig.h @@ -226,6 +230,8 @@ set(mmapper_SRCS mainwindow/MapZoomSlider.h mainwindow/UpdateDialog.cpp mainwindow/UpdateDialog.h + mainwindow/VisibilityFilterWidget.cpp + mainwindow/VisibilityFilterWidget.h mainwindow/WinDarkMode.cpp mainwindow/WinDarkMode.h mainwindow/aboutdialog.cpp @@ -466,12 +472,16 @@ set(mmapper_SRCS preferences/autologpage.h preferences/clientpage.cpp preferences/clientpage.h + preferences/commspage.cpp + preferences/commspage.h preferences/configdialog.cpp preferences/configdialog.h preferences/generalpage.cpp preferences/generalpage.h preferences/graphicspage.cpp preferences/graphicspage.h + preferences/hotkeyspage.cpp + preferences/hotkeyspage.h preferences/grouppage.cpp preferences/grouppage.h preferences/mumeprotocolpage.cpp diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 1541e3de1..3000e80d3 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -7,6 +7,7 @@ #include "configuration.h" #include "../global/utils.h" +#include "../map/infomark.h" #include #include @@ -52,6 +53,25 @@ NODISCARD const char *getPlatformEditor() } } +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(value); +} + } // namespace Configuration::Configuration() @@ -194,10 +214,12 @@ ConstString GRP_ACCOUNT = "Account"; ConstString GRP_AUTO_LOAD_WORLD = "Auto load world"; ConstString GRP_AUTO_LOG = "Auto log"; ConstString GRP_CANVAS = "Canvas"; +ConstString GRP_COMMS = "Communications"; ConstString GRP_CONNECTION = "Connection"; ConstString GRP_FINDROOMS_DIALOG = "FindRooms Dialog"; ConstString GRP_GENERAL = "General"; ConstString GRP_GROUP_MANAGER = "Group Manager"; +ConstString GRP_HOTKEYS = "Hotkeys"; ConstString GRP_INFOMARKS_DIALOG = "InfoMarks Dialog"; ConstString GRP_INTEGRATED_MUD_CLIENT = "Integrated Mud Client"; ConstString GRP_MUME_CLIENT_PROTOCOL = "Mume client protocol"; @@ -227,9 +249,12 @@ ConstString KEY_CONNECTION_NORMAL_COLOR = "Connection normal color"; ConstString KEY_CORRECT_POSITION_BONUS = "correct position bonus"; ConstString KEY_DISPLAY_XP_STATUS = "Display XP status bar widget"; ConstString KEY_DISPLAY_CLOCK = "Display clock"; +ConstString KEY_GMCP_BROADCAST_CLOCK = "GMCP broadcast clock"; +ConstString KEY_GMCP_BROADCAST_INTERVAL = "GMCP broadcast interval"; ConstString KEY_DRAW_DOOR_NAMES = "Draw door names"; ConstString KEY_DRAW_NOT_MAPPED_EXITS = "Draw not mapped exits"; ConstString KEY_DRAW_UPPER_LAYERS_TEXTURED = "Draw upper layers textured"; +ConstString KEY_LAYER_TRANSPARENCY = "Layer transparency"; ConstString KEY_EMOJI_ENCODE = "encode emoji"; ConstString KEY_EMOJI_DECODE = "decode emoji"; ConstString KEY_EMULATED_EXITS = "Emulated Exits"; @@ -256,6 +281,73 @@ ConstString KEY_3D_FOV = "canvas.advanced.fov"; ConstString KEY_3D_VERTICAL_ANGLE = "canvas.advanced.verticalAngle"; ConstString KEY_3D_HORIZONTAL_ANGLE = "canvas.advanced.horizontalAngle"; ConstString KEY_3D_LAYER_HEIGHT = "canvas.advanced.layerHeight"; +ConstString KEY_BACKGROUND_IMAGE_ENABLED = "canvas.advanced.backgroundImageEnabled"; +ConstString KEY_BACKGROUND_IMAGE_PATH = "canvas.advanced.backgroundImagePath"; +ConstString KEY_BACKGROUND_IMAGE_FIT_MODE = "canvas.advanced.backgroundFitMode"; +ConstString KEY_BACKGROUND_IMAGE_OPACITY = "canvas.advanced.backgroundOpacity"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_SCALE = "canvas.advanced.backgroundFocusedScale"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X = "canvas.advanced.backgroundFocusedOffsetX"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y = "canvas.advanced.backgroundFocusedOffsetY"; +ConstString KEY_VISIBLE_MARKER_GENERIC = "canvas.visibleMarkers.generic"; +ConstString KEY_VISIBLE_MARKER_HERB = "canvas.visibleMarkers.herb"; +ConstString KEY_VISIBLE_MARKER_RIVER = "canvas.visibleMarkers.river"; +ConstString KEY_VISIBLE_MARKER_PLACE = "canvas.visibleMarkers.place"; +ConstString KEY_VISIBLE_MARKER_MOB = "canvas.visibleMarkers.mob"; +ConstString KEY_VISIBLE_MARKER_COMMENT = "canvas.visibleMarkers.comment"; +ConstString KEY_VISIBLE_MARKER_ROAD = "canvas.visibleMarkers.road"; +ConstString KEY_VISIBLE_MARKER_OBJECT = "canvas.visibleMarkers.object"; +ConstString KEY_VISIBLE_MARKER_ACTION = "canvas.visibleMarkers.action"; +ConstString KEY_VISIBLE_MARKER_LOCALITY = "canvas.visibleMarkers.locality"; +ConstString KEY_VISIBLE_CONNECTIONS = "canvas.visibilityFilter.connections"; + +// Hotkey configuration keys +ConstString KEY_HOTKEY_FILE_OPEN = "hotkeys.fileOpen"; +ConstString KEY_HOTKEY_FILE_SAVE = "hotkeys.fileSave"; +ConstString KEY_HOTKEY_FILE_RELOAD = "hotkeys.fileReload"; +ConstString KEY_HOTKEY_FILE_QUIT = "hotkeys.fileQuit"; +ConstString KEY_HOTKEY_EDIT_UNDO = "hotkeys.editUndo"; +ConstString KEY_HOTKEY_EDIT_REDO = "hotkeys.editRedo"; +ConstString KEY_HOTKEY_EDIT_PREFERENCES = "hotkeys.editPreferences"; +ConstString KEY_HOTKEY_EDIT_PREFERENCES_ALT = "hotkeys.editPreferencesAlt"; +ConstString KEY_HOTKEY_EDIT_FIND_ROOMS = "hotkeys.editFindRooms"; +ConstString KEY_HOTKEY_EDIT_ROOM = "hotkeys.editRoom"; +ConstString KEY_HOTKEY_VIEW_ZOOM_IN = "hotkeys.viewZoomIn"; +ConstString KEY_HOTKEY_VIEW_ZOOM_OUT = "hotkeys.viewZoomOut"; +ConstString KEY_HOTKEY_VIEW_ZOOM_RESET = "hotkeys.viewZoomReset"; +ConstString KEY_HOTKEY_VIEW_LAYER_UP = "hotkeys.viewLayerUp"; +ConstString KEY_HOTKEY_VIEW_LAYER_DOWN = "hotkeys.viewLayerDown"; +ConstString KEY_HOTKEY_VIEW_LAYER_RESET = "hotkeys.viewLayerReset"; +ConstString KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY = "hotkeys.viewRadialTransparency"; +ConstString KEY_HOTKEY_VIEW_STATUS_BAR = "hotkeys.viewStatusBar"; +ConstString KEY_HOTKEY_VIEW_SCROLL_BARS = "hotkeys.viewScrollBars"; +ConstString KEY_HOTKEY_VIEW_MENU_BAR = "hotkeys.viewMenuBar"; +ConstString KEY_HOTKEY_VIEW_ALWAYS_ON_TOP = "hotkeys.viewAlwaysOnTop"; +ConstString KEY_HOTKEY_PANEL_LOG = "hotkeys.panelLog"; +ConstString KEY_HOTKEY_PANEL_CLIENT = "hotkeys.panelClient"; +ConstString KEY_HOTKEY_PANEL_GROUP = "hotkeys.panelGroup"; +ConstString KEY_HOTKEY_PANEL_ROOM = "hotkeys.panelRoom"; +ConstString KEY_HOTKEY_PANEL_ADVENTURE = "hotkeys.panelAdventure"; +ConstString KEY_HOTKEY_PANEL_COMMS = "hotkeys.panelComms"; +ConstString KEY_HOTKEY_PANEL_DESCRIPTION = "hotkeys.panelDescription"; +ConstString KEY_HOTKEY_MODE_MOVE_MAP = "hotkeys.modeMoveMap"; +ConstString KEY_HOTKEY_MODE_RAYPICK = "hotkeys.modeRaypick"; +ConstString KEY_HOTKEY_MODE_SELECT_ROOMS = "hotkeys.modeSelectRooms"; +ConstString KEY_HOTKEY_MODE_SELECT_MARKERS = "hotkeys.modeSelectMarkers"; +ConstString KEY_HOTKEY_MODE_SELECT_CONNECTION = "hotkeys.modeSelectConnection"; +ConstString KEY_HOTKEY_MODE_CREATE_MARKER = "hotkeys.modeCreateMarker"; +ConstString KEY_HOTKEY_MODE_CREATE_ROOM = "hotkeys.modeCreateRoom"; +ConstString KEY_HOTKEY_MODE_CREATE_CONNECTION = "hotkeys.modeCreateConnection"; +ConstString KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION = "hotkeys.modeCreateOnewayConnection"; +ConstString KEY_HOTKEY_ROOM_CREATE = "hotkeys.roomCreate"; +ConstString KEY_HOTKEY_ROOM_MOVE_UP = "hotkeys.roomMoveUp"; +ConstString KEY_HOTKEY_ROOM_MOVE_DOWN = "hotkeys.roomMoveDown"; +ConstString KEY_HOTKEY_ROOM_MERGE_UP = "hotkeys.roomMergeUp"; +ConstString KEY_HOTKEY_ROOM_MERGE_DOWN = "hotkeys.roomMergeDown"; +ConstString KEY_HOTKEY_ROOM_DELETE = "hotkeys.roomDelete"; +ConstString KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS = "hotkeys.roomConnectNeighbors"; +ConstString KEY_HOTKEY_ROOM_MOVE_TO_SELECTED = "hotkeys.roomMoveToSelected"; +ConstString KEY_HOTKEY_ROOM_UPDATE_SELECTED = "hotkeys.roomUpdateSelected"; + ConstString KEY_LAST_MAP_LOAD_DIRECTORY = "Last map load directory"; ConstString KEY_LINES_OF_INPUT_HISTORY = "Lines of input history"; ConstString KEY_LINES_OF_PEEK_PREVIEW = "Lines of peek preview"; @@ -270,6 +362,8 @@ ConstString KEY_PROXY_CONNECTION_STATUS = "Proxy connection status"; ConstString KEY_PROXY_LISTENS_ON_ANY_INTERFACE = "Proxy listens on any interface"; ConstString KEY_RELATIVE_PATH_ACCEPTANCE = "relative path acceptance"; ConstString KEY_RESOURCES_DIRECTORY = "canvas.resourcesDir"; +ConstString KEY_TEXTURE_SET = "canvas.textureSet"; +ConstString KEY_ENABLE_SEASONAL_TEXTURES = "canvas.enableSeasonalTextures"; ConstString KEY_MUME_REMOTE_PORT = "Remote port number"; ConstString KEY_REMEMBER_LOGIN = "remember login"; ConstString KEY_ROOM_CREATION_PENALTY = "room creation penalty"; @@ -441,6 +535,8 @@ NODISCARD static uint16_t sanitizeUint16(const int input, const uint16_t default GROUP_CALLBACK(callback, GRP_GENERAL, general); \ GROUP_CALLBACK(callback, GRP_CONNECTION, connection); \ GROUP_CALLBACK(callback, GRP_CANVAS, canvas); \ + GROUP_CALLBACK(callback, GRP_HOTKEYS, hotkeys); \ + GROUP_CALLBACK(callback, GRP_COMMS, comms); \ GROUP_CALLBACK(callback, GRP_ACCOUNT, account); \ GROUP_CALLBACK(callback, GRP_AUTO_LOAD_WORLD, autoLoad); \ GROUP_CALLBACK(callback, GRP_AUTO_LOG, autoLog); \ @@ -565,7 +661,7 @@ void Configuration::ConnectionSettings::read(const QSettings &conf) } // closest well-known color is "Outer Space" -static constexpr const std::string_view DEFAULT_BGCOLOR = "#2E3436"; +static constexpr const std::string_view DEFAULT_BGCOLOR = "#161f21"; // closest well-known color is "Dusty Gray" static constexpr const std::string_view DEFAULT_DARK_COLOR = "#A19494"; // closest well-known color is "Cold Turkey" @@ -587,11 +683,14 @@ void Configuration::CanvasSettings::read(const QSettings &conf) .append(DEFAULT_MMAPPER_SUBDIR) .append(DEFAULT_RESOURCES_SUBDIR)) .toString(); + textureSet = intToTextureSet(conf.value(KEY_TEXTURE_SET, 1).toInt()); // Default: MODERN + enableSeasonalTextures = conf.value(KEY_ENABLE_SEASONAL_TEXTURES, true).toBool(); showMissingMapId.set(conf.value(KEY_SHOW_MISSING_MAP_ID, true).toBool()); showUnsavedChanges.set(conf.value(KEY_SHOW_UNSAVED_CHANGES, true).toBool()); showUnmappedExits.set(conf.value(KEY_DRAW_NOT_MAPPED_EXITS, true).toBool()); drawUpperLayersTextured = conf.value(KEY_DRAW_UPPER_LAYERS_TEXTURED, false).toBool(); drawDoorNames = conf.value(KEY_DRAW_DOOR_NAMES, true).toBool(); + layerTransparency = conf.value(KEY_LAYER_TRANSPARENCY, 1.0).toFloat(); backgroundColor = lookupColor(KEY_BACKGROUND_COLOR, DEFAULT_BGCOLOR); connectionNormalColor = lookupColor(KEY_CONNECTION_NORMAL_COLOR, Colors::white.toHex()); roomDarkColor = lookupColor(KEY_ROOM_DARK_COLOR, DEFAULT_DARK_COLOR); @@ -605,6 +704,30 @@ void Configuration::CanvasSettings::read(const QSettings &conf) advanced.verticalAngle.set(conf.value(KEY_3D_VERTICAL_ANGLE, 450).toInt()); advanced.horizontalAngle.set(conf.value(KEY_3D_HORIZONTAL_ANGLE, 0).toInt()); advanced.layerHeight.set(conf.value(KEY_3D_LAYER_HEIGHT, 15).toInt()); + + // Load background image settings + advanced.useBackgroundImage = conf.value(KEY_BACKGROUND_IMAGE_ENABLED, false).toBool(); + advanced.backgroundImagePath = conf.value(KEY_BACKGROUND_IMAGE_PATH, "").toString(); + advanced.backgroundFitMode = conf.value(KEY_BACKGROUND_IMAGE_FIT_MODE, 0).toInt(); + advanced.backgroundOpacity = conf.value(KEY_BACKGROUND_IMAGE_OPACITY, 1.0f).toFloat(); + advanced.backgroundFocusedScale = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_SCALE, 1.0f).toFloat(); + advanced.backgroundFocusedOffsetX = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X, 0.0f) + .toFloat(); + advanced.backgroundFocusedOffsetY = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y, 0.0f) + .toFloat(); + + // Load visible markers settings + visibilityFilter.generic.set(conf.value(KEY_VISIBLE_MARKER_GENERIC, true).toBool()); + visibilityFilter.herb.set(conf.value(KEY_VISIBLE_MARKER_HERB, true).toBool()); + visibilityFilter.river.set(conf.value(KEY_VISIBLE_MARKER_RIVER, true).toBool()); + visibilityFilter.place.set(conf.value(KEY_VISIBLE_MARKER_PLACE, true).toBool()); + visibilityFilter.mob.set(conf.value(KEY_VISIBLE_MARKER_MOB, true).toBool()); + visibilityFilter.comment.set(conf.value(KEY_VISIBLE_MARKER_COMMENT, true).toBool()); + visibilityFilter.road.set(conf.value(KEY_VISIBLE_MARKER_ROAD, true).toBool()); + visibilityFilter.object.set(conf.value(KEY_VISIBLE_MARKER_OBJECT, true).toBool()); + visibilityFilter.action.set(conf.value(KEY_VISIBLE_MARKER_ACTION, true).toBool()); + visibilityFilter.locality.set(conf.value(KEY_VISIBLE_MARKER_LOCALITY, true).toBool()); + visibilityFilter.connections.set(conf.value(KEY_VISIBLE_CONNECTIONS, true).toBool()); } void Configuration::AccountSettings::read(const QSettings &conf) @@ -691,11 +814,15 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); } +Configuration::MumeClockSettings::MumeClockSettings() = default; + void Configuration::MumeClockSettings::read(const QSettings &conf) { // NOTE: old values might be stored as int32 startEpoch = conf.value(KEY_MUME_START_EPOCH, 1517443173).toLongLong(); display = conf.value(KEY_DISPLAY_CLOCK, true).toBool(); + gmcpBroadcast.set(conf.value(KEY_GMCP_BROADCAST_CLOCK, true).toBool()); + gmcpBroadcastInterval.set(conf.value(KEY_GMCP_BROADCAST_INTERVAL, 2500).toInt()); } void Configuration::AdventurePanelSettings::read(const QSettings &conf) @@ -770,11 +897,14 @@ NODISCARD static auto getQColorName(const XNamedColor &color) void Configuration::CanvasSettings::write(QSettings &conf) const { conf.setValue(KEY_RESOURCES_DIRECTORY, resourcesDirectory); + conf.setValue(KEY_TEXTURE_SET, textureSetToInt(textureSet)); + conf.setValue(KEY_ENABLE_SEASONAL_TEXTURES, enableSeasonalTextures); conf.setValue(KEY_SHOW_MISSING_MAP_ID, showMissingMapId.get()); conf.setValue(KEY_SHOW_UNSAVED_CHANGES, showUnsavedChanges.get()); conf.setValue(KEY_DRAW_NOT_MAPPED_EXITS, showUnmappedExits.get()); conf.setValue(KEY_DRAW_UPPER_LAYERS_TEXTURED, drawUpperLayersTextured); conf.setValue(KEY_DRAW_DOOR_NAMES, drawDoorNames); + conf.setValue(KEY_LAYER_TRANSPARENCY, layerTransparency); conf.setValue(KEY_BACKGROUND_COLOR, getQColorName(backgroundColor)); conf.setValue(KEY_ROOM_DARK_COLOR, getQColorName(roomDarkColor)); conf.setValue(KEY_ROOM_DARK_LIT_COLOR, getQColorName(roomDarkLitColor)); @@ -788,6 +918,239 @@ void Configuration::CanvasSettings::write(QSettings &conf) const conf.setValue(KEY_3D_VERTICAL_ANGLE, advanced.verticalAngle.get()); conf.setValue(KEY_3D_HORIZONTAL_ANGLE, advanced.horizontalAngle.get()); conf.setValue(KEY_3D_LAYER_HEIGHT, advanced.layerHeight.get()); + + // Save background image settings + conf.setValue(KEY_BACKGROUND_IMAGE_ENABLED, advanced.useBackgroundImage); + conf.setValue(KEY_BACKGROUND_IMAGE_PATH, advanced.backgroundImagePath); + conf.setValue(KEY_BACKGROUND_IMAGE_FIT_MODE, advanced.backgroundFitMode); + conf.setValue(KEY_BACKGROUND_IMAGE_OPACITY, advanced.backgroundOpacity); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_SCALE, advanced.backgroundFocusedScale); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X, advanced.backgroundFocusedOffsetX); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y, advanced.backgroundFocusedOffsetY); + + // Save visible markers settings + conf.setValue(KEY_VISIBLE_MARKER_GENERIC, visibilityFilter.generic.get()); + conf.setValue(KEY_VISIBLE_MARKER_HERB, visibilityFilter.herb.get()); + conf.setValue(KEY_VISIBLE_MARKER_RIVER, visibilityFilter.river.get()); + conf.setValue(KEY_VISIBLE_MARKER_PLACE, visibilityFilter.place.get()); + conf.setValue(KEY_VISIBLE_MARKER_MOB, visibilityFilter.mob.get()); + conf.setValue(KEY_VISIBLE_MARKER_COMMENT, visibilityFilter.comment.get()); + conf.setValue(KEY_VISIBLE_MARKER_ROAD, visibilityFilter.road.get()); + conf.setValue(KEY_VISIBLE_MARKER_OBJECT, visibilityFilter.object.get()); + conf.setValue(KEY_VISIBLE_MARKER_ACTION, visibilityFilter.action.get()); + conf.setValue(KEY_VISIBLE_MARKER_LOCALITY, visibilityFilter.locality.get()); + conf.setValue(KEY_VISIBLE_CONNECTIONS, visibilityFilter.connections.get()); +} + +void Configuration::Hotkeys::read(const QSettings &conf) +{ + // File operations + fileOpen.set(conf.value(KEY_HOTKEY_FILE_OPEN, "Ctrl+O").toString()); + fileSave.set(conf.value(KEY_HOTKEY_FILE_SAVE, "Ctrl+S").toString()); + fileReload.set(conf.value(KEY_HOTKEY_FILE_RELOAD, "Ctrl+R").toString()); + fileQuit.set(conf.value(KEY_HOTKEY_FILE_QUIT, "Ctrl+Q").toString()); + + // Edit operations + editUndo.set(conf.value(KEY_HOTKEY_EDIT_UNDO, "Ctrl+Z").toString()); + editRedo.set(conf.value(KEY_HOTKEY_EDIT_REDO, "Ctrl+Y").toString()); + editPreferences.set(conf.value(KEY_HOTKEY_EDIT_PREFERENCES, "Ctrl+P").toString()); + editPreferencesAlt.set(conf.value(KEY_HOTKEY_EDIT_PREFERENCES_ALT, "Esc").toString()); + editFindRooms.set(conf.value(KEY_HOTKEY_EDIT_FIND_ROOMS, "Ctrl+F").toString()); + editRoom.set(conf.value(KEY_HOTKEY_EDIT_ROOM, "Ctrl+E").toString()); + + // View operations + viewZoomIn.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_IN, "").toString()); + viewZoomOut.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_OUT, "").toString()); + viewZoomReset.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_RESET, "Ctrl+0").toString()); + viewLayerUp.set(conf.value(KEY_HOTKEY_VIEW_LAYER_UP, "").toString()); + viewLayerDown.set(conf.value(KEY_HOTKEY_VIEW_LAYER_DOWN, "").toString()); + viewLayerReset.set(conf.value(KEY_HOTKEY_VIEW_LAYER_RESET, "").toString()); + + // View toggles + viewRadialTransparency.set(conf.value(KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY, "").toString()); + viewStatusBar.set(conf.value(KEY_HOTKEY_VIEW_STATUS_BAR, "").toString()); + viewScrollBars.set(conf.value(KEY_HOTKEY_VIEW_SCROLL_BARS, "").toString()); + viewMenuBar.set(conf.value(KEY_HOTKEY_VIEW_MENU_BAR, "").toString()); + viewAlwaysOnTop.set(conf.value(KEY_HOTKEY_VIEW_ALWAYS_ON_TOP, "").toString()); + + // Side panels + panelLog.set(conf.value(KEY_HOTKEY_PANEL_LOG, "Ctrl+L").toString()); + panelClient.set(conf.value(KEY_HOTKEY_PANEL_CLIENT, "").toString()); + panelGroup.set(conf.value(KEY_HOTKEY_PANEL_GROUP, "").toString()); + panelRoom.set(conf.value(KEY_HOTKEY_PANEL_ROOM, "").toString()); + panelAdventure.set(conf.value(KEY_HOTKEY_PANEL_ADVENTURE, "").toString()); + panelComms.set(conf.value(KEY_HOTKEY_PANEL_COMMS, "").toString()); + panelDescription.set(conf.value(KEY_HOTKEY_PANEL_DESCRIPTION, "").toString()); + + // Mouse modes + modeMoveMap.set(conf.value(KEY_HOTKEY_MODE_MOVE_MAP, "").toString()); + modeRaypick.set(conf.value(KEY_HOTKEY_MODE_RAYPICK, "").toString()); + modeSelectRooms.set(conf.value(KEY_HOTKEY_MODE_SELECT_ROOMS, "").toString()); + modeSelectMarkers.set(conf.value(KEY_HOTKEY_MODE_SELECT_MARKERS, "").toString()); + modeSelectConnection.set(conf.value(KEY_HOTKEY_MODE_SELECT_CONNECTION, "").toString()); + modeCreateMarker.set(conf.value(KEY_HOTKEY_MODE_CREATE_MARKER, "").toString()); + modeCreateRoom.set(conf.value(KEY_HOTKEY_MODE_CREATE_ROOM, "").toString()); + modeCreateConnection.set(conf.value(KEY_HOTKEY_MODE_CREATE_CONNECTION, "").toString()); + modeCreateOnewayConnection.set( + conf.value(KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION, "").toString()); + + // Room operations + roomCreate.set(conf.value(KEY_HOTKEY_ROOM_CREATE, "").toString()); + roomMoveUp.set(conf.value(KEY_HOTKEY_ROOM_MOVE_UP, "").toString()); + roomMoveDown.set(conf.value(KEY_HOTKEY_ROOM_MOVE_DOWN, "").toString()); + roomMergeUp.set(conf.value(KEY_HOTKEY_ROOM_MERGE_UP, "").toString()); + roomMergeDown.set(conf.value(KEY_HOTKEY_ROOM_MERGE_DOWN, "").toString()); + roomDelete.set(conf.value(KEY_HOTKEY_ROOM_DELETE, "Del").toString()); + roomConnectNeighbors.set(conf.value(KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS, "").toString()); + roomMoveToSelected.set(conf.value(KEY_HOTKEY_ROOM_MOVE_TO_SELECTED, "").toString()); + roomUpdateSelected.set(conf.value(KEY_HOTKEY_ROOM_UPDATE_SELECTED, "").toString()); +} + +void Configuration::Hotkeys::write(QSettings &conf) const +{ + // File operations + conf.setValue(KEY_HOTKEY_FILE_OPEN, fileOpen.get()); + conf.setValue(KEY_HOTKEY_FILE_SAVE, fileSave.get()); + conf.setValue(KEY_HOTKEY_FILE_RELOAD, fileReload.get()); + conf.setValue(KEY_HOTKEY_FILE_QUIT, fileQuit.get()); + + // Edit operations + conf.setValue(KEY_HOTKEY_EDIT_UNDO, editUndo.get()); + conf.setValue(KEY_HOTKEY_EDIT_REDO, editRedo.get()); + conf.setValue(KEY_HOTKEY_EDIT_PREFERENCES, editPreferences.get()); + conf.setValue(KEY_HOTKEY_EDIT_PREFERENCES_ALT, editPreferencesAlt.get()); + conf.setValue(KEY_HOTKEY_EDIT_FIND_ROOMS, editFindRooms.get()); + conf.setValue(KEY_HOTKEY_EDIT_ROOM, editRoom.get()); + + // View operations + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_IN, viewZoomIn.get()); + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_OUT, viewZoomOut.get()); + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_RESET, viewZoomReset.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_UP, viewLayerUp.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_DOWN, viewLayerDown.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_RESET, viewLayerReset.get()); + + // View toggles + conf.setValue(KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY, viewRadialTransparency.get()); + conf.setValue(KEY_HOTKEY_VIEW_STATUS_BAR, viewStatusBar.get()); + conf.setValue(KEY_HOTKEY_VIEW_SCROLL_BARS, viewScrollBars.get()); + conf.setValue(KEY_HOTKEY_VIEW_MENU_BAR, viewMenuBar.get()); + conf.setValue(KEY_HOTKEY_VIEW_ALWAYS_ON_TOP, viewAlwaysOnTop.get()); + + // Side panels + conf.setValue(KEY_HOTKEY_PANEL_LOG, panelLog.get()); + conf.setValue(KEY_HOTKEY_PANEL_CLIENT, panelClient.get()); + conf.setValue(KEY_HOTKEY_PANEL_GROUP, panelGroup.get()); + conf.setValue(KEY_HOTKEY_PANEL_ROOM, panelRoom.get()); + conf.setValue(KEY_HOTKEY_PANEL_ADVENTURE, panelAdventure.get()); + conf.setValue(KEY_HOTKEY_PANEL_COMMS, panelComms.get()); + conf.setValue(KEY_HOTKEY_PANEL_DESCRIPTION, panelDescription.get()); + + // Mouse modes + conf.setValue(KEY_HOTKEY_MODE_MOVE_MAP, modeMoveMap.get()); + conf.setValue(KEY_HOTKEY_MODE_RAYPICK, modeRaypick.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_ROOMS, modeSelectRooms.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_MARKERS, modeSelectMarkers.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_CONNECTION, modeSelectConnection.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_MARKER, modeCreateMarker.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_ROOM, modeCreateRoom.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_CONNECTION, modeCreateConnection.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION, modeCreateOnewayConnection.get()); + + // Room operations + conf.setValue(KEY_HOTKEY_ROOM_CREATE, roomCreate.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_UP, roomMoveUp.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_DOWN, roomMoveDown.get()); + conf.setValue(KEY_HOTKEY_ROOM_MERGE_UP, roomMergeUp.get()); + conf.setValue(KEY_HOTKEY_ROOM_MERGE_DOWN, roomMergeDown.get()); + conf.setValue(KEY_HOTKEY_ROOM_DELETE, roomDelete.get()); + conf.setValue(KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS, roomConnectNeighbors.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_TO_SELECTED, roomMoveToSelected.get()); + conf.setValue(KEY_HOTKEY_ROOM_UPDATE_SELECTED, roomUpdateSelected.get()); +} + +void Configuration::CommsSettings::read(const QSettings &conf) +{ + // Communication colors + tellColor.set(conf.value(tellColor.getName(), QColor(32, 108, 9)).value()); + whisperColor.set(conf.value(whisperColor.getName(), QColor(103, 135, 149)).value()); + groupColor.set(conf.value(groupColor.getName(), QColor(15, 123, 255)).value()); + askColor.set(conf.value(askColor.getName(), QColor(Qt::yellow)).value()); + sayColor.set(conf.value(sayColor.getName(), QColor(80, 173, 199)).value()); + emoteColor.set(conf.value(emoteColor.getName(), QColor(203, 37, 111)).value()); + socialColor.set(conf.value(socialColor.getName(), QColor(217, 140, 151)).value()); + yellColor.set(conf.value(yellColor.getName(), QColor(176, 80, 189)).value()); + narrateColor.set(conf.value(narrateColor.getName(), QColor(119, 197, 203)).value()); + prayColor.set(conf.value(prayColor.getName(), QColor(173, 216, 230)).value()); + shoutColor.set(conf.value(shoutColor.getName(), QColor(160, 9, 198)).value()); + singColor.set(conf.value(singColor.getName(), QColor(144, 238, 144)).value()); + backgroundColor.set(conf.value(backgroundColor.getName(), QColor(22, 31, 33)).value()); + + // Font styling options + yellAllCaps.set(conf.value(yellAllCaps.getName(), true).toBool()); + whisperItalic.set(conf.value(whisperItalic.getName(), true).toBool()); + emoteItalic.set(conf.value(emoteItalic.getName(), true).toBool()); + + // Display options + showTimestamps.set(conf.value(showTimestamps.getName(), false).toBool()); + saveLogOnExit.set(conf.value(saveLogOnExit.getName(), false).toBool()); + logDirectory.set(conf.value(logDirectory.getName(), QString("")).toString()); + + // Talker colors + talkerYouColor.set(conf.value(talkerYouColor.getName(), QColor(228, 250, 255)).value()); + talkerPlayerColor.set( + conf.value(talkerPlayerColor.getName(), QColor(255, 187, 16)).value()); + talkerNpcColor.set(conf.value(talkerNpcColor.getName(), QColor(25, 138, 23)).value()); + talkerAllyColor.set(conf.value(talkerAllyColor.getName(), QColor(33, 166, 255)).value()); + talkerNeutralColor.set( + conf.value(talkerNeutralColor.getName(), QColor(166, 168, 168)).value()); + talkerEnemyColor.set(conf.value(talkerEnemyColor.getName(), QColor(173, 7, 37)).value()); + + // Tab muting (filters) + muteDirectTab.set(conf.value(muteDirectTab.getName(), false).toBool()); + muteLocalTab.set(conf.value(muteLocalTab.getName(), false).toBool()); + muteGlobalTab.set(conf.value(muteGlobalTab.getName(), false).toBool()); +} + +void Configuration::CommsSettings::write(QSettings &conf) const +{ + // Communication colors + conf.setValue(tellColor.getName(), tellColor.get()); + conf.setValue(whisperColor.getName(), whisperColor.get()); + conf.setValue(groupColor.getName(), groupColor.get()); + conf.setValue(askColor.getName(), askColor.get()); + conf.setValue(sayColor.getName(), sayColor.get()); + conf.setValue(emoteColor.getName(), emoteColor.get()); + conf.setValue(socialColor.getName(), socialColor.get()); + conf.setValue(yellColor.getName(), yellColor.get()); + conf.setValue(narrateColor.getName(), narrateColor.get()); + conf.setValue(prayColor.getName(), prayColor.get()); + conf.setValue(shoutColor.getName(), shoutColor.get()); + conf.setValue(singColor.getName(), singColor.get()); + conf.setValue(backgroundColor.getName(), backgroundColor.get()); + + // Font styling options + conf.setValue(yellAllCaps.getName(), yellAllCaps.get()); + conf.setValue(whisperItalic.getName(), whisperItalic.get()); + conf.setValue(emoteItalic.getName(), emoteItalic.get()); + + // Display options + conf.setValue(showTimestamps.getName(), showTimestamps.get()); + conf.setValue(saveLogOnExit.getName(), saveLogOnExit.get()); + conf.setValue(logDirectory.getName(), logDirectory.get()); + + // Talker colors + conf.setValue(talkerYouColor.getName(), talkerYouColor.get()); + conf.setValue(talkerPlayerColor.getName(), talkerPlayerColor.get()); + conf.setValue(talkerNpcColor.getName(), talkerNpcColor.get()); + conf.setValue(talkerAllyColor.getName(), talkerAllyColor.get()); + conf.setValue(talkerNeutralColor.getName(), talkerNeutralColor.get()); + conf.setValue(talkerEnemyColor.getName(), talkerEnemyColor.get()); + + // Tab muting (filters) + conf.setValue(muteDirectTab.getName(), muteDirectTab.get()); + conf.setValue(muteLocalTab.getName(), muteLocalTab.get()); + conf.setValue(muteGlobalTab.getName(), muteGlobalTab.get()); } void Configuration::AccountSettings::write(QSettings &conf) const @@ -862,6 +1225,8 @@ void Configuration::MumeClockSettings::write(QSettings &conf) const // Note: There's no QVariant(int64_t) constructor. conf.setValue(KEY_MUME_START_EPOCH, static_cast(startEpoch)); conf.setValue(KEY_DISPLAY_CLOCK, display); + conf.setValue(KEY_GMCP_BROADCAST_CLOCK, gmcpBroadcast.get()); + conf.setValue(KEY_GMCP_BROADCAST_INTERVAL, gmcpBroadcastInterval.get()); } void Configuration::AdventurePanelSettings::write(QSettings &conf) const @@ -994,6 +1359,118 @@ void Configuration::CanvasSettings::Advanced::registerChangeCallback( layerHeight.registerChangeCallback(lifetime, callback); } +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 +} + +void Configuration::CanvasSettings::VisibilityFilter::setVisible(InfomarkClassEnum markerClass, + bool visible) +{ + switch (markerClass) { + case InfomarkClassEnum::GENERIC: + generic.set(visible); + break; + case InfomarkClassEnum::HERB: + herb.set(visible); + break; + case InfomarkClassEnum::RIVER: + river.set(visible); + break; + case InfomarkClassEnum::PLACE: + place.set(visible); + break; + case InfomarkClassEnum::MOB: + mob.set(visible); + break; + case InfomarkClassEnum::COMMENT: + comment.set(visible); + break; + case InfomarkClassEnum::ROAD: + road.set(visible); + break; + case InfomarkClassEnum::OBJECT: + object.set(visible); + break; + case InfomarkClassEnum::ACTION: + action.set(visible); + break; + case InfomarkClassEnum::LOCALITY: + locality.set(visible); + break; + } +} + +void Configuration::CanvasSettings::VisibilityFilter::showAll() +{ + generic.set(true); + herb.set(true); + river.set(true); + place.set(true); + mob.set(true); + comment.set(true); + road.set(true); + object.set(true); + action.set(true); + locality.set(true); + connections.set(true); +} + +void Configuration::CanvasSettings::VisibilityFilter::hideAll() +{ + generic.set(false); + herb.set(false); + river.set(false); + place.set(false); + mob.set(false); + comment.set(false); + road.set(false); + object.set(false); + action.set(false); + locality.set(false); + connections.set(false); +} + +void Configuration::CanvasSettings::VisibilityFilter::registerChangeCallback( + const ChangeMonitor::Lifetime &lifetime, const ChangeMonitor::Function &callback) +{ + generic.registerChangeCallback(lifetime, callback); + herb.registerChangeCallback(lifetime, callback); + river.registerChangeCallback(lifetime, callback); + place.registerChangeCallback(lifetime, callback); + mob.registerChangeCallback(lifetime, callback); + comment.registerChangeCallback(lifetime, callback); + road.registerChangeCallback(lifetime, callback); + object.registerChangeCallback(lifetime, callback); + action.registerChangeCallback(lifetime, callback); + locality.registerChangeCallback(lifetime, callback); + connections.registerChangeCallback(lifetime, callback); +} + void setEnteredMain() { g_thread = std::this_thread::get_id(); diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 58d1d1e9f..d83f60ce5 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -29,6 +29,9 @@ #undef TRANSPARENT // Bad dog, Microsoft; bad dog!!! +// Forward declaration for InfomarkClassEnum +enum class InfomarkClassEnum : uint8_t; + #define SUBGROUP() \ friend class Configuration; \ void read(const QSettings &conf); \ @@ -79,6 +82,7 @@ class NODISCARD Configuration final char prefixChar = char_consts::C_UNDERSCORE; bool encodeEmoji = true; bool decodeEmoji = true; + bool enableYellFallbackParsing = true; // Parse yells from game text when GMCP unavailable private: SUBGROUP(); @@ -143,6 +147,10 @@ class NODISCARD Configuration final bool trilinearFiltering = false; bool softwareOpenGL = false; QString resourcesDirectory; + TextureSetEnum textureSet = TextureSetEnum::MODERN; + bool enableSeasonalTextures = true; + float layerTransparency = 1.0f; // 0.0 = only focused layer, 1.0 = maximum transparency + bool enableRadialTransparency = true; // Enable radial transparency zones on upper layers // not saved yet: bool drawCharBeacons = true; @@ -159,12 +167,21 @@ class NODISCARD Configuration final NamedConfig autoTilt{"MMAPPER_AUTO_TILT", true}; NamedConfig printPerfStats{"MMAPPER_GL_PERFSTATS", IS_DEBUG_BUILD}; + // Background image settings + bool useBackgroundImage = false; + QString backgroundImagePath; + int backgroundFitMode = 0; // BackgroundFitModeEnum::FIT + float backgroundOpacity = 1.0f; + float backgroundFocusedScale = 1.0f; // Scale factor for FOCUSED mode (0.1 to 10.0) + float backgroundFocusedOffsetX = 0.0f; // X offset for FOCUSED mode (-1000 to 1000) + float backgroundFocusedOffsetY = 0.0f; // Y offset for FOCUSED mode (-1000 to 1000) + // 5..90 degrees FixedPoint<1> fov{50, 900, 765}; // 0..90 degrees FixedPoint<1> verticalAngle{0, 900, 450}; - // -45..45 degrees - FixedPoint<1> horizontalAngle{-450, 450, 0}; + // -180..180 degrees (full rotation) + FixedPoint<1> horizontalAngle{-1800, 1800, 0}; // 1..10 rooms FixedPoint<1> layerHeight{10, 100, 15}; @@ -175,10 +192,147 @@ class NODISCARD Configuration final Advanced(); } advanced; + struct NODISCARD VisibilityFilter final + { + NamedConfig generic{"VISIBLE_MARKER_GENERIC", true}; + NamedConfig herb{"VISIBLE_MARKER_HERB", true}; + NamedConfig river{"VISIBLE_MARKER_RIVER", true}; + NamedConfig place{"VISIBLE_MARKER_PLACE", true}; + NamedConfig mob{"VISIBLE_MARKER_MOB", true}; + NamedConfig comment{"VISIBLE_MARKER_COMMENT", true}; + NamedConfig road{"VISIBLE_MARKER_ROAD", true}; + NamedConfig object{"VISIBLE_MARKER_OBJECT", true}; + NamedConfig action{"VISIBLE_MARKER_ACTION", true}; + NamedConfig locality{"VISIBLE_MARKER_LOCALITY", true}; + NamedConfig connections{"VISIBLE_CONNECTIONS", true}; + + public: + NODISCARD bool isVisible(InfomarkClassEnum markerClass) const; + void setVisible(InfomarkClassEnum markerClass, bool visible); + NODISCARD bool isConnectionsVisible() const { return connections.get(); } + void setConnectionsVisible(bool visible) { connections.set(visible); } + void showAll(); + void hideAll(); + void registerChangeCallback(const ChangeMonitor::Lifetime &lifetime, + const ChangeMonitor::Function &callback); + + VisibilityFilter(); + } visibilityFilter; + private: SUBGROUP(); } canvas; + struct NODISCARD Hotkeys final + { + // File operations + NamedConfig fileOpen{"HOTKEY_FILE_OPEN", "Ctrl+O"}; + NamedConfig fileSave{"HOTKEY_FILE_SAVE", "Ctrl+S"}; + NamedConfig fileReload{"HOTKEY_FILE_RELOAD", "Ctrl+R"}; + NamedConfig fileQuit{"HOTKEY_FILE_QUIT", "Ctrl+Q"}; + + // Edit operations + NamedConfig editUndo{"HOTKEY_EDIT_UNDO", "Ctrl+Z"}; + NamedConfig editRedo{"HOTKEY_EDIT_REDO", "Ctrl+Y"}; + NamedConfig editPreferences{"HOTKEY_EDIT_PREFERENCES", "Ctrl+P"}; + NamedConfig editPreferencesAlt{"HOTKEY_EDIT_PREFERENCES_ALT", "Esc"}; + NamedConfig editFindRooms{"HOTKEY_EDIT_FIND_ROOMS", "Ctrl+F"}; + NamedConfig editRoom{"HOTKEY_EDIT_ROOM", "Ctrl+E"}; + + // View operations + NamedConfig viewZoomIn{"HOTKEY_VIEW_ZOOM_IN", ""}; + NamedConfig viewZoomOut{"HOTKEY_VIEW_ZOOM_OUT", ""}; + NamedConfig viewZoomReset{"HOTKEY_VIEW_ZOOM_RESET", "Ctrl+0"}; + NamedConfig viewLayerUp{"HOTKEY_VIEW_LAYER_UP", ""}; + NamedConfig viewLayerDown{"HOTKEY_VIEW_LAYER_DOWN", ""}; + NamedConfig viewLayerReset{"HOTKEY_VIEW_LAYER_RESET", ""}; + + // View toggles + NamedConfig viewRadialTransparency{"HOTKEY_VIEW_RADIAL_TRANSPARENCY", ""}; + NamedConfig viewStatusBar{"HOTKEY_VIEW_STATUS_BAR", ""}; + NamedConfig viewScrollBars{"HOTKEY_VIEW_SCROLL_BARS", ""}; + NamedConfig viewMenuBar{"HOTKEY_VIEW_MENU_BAR", ""}; + NamedConfig viewAlwaysOnTop{"HOTKEY_VIEW_ALWAYS_ON_TOP", ""}; + + // Side panels + NamedConfig panelLog{"HOTKEY_PANEL_LOG", "Ctrl+L"}; + NamedConfig panelClient{"HOTKEY_PANEL_CLIENT", ""}; + NamedConfig panelGroup{"HOTKEY_PANEL_GROUP", ""}; + NamedConfig panelRoom{"HOTKEY_PANEL_ROOM", ""}; + NamedConfig panelAdventure{"HOTKEY_PANEL_ADVENTURE", ""}; + NamedConfig panelDescription{"HOTKEY_PANEL_DESCRIPTION", ""}; + NamedConfig panelComms{"HOTKEY_PANEL_COMMS", ""}; + + // Mouse modes + NamedConfig modeMoveMap{"HOTKEY_MODE_MOVE_MAP", ""}; + NamedConfig modeRaypick{"HOTKEY_MODE_RAYPICK", ""}; + NamedConfig modeSelectRooms{"HOTKEY_MODE_SELECT_ROOMS", ""}; + NamedConfig modeSelectMarkers{"HOTKEY_MODE_SELECT_MARKERS", ""}; + NamedConfig modeSelectConnection{"HOTKEY_MODE_SELECT_CONNECTION", ""}; + NamedConfig modeCreateMarker{"HOTKEY_MODE_CREATE_MARKER", ""}; + NamedConfig modeCreateRoom{"HOTKEY_MODE_CREATE_ROOM", ""}; + NamedConfig modeCreateConnection{"HOTKEY_MODE_CREATE_CONNECTION", ""}; + NamedConfig modeCreateOnewayConnection{"HOTKEY_MODE_CREATE_ONEWAY_CONNECTION", ""}; + + // Room operations + NamedConfig roomCreate{"HOTKEY_ROOM_CREATE", ""}; + NamedConfig roomMoveUp{"HOTKEY_ROOM_MOVE_UP", ""}; + NamedConfig roomMoveDown{"HOTKEY_ROOM_MOVE_DOWN", ""}; + NamedConfig roomMergeUp{"HOTKEY_ROOM_MERGE_UP", ""}; + NamedConfig roomMergeDown{"HOTKEY_ROOM_MERGE_DOWN", ""}; + NamedConfig roomDelete{"HOTKEY_ROOM_DELETE", "Del"}; + NamedConfig roomConnectNeighbors{"HOTKEY_ROOM_CONNECT_NEIGHBORS", ""}; + NamedConfig roomMoveToSelected{"HOTKEY_ROOM_MOVE_TO_SELECTED", ""}; + NamedConfig roomUpdateSelected{"HOTKEY_ROOM_UPDATE_SELECTED", ""}; + + private: + SUBGROUP(); + } hotkeys; + + struct NODISCARD CommsSettings final + { + // Colors for each communication type + NamedConfig tellColor{"COMMS_TELL_COLOR", QColor(Qt::cyan)}; + NamedConfig whisperColor{"COMMS_WHISPER_COLOR", QColor(135, 206, 250)}; // Light sky blue + NamedConfig groupColor{"COMMS_GROUP_COLOR", QColor(Qt::green)}; + NamedConfig askColor{"COMMS_ASK_COLOR", QColor(Qt::yellow)}; + NamedConfig sayColor{"COMMS_SAY_COLOR", QColor(Qt::white)}; + NamedConfig emoteColor{"COMMS_EMOTE_COLOR", QColor(Qt::magenta)}; + NamedConfig socialColor{"COMMS_SOCIAL_COLOR", QColor(255, 182, 193)}; // Light pink + NamedConfig yellColor{"COMMS_YELL_COLOR", QColor(Qt::red)}; + NamedConfig narrateColor{"COMMS_NARRATE_COLOR", QColor(255, 165, 0)}; // Orange + NamedConfig prayColor{"COMMS_PRAY_COLOR", QColor(173, 216, 230)}; // Light blue + NamedConfig shoutColor{"COMMS_SHOUT_COLOR", QColor(139, 0, 0)}; // Dark red + NamedConfig singColor{"COMMS_SING_COLOR", QColor(144, 238, 144)}; // Light green + NamedConfig backgroundColor{"COMMS_BG_COLOR", QColor(Qt::black)}; + + // Talker colors (based on GMCP Comm.Channel talker-type) + NamedConfig talkerYouColor{"COMMS_TALKER_YOU_COLOR", QColor(255, 215, 0)}; // Gold + NamedConfig talkerPlayerColor{"COMMS_TALKER_PLAYER_COLOR", QColor(Qt::white)}; + NamedConfig talkerNpcColor{"COMMS_TALKER_NPC_COLOR", QColor(192, 192, 192)}; // Silver/Gray + NamedConfig talkerAllyColor{"COMMS_TALKER_ALLY_COLOR", QColor(0, 255, 0)}; // Bright green + NamedConfig talkerNeutralColor{"COMMS_TALKER_NEUTRAL_COLOR", QColor(255, 255, 0)}; // Yellow + NamedConfig talkerEnemyColor{"COMMS_TALKER_ENEMY_COLOR", QColor(255, 0, 0)}; // Red + + // Font styling options + NamedConfig yellAllCaps{"COMMS_YELL_ALL_CAPS", true}; + NamedConfig whisperItalic{"COMMS_WHISPER_ITALIC", true}; + NamedConfig emoteItalic{"COMMS_EMOTE_ITALIC", true}; + + // Display options + NamedConfig showTimestamps{"COMMS_SHOW_TIMESTAMPS", false}; + NamedConfig saveLogOnExit{"COMMS_SAVE_LOG_ON_EXIT", false}; + NamedConfig logDirectory{"COMMS_LOG_DIR", ""}; + + // Tab muting (acts as a filter) + NamedConfig muteDirectTab{"COMMS_MUTE_DIRECT", false}; + NamedConfig muteLocalTab{"COMMS_MUTE_LOCAL", false}; + NamedConfig muteGlobalTab{"COMMS_MUTE_GLOBAL", false}; + + private: + SUBGROUP(); + } comms; + #define XFOREACH_NAMED_COLOR_OPTIONS(X) \ X(BACKGROUND, BACKGROUND_NAME) \ X(CONNECTION_NORMAL, CONNECTION_NORMAL_NAME) \ @@ -300,6 +454,10 @@ class NODISCARD Configuration final { int64_t startEpoch = 0; bool display = false; + NamedConfig gmcpBroadcast{"GMCP_BROADCAST_CLOCK", true}; // Enable GMCP clock broadcasting + NamedConfig gmcpBroadcastInterval{"GMCP_BROADCAST_INTERVAL", 2500}; // Update interval in milliseconds (default: 2.5 seconds = 1 MUME minute) + + MumeClockSettings(); private: SUBGROUP(); diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 939d3660f..1767f37b5 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -10,8 +10,11 @@ #include "../adventure/adventurewidget.h" #include "../adventure/xpstatuswidget.h" #include "../client/ClientWidget.h" +#include "../comms/CommsManager.h" +#include "../comms/CommsWidget.h" #include "../clock/mumeclock.h" #include "../clock/mumeclockwidget.h" +#include "../display/Filenames.h" #include "../display/InfomarkSelection.h" #include "../display/MapCanvasData.h" #include "../display/mapcanvas.h" @@ -32,6 +35,7 @@ #include "DescriptionWidget.h" #include "MapZoomSlider.h" #include "UpdateDialog.h" +#include "VisibilityFilterWidget.h" #include "aboutdialog.h" #include "findroomsdlg.h" #include "infomarkseditdlg.h" @@ -128,7 +132,11 @@ MainWindow::MainWindow() addApplicationFont(); registerMetatypes(); - m_mapData = new MapData(this); + // Create game observer and clock first, as MapData depends on them + m_gameObserver = std::make_unique(); + m_mumeClock = new MumeClock(getConfig().mumeClock.startEpoch, deref(m_gameObserver), this); + + m_mapData = new MapData(deref(m_mumeClock), this); MapData &mapData = deref(m_mapData); m_mapData->setObjectName("MapData"); @@ -145,9 +153,21 @@ MainWindow::MainWindow() m_pathMachine = new Mmapper2PathMachine(mapData, this); m_pathMachine->setObjectName("Mmapper2PathMachine"); - m_gameObserver = std::make_unique(); m_adventureTracker = new AdventureTracker(deref(m_gameObserver), this); + // Create AutoLogger early (needed by CommsWidget) + m_logger = new AutoLogger(this); + + // Communications Manager + m_commsManager = new CommsManager(this); + deref(m_gameObserver).sig2_sentToUserGmcp.connect(m_lifetime, [this](const GmcpMessage &gmcp) { + deref(m_commsManager).slot_parseGmcpInput(gmcp); + }); + deref(m_gameObserver).sig2_rawGameText.connect(m_lifetime, [this](const QString &text) { + deref(m_commsManager).slot_parseRawGameText(text); + }); + connect(m_commsManager, &CommsManager::sig_log, this, &MainWindow::slot_log); + // View -> Side Panels -> Client Panel m_clientWidget = new ClientWidget(this); m_clientWidget->setObjectName("InternalMudClientWidget"); @@ -166,7 +186,6 @@ MainWindow::MainWindow() m_dockDialogLog->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); m_dockDialogLog->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); - m_dockDialogLog->toggleViewAction()->setShortcut(tr("Ctrl+L")); addDockWidget(Qt::BottomDockWidgetArea, m_dockDialogLog); logWindow = new QTextBrowser(m_dockDialogLog); @@ -218,6 +237,18 @@ MainWindow::MainWindow() m_dockDialogAdventure->setWidget(m_adventureWidget); m_dockDialogAdventure->hide(); + // View -> Side Panels -> Communications Panel + m_dockDialogComms = new QDockWidget(tr("Communications"), this); + m_dockDialogComms->setObjectName("DockWidgetComms"); + m_dockDialogComms->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); + m_dockDialogComms->setFeatures(QDockWidget::DockWidgetClosable + | QDockWidget::DockWidgetFloatable + | QDockWidget::DockWidgetMovable); + addDockWidget(Qt::BottomDockWidgetArea, m_dockDialogComms); + m_commsWidget = new CommsWidget(deref(m_commsManager), m_logger, this); + m_dockDialogComms->setWidget(m_commsWidget); + m_dockDialogComms->hide(); + // View -> Side Panels -> Description / Area Panel m_descriptionWidget = new DescriptionWidget(this); m_dockDialogDescription = new QDockWidget(tr("Description Panel"), this); @@ -229,12 +260,40 @@ MainWindow::MainWindow() addDockWidget(Qt::RightDockWidgetArea, m_dockDialogDescription); m_dockDialogDescription->setWidget(m_descriptionWidget); - m_mumeClock = new MumeClock(getConfig().mumeClock.startEpoch, deref(m_gameObserver), this); + // View -> Toolbars -> Visibility Filter + m_visibilityFilterWidget = new VisibilityFilterWidget(this); + m_dockDialogVisibleMarkers = new QDockWidget(tr("Visibility Filter"), this); + m_dockDialogVisibleMarkers->setObjectName("DockWidgetVisibilityFilter"); + m_dockDialogVisibleMarkers->setAllowedAreas(Qt::AllDockWidgetAreas); + m_dockDialogVisibleMarkers->setFeatures(QDockWidget::DockWidgetMovable + | QDockWidget::DockWidgetFloatable + | QDockWidget::DockWidgetClosable); + addDockWidget(Qt::RightDockWidgetArea, m_dockDialogVisibleMarkers); + m_dockDialogVisibleMarkers->setWidget(m_visibilityFilterWidget); + m_dockDialogVisibleMarkers->hide(); + + // Connect visibility filter changes to map update + // Separate signals ensure we only rebuild what's necessary: + // - Infomarks visibility -> only rebuild infomark meshes + // - Connections visibility -> only rebuild map/connection batches + connect(m_visibilityFilterWidget, &VisibilityFilterWidget::sig_visibilityChanged, + this, [this]() { + m_mapWindow->getCanvas()->infomarksChanged(); + }); + + connect(m_visibilityFilterWidget, &VisibilityFilterWidget::sig_connectionsVisibilityChanged, + this, [this]() { + // Just trigger a repaint - connections use alpha transparency so no batch rebuild needed + m_mapWindow->getCanvas()->update(); + }); + if constexpr (!NO_UPDATER) { m_updateDialog = new UpdateDialog(this); } createActions(); + applyHotkeys(); + registerGlobalShortcuts(); setupToolBars(); setupMenuBar(); setupStatusBar(); @@ -244,8 +303,6 @@ MainWindow::MainWindow() setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - m_logger = new AutoLogger(this); - // TODO move this connect() wiring into AutoLogger::ctor ? GameObserver &observer = deref(m_gameObserver); observer.sig2_connected.connect(m_lifetime, [this]() { @@ -294,6 +351,8 @@ MainWindow::MainWindow() setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); } + radialTransparencyAct->setChecked(getConfig().canvas.enableRadialTransparency); + showStatusBarAct->setChecked(getConfig().general.showStatusBar); slot_setShowStatusBar(); @@ -483,6 +542,13 @@ void MainWindow::wireConnections() connect(m_mapData, &MapFrontend::sig_clearingMap, m_groupWidget, &GroupWidget::slot_mapUnloaded); connect(m_mumeClock, &MumeClock::sig_log, this, &MainWindow::slot_log); + connect(m_mumeClock, &MumeClock::sig_seasonChanged, m_mapWindow->getCanvas(), &MapCanvas::slot_onSeasonChanged); + + // Initialize the current season for seasonal textures + // This sets the global season variable so textures load correctly on startup + // Note: We only call setCurrentSeason() here, NOT slot_onSeasonChanged() + // because the OpenGL context isn't ready yet during construction + setCurrentSeason(m_mumeClock->getMumeMoment().toSeason()); connect(m_listener, &ConnectionListener::sig_log, this, &MainWindow::slot_log); connect(m_dockDialogClient, @@ -534,21 +600,18 @@ void MainWindow::createActions() openAct = new QAction(QIcon::fromTheme("document-open", QIcon(":/icons/open.png")), tr("&Open..."), this); - openAct->setShortcut(tr("Ctrl+O")); openAct->setStatusTip(tr("Open an existing file")); connect(openAct, &QAction::triggered, this, &MainWindow::slot_open); reloadAct = new QAction(QIcon::fromTheme("document-open-recent", QIcon(":/icons/reload.png")), tr("&Reload"), this); - reloadAct->setShortcut(tr("Ctrl+R")); reloadAct->setStatusTip(tr("Reload the current map")); connect(reloadAct, &QAction::triggered, this, &MainWindow::slot_reload); saveAct = new QAction(QIcon::fromTheme("document-save", QIcon(":/icons/save.png")), tr("&Save"), this); - saveAct->setShortcut(tr("Ctrl+S")); saveAct->setStatusTip(tr("Save the document to disk")); saveAct->setEnabled(false); connect(saveAct, &QAction::triggered, this, &MainWindow::slot_save); @@ -578,19 +641,16 @@ void MainWindow::createActions() connect(mergeAct, &QAction::triggered, this, &MainWindow::slot_merge); exitAct = new QAction(QIcon::fromTheme("application-exit"), tr("E&xit"), this); - exitAct->setShortcut(tr("Ctrl+Q")); exitAct->setStatusTip(tr("Exit the application")); connect(exitAct, &QAction::triggered, this, &QWidget::close); m_undoAction = new QAction(QIcon::fromTheme("edit-undo"), tr("&Undo"), this); - m_undoAction->setShortcut(QKeySequence::Undo); m_undoAction->setStatusTip(tr("Undo the last action")); connect(m_undoAction, &QAction::triggered, m_mapData, &MapData::slot_undo); connect(m_mapData, &MapData::sig_undoAvailable, m_undoAction, &QAction::setEnabled); m_undoAction->setEnabled(false); m_redoAction = new QAction(QIcon::fromTheme("edit-redo"), tr("&Redo"), this); - m_redoAction->setShortcut(QKeySequence::Redo); m_redoAction->setStatusTip(tr("Redo the last undone action")); connect(m_redoAction, &QAction::triggered, m_mapData, &MapData::slot_redo); connect(m_mapData, &MapData::sig_redoAvailable, m_redoAction, &QAction::setEnabled); @@ -600,7 +660,6 @@ void MainWindow::createActions() QIcon(":/icons/preferences.png")), tr("&Preferences"), this); - preferencesAct->setShortcut(tr("Ctrl+P")); preferencesAct->setStatusTip(tr("MMapper preferences")); connect(preferencesAct, &QAction::triggered, this, &MainWindow::slot_onPreferences); @@ -647,22 +706,23 @@ void MainWindow::createActions() tr("Zoom In"), this); zoomInAct->setStatusTip(tr("Zooms In current map")); - zoomInAct->setShortcut(tr("Ctrl++")); zoomOutAct = new QAction(QIcon::fromTheme("zoom-out", QIcon(":/icons/viewmag-.png")), tr("Zoom Out"), this); - zoomOutAct->setShortcut(tr("Ctrl+-")); zoomOutAct->setStatusTip(tr("Zooms Out current map")); zoomResetAct = new QAction(QIcon::fromTheme("zoom-original", QIcon(":/icons/viewmagfit.png")), tr("Zoom Reset"), this); - zoomResetAct->setShortcut(tr("Ctrl+0")); zoomResetAct->setStatusTip(tr("Zoom to original size")); alwaysOnTopAct = new QAction(tr("Always On Top"), this); alwaysOnTopAct->setCheckable(true); connect(alwaysOnTopAct, &QAction::triggered, this, &MainWindow::slot_alwaysOnTop); + radialTransparencyAct = new QAction(tr("Radial Transparency"), this); + radialTransparencyAct->setCheckable(true); + connect(radialTransparencyAct, &QAction::triggered, this, &MainWindow::slot_setRadialTransparency); + showStatusBarAct = new QAction(tr("Always Show Status Bar"), this); showStatusBarAct->setCheckable(true); connect(showStatusBarAct, &QAction::triggered, this, &MainWindow::slot_setShowStatusBar); @@ -680,26 +740,11 @@ void MainWindow::createActions() layerUpAct = new QAction(QIcon::fromTheme("go-up", QIcon(":/icons/layerup.png")), tr("Layer Up"), this); - layerUpAct->setShortcut(tr([]() -> const char * { - // Technically tr() could convert Ctrl to Meta, right? - if constexpr (CURRENT_PLATFORM == PlatformEnum::Mac) { - return "Meta+Tab"; - } - return "Ctrl+Tab"; - }())); layerUpAct->setStatusTip(tr("Layer Up")); connect(layerUpAct, &QAction::triggered, this, &MainWindow::slot_onLayerUp); layerDownAct = new QAction(QIcon::fromTheme("go-down", QIcon(":/icons/layerdown.png")), tr("Layer Down"), this); - - layerDownAct->setShortcut(tr([]() -> const char * { - // Technically tr() could convert Ctrl to Meta, right? - if constexpr (CURRENT_PLATFORM == PlatformEnum::Mac) { - return "Meta+Shift+Tab"; - } - return "Ctrl+Shift+Tab"; - }())); layerDownAct->setStatusTip(tr("Layer Down")); connect(layerDownAct, &QAction::triggered, this, &MainWindow::slot_onLayerDown); @@ -816,7 +861,6 @@ void MainWindow::createActions() tr("Edit Selected Rooms"), this); editRoomSelectionAct->setStatusTip(tr("Edit Selected Rooms")); - editRoomSelectionAct->setShortcut(tr("Ctrl+E")); connect(editRoomSelectionAct, &QAction::triggered, this, &MainWindow::slot_onEditRoomSelection); deleteRoomSelectionAct = new QAction(QIcon(":/icons/roomdelete.png"), @@ -871,7 +915,6 @@ void MainWindow::createActions() findRoomsAct = new QAction(QIcon(":/icons/roomfind.png"), tr("&Find Rooms"), this); findRoomsAct->setStatusTip(tr("Find matching rooms")); - findRoomsAct->setShortcut(tr("Ctrl+F")); connect(findRoomsAct, &QAction::triggered, this, &MainWindow::slot_onFindRoom); clientAct = new QAction(QIcon(":/icons/online.png"), tr("&Launch mud client"), this); @@ -884,6 +927,12 @@ void MainWindow::createActions() connect(saveLogAct, &QAction::triggered, m_clientWidget, &ClientWidget::slot_saveLog); saveLogAct->setStatusTip(tr("Save log as file")); + saveCommsLogAct = new QAction(QIcon::fromTheme("document-save", QIcon(":/icons/save.png")), + tr("Save &communications log as..."), + this); + connect(saveCommsLogAct, &QAction::triggered, m_commsWidget, &CommsWidget::slot_saveLog); + saveCommsLogAct->setStatusTip(tr("Save communications log as file")); + releaseAllPathsAct = new QAction(QIcon(":/icons/cancel.png"), tr("Release All Paths"), this); releaseAllPathsAct->setStatusTip(tr("Release all paths")); releaseAllPathsAct->setCheckable(false); @@ -1005,6 +1054,104 @@ void MainWindow::createActions() connect(rebuildMeshesAct, &QAction::triggered, getCanvas(), &MapCanvas::slot_rebuildMeshes); } +void MainWindow::registerGlobalShortcuts() +{ + // Register all actions with the main window so their shortcuts work globally + // This is required for keyboard shortcuts to work anywhere in the application + + // File operations + addAction(newAct); + addAction(openAct); + addAction(mergeAct); + addAction(reloadAct); + addAction(saveAct); + addAction(saveAsAct); + addAction(exportBaseMapAct); + addAction(exportMm2xmlMapAct); + addAction(exportWebMapAct); + addAction(exportMmpMapAct); + addAction(exitAct); + + // Edit operations + addAction(m_undoAction); + addAction(m_redoAction); + addAction(preferencesAct); + addAction(findRoomsAct); + addAction(editRoomSelectionAct); + + // View operations + addAction(zoomInAct); + addAction(zoomOutAct); + addAction(zoomResetAct); + addAction(layerUpAct); + addAction(layerDownAct); + addAction(layerResetAct); + + // View toggles + addAction(radialTransparencyAct); + addAction(showStatusBarAct); + addAction(showScrollBarsAct); + addAction(showMenuBarAct); + addAction(alwaysOnTopAct); + + // Side panels + if (m_dockDialogLog && m_dockDialogLog->toggleViewAction()) { + addAction(m_dockDialogLog->toggleViewAction()); + } + if (m_dockDialogClient && m_dockDialogClient->toggleViewAction()) { + addAction(m_dockDialogClient->toggleViewAction()); + } + if (m_dockDialogGroup && m_dockDialogGroup->toggleViewAction()) { + addAction(m_dockDialogGroup->toggleViewAction()); + } + if (m_dockDialogRoom && m_dockDialogRoom->toggleViewAction()) { + addAction(m_dockDialogRoom->toggleViewAction()); + } + if (m_dockDialogAdventure && m_dockDialogAdventure->toggleViewAction()) { + addAction(m_dockDialogAdventure->toggleViewAction()); + } + if (m_dockDialogComms && m_dockDialogComms->toggleViewAction()) { + addAction(m_dockDialogComms->toggleViewAction()); + } + if (m_dockDialogDescription && m_dockDialogDescription->toggleViewAction()) { + addAction(m_dockDialogDescription->toggleViewAction()); + } + + // Mouse modes + addAction(mouseMode.modeMoveSelectAct); + addAction(mouseMode.modeRoomRaypickAct); + addAction(mouseMode.modeRoomSelectAct); + addAction(mouseMode.modeConnectionSelectAct); + addAction(mouseMode.modeInfomarkSelectAct); + addAction(mouseMode.modeCreateInfomarkAct); + addAction(mouseMode.modeCreateRoomAct); + addAction(mouseMode.modeCreateConnectionAct); + addAction(mouseMode.modeCreateOnewayConnectionAct); + + // Room operations + addAction(createRoomAct); + addAction(moveUpRoomSelectionAct); + addAction(moveDownRoomSelectionAct); + addAction(mergeUpRoomSelectionAct); + addAction(mergeDownRoomSelectionAct); + addAction(deleteRoomSelectionAct); + addAction(connectToNeighboursRoomSelectionAct); + addAction(gotoRoomAct); + addAction(forceRoomAct); + + // Connection operations + addAction(deleteConnectionSelectionAct); + + // Infomark operations + addAction(infomarkActions.deleteInfomarkAct); + addAction(infomarkActions.editInfomarkAct); + + // Other + addAction(rebuildMeshesAct); + + qDebug() << "Registered all actions with main window for global shortcuts"; +} + static void setConfigMapMode(const MapModeEnum mode) { setConfig().general.mapMode = mode; @@ -1156,12 +1303,14 @@ void MainWindow::setupMenuBar() toolbars->addAction(roomToolBar->toggleViewAction()); toolbars->addAction(connectionToolBar->toggleViewAction()); toolbars->addAction(settingsToolBar->toggleViewAction()); + toolbars->addAction(m_dockDialogVisibleMarkers->toggleViewAction()); QMenu *sidepanels = viewMenu->addMenu(tr("&Side Panels")); sidepanels->addAction(m_dockDialogLog->toggleViewAction()); sidepanels->addAction(m_dockDialogClient->toggleViewAction()); sidepanels->addAction(m_dockDialogGroup->toggleViewAction()); sidepanels->addAction(m_dockDialogRoom->toggleViewAction()); sidepanels->addAction(m_dockDialogAdventure->toggleViewAction()); + sidepanels->addAction(m_dockDialogComms->toggleViewAction()); sidepanels->addAction(m_dockDialogDescription->toggleViewAction()); viewMenu->addSeparator(); viewMenu->addAction(zoomInAct); @@ -1174,6 +1323,8 @@ void MainWindow::setupMenuBar() viewMenu->addSeparator(); viewMenu->addAction(rebuildMeshesAct); viewMenu->addSeparator(); + viewMenu->addAction(radialTransparencyAct); + viewMenu->addAction(showStatusBarAct); viewMenu->addAction(showScrollBarsAct); if constexpr (CURRENT_PLATFORM != PlatformEnum::Mac) { @@ -1186,6 +1337,8 @@ void MainWindow::setupMenuBar() tr("&Integrated Mud Client")); clientMenu->addAction(clientAct); clientMenu->addAction(saveLogAct); + clientMenu->addSeparator(); + clientMenu->addAction(saveCommsLogAct); QMenu *pathMachineMenu = settingsMenu->addMenu(QIcon(":/icons/goto.png"), tr("&Path Machine")); pathMachineMenu->addAction(mouseMode.modeRoomSelectAct); pathMachineMenu->addSeparator(); @@ -1269,6 +1422,15 @@ void MainWindow::slot_alwaysOnTop() show(); } +void MainWindow::slot_setRadialTransparency() +{ + const bool enableRadialTransparency = this->radialTransparencyAct->isChecked(); + setConfig().canvas.enableRadialTransparency = enableRadialTransparency; + if (m_mapWindow) { + m_mapWindow->update(); + } +} + void MainWindow::slot_setShowStatusBar() { const bool showStatusBar = this->showStatusBarAct->isChecked(); @@ -1414,10 +1576,22 @@ void MainWindow::slot_onPreferences() &ConfigDialog::sig_graphicsSettingsChanged, m_mapWindow, &MapWindow::slot_graphicsSettingsChanged); + connect(m_configDialog.get(), + &ConfigDialog::sig_textureSettingsChanged, + m_mapWindow->getCanvas(), + &MapCanvas::slot_reloadTextures); connect(m_configDialog.get(), &ConfigDialog::sig_groupSettingsChanged, m_groupManager, &Mmapper2Group::slot_groupSettingsChanged); + connect(m_configDialog.get(), + &ConfigDialog::sig_commsSettingsChanged, + m_commsWidget, + &CommsWidget::slot_loadSettings); + connect(m_configDialog.get(), + &ConfigDialog::sig_hotkeysChanged, + this, + &MainWindow::applyHotkeys); m_configDialog->show(); } @@ -1478,6 +1652,12 @@ bool MainWindow::eventFilter(QObject *const obj, QEvent *const event) void MainWindow::closeEvent(QCloseEvent *const event) { // REVISIT: wait and see if we're actually exiting first? + + // Save communications log if enabled + if (m_commsWidget) { + m_commsWidget->slot_saveLogOnExit(); + } + writeSettings(); if (!maybeSave()) { @@ -2131,3 +2311,107 @@ void MainWindow::onSuccessfulSave(const SaveModeEnum mode, } } } + +void MainWindow::applyHotkeys() +{ + const auto &hotkeys = getConfig().hotkeys; + + qDebug() << "=== Applying hotkeys ==="; + + // Helper lambda to apply shortcut only if not empty + auto applyShortcut = [](QAction *action, const QString &shortcut) { + if (action && !shortcut.isEmpty()) { + action->setShortcut(QKeySequence(shortcut)); + qDebug() << " Setting shortcut:" << action->text() << "=" << shortcut; + } else if (action) { + action->setShortcut(QKeySequence()); + qDebug() << " Clearing shortcut:" << action->text(); + } + }; + + // File operations + applyShortcut(openAct, hotkeys.fileOpen.get()); + applyShortcut(saveAct, hotkeys.fileSave.get()); + applyShortcut(reloadAct, hotkeys.fileReload.get()); + applyShortcut(exitAct, hotkeys.fileQuit.get()); + + // Edit operations + applyShortcut(m_undoAction, hotkeys.editUndo.get()); + applyShortcut(m_redoAction, hotkeys.editRedo.get()); + applyShortcut(preferencesAct, hotkeys.editPreferences.get()); + applyShortcut(findRoomsAct, hotkeys.editFindRooms.get()); + applyShortcut(editRoomSelectionAct, hotkeys.editRoom.get()); + + // View operations + applyShortcut(zoomInAct, hotkeys.viewZoomIn.get()); + applyShortcut(zoomOutAct, hotkeys.viewZoomOut.get()); + applyShortcut(zoomResetAct, hotkeys.viewZoomReset.get()); + applyShortcut(layerUpAct, hotkeys.viewLayerUp.get()); + applyShortcut(layerDownAct, hotkeys.viewLayerDown.get()); + applyShortcut(layerResetAct, hotkeys.viewLayerReset.get()); + + // View toggles + applyShortcut(radialTransparencyAct, hotkeys.viewRadialTransparency.get()); + applyShortcut(showStatusBarAct, hotkeys.viewStatusBar.get()); + applyShortcut(showScrollBarsAct, hotkeys.viewScrollBars.get()); + applyShortcut(showMenuBarAct, hotkeys.viewMenuBar.get()); + applyShortcut(alwaysOnTopAct, hotkeys.viewAlwaysOnTop.get()); + + // Side panels + if (m_dockDialogLog && m_dockDialogLog->toggleViewAction()) { + applyShortcut(m_dockDialogLog->toggleViewAction(), hotkeys.panelLog.get()); + } + if (m_dockDialogClient && m_dockDialogClient->toggleViewAction()) { + applyShortcut(m_dockDialogClient->toggleViewAction(), hotkeys.panelClient.get()); + } + if (m_dockDialogGroup && m_dockDialogGroup->toggleViewAction()) { + applyShortcut(m_dockDialogGroup->toggleViewAction(), hotkeys.panelGroup.get()); + } + if (m_dockDialogRoom && m_dockDialogRoom->toggleViewAction()) { + applyShortcut(m_dockDialogRoom->toggleViewAction(), hotkeys.panelRoom.get()); + } + if (m_dockDialogAdventure && m_dockDialogAdventure->toggleViewAction()) { + applyShortcut(m_dockDialogAdventure->toggleViewAction(), hotkeys.panelAdventure.get()); + } + if (m_dockDialogComms && m_dockDialogComms->toggleViewAction()) { + applyShortcut(m_dockDialogComms->toggleViewAction(), hotkeys.panelComms.get()); + } + if (m_dockDialogDescription && m_dockDialogDescription->toggleViewAction()) { + applyShortcut(m_dockDialogDescription->toggleViewAction(), hotkeys.panelDescription.get()); + } + + // Mouse modes + applyShortcut(mouseMode.modeMoveSelectAct, hotkeys.modeMoveMap.get()); + applyShortcut(mouseMode.modeRoomRaypickAct, hotkeys.modeRaypick.get()); + applyShortcut(mouseMode.modeRoomSelectAct, hotkeys.modeSelectRooms.get()); + applyShortcut(mouseMode.modeInfomarkSelectAct, hotkeys.modeSelectMarkers.get()); + applyShortcut(mouseMode.modeConnectionSelectAct, hotkeys.modeSelectConnection.get()); + applyShortcut(mouseMode.modeCreateInfomarkAct, hotkeys.modeCreateMarker.get()); + applyShortcut(mouseMode.modeCreateRoomAct, hotkeys.modeCreateRoom.get()); + applyShortcut(mouseMode.modeCreateConnectionAct, hotkeys.modeCreateConnection.get()); + applyShortcut(mouseMode.modeCreateOnewayConnectionAct, hotkeys.modeCreateOnewayConnection.get()); + + // Room operations + applyShortcut(createRoomAct, hotkeys.roomCreate.get()); + applyShortcut(moveUpRoomSelectionAct, hotkeys.roomMoveUp.get()); + applyShortcut(moveDownRoomSelectionAct, hotkeys.roomMoveDown.get()); + applyShortcut(mergeUpRoomSelectionAct, hotkeys.roomMergeUp.get()); + applyShortcut(mergeDownRoomSelectionAct, hotkeys.roomMergeDown.get()); + applyShortcut(deleteRoomSelectionAct, hotkeys.roomDelete.get()); + applyShortcut(connectToNeighboursRoomSelectionAct, hotkeys.roomConnectNeighbors.get()); + applyShortcut(gotoRoomAct, hotkeys.roomMoveToSelected.get()); + applyShortcut(forceRoomAct, hotkeys.roomUpdateSelected.get()); + + // Apply alternative preferences shortcut (Esc) + if (preferencesAct && !hotkeys.editPreferencesAlt.get().isEmpty()) { + QList shortcuts; + if (!hotkeys.editPreferences.get().isEmpty()) { + shortcuts << QKeySequence(hotkeys.editPreferences.get()); + } + shortcuts << QKeySequence(hotkeys.editPreferencesAlt.get()); + preferencesAct->setShortcuts(shortcuts); + qDebug() << " Setting dual shortcuts for Preferences:" << shortcuts; + } + + qDebug() << "=== Hotkeys applied ==="; +} diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index 674860557..42603a50d 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -34,6 +34,8 @@ class AdventureTracker; class AdventureWidget; class AutoLogger; class ClientWidget; +class CommsManager; +class CommsWidget; class ConfigDialog; class ConnectionListener; class ConnectionSelection; @@ -66,6 +68,7 @@ class RoomSelection; class RoomWidget; class UpdateDialog; class DescriptionWidget; +class VisibilityFilterWidget; struct MapLoadData; @@ -85,6 +88,8 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QDockWidget *m_dockDialogGroup = nullptr; QDockWidget *m_dockDialogAdventure = nullptr; QDockWidget *m_dockDialogDescription = nullptr; + QDockWidget *m_dockDialogComms = nullptr; + QDockWidget *m_dockDialogVisibleMarkers = nullptr; std::unique_ptr m_gameObserver; AutoLogger *m_logger = nullptr; @@ -108,7 +113,11 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow AdventureTracker *m_adventureTracker = nullptr; AdventureWidget *m_adventureWidget = nullptr; + CommsManager *m_commsManager = nullptr; + CommsWidget *m_commsWidget = nullptr; + DescriptionWidget *m_descriptionWidget = nullptr; + VisibilityFilterWidget *m_visibilityFilterWidget = nullptr; SharedRoomSelection m_roomSelection; std::shared_ptr m_connectionSelection; @@ -166,6 +175,7 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QAction *zoomOutAct = nullptr; QAction *zoomResetAct = nullptr; QAction *alwaysOnTopAct = nullptr; + QAction *radialTransparencyAct = nullptr; QAction *showStatusBarAct = nullptr; QAction *showScrollBarsAct = nullptr; QAction *showMenuBarAct = nullptr; @@ -222,6 +232,7 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QAction *clientAct = nullptr; QAction *saveLogAct = nullptr; + QAction *saveCommsLogAct = nullptr; QAction *gotoRoomAct = nullptr; QAction *forceRoomAct = nullptr; @@ -304,6 +315,8 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow void setupMenuBar(); void setupToolBars(); void setupStatusBar(); + void applyHotkeys(); + void registerGlobalShortcuts(); void readSettings(); void writeSettings(); @@ -432,6 +445,7 @@ public slots: void slot_onOfflineMode(); void slot_setMode(MapModeEnum mode); void slot_alwaysOnTop(); + void slot_setRadialTransparency(); void slot_setShowStatusBar(); void slot_setShowScrollBars(); void slot_setShowMenuBar(); diff --git a/src/preferences/configdialog.cpp b/src/preferences/configdialog.cpp index cac8ffb69..870091db2 100644 --- a/src/preferences/configdialog.cpp +++ b/src/preferences/configdialog.cpp @@ -8,9 +8,11 @@ #include "autologpage.h" #include "clientpage.h" +#include "commspage.h" #include "generalpage.h" #include "graphicspage.h" #include "grouppage.h" +#include "hotkeyspage.h" #include "mumeprotocolpage.h" #include "parserpage.h" #include "pathmachinepage.h" @@ -38,12 +40,16 @@ ConfigDialog::ConfigDialog(QWidget *const parent) auto autoLogPage = new AutoLogPage(this); auto mumeProtocolPage = new MumeProtocolPage(this); auto pathmachinePage = new PathmachinePage(this); + auto hotkeysPage = new HotkeysPage(this); + auto commsPage = new CommsPage(this); m_pagesWidget = new QStackedWidget(this); auto *const pagesWidget = m_pagesWidget; pagesWidget->addWidget(generalPage); pagesWidget->addWidget(graphicsPage); + pagesWidget->addWidget(hotkeysPage); + pagesWidget->addWidget(commsPage); pagesWidget->addWidget(parserPage); pagesWidget->addWidget(clientPage); pagesWidget->addWidget(groupPage); @@ -80,10 +86,21 @@ ConfigDialog::ConfigDialog(QWidget *const parent) mumeProtocolPage, &MumeProtocolPage::slot_loadConfig); connect(this, &ConfigDialog::sig_loadConfig, pathmachinePage, &PathmachinePage::slot_loadConfig); + connect(this, &ConfigDialog::sig_loadConfig, hotkeysPage, &HotkeysPage::slot_loadConfig); + connect(hotkeysPage, &HotkeysPage::sig_hotkeysChanged, this, &ConfigDialog::sig_hotkeysChanged); + connect(this, &ConfigDialog::sig_loadConfig, commsPage, &CommsPage::slot_loadConfig); + connect(commsPage, + &CommsPage::sig_commsSettingsChanged, + this, + &ConfigDialog::sig_commsSettingsChanged); connect(graphicsPage, &GraphicsPage::sig_graphicsSettingsChanged, this, &ConfigDialog::sig_graphicsSettingsChanged); + connect(graphicsPage, + &GraphicsPage::sig_textureSettingsChanged, + this, + &ConfigDialog::sig_textureSettingsChanged); } ConfigDialog::~ConfigDialog() @@ -122,6 +139,8 @@ void ConfigDialog::createIcons() addItem(":/icons/generalcfg.png", tr("General")); addItem(":/icons/graphicscfg.png", tr("Graphics")); + addItem(":/icons/hotkeys.png", tr("Hotkeys")); + addItem(":/icons/comms.png", tr("Comms")); addItem(":/icons/parsercfg.png", tr("Parser")); addItem(":/icons/terminal.png", tr("Integrated\nMud Client")); addItem(":/icons/group-recolor.png", tr("Group Panel")); diff --git a/src/preferences/configdialog.h b/src/preferences/configdialog.h index 839ad0a0f..a000e42a4 100644 --- a/src/preferences/configdialog.h +++ b/src/preferences/configdialog.h @@ -40,7 +40,10 @@ class NODISCARD_QOBJECT ConfigDialog final : public QDialog signals: void sig_graphicsSettingsChanged(); + void sig_textureSettingsChanged(); void sig_groupSettingsChanged(); + void sig_hotkeysChanged(); + void sig_commsSettingsChanged(); void sig_loadConfig(); public slots: diff --git a/src/preferences/hotkeyspage.cpp b/src/preferences/hotkeyspage.cpp new file mode 100644 index 000000000..454139e0f --- /dev/null +++ b/src/preferences/hotkeyspage.cpp @@ -0,0 +1,461 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2025 The MMapper Authors + +#include "hotkeyspage.h" + +#include "../configuration/configuration.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +HotkeysPage::HotkeysPage(QWidget *parent) + : QWidget(parent) +{ + setupUI(); + connectSignals(); + loadSettings(); +} + +QWidget *HotkeysPage::createHotkeyRow(const QString &label, + QKeySequenceEdit **editor, + QPushButton **clearBtn) +{ + auto *widget = new QWidget(this); + auto *layout = new QHBoxLayout(widget); + layout->setContentsMargins(0, 0, 0, 0); + + auto *labelWidget = new QLabel(label, widget); + labelWidget->setMinimumWidth(250); + layout->addWidget(labelWidget); + + *editor = new QKeySequenceEdit(widget); + (*editor)->setMaximumSequenceLength(1); + layout->addWidget(*editor); + + *clearBtn = new QPushButton("Clear", widget); + (*clearBtn)->setMaximumWidth(60); + layout->addWidget(*clearBtn); + + return widget; +} + +void HotkeysPage::setupUI() +{ + auto *mainLayout = new QVBoxLayout(this); + + // File Operations Group + auto *fileGroup = new QGroupBox(tr("File Operations")); + auto *fileLayout = new QVBoxLayout(fileGroup); + QPushButton *clearBtn = nullptr; + fileLayout->addWidget(createHotkeyRow("Open", &m_fileOpen, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_fileOpen); }); + fileLayout->addWidget(createHotkeyRow("Save", &m_fileSave, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_fileSave); }); + fileLayout->addWidget(createHotkeyRow("Reload", &m_fileReload, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_fileReload); }); + fileLayout->addWidget(createHotkeyRow("Quit", &m_fileQuit, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_fileQuit); }); + mainLayout->addWidget(fileGroup); + + // Edit Operations Group + auto *editGroup = new QGroupBox(tr("Edit Operations")); + auto *editLayout = new QVBoxLayout(editGroup); + editLayout->addWidget(createHotkeyRow("Undo", &m_editUndo, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editUndo); }); + editLayout->addWidget(createHotkeyRow("Redo", &m_editRedo, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editRedo); }); + editLayout->addWidget(createHotkeyRow("Preferences", &m_editPreferences, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editPreferences); }); + editLayout->addWidget(createHotkeyRow("Preferences (Alt)", &m_editPreferencesAlt, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editPreferencesAlt); }); + editLayout->addWidget(createHotkeyRow("Find Rooms", &m_editFindRooms, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editFindRooms); }); + editLayout->addWidget(createHotkeyRow("Edit Room", &m_editRoom, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_editRoom); }); + mainLayout->addWidget(editGroup); + + // View Operations Group + auto *viewGroup = new QGroupBox(tr("View Operations")); + auto *viewLayout = new QVBoxLayout(viewGroup); + viewLayout->addWidget(createHotkeyRow("Zoom In", &m_viewZoomIn, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewZoomIn); }); + viewLayout->addWidget(createHotkeyRow("Zoom Out", &m_viewZoomOut, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewZoomOut); }); + viewLayout->addWidget(createHotkeyRow("Zoom Reset", &m_viewZoomReset, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewZoomReset); }); + viewLayout->addWidget(createHotkeyRow("Layer Up", &m_viewLayerUp, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewLayerUp); }); + viewLayout->addWidget(createHotkeyRow("Layer Down", &m_viewLayerDown, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewLayerDown); }); + viewLayout->addWidget(createHotkeyRow("Layer Reset", &m_viewLayerReset, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewLayerReset); }); + mainLayout->addWidget(viewGroup); + + // View Toggles Group + auto *togglesGroup = new QGroupBox(tr("View Toggles")); + auto *togglesLayout = new QVBoxLayout(togglesGroup); + togglesLayout->addWidget(createHotkeyRow("Radial Transparency", &m_viewRadialTransparency, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewRadialTransparency); }); + togglesLayout->addWidget(createHotkeyRow("Status Bar", &m_viewStatusBar, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewStatusBar); }); + togglesLayout->addWidget(createHotkeyRow("Scroll Bars", &m_viewScrollBars, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewScrollBars); }); + togglesLayout->addWidget(createHotkeyRow("Menu Bar", &m_viewMenuBar, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewMenuBar); }); + togglesLayout->addWidget(createHotkeyRow("Always on Top", &m_viewAlwaysOnTop, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_viewAlwaysOnTop); }); + mainLayout->addWidget(togglesGroup); + + // Side Panels Group + auto *panelsGroup = new QGroupBox(tr("Side Panels")); + auto *panelsLayout = new QVBoxLayout(panelsGroup); + panelsLayout->addWidget(createHotkeyRow("Log Panel", &m_panelLog, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelLog); }); + panelsLayout->addWidget(createHotkeyRow("Client Panel", &m_panelClient, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelClient); }); + panelsLayout->addWidget(createHotkeyRow("Group Panel", &m_panelGroup, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelGroup); }); + panelsLayout->addWidget(createHotkeyRow("Room Panel", &m_panelRoom, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelRoom); }); + panelsLayout->addWidget(createHotkeyRow("Adventure Panel", &m_panelAdventure, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelAdventure); }); + panelsLayout->addWidget(createHotkeyRow("Communications Panel", &m_panelComms, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelComms); }); + panelsLayout->addWidget(createHotkeyRow("Description Panel", &m_panelDescription, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_panelDescription); }); + mainLayout->addWidget(panelsGroup); + + // Mouse Modes Group + auto *modesGroup = new QGroupBox(tr("Mouse Modes")); + auto *modesLayout = new QVBoxLayout(modesGroup); + modesLayout->addWidget(createHotkeyRow("Move Map", &m_modeMoveMap, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeMoveMap); }); + modesLayout->addWidget(createHotkeyRow("Ray-pick Rooms", &m_modeRaypick, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeRaypick); }); + modesLayout->addWidget(createHotkeyRow("Select Rooms", &m_modeSelectRooms, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeSelectRooms); }); + modesLayout->addWidget(createHotkeyRow("Select Markers", &m_modeSelectMarkers, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeSelectMarkers); }); + modesLayout->addWidget(createHotkeyRow("Select Connection", &m_modeSelectConnection, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeSelectConnection); }); + modesLayout->addWidget(createHotkeyRow("Create Marker", &m_modeCreateMarker, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeCreateMarker); }); + modesLayout->addWidget(createHotkeyRow("Create Room", &m_modeCreateRoom, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeCreateRoom); }); + modesLayout->addWidget(createHotkeyRow("Create Connection", &m_modeCreateConnection, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeCreateConnection); }); + modesLayout->addWidget(createHotkeyRow("Create One-way Connection", &m_modeCreateOnewayConnection, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_modeCreateOnewayConnection); }); + mainLayout->addWidget(modesGroup); + + // Room Operations Group + auto *roomGroup = new QGroupBox(tr("Room Operations")); + auto *roomLayout = new QVBoxLayout(roomGroup); + roomLayout->addWidget(createHotkeyRow("Create New Room", &m_roomCreate, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomCreate); }); + roomLayout->addWidget(createHotkeyRow("Move Up Selected Rooms", &m_roomMoveUp, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomMoveUp); }); + roomLayout->addWidget(createHotkeyRow("Move Down Selected Rooms", &m_roomMoveDown, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomMoveDown); }); + roomLayout->addWidget(createHotkeyRow("Merge Up Selected Rooms", &m_roomMergeUp, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomMergeUp); }); + roomLayout->addWidget(createHotkeyRow("Merge Down Selected Rooms", &m_roomMergeDown, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomMergeDown); }); + roomLayout->addWidget(createHotkeyRow("Delete Selected Rooms", &m_roomDelete, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomDelete); }); + roomLayout->addWidget(createHotkeyRow("Connect Rooms to Neighbors", &m_roomConnectNeighbors, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomConnectNeighbors); }); + roomLayout->addWidget(createHotkeyRow("Move to Selected Room", &m_roomMoveToSelected, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomMoveToSelected); }); + roomLayout->addWidget(createHotkeyRow("Update Selected Room", &m_roomUpdateSelected, &clearBtn)); + connect(clearBtn, &QPushButton::clicked, [this]() { clearShortcut(m_roomUpdateSelected); }); + mainLayout->addWidget(roomGroup); + + mainLayout->addStretch(); + + // Reset to Defaults button at bottom + auto *buttonLayout = new QHBoxLayout(); + m_resetButton = new QPushButton(tr("Reset to Defaults"), this); + buttonLayout->addStretch(); + buttonLayout->addWidget(m_resetButton); + mainLayout->addLayout(buttonLayout); + + setLayout(mainLayout); +} + +void HotkeysPage::connectSignals() +{ + // Connect all QKeySequenceEdit editingFinished signals to save and emit signal + auto connectEditor = [this](QKeySequenceEdit *editor) { + connect(editor, &QKeySequenceEdit::editingFinished, this, [this]() { + saveSettings(); + emit sig_hotkeysChanged(); + }); + }; + + // File operations + connectEditor(m_fileOpen); + connectEditor(m_fileSave); + connectEditor(m_fileReload); + connectEditor(m_fileQuit); + + // Edit operations + connectEditor(m_editUndo); + connectEditor(m_editRedo); + connectEditor(m_editPreferences); + connectEditor(m_editPreferencesAlt); + connectEditor(m_editFindRooms); + connectEditor(m_editRoom); + + // View operations + connectEditor(m_viewZoomIn); + connectEditor(m_viewZoomOut); + connectEditor(m_viewZoomReset); + connectEditor(m_viewLayerUp); + connectEditor(m_viewLayerDown); + connectEditor(m_viewLayerReset); + + // View toggles + connectEditor(m_viewRadialTransparency); + connectEditor(m_viewStatusBar); + connectEditor(m_viewScrollBars); + connectEditor(m_viewMenuBar); + connectEditor(m_viewAlwaysOnTop); + + // Side panels + connectEditor(m_panelLog); + connectEditor(m_panelClient); + connectEditor(m_panelGroup); + connectEditor(m_panelRoom); + connectEditor(m_panelAdventure); + connectEditor(m_panelComms); + connectEditor(m_panelDescription); + + // Mouse modes + connectEditor(m_modeMoveMap); + connectEditor(m_modeRaypick); + connectEditor(m_modeSelectRooms); + connectEditor(m_modeSelectMarkers); + connectEditor(m_modeSelectConnection); + connectEditor(m_modeCreateMarker); + connectEditor(m_modeCreateRoom); + connectEditor(m_modeCreateConnection); + connectEditor(m_modeCreateOnewayConnection); + + // Room operations + connectEditor(m_roomCreate); + connectEditor(m_roomMoveUp); + connectEditor(m_roomMoveDown); + connectEditor(m_roomMergeUp); + connectEditor(m_roomMergeDown); + connectEditor(m_roomDelete); + connectEditor(m_roomConnectNeighbors); + connectEditor(m_roomMoveToSelected); + connectEditor(m_roomUpdateSelected); + + // Reset button + connect(m_resetButton, &QPushButton::clicked, this, &HotkeysPage::resetToDefaults); +} + +void HotkeysPage::loadSettings() +{ + const auto &hotkeys = getConfig().hotkeys; + + m_fileOpen->setKeySequence(QKeySequence(hotkeys.fileOpen.get())); + m_fileSave->setKeySequence(QKeySequence(hotkeys.fileSave.get())); + m_fileReload->setKeySequence(QKeySequence(hotkeys.fileReload.get())); + m_fileQuit->setKeySequence(QKeySequence(hotkeys.fileQuit.get())); + + m_editUndo->setKeySequence(QKeySequence(hotkeys.editUndo.get())); + m_editRedo->setKeySequence(QKeySequence(hotkeys.editRedo.get())); + m_editPreferences->setKeySequence(QKeySequence(hotkeys.editPreferences.get())); + m_editPreferencesAlt->setKeySequence(QKeySequence(hotkeys.editPreferencesAlt.get())); + m_editFindRooms->setKeySequence(QKeySequence(hotkeys.editFindRooms.get())); + m_editRoom->setKeySequence(QKeySequence(hotkeys.editRoom.get())); + + m_viewZoomIn->setKeySequence(QKeySequence(hotkeys.viewZoomIn.get())); + m_viewZoomOut->setKeySequence(QKeySequence(hotkeys.viewZoomOut.get())); + m_viewZoomReset->setKeySequence(QKeySequence(hotkeys.viewZoomReset.get())); + m_viewLayerUp->setKeySequence(QKeySequence(hotkeys.viewLayerUp.get())); + m_viewLayerDown->setKeySequence(QKeySequence(hotkeys.viewLayerDown.get())); + m_viewLayerReset->setKeySequence(QKeySequence(hotkeys.viewLayerReset.get())); + + m_viewRadialTransparency->setKeySequence(QKeySequence(hotkeys.viewRadialTransparency.get())); + m_viewStatusBar->setKeySequence(QKeySequence(hotkeys.viewStatusBar.get())); + m_viewScrollBars->setKeySequence(QKeySequence(hotkeys.viewScrollBars.get())); + m_viewMenuBar->setKeySequence(QKeySequence(hotkeys.viewMenuBar.get())); + m_viewAlwaysOnTop->setKeySequence(QKeySequence(hotkeys.viewAlwaysOnTop.get())); + + m_panelLog->setKeySequence(QKeySequence(hotkeys.panelLog.get())); + m_panelClient->setKeySequence(QKeySequence(hotkeys.panelClient.get())); + m_panelGroup->setKeySequence(QKeySequence(hotkeys.panelGroup.get())); + m_panelRoom->setKeySequence(QKeySequence(hotkeys.panelRoom.get())); + m_panelAdventure->setKeySequence(QKeySequence(hotkeys.panelAdventure.get())); + m_panelComms->setKeySequence(QKeySequence(hotkeys.panelComms.get())); + m_panelDescription->setKeySequence(QKeySequence(hotkeys.panelDescription.get())); + + m_modeMoveMap->setKeySequence(QKeySequence(hotkeys.modeMoveMap.get())); + m_modeRaypick->setKeySequence(QKeySequence(hotkeys.modeRaypick.get())); + m_modeSelectRooms->setKeySequence(QKeySequence(hotkeys.modeSelectRooms.get())); + m_modeSelectMarkers->setKeySequence(QKeySequence(hotkeys.modeSelectMarkers.get())); + m_modeSelectConnection->setKeySequence(QKeySequence(hotkeys.modeSelectConnection.get())); + m_modeCreateMarker->setKeySequence(QKeySequence(hotkeys.modeCreateMarker.get())); + m_modeCreateRoom->setKeySequence(QKeySequence(hotkeys.modeCreateRoom.get())); + m_modeCreateConnection->setKeySequence(QKeySequence(hotkeys.modeCreateConnection.get())); + m_modeCreateOnewayConnection->setKeySequence(QKeySequence(hotkeys.modeCreateOnewayConnection.get())); + + m_roomCreate->setKeySequence(QKeySequence(hotkeys.roomCreate.get())); + m_roomMoveUp->setKeySequence(QKeySequence(hotkeys.roomMoveUp.get())); + m_roomMoveDown->setKeySequence(QKeySequence(hotkeys.roomMoveDown.get())); + m_roomMergeUp->setKeySequence(QKeySequence(hotkeys.roomMergeUp.get())); + m_roomMergeDown->setKeySequence(QKeySequence(hotkeys.roomMergeDown.get())); + m_roomDelete->setKeySequence(QKeySequence(hotkeys.roomDelete.get())); + m_roomConnectNeighbors->setKeySequence(QKeySequence(hotkeys.roomConnectNeighbors.get())); + m_roomMoveToSelected->setKeySequence(QKeySequence(hotkeys.roomMoveToSelected.get())); + m_roomUpdateSelected->setKeySequence(QKeySequence(hotkeys.roomUpdateSelected.get())); +} + +void HotkeysPage::saveSettings() +{ + auto &hotkeys = setConfig().hotkeys; + + hotkeys.fileOpen.set(m_fileOpen->keySequence().toString()); + hotkeys.fileSave.set(m_fileSave->keySequence().toString()); + hotkeys.fileReload.set(m_fileReload->keySequence().toString()); + hotkeys.fileQuit.set(m_fileQuit->keySequence().toString()); + + hotkeys.editUndo.set(m_editUndo->keySequence().toString()); + hotkeys.editRedo.set(m_editRedo->keySequence().toString()); + hotkeys.editPreferences.set(m_editPreferences->keySequence().toString()); + hotkeys.editPreferencesAlt.set(m_editPreferencesAlt->keySequence().toString()); + hotkeys.editFindRooms.set(m_editFindRooms->keySequence().toString()); + hotkeys.editRoom.set(m_editRoom->keySequence().toString()); + + hotkeys.viewZoomIn.set(m_viewZoomIn->keySequence().toString()); + hotkeys.viewZoomOut.set(m_viewZoomOut->keySequence().toString()); + hotkeys.viewZoomReset.set(m_viewZoomReset->keySequence().toString()); + hotkeys.viewLayerUp.set(m_viewLayerUp->keySequence().toString()); + hotkeys.viewLayerDown.set(m_viewLayerDown->keySequence().toString()); + hotkeys.viewLayerReset.set(m_viewLayerReset->keySequence().toString()); + + hotkeys.viewRadialTransparency.set(m_viewRadialTransparency->keySequence().toString()); + hotkeys.viewStatusBar.set(m_viewStatusBar->keySequence().toString()); + hotkeys.viewScrollBars.set(m_viewScrollBars->keySequence().toString()); + hotkeys.viewMenuBar.set(m_viewMenuBar->keySequence().toString()); + hotkeys.viewAlwaysOnTop.set(m_viewAlwaysOnTop->keySequence().toString()); + + hotkeys.panelLog.set(m_panelLog->keySequence().toString()); + hotkeys.panelClient.set(m_panelClient->keySequence().toString()); + hotkeys.panelGroup.set(m_panelGroup->keySequence().toString()); + hotkeys.panelRoom.set(m_panelRoom->keySequence().toString()); + hotkeys.panelAdventure.set(m_panelAdventure->keySequence().toString()); + hotkeys.panelComms.set(m_panelComms->keySequence().toString()); + hotkeys.panelDescription.set(m_panelDescription->keySequence().toString()); + + hotkeys.modeMoveMap.set(m_modeMoveMap->keySequence().toString()); + hotkeys.modeRaypick.set(m_modeRaypick->keySequence().toString()); + hotkeys.modeSelectRooms.set(m_modeSelectRooms->keySequence().toString()); + hotkeys.modeSelectMarkers.set(m_modeSelectMarkers->keySequence().toString()); + hotkeys.modeSelectConnection.set(m_modeSelectConnection->keySequence().toString()); + hotkeys.modeCreateMarker.set(m_modeCreateMarker->keySequence().toString()); + hotkeys.modeCreateRoom.set(m_modeCreateRoom->keySequence().toString()); + hotkeys.modeCreateConnection.set(m_modeCreateConnection->keySequence().toString()); + hotkeys.modeCreateOnewayConnection.set(m_modeCreateOnewayConnection->keySequence().toString()); + + hotkeys.roomCreate.set(m_roomCreate->keySequence().toString()); + hotkeys.roomMoveUp.set(m_roomMoveUp->keySequence().toString()); + hotkeys.roomMoveDown.set(m_roomMoveDown->keySequence().toString()); + hotkeys.roomMergeUp.set(m_roomMergeUp->keySequence().toString()); + hotkeys.roomMergeDown.set(m_roomMergeDown->keySequence().toString()); + hotkeys.roomDelete.set(m_roomDelete->keySequence().toString()); + hotkeys.roomConnectNeighbors.set(m_roomConnectNeighbors->keySequence().toString()); + hotkeys.roomMoveToSelected.set(m_roomMoveToSelected->keySequence().toString()); + hotkeys.roomUpdateSelected.set(m_roomUpdateSelected->keySequence().toString()); +} + +void HotkeysPage::clearShortcut(QKeySequenceEdit *editor) +{ + editor->clear(); + saveSettings(); + emit sig_hotkeysChanged(); +} + +void HotkeysPage::resetToDefaults() +{ + // Reset to default values defined in configuration.h + auto &hotkeys = setConfig().hotkeys; + + // File operations + hotkeys.fileOpen.set("Ctrl+O"); + hotkeys.fileSave.set("Ctrl+S"); + hotkeys.fileReload.set("Ctrl+R"); + hotkeys.fileQuit.set("Ctrl+Q"); + + // Edit operations + hotkeys.editUndo.set("Ctrl+Z"); + hotkeys.editRedo.set("Ctrl+Y"); + hotkeys.editPreferences.set("Ctrl+P"); + hotkeys.editPreferencesAlt.set("Esc"); + hotkeys.editFindRooms.set("Ctrl+F"); + hotkeys.editRoom.set("Ctrl+E"); + + // View operations (most blank, except Zoom Reset) + hotkeys.viewZoomIn.set(""); + hotkeys.viewZoomOut.set(""); + hotkeys.viewZoomReset.set("Ctrl+0"); + hotkeys.viewLayerUp.set(""); + hotkeys.viewLayerDown.set(""); + hotkeys.viewLayerReset.set(""); + + // View toggles (all blank by default) + hotkeys.viewRadialTransparency.set(""); + hotkeys.viewStatusBar.set(""); + hotkeys.viewScrollBars.set(""); + hotkeys.viewMenuBar.set(""); + hotkeys.viewAlwaysOnTop.set(""); + + // Side panels (only Log has default) + hotkeys.panelLog.set("Ctrl+L"); + hotkeys.panelClient.set(""); + hotkeys.panelGroup.set(""); + hotkeys.panelRoom.set(""); + hotkeys.panelAdventure.set(""); + hotkeys.panelComms.set(""); + hotkeys.panelDescription.set(""); + + // Mouse modes (all blank by default) + hotkeys.modeMoveMap.set(""); + hotkeys.modeRaypick.set(""); + hotkeys.modeSelectRooms.set(""); + hotkeys.modeSelectMarkers.set(""); + hotkeys.modeSelectConnection.set(""); + hotkeys.modeCreateMarker.set(""); + hotkeys.modeCreateRoom.set(""); + hotkeys.modeCreateConnection.set(""); + hotkeys.modeCreateOnewayConnection.set(""); + + // Room operations (only Delete has default) + hotkeys.roomCreate.set(""); + hotkeys.roomMoveUp.set(""); + hotkeys.roomMoveDown.set(""); + hotkeys.roomMergeUp.set(""); + hotkeys.roomMergeDown.set(""); + hotkeys.roomDelete.set("Del"); + hotkeys.roomConnectNeighbors.set(""); + hotkeys.roomMoveToSelected.set(""); + hotkeys.roomUpdateSelected.set(""); + + loadSettings(); + emit sig_hotkeysChanged(); +} + +void HotkeysPage::slot_loadConfig() +{ + loadSettings(); +} diff --git a/src/preferences/hotkeyspage.h b/src/preferences/hotkeyspage.h new file mode 100644 index 000000000..95fb4e74c --- /dev/null +++ b/src/preferences/hotkeyspage.h @@ -0,0 +1,98 @@ +#pragma once +// SPDX-License-Identifier: GPL-2.0-or-later +// Copyright (C) 2025 The MMapper Authors + +#include "../global/macros.h" + +#include + +class QKeySequenceEdit; +class QPushButton; +class QScrollArea; + +class NODISCARD_QOBJECT HotkeysPage final : public QWidget +{ + Q_OBJECT + +public: + explicit HotkeysPage(QWidget *parent); + ~HotkeysPage() final = default; + +private: + void setupUI(); + void connectSignals(); + void loadSettings(); + void saveSettings(); + void resetToDefaults(); + + QWidget *createHotkeyRow(const QString &label, QKeySequenceEdit **editor, QPushButton **clearBtn); + void clearShortcut(QKeySequenceEdit *editor); + + // File operations + QKeySequenceEdit *m_fileOpen = nullptr; + QKeySequenceEdit *m_fileSave = nullptr; + QKeySequenceEdit *m_fileReload = nullptr; + QKeySequenceEdit *m_fileQuit = nullptr; + + // Edit operations + QKeySequenceEdit *m_editUndo = nullptr; + QKeySequenceEdit *m_editRedo = nullptr; + QKeySequenceEdit *m_editPreferences = nullptr; + QKeySequenceEdit *m_editPreferencesAlt = nullptr; + QKeySequenceEdit *m_editFindRooms = nullptr; + QKeySequenceEdit *m_editRoom = nullptr; + + // View operations + QKeySequenceEdit *m_viewZoomIn = nullptr; + QKeySequenceEdit *m_viewZoomOut = nullptr; + QKeySequenceEdit *m_viewZoomReset = nullptr; + QKeySequenceEdit *m_viewLayerUp = nullptr; + QKeySequenceEdit *m_viewLayerDown = nullptr; + QKeySequenceEdit *m_viewLayerReset = nullptr; + + // View toggles + QKeySequenceEdit *m_viewRadialTransparency = nullptr; + QKeySequenceEdit *m_viewStatusBar = nullptr; + QKeySequenceEdit *m_viewScrollBars = nullptr; + QKeySequenceEdit *m_viewMenuBar = nullptr; + QKeySequenceEdit *m_viewAlwaysOnTop = nullptr; + + // Side panels + QKeySequenceEdit *m_panelLog = nullptr; + QKeySequenceEdit *m_panelClient = nullptr; + QKeySequenceEdit *m_panelGroup = nullptr; + QKeySequenceEdit *m_panelRoom = nullptr; + QKeySequenceEdit *m_panelAdventure = nullptr; + QKeySequenceEdit *m_panelComms = nullptr; + QKeySequenceEdit *m_panelDescription = nullptr; + + // Mouse modes + QKeySequenceEdit *m_modeMoveMap = nullptr; + QKeySequenceEdit *m_modeRaypick = nullptr; + QKeySequenceEdit *m_modeSelectRooms = nullptr; + QKeySequenceEdit *m_modeSelectMarkers = nullptr; + QKeySequenceEdit *m_modeSelectConnection = nullptr; + QKeySequenceEdit *m_modeCreateMarker = nullptr; + QKeySequenceEdit *m_modeCreateRoom = nullptr; + QKeySequenceEdit *m_modeCreateConnection = nullptr; + QKeySequenceEdit *m_modeCreateOnewayConnection = nullptr; + + // Room operations + QKeySequenceEdit *m_roomCreate = nullptr; + QKeySequenceEdit *m_roomMoveUp = nullptr; + QKeySequenceEdit *m_roomMoveDown = nullptr; + QKeySequenceEdit *m_roomMergeUp = nullptr; + QKeySequenceEdit *m_roomMergeDown = nullptr; + QKeySequenceEdit *m_roomDelete = nullptr; + QKeySequenceEdit *m_roomConnectNeighbors = nullptr; + QKeySequenceEdit *m_roomMoveToSelected = nullptr; + QKeySequenceEdit *m_roomUpdateSelected = nullptr; + + QPushButton *m_resetButton = nullptr; + +signals: + void sig_hotkeysChanged(); + +public slots: + void slot_loadConfig(); +};