From 06aa9080c311bf3cbcc8e02555c27031d88998a8 Mon Sep 17 00:00:00 2001 From: InnerCircleTFS Date: Tue, 17 Dec 2024 18:27:38 -0300 Subject: [PATCH] feat: keybinds by oen | (limited) Part 1 (#996) --- README.md | 1 + data/images/ui/icon-edit.png | Bin 0 -> 127 bytes data/styles/10-scrollbars.otui | 20 + modules/client_debug_info/debug_info.lua | 10 +- modules/client_entergame/entergame.lua | 10 +- modules/client_options/data_options.lua | 6 + modules/client_options/keybins.lua | 756 ++++++++++++ modules/client_options/options.lua | 81 +- modules/client_options/options.otmod | 2 +- .../styles/controls/key_edit.otui | 66 ++ .../styles/controls/keybinds.otui | 285 +++++ .../styles/controls/preset.otui | 43 + modules/client_terminal/terminal.lua | 9 +- modules/client_topmenu/topmenu.lua | 10 +- modules/corelib/corelib.otmod | 6 +- modules/corelib/keybind.lua | 1036 +++++++++++++++++ modules/corelib/ui/uicombobox.lua | 9 + modules/corelib/ui/uipopupmenu.lua | 6 +- modules/corelib/ui/uitabbar.lua | 4 +- modules/corelib/ui/uitable.lua | 23 +- modules/corelib/ui/uiwidget.lua | 25 + modules/game_battle/battle.lua | 10 +- modules/game_bugreport/bugreport.lua | 10 +- modules/game_console/console.lua | 80 +- modules/game_cooldown/cooldown.otui | 2 + modules/game_cyclopedia/game_cyclopedia.lua | 16 +- modules/game_hotkeys/hotkeys_manager.lua | 10 +- modules/game_interface/gameinterface.lua | 45 +- modules/game_playermount/playermount.lua | 10 +- modules/game_questlog/questlog.lua | 11 + modules/game_ruleviolation/ruleviolation.lua | 4 +- modules/game_shaders/shaders.lua | 12 +- modules/game_skills/skills.lua | 10 +- modules/game_spelllist/spelllist.lua | 8 + modules/game_tasks/tasks.lua | 10 +- modules/game_viplist/viplist.lua | 11 +- modules/gamelib/const.lua | 11 + src/framework/ui/uiboxlayout.h | 4 +- 38 files changed, 2590 insertions(+), 82 deletions(-) create mode 100644 data/images/ui/icon-edit.png create mode 100644 modules/client_options/keybins.lua create mode 100644 modules/client_options/styles/controls/key_edit.otui create mode 100644 modules/client_options/styles/controls/keybinds.otui create mode 100644 modules/client_options/styles/controls/preset.otui create mode 100644 modules/corelib/keybind.lua diff --git a/README.md b/README.md index 71233120f0..27a7425a18 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,7 @@ Beyond of it's flexibility with scripts, otclient comes with tons of other featu - Module Oufit - Placeholder - UIGraph +- keybinds ## Android The Mobile Project The Mobile Project diff --git a/data/images/ui/icon-edit.png b/data/images/ui/icon-edit.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b810a70db15501bbd3c2db82ef3363e755142c GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqZk{fVAr-f3dp-FM81OKu|D7+N z{kv9dugb((OFiZ!FnG?95uD^Q`N*m>A`YR2s{P!}EC!p|KKyCjT)@p(@csPrS35UM ajMXpOoMN9CJI4iRB7>)^pUXO@geCy actionNameLimit then + tooltip = rawText + -- 15 and 8 are length of color codes + text = text:sub(1, actionNameLimit + 15 + 8) .. '...' + end + + local row = panels.keybindsPanel.tablePanel.keybinds:addRow({ { + coloredText = { + text = text, + color = '#c0c0c0' + }, + width = 286 + }, { + style = 'VerticalSeparator' + }, { + style = 'EditableKeybindsTableColumn', + text = primary, + width = 100 + }, { + style = 'VerticalSeparator' + }, { + style = 'EditableKeybindsTableColumn', + text = secondary, + width = 90 + } }) + + row.category = category + row.action = action + + if tooltip then + row:setTooltip(tooltip) + end + + row:getChildByIndex(3).edit.onClick = editKeybindPrimary + row:getChildByIndex(5).edit.onClick = editKeybindSecondary +end + +function clearHotkey(row) + table.insert(changedHotkeys, { + hotkeyId = row.hotkeyId, + remove = true + }) + panels.keybindsPanel.tablePanel.keybinds:removeRow(row) +end + +function editHotkeyKey(text) + keyEditWindow.buttons.cancel.onClick = function() + disconnect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + keyEditWindow:hide() + keyEditWindow:ungrabKeyboard() + show() + end + + keyEditWindow.info:setText(tr( + 'Click \'Ok\' to assign the keybind. Click \'Clear\' to remove the keybind from \'%s\'.', text)) + keyEditWindow.alone:setVisible(false) + + connect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + + keyEditWindow:show() + keyEditWindow:raise() + keyEditWindow:focus() + keyEditWindow:grabKeyboard() + hide() +end + +function editHotkeyPrimary(button) + local column = button:getParent() + local row = column:getParent() + local text = row:getChildByIndex(1):getText() + local hotkeyId = row.hotkeyId + local preset = panels.keybindsPanel.presets.list:getCurrentOption().text + + keyEditWindow:setText(tr('Edit Primary Key for \'%s\'', text)) + keyEditWindow.keyCombo:setText(Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).primary) + + editHotkeyKey(text) + + keyEditWindow.buttons.ok.onClick = function() + local keyCombo = keyEditWindow.keyCombo:getText() + + column:setText(keyEditWindow.keyCombo:getText()) + + local changed = table.findbyfield(changedHotkeys, 'hotkeyId', hotkeyId) + if changed then + changed.primary = keyCombo + if not changed.secondary then + changed.secondary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).secondary + end + changed.editKey = true + else + table.insert(changedHotkeys, { + hotkeyId = hotkeyId, + primary = keyCombo, + secondary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).secondary, + editKey = true + }) + end + + disconnect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + keyEditWindow:hide() + keyEditWindow:ungrabKeyboard() + show() + end + + keyEditWindow.buttons.clear.onClick = function() + column:setText('') + + local changed = table.findbyfield(changedHotkeys, 'hotkeyId', hotkeyId) + if changed then + changed.primary = nil + if not changed.secondary then + changed.secondary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).secondary + end + changed.editKey = true + else + table.insert(changedHotkeys, { + hotkeyId = hotkeyId, + secondary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).secondary, + editKey = true + }) + end + + disconnect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + keyEditWindow:hide() + keyEditWindow:ungrabKeyboard() + show() + end +end + +function editHotkeySecondary(button) + local column = button:getParent() + local row = column:getParent() + local text = row:getChildByIndex(1):getText() + local hotkeyId = row.hotkeyId + local preset = panels.keybindsPanel.presets.list:getCurrentOption().text + + keyEditWindow:setText(tr('Edit Secondary Key for \'%s\'', text)) + keyEditWindow.keyCombo:setText(Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).secondary) + + editHotkeyKey(text) + + keyEditWindow.buttons.ok.onClick = function() + local keyCombo = keyEditWindow.keyCombo:getText() + + column:setText(keyEditWindow.keyCombo:getText()) + + if changedHotkeys[hotkeyId] then + if not changedHotkeys[hotkeyId].primary then + changedHotkeys[hotkeyId].primary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).primary + end + changedHotkeys[hotkeyId].secondary = keyCombo + changedHotkeys[hotkeyId].editKey = true + else + table.insert(changedHotkeys, { + hotkeyId = hotkeyId, + primary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).primary, + secondary = keyCombo, + editKey = true + }) + end + + disconnect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + keyEditWindow:hide() + keyEditWindow:ungrabKeyboard() + show() + end + + keyEditWindow.buttons.clear.onClick = function() + column:setText('') + + if changedHotkeys[hotkeyId] then + if not changedHotkeys[hotkeyId].primary then + changedHotkeys[hotkeyId].primary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).primary + end + changedHotkeys[hotkeyId].secondary = nil + changedHotkeys[hotkeyId].editKey = true + else + table.insert(changedHotkeys, { + hotkeyId = hotkeyId, + primary = Keybind.getHotkeyKeys(hotkeyId, preset, getChatMode()).primary, + editKey = true + }) + end + + disconnect(keyEditWindow, { + onKeyDown = editKeybindKeyDown + }) + keyEditWindow:hide() + keyEditWindow:ungrabKeyboard() + show() + end +end + +function searchActions(field, text, oldText) + if actionSearchEvent then + removeEvent(actionSearchEvent) + end + + actionSearchEvent = scheduleEvent(performeSearchActions, 200) +end + +function performeSearchActions() + local searchText = panels.keybindsPanel.search.field:getText():trim():lower() + + local rows = panels.keybindsPanel.tablePanel.keybinds.dataSpace:getChildren() + if searchText:len() > 0 then + for _, row in ipairs(rows) do + row:hide() + end + + for _, row in ipairs(rows) do + local actionText = row:getChildByIndex(1):getText():lower() + local primaryText = row:getChildByIndex(3):getText():lower() + local secondaryText = row:getChildByIndex(5):getText():lower() + if actionText:find(searchText) or primaryText:find(searchText) or secondaryText:find(searchText) then + row:show() + end + end + else + for _, row in ipairs(rows) do + row:show() + end + end + + removeEvent(actionSearchEvent) + actionSearchEvent = nil +end + +function chatModeChange() + changedHotkeys = {} + changedKeybinds = {} + + panels.keybindsPanel.search.field:clearText() + + updateKeybinds() +end + +function getChatMode() + if chatModeGroup:getSelectedWidget() == panels.keybindsPanel.panel.chatMode.on then + return CHAT_MODE.ON + end + + return CHAT_MODE.OFF +end + +function applyChangedOptions() + local needKeybindsUpdate = false + local needHotkeysUpdate = false + + for key, option in pairs(changedOptions) do + if key == 'resetKeybinds' then + Keybind.resetKeybindsToDefault(option.value, option.chatMode) + needKeybindsUpdate = true + end + end + changedOptions = {} + + for preset, keybinds in pairs(changedKeybinds) do + for index, keybind in pairs(keybinds) do + if keybind.primary then + if Keybind.setPrimaryActionKey(keybind.primary.category, keybind.primary.action, preset, + keybind.primary.keyCombo, getChatMode()) then + needKeybindsUpdate = true + end + elseif keybind.secondary then + if Keybind.setSecondaryActionKey(keybind.secondary.category, keybind.secondary.action, preset, + keybind.secondary.keyCombo, getChatMode()) then + needKeybindsUpdate = true + end + end + end + end + changedKeybinds = {} + + if needKeybindsUpdate then + updateKeybinds() + end + g_settings.save() +end + +function presetOption(widget, key, value, force) + if not controller.ui:isVisible() then + return + end + + changedOptions[key] = { widget = widget, value = value, force = force } + if key == "currentPreset" then + Keybind.selectPreset(value) + panels.keybindsPanel.presets.list:setCurrentOption(value, true) + end +end + +function init_binds() + chatModeGroup = UIRadioGroup.create() + chatModeGroup:addWidget(panels.keybindsPanel.panel.chatMode.on) + chatModeGroup:addWidget(panels.keybindsPanel.panel.chatMode.off) + chatModeGroup.onSelectionChange = chatModeChange + chatModeGroup:selectWidget(panels.keybindsPanel.panel.chatMode.on) + + keyEditWindow = g_ui.displayUI("styles/controls/key_edit") + keyEditWindow:hide() + presetWindow = g_ui.displayUI("styles/controls/preset") + presetWindow:hide() + panels.keybindsPanel.presets.add.onClick = addNewPreset + panels.keybindsPanel.presets.copy.onClick = copyPreset + panels.keybindsPanel.presets.rename.onClick = renamePreset + panels.keybindsPanel.presets.remove.onClick = removePreset + panels.keybindsPanel.buttons.newAction:disable() + panels.keybindsPanel.buttons.newAction.onClick = newHotkeyAction + panels.keybindsPanel.buttons.reset.onClick = resetActions + panels.keybindsPanel.search.field.onTextChange = searchActions + panels.keybindsPanel.search.clear.onClick = function() panels.keybindsPanel.search.field:clearText() end + presetWindow.onEnter = okPresetWindow + presetWindow.onEscape = cancelPresetWindow + presetWindow.buttons.ok.onClick = okPresetWindow + presetWindow.buttons.cancel.onClick = cancelPresetWindow +end + +function terminate_binds() + if presetWindow then + presetWindow:destroy() + presetWindow = nil + end + + if chatModeGroup then + chatModeGroup:destroy() + chatModeGroup = nil + end + + if keyEditWindow then + if keyEditWindow:isVisible() then + keyEditWindow:ungrabKeyboard() + disconnect(keyEditWindow, { onKeyDown = editKeybindKeyDown }) + end + keyEditWindow:destroy() + keyEditWindow = nil + end + + actionSearchEvent = nil +end + +function listKeybindsComboBox(value) + local widget = panels.keybindsPanel.presets.list + presetOption(widget, 'currentPreset', value, false) + changedKeybinds = {} + changedHotkeys = {} + applyChangedOptions() + updateKeybinds() +end + +function debug() + local currentOptionText = Keybind.currentPreset + local chatMode = Keybind.chatMode + local chatModeText = (chatMode == 1) and "Chat mode ON" or (chatMode == 2) and "Chat mode OFF" or "Unknown chat mode" + print(string.format("The current configuration is: %s, and the mode is: %s", currentOptionText, chatModeText)) +end diff --git a/modules/client_options/options.lua b/modules/client_options/options.lua index e9cbc62442..d89f726e7f 100644 --- a/modules/client_options/options.lua +++ b/modules/client_options/options.lua @@ -1,6 +1,5 @@ local options = dofile("data_options") - -local panels = { +panels = { generalPanel = nil, graphicsPanel = nil, soundPanel = nil, @@ -9,24 +8,19 @@ local panels = { interfaceHUD = nil, interface = nil, misc = nil, - miscHelp = nil + miscHelp = nil, + keybindsPanel = nil } -- LuaFormatter off local buttons = {{ text = "Controls", icon = "/images/icons/icon_controls", - open = "generalPanel" - --[[ subCategories = {{ + open = "generalPanel", + subCategories = {{ text = "General Hotkeys", - open = "generalPanel" - }, { - text = "Action Bar Hotkeys", - open = "Action_Bar_Hotkeys" - }, { - text = "Custom Hotkeys", - open = "Custom_Hotkeys" - }} ]] + open = "keybindsPanel" + }} }, { text = "Interface", icon = "/images/icons/icon_interface", @@ -106,6 +100,8 @@ local function setupComboBox() local antialiasingModeCombobox = panels.graphicsPanel:recursiveGetChildById('antialiasingMode') local floorViewModeCombobox = panels.graphicsEffectsPanel:recursiveGetChildById('floorViewMode') local framesRarityCombobox = panels.interface:recursiveGetChildById('frames') + local vocationPresetsCombobox = panels.keybindsPanel:recursiveGetChildById('list') + local listKeybindsPanel = panels.keybindsPanel:recursiveGetChildById('list') for k, v in pairs({ { 'Disabled', 'disabled' }, { 'Default', 'default' }, { 'Full', 'full' } }) do crosshairCombo:addOption(v[1], v[2]) @@ -153,6 +149,13 @@ local function setupComboBox() end end + for _, preset in ipairs(Keybind.presets) do + listKeybindsPanel:addOption(preset) + end + listKeybindsPanel.onOptionChange = function(comboBox, option) + setOption('listKeybindsPanel', option) + end + panels.keybindsPanel.presets.list:setCurrentOption(Keybind.currentPreset) end local function setup() @@ -177,8 +180,6 @@ end controller = Controller:new() controller:setUI('options') -controller:bindKeyDown('Ctrl+Shift+F', function() toggleOption('fullscreen') end) -controller:bindKeyDown('Ctrl+N', toggleDisplays) function controller:onInit() for k, obj in pairs(options) do @@ -199,6 +200,7 @@ function controller:onInit() '/images/topbuttons/logout', toggle) panels.generalPanel = g_ui.loadUI('styles/controls/general',controller.ui.optionsTabContent) + panels.keybindsPanel = g_ui.loadUI('styles/controls/keybinds',controller.ui.optionsTabContent) panels.graphicsPanel = g_ui.loadUI('styles/graphics/graphics',controller.ui.optionsTabContent) panels.graphicsEffectsPanel = g_ui.loadUI('styles/graphics/effects',controller.ui.optionsTabContent) @@ -216,6 +218,39 @@ function controller:onInit() configureCharacterCategories() addEvent(setup) + init_binds() + + Keybind.new("UI", "Toggle Fullscreen", "Ctrl+Shift+F", "") + Keybind.bind("UI", "Toggle Fullscreen", { + { + type = KEY_DOWN, + callback = function() toggleOption('fullscreen') end, + } + }) + Keybind.new("UI", "Show/hide FPS / lag indicator", "", "") + Keybind.bind("UI", "Show/hide FPS / lag indicator", {{ + type = KEY_DOWN, + callback = function() + toggleOption('showPing') + toggleOption('showFps') + end + }}) + + Keybind.new("UI", "Show/hide Creature Names and Bars", "Ctrl+N", "") + Keybind.bind("UI", "Show/hide Creature Names and Bars", { + { + type = KEY_DOWN, + callback = toggleDisplays, + } + }) + + Keybind.new("Sound", "Mute/unmute", "", "") + Keybind.bind("Sound", "Mute/unmute", { + { + type = KEY_DOWN, + callback = function() toggleOption('enableAudio') end, + } + }) end function controller:onTerminate() @@ -224,6 +259,21 @@ function controller:onTerminate() panels = {} extraWidgets = {} buttons = {} + Keybind.delete("UI", "Toggle Full Screen") + Keybind.delete("UI", "Show/hide Creature Names and Bars") + Keybind.delete("Sound", "Mute/unmute") + + terminate_binds() +end + +function controller:onGameStart() + if g_settings.getBoolean("autoSwitchPreset") then + local name = g_game.getCharacterName() + if Keybind.selectPreset(name) then + panels.keybindsPanel.presets.list:setCurrentOption(name, true) + updateKeybinds() + end + end end function setOption(key, value, force) @@ -299,6 +349,7 @@ function toggle() end end show() + updateKeybinds() end function addTab(name, panel, icon) diff --git a/modules/client_options/options.otmod b/modules/client_options/options.otmod index 7671184cab..2b44193a17 100644 --- a/modules/client_options/options.otmod +++ b/modules/client_options/options.otmod @@ -4,6 +4,6 @@ Module author: edubart, BeniS website: https://github.com/edubart/otclient sandboxed: true - scripts: [ options ] + scripts: [ options, keybins ] @onLoad: controller:init() @onUnload: controller:terminate() diff --git a/modules/client_options/styles/controls/key_edit.otui b/modules/client_options/styles/controls/key_edit.otui new file mode 100644 index 0000000000..aeac99932a --- /dev/null +++ b/modules/client_options/styles/controls/key_edit.otui @@ -0,0 +1,66 @@ +MainWindow + size: 400 225 + layout: + type: verticalBox + spacing: 10 + fit-children: true + + Label + font: verdana-11px-monochrome + text-align: center + color: #c0c0c0 + text: Mode: "Chat On" + + FlatPanel + id: keyCombo + height: 33 + font: verdana-11px-monochrome + text-align: center + color: #c0c0c0 + + Label + id: info + font: verdana-11px-monochrome + color: #c0c0c0 + text-wrap: true + text-auto-resize: true + + Label + id: alone + font: verdana-11px-monochrome + color: #c0c0c0 + !text: tr("This action must be bound to a single key.") + visible: false + + Label + id: used + font: verdana-11px-monochrome + text-align: center + color: #f75f5f + !text: tr("This hotkey is already in use.") + visible: false + + HorizontalSeparator + + Panel + id: buttons + height: 20 + layout: + type: horizontalBox + align-right: true + spacing: -5 + + SmallButton + id: ok + width: 40 + !text: tr("Ok") + + SmallButton + id: clear + width: 45 + !text: tr("Clear") + SmallButton + id: cancel + width: 45 + !text: tr("Cancel") + diff --git a/modules/client_options/styles/controls/keybinds.otui b/modules/client_options/styles/controls/keybinds.otui new file mode 100644 index 0000000000..98ca8999c4 --- /dev/null +++ b/modules/client_options/styles/controls/keybinds.otui @@ -0,0 +1,285 @@ +RadioBox < CheckBox + image-source: /images/ui/outfits/checkbox_round + +OptionContainer < FlatPanel + height: 22 + padding: 3 +KeybindsTableRow < UITableRow + focusable: true + height: 20 + background-color: alpha + even-background-color: alpha + odd-background-color: #484848 + text-align: left + @onFocusChange: for _, child in ipairs(self:getChildren()) do child:setChecked(self:isFocused()) end + layout: horizontalBox + + $focus: + background-color: #585858 + +KeybindsTableColumn < Label + color: #c0c0c0 + text-align: left + focusable: false + text-offset: 2 0 + font: verdana-11px-monochrome + +EditableKeybindsTableColumn < Label + color: #c0c0c0 + text-align: left + focusable: false + text-offset: 2 0 + font: verdana-11px-monochrome + @onCheckChange: self.edit:setVisible(self:isChecked()) + + Button + id: edit + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + size: 13 13 + margin-right: 2 + icon: /images/ui/icon-edit + visible: false + +EditableHotkeysTableColumn < Label + color: #c0c0c0 + text-align: left + focusable: false + text-offset: 18 0 + font: verdana-11px-monochrome + @onCheckChange: self.edit:setVisible(self:isChecked()) + + UIItem + id: item + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + size: 16 16 + virtual: true + phantom: true + + Button + id: edit + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + size: 13 13 + margin-right: 2 + icon: /images/options/icon-edit + visible: false + +KeybindsTableHeaderRow < TableHeaderRow + padding: 0 + height: 18 + +KeybindsTableHeaderColumn < UITableHeaderColumn + font: verdana-11px-monochrome + text-offset: 2 0 + text-align: left + height: 14 + color: #c0c0c0 + background: #363636 + +UIWidget + anchors.fill: parent + visible: false + Panel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + id: presets + height: 20 + layout: + type: horizontalBox + spacing: 5 + + QtComboBox + id: list + width: 320 + + SmallButton + id: add + width: 30 + !text: tr("Add") + @onClick: addNewPreset() + + SmallButton + id: copy + width: 35 + !text: tr("Copy") + @onClick: copyPreset() + + SmallButton + id: rename + width: 45 + !text: tr("Rename") + @onClick : renamePreset() + + SmallButton + id: remove + width: 45 + !text: tr("Remove") + @onClick : removePreset() + + SmallReversedQtPanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 22 + margin-top: 7 + + OptionCheckBoxMarked + id: autoSwitchPreset + !text: tr('Auto-Switch Hotkey Preset') + !tooltip: tr('If you have named your hotkey presets the same like your\ncharacters and have checked this option, the preset with the same\nname as the corresponding character will be activated upon login\nautomatically.') + + SmallReversedQtPanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 22 + margin-top: 7 + id: panel + OptionContainer + anchors.left: parent.left + anchors.right: parent.right + image-source: "" + !tooltip: tr('There are two chat modes for each hotkey preset: Chat On and Chat\n Off.You can find a small button that tells you which chat mode is\n currently active on the right side of the entry line of the chat console.Press this button to switch between the two chat modes.\nOf course, you can also set a hotkey to toggle between Chat On and Off.\n- Chat On\nIn this mode, you will be able to write text in the entry line of the chat console.\nHowever, if you have assigned an action to a letter key, the action will be triggered.\n- Chat On*\nThis is the temporary Chat On mode.\nIn the default hotkey preset, the Return key switches between Chat Off and this mode.\nIf the chat mode is off and you press this hotkey, it temporarily activates the chat mode so that you can write one message in the console.\nAs soon as you press Return to send the message, your chat mode will be set to Chat Off again so that you can continue walking with WASD, for example.\n- Chat Off\nIn this mode, you will not be able to write any text in the entry line of the console, but you can use your predefined hotkeys to walk or cast spells, for example.\nYou can assign functions to all keys of your keyboard, e.g. walk with WASD.') + + id: chatMode + $!first: + margin-left:40 + layout: + type: horizontalBox + spacing: 127 + + RadioBox + id: on + text-horizontal-auto-resize: true + !text: tr("Chat Mode On") + margin-bottom: -5 + text-offset: 20 -3 + RadioBox + id: off + text-horizontal-auto-resize: true + !text: tr("Chat Mode Off") + margin-bottom: -5 + text-offset: 20 -3 + UIWidget + id: toolTipWidget + image-source: /images/icons/show_gui_help_grey + size: 12 12 + anchors.right: parent.right + margin-right: 3 + !tooltip: tr('There are two chat modes for each hotkey preset: Chat On and Chat \nOff. You can find a small button that tells you which chat mode is \ncurrently active on the right side of the entry line of the chat \nconsole. Press this button to switch between the two chat modes. Of \ncourse, you can also set a hotkey to toggle between Chat On and Off.\n\tChat On\n\t\tIn this mode, you will be able to write text in the entry line of\n the chat console. However, if you have assigned an action to \na letter key, the action will be \t\ttriggered.\n\n\n\tChat On\n\t\tThis is the temporary Chat On mode. In the default hotkey \npreset, the Return key switches between Chat Off and this\n mode. If the chat mode is off and you press \t\tthis hotkey, it \ntemporarily activates the chat mode so that you can write \none message in the console. As soon as you press Return to \nsend the message, your chat \t\tmode will be set to \nChat Off again so that you can continue walking with WASD, for\n example.\n\n\tChat Off\n\t\tIn this mode, you will not be able to write any text in the entry line of the\n console, but you can use your predefined\n hotkeys to walk or cast spells, for \t\texample. You can assign\n functions to all keys of your keyboard, e.g. walk with WASD') + + Panel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + id: search + height: 20 + margin-top: 5 + layout: + type: horizontalBox + spacing: 6 + + Label + text-offset: 0 3 + font: verdana-11px-monochrome + !text: tr('Type to search for a hotkey:') + + TextEdit + id: field + width: 300 + @onTextChange: searchActions() + + UIButton + id: clear + image-source: /images/ui/button-clear-18x18-up + image-clip: 0 0 20 20 + width: 20 + + $pressed: + image-source: /images/ui/button-clear-18x18-down + @onClick : self:getParent().field:clearText() + + Panel + id: tablePanel + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + height: 305 + margin-top: 5 + image-source: /images/game/actionbar/1pixel-down-frame + image-border: 1 + background: #404040 + + Table + id: keybinds + anchors.fill: parent + margin: 1 + table-data: keybindsData + row-style: KeybindsTableRow + column-style: KeybindsTableColumn + header-row-style: KeybindsTableHeaderRow + header-column-style: KeybindsTableHeaderColumn + + KeybindsTableHeaderRow + id: header + + KeybindsTableHeaderColumn + !text: tr("Action") + width: 286 + + VerticalSeparator + + KeybindsTableHeaderColumn + !text: tr("Primary Key") + width: 100 + + VerticalSeparator + + KeybindsTableHeaderColumn + !text: tr("Secondary Key") + width: 100 + + HorizontalSeparator + + TableData + id: keybindsData + anchors.fill: keybinds + margin-top: 20 + padding-bottom: 1 + vertical-scrollbar: scrollBar + + VerticalQtScrollBar + id: scrollBar + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + step: 16 + pixels-scroll: true + + Panel + id: buttons + height: 20 + anchors.left: parent.left + anchors.right: parent.right + anchors.top: prev.bottom + + SmallButton + id: newAction + anchors.top: parent.top + anchors.left: parent.left + width: 75 + !text: tr('New Action') + visible: false + @onClick : newHotkeyAction() + + SmallButton + id: reset + anchors.top: parent.top + anchors.right: parent.right + width: 45 + margin-top: 8 + !text: tr('Reset') + @onClick : resetActions() diff --git a/modules/client_options/styles/controls/preset.otui b/modules/client_options/styles/controls/preset.otui new file mode 100644 index 0000000000..9b3d4ab223 --- /dev/null +++ b/modules/client_options/styles/controls/preset.otui @@ -0,0 +1,43 @@ +MainWindow + width: 360 + text: Add hotkey preset + layout: + type: verticalBox + fit-children: true + + Label + id: info + font: verdana-11px-monochrome + color: #c0c0c0 + text: Enter a name for the new preset: + + TextEdit + id: field + margin-top: 3 + height: 16 + padding: 1 4 + font: verdana-11px-monochrome + color: #c0c0c0 + + HorizontalSeparator + margin-top: 10 + + Panel + id: buttons + margin-top: 10 + height: 20 + layout: + type: horizontalBox + align-right: true + spacing: -8 + + SmallButton + id: ok + width: 40 + !text: tr("Ok") + + SmallButton + id: cancel + width: 45 + !text: tr("Cancel") + diff --git a/modules/client_terminal/terminal.lua b/modules/client_terminal/terminal.lua index 97cb13ecc4..2bc613e1ff 100644 --- a/modules/client_terminal/terminal.lua +++ b/modules/client_terminal/terminal.lua @@ -150,7 +150,12 @@ function init() terminalButton = modules.client_topmenu.addTopRightToggleButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle) - g_keyboard.bindKeyDown('Ctrl+T', toggle) + Keybind.new("Misc.", "Toggle Terminal", "Ctrl+T", "") + Keybind.bind("Misc.", "Toggle Terminal", {{ + type = KEY_DOWN, + callback = toggle + }}) + commandHistory = g_settings.getList('terminal-history') @@ -214,7 +219,7 @@ function terminate() } g_settings.setNode('terminal-window', settings) - g_keyboard.unbindKeyDown('Ctrl+T') + Keybind.delete("Misc.", "Toggle Terminal") g_logger.setOnLog(nil) terminalWindow:destroy() terminalButton:destroy() diff --git a/modules/client_topmenu/topmenu.lua b/modules/client_topmenu/topmenu.lua index 2c5dcd175d..40dab3f738 100644 --- a/modules/client_topmenu/topmenu.lua +++ b/modules/client_topmenu/topmenu.lua @@ -92,7 +92,13 @@ function init() topLeftYoutubeLink = topMenu:recursiveGetChildById('youtubeIcon') topLeftDiscordLink = topMenu:recursiveGetChildById('discordIcon') - g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle) + Keybind.new("UI", "Toggle Top Menu", "Ctrl+Shift+T", "") + Keybind.bind("UI", "Toggle Top Menu", { + { + type = KEY_DOWN, + callback = toggle, + } + }) if Services.websites then managerAccountsButton = modules.client_topmenu.addTopRightRegularButton('hotkeysButton', tr('Manage Account'), nil, openManagerAccounts) @@ -121,7 +127,7 @@ function terminate() managerAccountsButton:destroy() managerAccountsButton = nil end - + Keybind.delete("UI", "Toggle Top Menu") end function hide() diff --git a/modules/corelib/corelib.otmod b/modules/corelib/corelib.otmod index 80c3359b77..7c665fb4f5 100644 --- a/modules/corelib/corelib.otmod +++ b/modules/corelib/corelib.otmod @@ -17,6 +17,7 @@ Module dofile 'globals' dofile 'config' dofile 'settings' + dofile 'keybind' dofile 'keyboard' dofile 'mouse' dofile 'net' @@ -28,4 +29,7 @@ Module dofile 'outputmessage' dofile 'json' - dofile 'http' \ No newline at end of file + dofile 'http' + Keybind.init() + + @onUnload: Keybind.terminate() diff --git a/modules/corelib/keybind.lua b/modules/corelib/keybind.lua new file mode 100644 index 0000000000..a61cfdf553 --- /dev/null +++ b/modules/corelib/keybind.lua @@ -0,0 +1,1036 @@ + +CHAT_MODE = { + ON = 1, + OFF = 2 +} + +Keybind = { + presets = {}, + presetToIndex = {}, + currentPreset = nil, + configs = { + keybinds = {}, + hotkeys = {} + }, + defaultKeys = { + [CHAT_MODE.ON] = {}, + [CHAT_MODE.OFF] = {} + }, + defaultKeybinds = {}, + hotkeys = { + [CHAT_MODE.ON] = {}, + [CHAT_MODE.OFF] = {} + }, + chatMode = CHAT_MODE.ON, + + reservedKeys = { + ["Up"] = true, + ["Down"] = true, + ["Left"] = true, + ["Right"] = true + } +} + +KEY_UP = 1 +KEY_DOWN = 2 +KEY_PRESS = 3 + +HOTKEY_ACTION = { + USE_YOURSELF = 1, + USE_CROSSHAIR = 2, + USE_TARGET = 3, + EQUIP = 4, + USE = 5, + TEXT = 6, + TEXT_AUTO = 7, + SPELL = 8 +} + +function Keybind.init() + connect(g_game, { onGameStart = Keybind.online, onGameEnd = Keybind.offline }) + + Keybind.presets = g_settings.getList("controls-presets") + + if #Keybind.presets == 0 then + Keybind.presets = { "Druid", "Knight", "Paladin", "Sorcerer" } + Keybind.currentPreset = "Druid" + else + Keybind.currentPreset = g_settings.getValue("controls-preset-current") + end + + for index, preset in ipairs(Keybind.presets) do + Keybind.presetToIndex[preset] = index + end + + if not g_resources.directoryExists("/controls") then + g_resources.makeDir("/controls") + end + + if not g_resources.directoryExists("/controls/keybinds") then + g_resources.makeDir("/controls/keybinds") + end + + if not g_resources.directoryExists("/controls/hotkeys") then + g_resources.makeDir("/controls/hotkeys") + end + + for _, preset in ipairs(Keybind.presets) do + Keybind.configs.keybinds[preset] = g_configs.create("/controls/keybinds/" .. preset .. ".otml") + Keybind.configs.hotkeys[preset] = g_configs.create("/controls/hotkeys/" .. preset .. ".otml") + end + + for preset, config in pairs(Keybind.configs.hotkeys) do + for chatMode = CHAT_MODE.ON, CHAT_MODE.OFF do + Keybind.hotkeys[chatMode][preset] = {} + local hotkeyId = 1 + local hotkeys = config:getNode(chatMode) + + if hotkeys then + local hotkey = hotkeys[tostring(hotkeyId)] + while hotkey do + if hotkey.data.parameter then + hotkey.data.parameter = "\"" .. hotkey.data.parameter .. "\"" -- forcing quotes cause OTML is not saving them, just wow + end + + table.insert(Keybind.hotkeys[chatMode][preset], hotkey) + hotkeyId = hotkeyId + 1 + + hotkey = hotkeys[tostring(hotkeyId)] + end + end + end + end +end + +function Keybind.terminate() + disconnect(g_game, { onGameStart = Keybind.online, onGameEnd = Keybind.offline }) + + for _, preset in ipairs(Keybind.presets) do + Keybind.configs.keybinds[preset]:save() + Keybind.configs.hotkeys[preset]:save() + end + + g_settings.setList("controls-presets", Keybind.presets) + g_settings.setValue("controls-preset-current", Keybind.currentPreset) + g_settings.save() +end + +function Keybind.online() + for _, hotkey in ipairs(Keybind.hotkeys[Keybind.chatMode][Keybind.currentPreset]) do + Keybind.bindHotkey(hotkey.hotkeyId, Keybind.chatMode) + end +end + +function Keybind.offline() + for _, hotkey in ipairs(Keybind.hotkeys[Keybind.chatMode][Keybind.currentPreset]) do + Keybind.unbindHotkey(hotkey.hotkeyId, Keybind.chatMode) + end +end + +function Keybind.new(category, action, primary, secondary, alone) + local index = category .. '_' .. action + if Keybind.defaultKeybinds[index] then + pwarning(string.format("Keybind for [%s: %s] is already in use", category, action)) + return + end + + local keys = {} + if type(primary) == "string" then + keys[CHAT_MODE.ON] = { primary = primary } + keys[CHAT_MODE.OFF] = { primary = primary } + else + keys[CHAT_MODE.ON] = { primary = primary[CHAT_MODE.ON] } + keys[CHAT_MODE.OFF] = { primary = primary[CHAT_MODE.OFF] } + end + + if type(secondary) == "string" then + keys[CHAT_MODE.ON].secondary = secondary + keys[CHAT_MODE.OFF].secondary = secondary + else + keys[CHAT_MODE.ON].secondary = secondary[CHAT_MODE.ON] + keys[CHAT_MODE.OFF].secondary = secondary[CHAT_MODE.OFF] + end + + keys[CHAT_MODE.ON].primary = retranslateKeyComboDesc(keys[CHAT_MODE.ON].primary) + + if keys[CHAT_MODE.ON].secondary then + keys[CHAT_MODE.ON].secondary = retranslateKeyComboDesc(keys[CHAT_MODE.ON].secondary) + end + + keys[CHAT_MODE.OFF].primary = retranslateKeyComboDesc(keys[CHAT_MODE.OFF].primary) + + if keys[CHAT_MODE.OFF].secondary then + keys[CHAT_MODE.OFF].secondary = retranslateKeyComboDesc(keys[CHAT_MODE.OFF].secondary) + end + + if Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].primary] then + local primaryIndex = Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].primary] + local primaryKeybind = Keybind.defaultKeybinds[primaryIndex] + perror(string.format("Default primary key (Chat Mode On) assigned to [%s: %s] is already in use by [%s: %s]", + category, action, primaryKeybind.category, primaryKeybind.action)) + return + end + + if Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].primary] then + local primaryIndex = Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].primary] + local primaryKeybind = Keybind.defaultKeybinds[primaryIndex] + perror(string.format("Default primary key (Chat Mode Off) assigned to [%s: %s] is already in use by [%s: %s]", + category, action, primaryKeybind.category, primaryKeybind.action)) + return + end + + if keys[CHAT_MODE.ON].secondary and Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].secondary] then + local secondaryIndex = Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].secondary] + local secondaryKeybind = Keybind.defaultKeybinds[secondaryIndex] + perror(string.format("Default secondary key (Chat Mode On) assigned to [%s: %s] is already in use by [%s: %s]", + category, action, secondaryKeybind.category, secondaryKeybind.action)) + return + end + + if keys[CHAT_MODE.OFF].secondary and Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].secondary] then + local secondaryIndex = Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].secondary] + local secondaryKeybind = Keybind.defaultKeybinds[secondaryIndex] + perror(string.format("Default secondary key (Chat Mode Off) assigned to [%s: %s] is already in use by [%s: %s]", + category, action, secondaryKeybind.category, secondaryKeybind.action)) + return + end + + if keys[CHAT_MODE.ON].primary then + Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].primary] = index + end + + if keys[CHAT_MODE.OFF].primary then + Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].primary] = index + end + + Keybind.defaultKeybinds[index] = { + category = category, + action = action, + keys = keys, + alone = alone + } + + if keys[CHAT_MODE.ON].secondary then + Keybind.defaultKeys[CHAT_MODE.ON][keys[CHAT_MODE.ON].secondary] = index + end + if keys[CHAT_MODE.OFF].secondary then + Keybind.defaultKeys[CHAT_MODE.OFF][keys[CHAT_MODE.OFF].secondary] = index + end +end + +function Keybind.delete(category, action) + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + + if not keybind then + return + end + + Keybind.unbind(category, action) + + local keysOn = keybind.keys[CHAT_MODE.ON] + local keysOff = keybind.keys[CHAT_MODE.OFF] + + local primaryOn = keysOn.primary and tostring(keysOn.primary) or nil + local primaryOff = keysOff.primary and tostring(keysOff.primary) or nil + local secondaryOn = keysOn.secondary and tostring(keysOn.secondary) or nil + local secondaryOff = keysOff.secondary and tostring(keysOff.secondary) or nil + + if primaryOn and primaryOn:len() > 0 then + Keybind.defaultKeys[CHAT_MODE.ON][primaryOn] = nil + end + if secondaryOn and secondaryOn:len() > 0 then + Keybind.defaultKeys[CHAT_MODE.ON][secondaryOn] = nil + end + + if primaryOff and primaryOff:len() > 0 then + Keybind.defaultKeys[CHAT_MODE.OFF][primaryOff] = nil + end + if secondaryOff and secondaryOff:len() > 0 then + Keybind.defaultKeys[CHAT_MODE.OFF][secondaryOff] = nil + end + + Keybind.defaultKeybinds[index] = nil +end + +function Keybind.bind(category, action, callbacks, widget) + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + + if not keybind then + return + end + + keybind.callbacks = callbacks + keybind.widget = widget + + local keys = Keybind.getKeybindKeys(category, action) + + for _, callback in ipairs(keybind.callbacks) do + if callback.type == KEY_UP then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.bindKeyUp(keys.primary, callback.callback, keybind.widget, callback.alone) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.bindKeyUp(keys.secondary, callback.callback, keybind.widget, callback.alone) + end + end + elseif callback.type == KEY_DOWN then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.bindKeyDown(keys.primary, callback.callback, keybind.widget, callback.alone) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.bindKeyDown(keys.secondary, callback.callback, keybind.widget, callback.alone) + end + end + elseif callback.type == KEY_PRESS then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.bindKeyPress(keys.primary, callback.callback, keybind.widget) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.bindKeyPress(keys.secondary, callback.callback, keybind.widget) + end + end + end + end +end + +function Keybind.unbind(category, action) + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + + if not keybind or not keybind.callbacks then + return + end + + local keys = Keybind.getKeybindKeys(category, action) + + for _, callback in ipairs(keybind.callbacks) do + if callback.type == KEY_UP then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.unbindKeyUp(keys.primary, callback.callback, keybind.widget) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.unbindKeyUp(keys.secondary, callback.callback, keybind.widget) + end + end + elseif callback.type == KEY_DOWN then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.unbindKeyDown(keys.primary, callback.callback, keybind.widget) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.unbindKeyDown(keys.secondary, callback.callback, keybind.widget) + end + end + elseif callback.type == KEY_PRESS then + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + g_keyboard.unbindKeyPress(keys.primary, callback.callback, keybind.widget) + end + end + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + g_keyboard.unbindKeyPress(keys.secondary, callback.callback, keybind.widget) + end + end + end + end +end + +function Keybind.newPreset(presetName) + if Keybind.presetToIndex[presetName] then + return + end + + table.insert(Keybind.presets, presetName) + Keybind.presetToIndex[presetName] = #Keybind.presets + + Keybind.configs.keybinds[presetName] = g_configs.create("/controls/keybinds/" .. presetName .. ".otml") + Keybind.configs.hotkeys[presetName] = g_configs.create("/controls/hotkeys/" .. presetName .. ".otml") + + Keybind.hotkeys[CHAT_MODE.ON][presetName] = {} + Keybind.hotkeys[CHAT_MODE.OFF][presetName] = {} + + g_settings.setList("controls-presets", Keybind.presets) + g_settings.save() +end + +function Keybind.copyPreset(fromPreset, toPreset) + if Keybind.presetToIndex[toPreset] then + return false + end + + table.insert(Keybind.presets, toPreset) + Keybind.presetToIndex[toPreset] = #Keybind.presets + + Keybind.configs.keybinds[fromPreset]:save() + Keybind.configs.hotkeys[fromPreset]:save() + + local keybindsConfigPath = Keybind.configs.keybinds[fromPreset]:getFileName() + local keybindsConfigContent = g_resources.readFileContents(keybindsConfigPath) + g_resources.writeFileContents("/controls/keybinds/" .. toPreset .. ".otml", keybindsConfigContent) + Keybind.configs.keybinds[toPreset] = g_configs.create("/controls/keybinds/" .. toPreset .. ".otml") + + local hotkeysConfigPath = Keybind.configs.hotkeys[fromPreset]:getFileName() + local hotkeysConfigContent = g_resources.readFileContents(hotkeysConfigPath) + g_resources.writeFileContents("/controls/hotkeys/" .. toPreset .. ".otml", hotkeysConfigContent) + Keybind.configs.hotkeys[toPreset] = g_configs.create("/controls/hotkeys/" .. toPreset .. ".otml") + + for chatMode = CHAT_MODE.ON, CHAT_MODE.OFF do + Keybind.hotkeys[chatMode][toPreset] = {} + + local hotkeyId = 1 + local hotkeys = Keybind.configs.hotkeys[toPreset]:getNode(chatMode) + + if hotkeys then + local hotkey = hotkeys[tostring(hotkeyId)] + while hotkey do + if hotkey.data.parameter then + hotkey.data.parameter = "\"" .. hotkey.data.parameter .. "\"" -- forcing quotes cause OTML is not saving them, just wow + end + + table.insert(Keybind.hotkeys[chatMode][toPreset], hotkey) + hotkeyId = hotkeyId + 1 + + hotkey = hotkeys[tostring(hotkeyId)] + end + end + end + + g_settings.setList("controls-presets", Keybind.presets) + g_settings.save() + + return true +end + +function Keybind.renamePreset(oldPresetName, newPresetName) + if Keybind.currentPreset == oldPresetName then + Keybind.currentPreset = newPresetName + end + + local index = Keybind.presetToIndex[oldPresetName] + Keybind.presetToIndex[oldPresetName] = nil + Keybind.presetToIndex[newPresetName] = index + Keybind.presets[index] = newPresetName + + local keybindsConfigPath = Keybind.configs.keybinds[oldPresetName]:getFileName() + Keybind.configs.keybinds[oldPresetName]:save() + Keybind.configs.keybinds[oldPresetName] = nil + + local keybindsConfigContent = g_resources.readFileContents(keybindsConfigPath) + g_resources.deleteFile(keybindsConfigPath) + g_resources.writeFileContents("/controls/keybinds/" .. newPresetName .. ".otml", keybindsConfigContent) + Keybind.configs.keybinds[newPresetName] = g_configs.create("/controls/keybinds/" .. newPresetName .. ".otml") + + local hotkeysConfigPath = Keybind.configs.hotkeys[oldPresetName]:getFileName() + Keybind.configs.hotkeys[oldPresetName]:save() + Keybind.configs.hotkeys[oldPresetName] = nil + + local hotkeysConfigContent = g_resources.readFileContents(hotkeysConfigPath) + g_resources.deleteFile(hotkeysConfigPath) + g_resources.writeFileContents("/controls/hotkeys/" .. newPresetName .. ".otml", hotkeysConfigContent) + Keybind.configs.hotkeys[newPresetName] = g_configs.create("/controls/hotkeys/" .. newPresetName .. ".otml") + + Keybind.hotkeys[CHAT_MODE.ON][newPresetName] = Keybind.hotkeys[CHAT_MODE.ON][oldPresetName] + Keybind.hotkeys[CHAT_MODE.OFF][newPresetName] = Keybind.hotkeys[CHAT_MODE.OFF][oldPresetName] + + g_settings.setList("controls-presets", Keybind.presets) + g_settings.save() +end + +function Keybind.removePreset(presetName) + if #Keybind.presets == 1 then + return false + end + + table.remove(Keybind.presets, Keybind.presetToIndex[presetName]) + Keybind.presetToIndex[presetName] = nil + + Keybind.configs.keybinds[presetName] = nil + g_configs.unload("/controls/keybinds/" .. presetName .. ".otml") + g_resources.deleteFile("/controls/keybinds/" .. presetName .. ".otml") + + Keybind.configs.hotkeys[presetName] = nil + g_configs.unload("/controls/hotkeys/" .. presetName .. ".otml") + g_resources.deleteFile("/controls/hotkeys/" .. presetName .. ".otml") + + if Keybind.currentPreset == presetName then + Keybind.currentPreset = Keybind.presets[1] + end + + g_settings.setList("controls-presets", Keybind.presets) + g_settings.save() + + return true +end + +function Keybind.selectPreset(presetName) + if Keybind.currentPreset == presetName then + return false + end + + if not Keybind.presetToIndex[presetName] then + return false + end + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.unbind(keybind.category, keybind.action) + end + end + + for _, hotkey in ipairs(Keybind.hotkeys[Keybind.chatMode][Keybind.currentPreset]) do + Keybind.unbindHotkey(hotkey.hotkeyId, Keybind.chatMode) + end + + Keybind.currentPreset = presetName + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.bind(keybind.category, keybind.action, keybind.callbacks, keybind.widget) + end + end + + for _, hotkey in ipairs(Keybind.hotkeys[Keybind.chatMode][Keybind.currentPreset]) do + Keybind.bindHotkey(hotkey.hotkeyId, Keybind.chatMode) + end + + return true +end + +function Keybind.getAction(category, action) + local index = category .. '_' .. action + return Keybind.defaultKeybinds[index] +end + +function Keybind.setPrimaryActionKey(category, action, preset, keyCombo, chatMode) + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + + local keys = Keybind.configs.keybinds[preset]:getNode(index) + if not keys then + keys = table.recursivecopy(keybind.keys) + else + chatMode = tostring(chatMode) + end + + if keybind.callbacks then + Keybind.unbind(category, action) + end + + if not keys[chatMode] then + keys[chatMode] = { primary = keyCombo, secondary = keybind.keys[tonumber(chatMode)].secondary } + end + + keys[chatMode].primary = keyCombo + + local ret = false + if keys[chatMode].secondary == keyCombo then + keys[chatMode].secondary = nil + ret = true + end + + Keybind.configs.keybinds[preset]:setNode(index, keys) + + if keybind.callbacks then + Keybind.bind(category, action, keybind.callbacks, keybind.widget) + end + + return ret +end + +function Keybind.setSecondaryActionKey(category, action, preset, keyCombo, chatMode) + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + + local keys = Keybind.configs.keybinds[preset]:getNode(index) + if not keys then + keys = table.recursivecopy(keybind.keys) + else + chatMode = tostring(chatMode) + end + + if keybind.callbacks then + Keybind.unbind(category, action) + end + + if not keys[chatMode] then + keys[chatMode] = { primary = keybind.keys[tonumber(chatMode)].primary, secondary = keyCombo } + end + + keys[chatMode].secondary = keyCombo + + local ret = false + if keys[chatMode].primary == keyCombo then + keys[chatMode].primary = nil + ret = true + end + + Keybind.configs.keybinds[preset]:setNode(index, keys) + + if keybind.callbacks then + Keybind.bind(category, action, keybind.callbacks, keybind.widget) + end + + return ret +end + +function Keybind.resetKeybindsToDefault(presetName, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.unbind(keybind.category, keybind.action) + end + end + + for _, keybind in pairs(Keybind.defaultKeybinds) do + local index = keybind.category .. '_' .. keybind.action + Keybind.configs.keybinds[presetName]:setNode(index, keybind.keys) + end + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.bind(keybind.category, keybind.action, keybind.callbacks, keybind.widget) + end + end +end + +function Keybind.getKeybindKeys(category, action, chatMode, preset, forceDefault) + if not chatMode then + chatMode = Keybind.chatMode + end + + local index = category .. '_' .. action + local keybind = Keybind.defaultKeybinds[index] + local keys = Keybind.configs.keybinds[preset or Keybind.currentPreset]:getNode(index) + + if not keys or forceDefault then + keys = { + primary = keybind.keys[chatMode].primary, + secondary = keybind.keys[chatMode].secondary + } + else + keys = keys[chatMode] or keys[tostring(chatMode)] + end + + if not keys then + keys = { + primary = "", + secondary = "" + } + end + + return keys +end + +function Keybind.isKeyComboUsed(keyCombo, category, action, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + if Keybind.reservedKeys[keyCombo] then + return true + end + + if category and action then + local targetKeys = Keybind.getKeybindKeys(category, action, chatMode, Keybind.currentPreset) + + for _, keybind in pairs(Keybind.defaultKeybinds) do + local keys = Keybind.getKeybindKeys(keybind.category, keybind.action, chatMode, Keybind.currentPreset) + if (keys.primary == keyCombo and targetKeys.primary ~= keyCombo) or (keys.secondary == keyCombo and targetKeys.secondary ~= keyCombo) then + return true + end + end + else + for _, keybind in pairs(Keybind.defaultKeybinds) do + local keys = Keybind.getKeybindKeys(keybind.category, keybind.action, chatMode, Keybind.currentPreset) + if keys.primary == keyCombo or keys.secondary == keyCombo then + return true + end + end + + if Keybind.hotkeys[chatMode][Keybind.currentPreset] then + for _, hotkey in ipairs(Keybind.hotkeys[chatMode][Keybind.currentPreset]) do + if hotkey.primary == keyCombo or hotkey.secondary == keyCombo then + return true + end + end + end + end + + return false +end + +function Keybind.newHotkey(action, data, primary, secondary, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + local hotkey = { + action = action, + data = data, + primary = primary or "", + secondary = secondary or "" + } + + if not Keybind.hotkeys[chatMode][Keybind.currentPreset] then + Keybind.hotkeys[chatMode][Keybind.currentPreset] = {} + end + + table.insert(Keybind.hotkeys[chatMode][Keybind.currentPreset], hotkey) + + local hotkeyId = #Keybind.hotkeys[chatMode][Keybind.currentPreset] + hotkey.hotkeyId = hotkeyId + Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(chatMode, Keybind.hotkeys[chatMode][Keybind.currentPreset]) + Keybind.configs.hotkeys[Keybind.currentPreset]:save() + + Keybind.bindHotkey(hotkeyId, chatMode) +end + +function Keybind.removeHotkey(hotkeyId, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + if not Keybind.hotkeys[chatMode][Keybind.currentPreset] then + return + end + + Keybind.unbindHotkey(hotkeyId, chatMode) + + table.remove(Keybind.hotkeys[chatMode][Keybind.currentPreset], hotkeyId) + + Keybind.configs.hotkeys[Keybind.currentPreset]:clear() + + for id, hotkey in ipairs(Keybind.hotkeys[chatMode][Keybind.currentPreset]) do + hotkey.hotkeyId = id + Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(id, hotkey) + end + + Keybind.configs.hotkeys[Keybind.currentPreset]:save() +end + +function Keybind.editHotkey(hotkeyId, action, data, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + Keybind.unbindHotkey(hotkeyId, chatMode) + + local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + hotkey.action = action + hotkey.data = data + Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(chatMode, Keybind.hotkeys[chatMode][Keybind.currentPreset]) + Keybind.configs.hotkeys[Keybind.currentPreset]:save() + + Keybind.bindHotkey(hotkeyId, chatMode) +end + +function Keybind.editHotkeyKeys(hotkeyId, primary, secondary, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + Keybind.unbindHotkey(hotkeyId, chatMode) + + local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + hotkey.primary = primary or "" + hotkey.secondary = secondary or "" + Keybind.configs.hotkeys[Keybind.currentPreset]:setNode(chatMode, Keybind.hotkeys[chatMode][Keybind.currentPreset]) + Keybind.configs.hotkeys[Keybind.currentPreset]:save() + + Keybind.bindHotkey(hotkeyId, chatMode) +end + +function Keybind.removeAllHotkeys(chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + for _, hotkey in ipairs(Keybind.hotkeys[chatMode][Keybind.currentPreset]) do + Keybind.unbindHotkey(hotkey.hotkeyId) + end + + Keybind.hotkeys[chatMode][Keybind.currentPreset] = {} + + Keybind.configs.hotkeys[Keybind.currentPreset]:remove(chatMode) + Keybind.configs.hotkeys[Keybind.currentPreset]:save() +end + +function Keybind.getHotkeyKeys(hotkeyId, preset, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + if not preset then + preset = Keybind.currentPreset + end + + local keys = { primary = "", secondary = "" } + if not Keybind.hotkeys[chatMode][preset] then + return keys + end + + local hotkey = Keybind.hotkeys[chatMode][preset][hotkeyId] + if not hotkey then + return keys + end + + local config = Keybind.configs.hotkeys[preset]:getNode(chatMode) + if not config then + return keys + end + + return config[tostring(hotkeyId)] or keys +end + +function Keybind.hotkeyCallback(hotkeyId, chatMode) + if not chatMode then + chatMode = Keybind.chatMode + end + + local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + + if not hotkey then + return + end + + local action = hotkey.action + local data = hotkey.data + + if action == HOTKEY_ACTION.USE_YOURSELF then + if g_game.getClientVersion() < 780 then + local item = g_game.findPlayerItem(data.itemId, data.subType or -1) + + if item then + g_game.useWith(item, g_game.getLocalPlayer()) + end + else + g_game.useInventoryItemWith(data.itemId, g_game.getLocalPlayer(), data.subType or -1) + end + elseif action == HOTKEY_ACTION.USE_CROSSHAIR then + local item = Item.create(data.itemId) + + if g_game.getClientVersion() < 780 then + item = g_game.findPlayerItem(data.itemId, data.subType or -1) + end + + if item then + modules.game_interface.startUseWith(item, data.subType or -1) + end + elseif action == HOTKEY_ACTION.USE_TARGET then + local attackingCreature = g_game.getAttackingCreature() + if not attackingCreature then + local item = Item.create(data.itemId) + + if g_game.getClientVersion() < 780 then + item = g_game.findPlayerItem(data.itemId, data.subType or -1) + end + + if item then + modules.game_interface.startUseWith(item, data.subType or -1) + end + + return + end + + if attackingCreature:getTile() then + if g_game.getClientVersion() < 780 then + local item = g_game.findPlayerItem(data.itemId, data.subType or -1) + if item then + g_game.useWith(item, attackingCreature, data.subType or -1) + end + else + g_game.useInventoryItemWith(data.itemId, attackingCreature, data.subType or -1) + end + end + elseif action == HOTKEY_ACTION.EQUIP then + if g_game.getClientVersion() >= 910 then + local item = Item.create(data.itemId) + + g_game.equipItem(item) + end + elseif action == HOTKEY_ACTION.USE then + if g_game.getClientVersion() < 780 then + local item = g_game.findPlayerItem(data.itemId, data.subType or -1) + + if item then + g_game.use(item) + end + else + g_game.useInventoryItem(data.itemId) + end + elseif action == HOTKEY_ACTION.TEXT then + if modules.game_interface.isChatVisible() then + modules.game_console.setTextEditText(hotkey.data.text) + end + elseif action == HOTKEY_ACTION.TEXT_AUTO then + if modules.game_interface.isChatVisible() then + modules.game_console.sendMessage(hotkey.data.text) + else + g_game.talk(hotkey.data.text) + end + elseif action == HOTKEY_ACTION.SPELL then + local text = data.words + if data.parameter then + text = text .. " " .. data.parameter + end + + if modules.game_interface.isChatVisible() then + modules.game_console.sendMessage(text) + else + g_game.talk(text) + end + end +end + +function Keybind.bindHotkey(hotkeyId, chatMode) + if not chatMode or chatMode ~= Keybind.chatMode then + return + end + + if not modules.game_interface then + return + end + + local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + + if not hotkey then + return + end + + local keys = Keybind.getHotkeyKeys(hotkeyId, Keybind.currentPreset, chatMode) + local gameRootPanel = modules.game_interface.getRootPanel() + local action = hotkey.action + + hotkey.callback = function() Keybind.hotkeyCallback(hotkeyId, chatMode) end + + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + if action == HOTKEY_ACTION.EQUIP or action == HOTKEY_ACTION.USE or action == HOTKEY_ACTION.TEXT or action == HOTKEY_ACTION.TEXT_AUTO then + g_keyboard.bindKeyDown(keys.primary, hotkey.callback, gameRootPanel) + else + g_keyboard.bindKeyPress(keys.primary, hotkey.callback, gameRootPanel) + end + end + end + + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + if action == HOTKEY_ACTION.EQUIP or action == HOTKEY_ACTION.USE or action == HOTKEY_ACTION.TEXT or action == HOTKEY_ACTION.TEXT_AUTO then + g_keyboard.bindKeyDown(keys.secondary, hotkey.callback, gameRootPanel) + else + g_keyboard.bindKeyPress(keys.secondary, hotkey.callback, gameRootPanel) + end + end + end +end + +function Keybind.unbindHotkey(hotkeyId, chatMode) + if not chatMode or chatMode ~= Keybind.chatMode then + return + end + + if not modules.game_interface then + return + end + + local hotkey = Keybind.hotkeys[chatMode][Keybind.currentPreset][hotkeyId] + + if not hotkey then + return + end + + local keys = Keybind.getHotkeyKeys(hotkeyId, Keybind.currentPreset, chatMode) + local gameRootPanel = modules.game_interface.getRootPanel() + local action = hotkey.action + + if keys.primary then + keys.primary = tostring(keys.primary) + if keys.primary:len() > 0 then + if action == HOTKEY_ACTION.EQUIP or action == HOTKEY_ACTION.USE or action == HOTKEY_ACTION.TEXT or action == HOTKEY_ACTION.TEXT_AUTO then + g_keyboard.unbindKeyDown(keys.primary, hotkey.callback, gameRootPanel) + else + g_keyboard.unbindKeyPress(keys.primary, hotkey.callback, gameRootPanel) + end + end + end + + if keys.secondary then + keys.secondary = tostring(keys.secondary) + if keys.secondary:len() > 0 then + if action == HOTKEY_ACTION.EQUIP or action == HOTKEY_ACTION.USE or action == HOTKEY_ACTION.TEXT or action == HOTKEY_ACTION.TEXT_AUTO then + g_keyboard.unbindKeyDown(keys.secondary, hotkey.callback, gameRootPanel) + else + g_keyboard.unbindKeyPress(keys.secondary, hotkey.callback, gameRootPanel) + end + end + end +end + +function Keybind.setChatMode(chatMode) + if Keybind.chatMode == chatMode then + return + end + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.unbind(keybind.category, keybind.action) + end + end + + for _, hotkey in ipairs(Keybind.hotkeys[Keybind.chatMode][Keybind.currentPreset]) do + Keybind.unbindHotkey(hotkey.hotkeyId, Keybind.chatMode) + end + + if modules.game_walking then + modules.game_walking.unbindTurnKeys() + end + + Keybind.chatMode = chatMode + + for _, keybind in pairs(Keybind.defaultKeybinds) do + if keybind.callbacks then + Keybind.bind(keybind.category, keybind.action, keybind.callbacks, keybind.widget) + end + end + + for _, hotkey in ipairs(Keybind.hotkeys[chatMode][Keybind.currentPreset]) do + Keybind.bindHotkey(hotkey.hotkeyId, chatMode) + end + + if modules.game_walking then + modules.game_walking.bindTurnKeys() + end +end diff --git a/modules/corelib/ui/uicombobox.lua b/modules/corelib/ui/uicombobox.lua index feb856727c..d41dc122c7 100644 --- a/modules/corelib/ui/uicombobox.lua +++ b/modules/corelib/ui/uicombobox.lua @@ -199,3 +199,12 @@ function UIComboBox:HTML_onReadNodes(nodes) return false end + +function UIComboBox:getCurrentIndex() + return self.currentIndex +end + +function UIComboBox:updateCurrentOption(newText) + self.options[self.currentIndex].text = newText + self:setText(newText) +end diff --git a/modules/corelib/ui/uipopupmenu.lua b/modules/corelib/ui/uipopupmenu.lua index dd6e44f896..d02ae72bea 100644 --- a/modules/corelib/ui/uipopupmenu.lua +++ b/modules/corelib/ui/uipopupmenu.lua @@ -62,11 +62,11 @@ function UIPopupMenu:onGeometryChange(newRect, oldRect) self:bindRectToParent() end -function UIPopupMenu:addOption(optionName, optionCallback, shortcut) +function UIPopupMenu:addOption(optionName, optionCallback, shortcut, disabled) local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self) optionWidget.onClick = function(widget) self:destroy() - optionCallback() + optionCallback(self:getPosition()) end optionWidget:setText(optionName) local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15 @@ -77,7 +77,7 @@ function UIPopupMenu:addOption(optionName, optionCallback, shortcut) width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight() end - + optionWidget:setEnabled(not disabled) self:setWidth(math.max(190, math.max(self:getWidth(), width))) end diff --git a/modules/corelib/ui/uitabbar.lua b/modules/corelib/ui/uitabbar.lua index 5a9722321b..5daf117a9c 100644 --- a/modules/corelib/ui/uitabbar.lua +++ b/modules/corelib/ui/uitabbar.lua @@ -48,7 +48,9 @@ function UITabBar:addTab(text, panel, icon) tab.onClick = onTabClick tab.onMouseRelease = onTabMouseRelease tab.onDestroy = function() - tab.tabPanel:destroy() + if not tab.tabPanel:isDestroyed() then + tab.tabPanel:destroy() + end end table.insert(self.tabs, tab) diff --git a/modules/corelib/ui/uitable.lua b/modules/corelib/ui/uitable.lua index a19e101f59..e1bae68fa5 100644 --- a/modules/corelib/ui/uitable.lua +++ b/modules/corelib/ui/uitable.lua @@ -223,7 +223,10 @@ function UITable:addRow(data, height) self.columns[rowId] = {} for colId, column in pairs(data) do - local col = g_ui.createWidget(self.columBaseStyle, row) + local col = g_ui.createWidget(column.style or self.columBaseStyle, row) + if column.id then + col:setId(column.id) + end if column.width then col:setWidth(column.width) else @@ -235,11 +238,29 @@ function UITable:addRow(data, height) if column.text then col:setText(column.text) end + if column.color then + col:setColor(column.color) + end + if column.coloredText then + col:parseColoredText(column.coloredText.text, column.coloredText.color) + end if column.sortvalue then col.sortvalue = column.sortvalue else col.sortvalue = column.text or 0 end + if column.marginTop then + col:setMarginTop(column.marginTop) + end + if column.marginBottom then + col:setMarginBottom(column.marginBottom) + end + if column.comboBox then + for _, comboValue in ipairs(column.comboBox) do + col:addOption(comboValue[1], comboValue[2]) + end + end + table.insert(self.columns[rowId], col) end diff --git a/modules/corelib/ui/uiwidget.lua b/modules/corelib/ui/uiwidget.lua index 85a26dbf53..9e6da7fbe2 100644 --- a/modules/corelib/ui/uiwidget.lua +++ b/modules/corelib/ui/uiwidget.lua @@ -18,3 +18,28 @@ function UIWidget:setMargin(...) self:setMarginLeft(params[4]) end end + +function UIWidget:parseColoredText(text, default_color) + local result = "" + local i = 1 + while i <= #text do + local start, stop = text:find("%[color=.-%]", i) + if start then + result = result .. text:sub(i, start - 1) + local closing_tag_start, closing_tag_stop = text:find("%[/color%]", stop + 1) + if closing_tag_start then + local content = text:sub(stop + 1, closing_tag_start - 1) + local color_start, color_stop = text:find("#%x+", start) + local color = text:sub(color_start, color_stop) or default_color + result = result .. "{" .. content .. ", " .. color .. "}" + i = closing_tag_stop + 1 + else + break + end + else + result = result .. text:sub(i) + break + end + end + self:setColoredText(result) +end diff --git a/modules/game_battle/battle.lua b/modules/game_battle/battle.lua index dda3268f5a..7f9e438c28 100644 --- a/modules/game_battle/battle.lua +++ b/modules/game_battle/battle.lua @@ -69,7 +69,13 @@ function init() -- Initiating the module (load) battleWindow = g_ui.loadUI('battle') -- Binding Ctrl + B shortcut - g_keyboard.bindKeyDown('Ctrl+B', toggle) + Keybind.new("Windows", "Show/hide battle list", "Ctrl+B", "") + Keybind.bind("Windows", "Show/hide battle list", { + { + type = KEY_DOWN, + callback = toggle, + } + }) -- Disabling scrollbar auto hiding local scrollbar = battleWindow:getChildById('miniwindowScrollBar') @@ -1087,7 +1093,7 @@ function terminate() -- Terminating the Module (unload) filterPanel = nil toggleFilterButton = nil - g_keyboard.unbindKeyDown('Ctrl+B') + Keybind.delete("Windows", "Show/hide battle list") disconnect(g_game, { onAttackingCreatureChange = onAttack, diff --git a/modules/game_bugreport/bugreport.lua b/modules/game_bugreport/bugreport.lua index 56b622f296..0cbc0a838d 100644 --- a/modules/game_bugreport/bugreport.lua +++ b/modules/game_bugreport/bugreport.lua @@ -12,11 +12,17 @@ function init() bugTextEdit = bugReportWindow:getChildById('bugTextEdit') - g_keyboard.bindKeyDown(HOTKEY, show) + Keybind.new("Dialogs", "Open Bugreport", HOTKEY, "") + Keybind.bind("Dialogs", "Open Bugreport", { + { + type = KEY_DOWN, + callback = show, + } + }, modules.game_interface.getRootPanel()) end function terminate() - g_keyboard.unbindKeyDown(HOTKEY) + Keybind.delete("Dialogs", "Open Bugreport") bugReportWindow:destroy() end diff --git a/modules/game_console/console.lua b/modules/game_console/console.lua index 3d92211f0e..3b07f6e6a6 100644 --- a/modules/game_console/console.lua +++ b/modules/game_console/console.lua @@ -222,14 +222,8 @@ function init() g_keyboard.bindKeyPress('Shift+Down', function() navigateMessageHistory(-1) end, consolePanel) - g_keyboard.bindKeyPress('Tab', function() - consoleTabBar:selectNextTab() - end, consolePanel) - g_keyboard.bindKeyPress('Shift+Tab', function() - consoleTabBar:selectPrevTab() - end, consolePanel) + g_keyboard.bindKeyDown('Enter', switchChatOnCall, consolePanel) - g_keyboard.bindKeyDown('Enter', sendCurrentMessage, consolePanel) g_keyboard.bindKeyDown('Escape', disableChatOnCall, consolePanel) g_keyboard.bindKeyPress('Ctrl+A', function() consoleTextEdit:clearText() @@ -241,9 +235,52 @@ function init() consoleTabBar.onTabChange = onTabChange -- tibia like hotkeys - g_keyboard.bindKeyDown('Ctrl+O', g_game.requestChannels) - g_keyboard.bindKeyDown('Ctrl+E', removeCurrentTab) - g_keyboard.bindKeyDown('Ctrl+H', openHelp) + local gameRootPanel = modules.game_interface.getRootPanel() + Keybind.new("Chat Channel", "Next Channel", "Tab", "") + Keybind.bind("Chat Channel", "Next Channel", { + { + type = KEY_PRESS, + callback = function() consoleTabBar:selectNextTab() end, + } + }, consolePanel) + + Keybind.new("Chat Channel", "Previous Channel", "Shift+Tab", "") + Keybind.bind("Chat Channel", "Previous Channel", { + { + type = KEY_PRESS, + callback = function() consoleTabBar:selectPrevTab() end, + } + }, consolePanel) + Keybind.new("Chat", "Send current chat line", { [CHAT_MODE.ON] = "Enter", [CHAT_MODE.OFF] = "" }, "") + Keybind.bind("Chat", "Send current chat line", { + { + type = KEY_DOWN, + callback = sendCurrentMessage, + } + }, consolePanel) + Keybind.new("Chat Channel", "Open Channel List", "Ctrl+O", "") + Keybind.bind("Chat Channel", "Open Channel List", { + { + type = KEY_DOWN, + callback = g_game.requestChannels, + } + }, gameRootPanel) + Keybind.new("Chat Channel", "Close Current Channel", "Ctrl+E", "") + + Keybind.bind("Chat Channel", "Close Current Channel", { + { + type = KEY_DOWN, + callback = removeCurrentTab, + } + }, gameRootPanel) + + Keybind.new("Chat Channel", "Open Help Channel", "Ctrl+H", "") + Keybind.bind("Chat Channel", "Open Help Channel", { + { + type = KEY_DOWN, + callback = openHelp, + } + }, consolePanel) -- toggle WASD consoleToggleChat = consolePanel:getChildById('toggleChat') @@ -340,9 +377,11 @@ function switchChat(enabled) if enabled then unbindMovingKeys() consoleToggleChat:setTooltip(tr('Disable chat mode, allow to walk using WASD')) + Keybind.setChatMode(CHAT_MODE.ON) else bindMovingKeys() consoleToggleChat:setTooltip(tr('Enable chat mode')) + Keybind.setChatMode(CHAT_MODE.OFF) end end @@ -401,10 +440,12 @@ function terminate() clear() end - g_keyboard.unbindKeyDown('Ctrl+O') - g_keyboard.unbindKeyDown('Ctrl+E') - g_keyboard.unbindKeyDown('Ctrl+H') - + Keybind.delete("Chat Channel", "Close Current Channel")-- + Keybind.delete("Chat Channel", "Next Channel")-- + Keybind.delete("Chat Channel", "Previous Channel")-- + Keybind.delete("Chat Channel", "Open Channel List")-- + Keybind.delete("Chat Channel", "Open Help Channel")-- + Keybind.delete("Chat", "Send current chat line") saveCommunicationSettings() if channelsWindow then @@ -1994,7 +2035,14 @@ function online() tab.npcChat = true end if g_game.getClientVersion() < 862 then - g_keyboard.bindKeyDown('Ctrl+R', openPlayerReportRuleViolationWindow) + Keybind.new("Dialogs", "Open Rule Violation", "Ctrl+R", "") + local gameRootPanel = modules.game_interface.getRootPanel() + Keybind.bind("Dialogs", "Open Rule Violation", { + { + type = KEY_DOWN, + callback = openPlayerReportRuleViolationWindow, + } + }, gameRootPanel) end -- open last channels local lastChannelsOpen = g_settings.getNode('lastChannelsOpen') @@ -2019,7 +2067,7 @@ end function offline() if g_game.getClientVersion() < 862 then - g_keyboard.unbindKeyDown('Ctrl+R') + Keybind.delete("Dialogs", "Open Rule Violation") end clear() end diff --git a/modules/game_cooldown/cooldown.otui b/modules/game_cooldown/cooldown.otui index 8153669ec8..b4d1086631 100644 --- a/modules/game_cooldown/cooldown.otui +++ b/modules/game_cooldown/cooldown.otui @@ -29,6 +29,8 @@ Panel anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right + focusable: false + phantom: true Panel id:contentsPanel2 anchors.fill: parent diff --git a/modules/game_cyclopedia/game_cyclopedia.lua b/modules/game_cyclopedia/game_cyclopedia.lua index ad1c906491..535085edb0 100644 --- a/modules/game_cyclopedia/game_cyclopedia.lua +++ b/modules/game_cyclopedia/game_cyclopedia.lua @@ -33,7 +33,6 @@ controllerCyclopedia = Controller:new() controllerCyclopedia:setUI('game_cyclopedia') function controllerCyclopedia:onInit() - end function controllerCyclopedia:onGameStart() @@ -168,6 +167,19 @@ function controllerCyclopedia:onGameStart() trackerMiniWindow:setupOnStart() loadFilters() Cyclopedia.BossSlots.UnlockBosses = {} + Keybind.new("Windows", "Show/hide Bosstiary Tracker", "", "") + + Keybind.bind("Windows", "Show/hide Bosstiary Tracker", {{ + type = KEY_DOWN, + callback = Cyclopedia.toggleBosstiaryTracker + }}) + + Keybind.new("Windows", "Show/hide Bestiary Tracker", "", "") + Keybind.bind("Windows", "Show/hide Bestiary Tracker", {{ + type = KEY_DOWN, + callback = Cyclopedia.toggleBestiaryTracker + }}) + end end @@ -179,6 +191,8 @@ function controllerCyclopedia:onGameEnd() end hide() saveFilters() + Keybind.delete("Windows", "Show/hide Bosstiary Tracker") + Keybind.delete("Windows", "Show/hide Bestiary Tracker") end function controllerCyclopedia:onTerminate() diff --git a/modules/game_hotkeys/hotkeys_manager.lua b/modules/game_hotkeys/hotkeys_manager.lua index 941758480c..3bef1c9ef6 100644 --- a/modules/game_hotkeys/hotkeys_manager.lua +++ b/modules/game_hotkeys/hotkeys_manager.lua @@ -64,7 +64,13 @@ local hotkeysWindowButton = nil -- public functions function init() - g_keyboard.bindKeyDown('Ctrl+K', toggle) + Keybind.new("Windows", "Show/hide Hotkeys", "Ctrl+K", "") + Keybind.bind("Windows", "Show/hide Hotkeys", { + { + type = KEY_DOWN, + callback = toggle, + } + }) hotkeysWindow = g_ui.displayUI('hotkeys_manager') hotkeysWindow:setVisible(false) hotkeysWindowButton = modules.client_topmenu.addRightGameToggleButton('hotkeysWindowButton', tr('Hotkeys'), '/images/options/hotkeys', toggle) @@ -128,7 +134,7 @@ function terminate() onGameEnd = offline }) - g_keyboard.unbindKeyDown('Ctrl+K') + Keybind.delete("Windows", "Show/hide Hotkeys") unload() diff --git a/modules/game_interface/gameinterface.lua b/modules/game_interface/gameinterface.lua index 1f87592706..09f30763b1 100644 --- a/modules/game_interface/gameinterface.lua +++ b/modules/game_interface/gameinterface.lua @@ -154,25 +154,41 @@ function bindKeys() bindTurnKey('Ctrl+Numpad2', South) bindTurnKey('Ctrl+Numpad4', West) - g_keyboard.bindKeyPress('Escape', function() - g_game.cancelAttackAndFollow() - end, gameRootPanel) g_keyboard.bindKeyPress('Ctrl+=', function() gameMapPanel:zoomIn() end, gameRootPanel) g_keyboard.bindKeyPress('Ctrl+-', function() gameMapPanel:zoomOut() end, gameRootPanel) - g_keyboard.bindKeyDown('Ctrl+Q', function() - tryLogout(false) - end, gameRootPanel) - g_keyboard.bindKeyDown('Ctrl+L', function() - tryLogout(false) - end, gameRootPanel) - g_keyboard.bindKeyDown('Alt+W', function() - g_map.cleanTexts() - modules.game_textmessage.clearMessages() - end, gameRootPanel) + + Keybind.new("Movement", "Stop All Actions", "Esc", "", true) + Keybind.bind("Movement", "Stop All Actions", { + { + type = KEY_PRESS, + callback = function() + g_game.cancelAttackAndFollow() + end, + } + }, gameRootPanel) + + Keybind.new("Misc", "Logout", "Ctrl+L", "Ctrl+Q") + Keybind.bind("Misc", "Logout", { + { + type = KEY_PRESS, + callback = function() tryLogout(false) end, + } + }, gameRootPanel) + + Keybind.new("UI", "Clear All Texts", "Ctrl+W", "") + Keybind.bind("UI", "Clear All Texts", { + { + type = KEY_DOWN, + callback = function() + g_map.cleanTexts() + modules.game_textmessage.clearMessages() + end, + } + }, gameRootPanel) g_keyboard.bindKeyDown('Ctrl+.', nextViewMode, gameRootPanel) end @@ -251,6 +267,9 @@ function terminate() logoutButton:destroy() gameRootPanel:destroy() + Keybind.delete("Movement", "Stop All Actions") + Keybind.delete("Misc", "Logout") + Keybind.delete("UI", "Clear All Texts") end function onGameStart() diff --git a/modules/game_playermount/playermount.lua b/modules/game_playermount/playermount.lua index d29596ff37..477c040851 100644 --- a/modules/game_playermount/playermount.lua +++ b/modules/game_playermount/playermount.lua @@ -18,13 +18,19 @@ end function online() if g_game.getFeature(GamePlayerMounts) then - g_keyboard.bindKeyDown('Ctrl+R', toggleMount) + Keybind.new("Movement", "Mount/dismount", "Ctrl+R", "") + Keybind.bind("Movement", "Mount/dismount", { + { + type = KEY_DOWN, + callback = toggleMount, + } + }) end end function offline() if g_game.getFeature(GamePlayerMounts) then - g_keyboard.unbindKeyDown('Ctrl+R') + Keybind.delete("Movement", "Mount/dismount") end end diff --git a/modules/game_questlog/questlog.lua b/modules/game_questlog/questlog.lua index bb3138e618..a6dccf23ea 100644 --- a/modules/game_questlog/questlog.lua +++ b/modules/game_questlog/questlog.lua @@ -15,6 +15,16 @@ function init() onQuestLine = onGameQuestLine, onGameEnd = destroyWindows }) + + Keybind.new("Windows", "Show/hide quest Log", "", "") + Keybind.bind("Windows", "Show/hide quest Log", { + { + type = KEY_DOWN, + callback = function() + g_game.requestQuestLog() + end, + } + }) end function terminate() @@ -27,6 +37,7 @@ function terminate() destroyWindows() questLogButton:destroy() questLogButton = nil + Keybind.delete("Windows", "Show/hide quest Log") end function destroyWindows() diff --git a/modules/game_ruleviolation/ruleviolation.lua b/modules/game_ruleviolation/ruleviolation.lua index 61e4609fca..66e7935dc3 100644 --- a/modules/game_ruleviolation/ruleviolation.lua +++ b/modules/game_ruleviolation/ruleviolation.lua @@ -45,7 +45,7 @@ function init() reasonsTextList = ruleViolationWindow:getChildById('reasonList') actionsTextList = ruleViolationWindow:getChildById('actionList') - g_keyboard.bindKeyDown('Ctrl+Y', function() + g_keyboard.bindKeyDown('Ctrl+U', function() show() end) @@ -58,7 +58,7 @@ function terminate() disconnect(g_game, { onGMActions = loadReasons }) - g_keyboard.unbindKeyDown('Ctrl+Y') + g_keyboard.unbindKeyDown('Ctrl+U') ruleViolationWindow:destroy() end diff --git a/modules/game_shaders/shaders.lua b/modules/game_shaders/shaders.lua index 1418b29fca..0b75c66a25 100644 --- a/modules/game_shaders/shaders.lua +++ b/modules/game_shaders/shaders.lua @@ -124,19 +124,23 @@ function ShaderController:onInit() for _, opts in pairs(MOUNT_SHADERS) do registerShader(opts, 'setupMountShader') end + Keybind.new('Windows', 'show/hide Shader Windows', HOTKEY, '') + Keybind.bind('Windows', 'show/hide Shader Windows', { + { + type = KEY_DOWN, + callback = function() ShaderController.ui:setVisible(not ShaderController.ui:isVisible()) end, + } + }) end function ShaderController:onTerminate() g_shaders.clear() + Keybind.delete('Windows', 'show/hide Shader Windows') end function ShaderController:onGameStart() attachShaders() - self:bindKeyDown(HOTKEY, function() - ShaderController.ui:setVisible(not ShaderController.ui:isVisible()) - end) - self:loadHtml('shaders.html', modules.game_interface.getMapPanel()) for _, opts in pairs(MAP_SHADERS) do diff --git a/modules/game_skills/skills.lua b/modules/game_skills/skills.lua index 9363934b7e..7c9294b383 100644 --- a/modules/game_skills/skills.lua +++ b/modules/game_skills/skills.lua @@ -31,7 +31,13 @@ function init() skillsButton:setOn(true) skillsWindow = g_ui.loadUI('skills') - g_keyboard.bindKeyDown('Alt+S', toggle) + Keybind.new("Windows", "Show/hide skills windows", "Alt+S", "") + Keybind.bind("Windows", "Show/hide skills windows", { + { + type = KEY_DOWN, + callback = toggle, + } + }) skillSettings = g_settings.getNode('skills-hide') if not skillSettings then @@ -69,7 +75,7 @@ function terminate() onGameEnd = offline }) - g_keyboard.unbindKeyDown('Alt+S') + Keybind.delete("Windows", "Show/hide skills windows") skillsWindow:destroy() skillsButton:destroy() diff --git a/modules/game_spelllist/spelllist.lua b/modules/game_spelllist/spelllist.lua index 7d9f682a8f..787ed76548 100644 --- a/modules/game_spelllist/spelllist.lua +++ b/modules/game_spelllist/spelllist.lua @@ -176,6 +176,13 @@ function init() if g_game.isOnline() then online() end + Keybind.new("Windows", "Show/hide spell list", "Alt+L", "") + Keybind.bind("Windows", "Show/hide spell list", { + { + type = KEY_DOWN, + callback = toggle, + } + }) end function terminate() @@ -199,6 +206,7 @@ function terminate() vocationRadioGroup:destroy() groupRadioGroup:destroy() premiumRadioGroup:destroy() + Keybind.delete("Windows", "Show/hide spell list") end function initializeSpelllist() diff --git a/modules/game_tasks/tasks.lua b/modules/game_tasks/tasks.lua index 91679d3a18..bedf3401ab 100644 --- a/modules/game_tasks/tasks.lua +++ b/modules/game_tasks/tasks.lua @@ -12,7 +12,14 @@ function init() window = g_ui.displayUI('tasks') window:setVisible(false) - g_keyboard.bindKeyDown('Ctrl+A', toggleWindow) + Keybind.new('Windows', 'show/hide Tasks Windows', 'Ctrl+A', '') + Keybind.bind('Windows', 'show/hide Tasks Windows', { + { + type = KEY_DOWN, + callback = toggleWindow, + } + }) + g_keyboard.bindKeyDown('Escape', hideWindowzz) taskButton = modules.client_topmenu.addLeftGameButton('taskButton', tr('Tasks'), '/modules/game_tasks/images/taskIcon', toggleWindow) ProtocolGame.registerExtendedJSONOpcode(215, parseOpcode) @@ -25,6 +32,7 @@ function terminate() ProtocolGame.unregisterExtendedJSONOpcode(215, parseOpcode) taskButton:destroy() destroy() + Keybind.delete('Windows', 'show/hide Tasks Windows') end function onGameStart() diff --git a/modules/game_viplist/viplist.lua b/modules/game_viplist/viplist.lua index 1c590d66b9..381ebe8b46 100644 --- a/modules/game_viplist/viplist.lua +++ b/modules/game_viplist/viplist.lua @@ -20,8 +20,13 @@ local globalSettings = { controllerVip = Controller:new() function controllerVip:onInit() - g_keyboard.bindKeyDown('Ctrl+P', toggle) - + Keybind.new("Windows", "Show/hide VIP list", "Ctrl+P", "") + Keybind.bind("Windows", "Show/hide VIP list", { + { + type = KEY_DOWN, + callback = toggle, + } + }) vipButton = modules.game_mainpanel.addToggleButton('vipListButton', tr('VIP List') .. ' (Ctrl+P)', '/images/options/button_vip', toggle, false, 3) vipWindow = g_ui.loadUI('viplist') @@ -50,7 +55,7 @@ function controllerVip:onInit() end function controllerVip:onTerminate() - g_keyboard.unbindKeyDown('Ctrl+P') + Keybind.delete("Windows", "Show/hide VIP list") local ArrayWidgets = {addVipWindow, editVipWindow, vipWindow, vipButton, addGroupWindow} for _, widget in ipairs(ArrayWidgets) do if widget ~= nil or widget then diff --git a/modules/gamelib/const.lua b/modules/gamelib/const.lua index 5323f505c1..5acb080ba6 100644 --- a/modules/gamelib/const.lua +++ b/modules/gamelib/const.lua @@ -74,6 +74,17 @@ SouthEast = Directions.SouthEast SouthWest = Directions.SouthWest NorthWest = Directions.NorthWest +DirectionString = { + [North] = "North", + [East] = "East", + [South] = "South", + [West] = "West", + [NorthEast] = "North East", + [SouthEast] = "South East", + [SouthWest] = "South West", + [NorthWest] = "North West" + } + FightOffensive = 1 FightBalanced = 2 FightDefensive = 3 diff --git a/src/framework/ui/uiboxlayout.h b/src/framework/ui/uiboxlayout.h index d2e5d1db21..40582178f5 100644 --- a/src/framework/ui/uiboxlayout.h +++ b/src/framework/ui/uiboxlayout.h @@ -34,12 +34,12 @@ class UIBoxLayout : public UILayout void addWidget(const UIWidgetPtr& /*widget*/) override { update(); } void removeWidget(const UIWidgetPtr& /*widget*/) override { update(); } - void setSpacing(const uint8_t spacing) { m_spacing = spacing; update(); } + void setSpacing(const int8_t spacing) { m_spacing = spacing; update(); } void setFitChildren(const bool fitParent) { m_fitChildren = fitParent; update(); } bool isUIBoxLayout() override { return true; } protected: bool m_fitChildren{ false }; - uint8_t m_spacing{ 0 }; + int8_t m_spacing{ 0 }; };