Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -141,7 +141,7 @@ ; See keys.txt for the list of key names. ; > SYSTEM SETTINGS -exit = "Ctrl+Break", "Super+Q" ; Exit to desktop +exit = "Ctrl+Break", "Super+Q", "Alt+F4" ; Exit to desktop cancel = Escape ; Close or cancel the current dialog box/popup confirm = Return ; Confirm the current command pause = Pause ; Pause/unpause game @@ -186,8 +186,8 @@ reset = "R" ; Reset camera rotation to default. follow = "F" ; Follow the first unit in the selection rallypointfocus = unused ; Focus the camera on the rally point of the selected building -zoom.in = Plus, Equals, NumPlus ; Zoom camera in (continuous control) -zoom.out = Minus, Underscore, NumMinus ; Zoom camera out (continuous control) +zoom.in = Plus, NumPlus ; Zoom camera in (continuous control) +zoom.out = Minus, NumMinus ; Zoom camera out (continuous control) zoom.wheel.in = WheelUp ; Zoom camera in (stepped control) zoom.wheel.out = WheelDown ; Zoom camera out (stepped control) rotate.up = "Ctrl+UpArrow", "Ctrl+W" ; Rotate camera to look upwards @@ -238,13 +238,13 @@ toggle = "Ctrl+F11" ; Enable/disable HTTP/GPU modes for new profiler [hotkey.selection] +cancel = Esc ; Un-select all units and cancel building placement add = Shift ; Add units to selection militaryonly = Alt ; Add only military units to the selection nonmilitaryonly = "Alt+Y" ; Add only non-military units to the selection idleonly = "I" ; Select only idle units woundedonly = "O" ; Select only wounded units remove = Ctrl ; Remove units from selection -cancel = Esc ; Un-select all units and cancel building placement idleworker = Period, NumPoint ; Select next idle worker idlewarrior = ForwardSlash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit @@ -312,8 +312,8 @@ ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel -highlightguarding = PgDn ; Toggle highlight of guarding units -highlightguarded = PgUp ; Toggle highlight of guarded units +highlightguarding = PageDown ; Toggle highlight of guarding units +highlightguarded = PageUp ; Toggle highlight of guarded units diplomacycolors = "Alt+X" ; Toggle diplomacy colors toggleattackrange = "Alt+C" ; Toggle display of attack range overlays of selected defensive structures toggleaurasrange = "Alt+V" ; Toggle display of aura range overlays of selected units and structures Index: binaries/data/config/keys.txt =================================================================== --- binaries/data/config/keys.txt +++ binaries/data/config/keys.txt @@ -1,107 +1,38 @@ ## This file documents keynames that can be used in .cfg files for specifying hotkeys +## Note: all are case-insensitive. ## Note: the keynames are not actually configured or implemented here -## Also note: if your keyboard supports keys that do not appear here (such as the French é letter), -## you should be able to use it directly. +## Note: those names map to 'scancodes', i.e. positions on a classic QWERTY keyboard. +## The in-game hotkey editor will show you what that corresponds to. -Backspace, BkSp -Tab -Clear -Return, Ret -Pause + +MouseLeft +MouseRight +MouseMiddle +MouseX1 +MouseX2 +WheelUp +WheelDown +WheelLeft +WheelRight + +A-Z +0-9 +Return, Break Escape, Esc -Space, Spc -!, Exclaim -", DoubleQuote -#, Hash -$, Dollar -&, Ampersand -', SingleQuote -(, LeftParen -), RightParen -*, Asterisk -+, Plus -,, Comma +Backspace, Del +Tab +Space -, Minus -., Period -/, ForwardSlash -0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -:, Colon -;, Semicolon -<, LessThan -=, Equals ->, GreaterThan -?, Question -@, At +=, Plus, Equals [, LeftBracket -\, BackSlash ], RightBracket -^, Caret -_, Underscore +\\, BackSlash ## You do need the espacing in the first case. +;, Semicolon +', Quote ## Maps to the quote/doubleQuote key on QWERTY. `, BackQuote -A -B -C -D -E -F -G -H -I -J -K -L -M -N -O -P -Q -R -S -T -U -V -W -X -Y -Z -Delete, Del -Numpad 0, Num0 -Numpad 1, Num1 -Numpad 2, Num2 -Numpad 3, Num3 -Numpad 4, Num4 -Numpad 5, Num5 -Numpad 6, Num6 -Numpad 7, Num7 -Numpad 8, Num8 -Numpad 9, Num9 -Numpad ., NumPoint -Numpad /, NumDivide -Numpad *, NumMultiply -Numpad -, NumMinus -Numpad +, NumPlus -Numpad Enter, NumEnter -Numpad =, NumEquals - -Arrow Up, UpArrow -Arrow Down, DownArrow -Arrow Right, RightArrow -Arrow Left, LeftArrow -Insert, Ins -Home -End -Page Up, PgUp -Page Down, PgDn - +,, Comma +., Period +/, ForwardSlash F1 F2 F3 @@ -114,41 +45,155 @@ F10 F11 F12 +PrintScreen +ScrollLock +Pause +Insert +Home +PageUp +Delete +End +PageDown +Right, RightArrow +Left, LeftArrow +Down, DownArrow +Up, UpArrow +Numlock +Keypad / +Keypad * +Keypad -, NumMinus +Keypad +, NumPlus +Keypad Enter +Keypad 1 +Keypad 2 +Keypad 3 +Keypad 4 +Keypad 5 +Keypad 6 +Keypad 7 +Keypad 8 +Keypad 9 +Keypad 0 +Keypad . +Application +Power +Keypad = F13 F14 F15 - -Num Lock, NumLock -Caps Lock, CapsLock -Scroll Lock, ScrlLock -Right Shift, RightShift -Left Shift, LeftShift -Shift, AnyShift -Right Ctrl, RightCtrl -Left Ctrl, LeftCtrl -Ctrl, AnyCtrl -Right Alt, RightAlt -Left Alt, LeftAlt -Alt, AnyAlt -Left Super, LeftWin -Right Super, RightWin -Super, AnyWindows ## Windows key, also Command/meta key on Macs -Alt Gr, AltGr -Compose - +F16 +F17 +F18 +F19 +F20 +F21 +F22 +F23 +F24 +Execute Help -Print Screen, PrtSc -SysRq -Break Menu -Power -Euro +Select +Stop +Again Undo -Left Mouse Button, MouseLeft -Right Mouse Button, MouseRight -Middle Mouse Button, MouseMiddle -Mouse Wheel Up, WheelUp -Mouse Wheel Down, WheelDown -MouseButtonX, MouseNX # where X is a number 1-255, for extra mouse buttons -## (note that some mice start numbering their buttons at higher numbers -## so the first extra button on your mouse might be #8 here) +Cut +Copy +Paste +Find +Mute +VolumeUp +VolumeDown +Keypad , +AltErase +SysReq +Cancel +Clear +Prior +Return +Separator +Out +Oper +Clear / Again +CrSel +ExSel +Keypad 00 +Keypad 000 +ThousandsSeparator +DecimalSeparator +CurrencyUnit +CurrencySubUnit +Keypad ( +Keypad ) +Keypad { +Keypad } +Keypad Tab +Keypad Backspace +Keypad A +Keypad B +Keypad C +Keypad D +Keypad E +Keypad F +Keypad XOR +Keypad ^ +Keypad % +Keypad < +Keypad > +Keypad & +Keypad && +Keypad | +Keypad || +Keypad : +Keypad # +Keypad Space +Keypad @ +Keypad ! +Keypad MemStore +Keypad MemRecall +Keypad MemClear +Keypad MemAdd +Keypad MemSubtract +Keypad MemMultiply +Keypad MemDivide +Keypad +/- +Keypad Clear +Keypad ClearEntry +Keypad Binary +Keypad Octal +Keypad Decimal +Keypad Hexadecimal +Left Ctrl, Ctrl +Left Shift, Shift +Left Alt, Alt +Left GUI, Super +Right Ctrl, Ctrl +Right Shift, Shift +Right Alt, Alt +Right GUI, Super +ModeSwitch +AudioNext +AudioPrev +AudioStop +AudioPlay +AudioMute +MediaSelect +WWW +Mail +Calculator +Computer +AC Search +AC Home +AC Back +AC Forward +AC Stop +AC Refresh +AC Bookmarks +BrightnessDown +BrightnessUp +DisplaySwitch +KBDIllumToggle +KBDIllumDown +KBDIllumUp +Eject +Sleep Index: binaries/data/mods/public/gui/common/hotkeys.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/common/hotkeys.js @@ -0,0 +1,31 @@ +/** + * Holds a map of scancode name -> user keyboard name + */ +var g_ScancodesMap; + +function hotkeySort(a, b) +{ + const specialKeys = ["Shift", "Alt", "Ctrl", "Super"]; + // Quick hack to put those first. + if (specialKeys.indexOf(a) !== -1) + a = ' ' + a; + if (specialKeys.indexOf(b) !== -1) + b = ' ' + b; + return a.localeCompare(b, Engine.GetCurrentLocale().substr(0, 2), { "numeric": true }); +} + +function formatHotkeyCombination(comb) +{ + if (!g_ScancodesMap) + g_ScancodesMap = Engine.GetScancodeKeyNames(); + + let str = comb.sort(hotkeySort).map(hk => g_ScancodesMap[hk]).join("+"); + return str;//.replace('\\', '\\\\').replace('[', '\\['); +} + +function formatHotkeyCombinations(combinations) +{ + let combs = combinations.map(formatHotkeyCombination); + combs.sort((a, b) => a.length > b.length || a > b); + return combs.join(", "); +} Index: binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js @@ -0,0 +1,113 @@ +/** + * Handle the interface to pick a hotkey combination. + * The player must keep a key combination for 2s in the input field for it to be registered. + */ +class HotkeyPicker +{ + constructor(onClose, name, combinations) + { + this.name = name; + this.combinations = combinations; + this.window = Engine.GetGUIObjectByName("hotkeyPicker"); + this.window.hidden = false; + + this.enteringInput = -1; + + Engine.GetGUIObjectByName("hotkeyPickerTitle").caption = translate(this.name); + + this.setupCombinations(); + this.render(); + + Engine.GetGUIObjectByName("hotkeyPickerReset").onPress = () => { + // This is a bit "using a bazooka to kill a fly" + Engine.ConfigDB_RemoveValue("user", "hotkey." + this.name); + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + Engine.ReloadHotkeys(); + let data = Engine.GetHotkeyMap(); + this.combinations = data[this.name]; + this.setupCombinations(); + this.render(); + }; + Engine.GetGUIObjectByName("hotkeyPickerCancel").onPress = () => { + onClose(this, false); + }; + Engine.GetGUIObjectByName("hotkeyPickerSave").onPress = () => { + onClose(this, true); + }; + } + + setupCombinations() + { + for (let i = 0; i < 4; ++i) + { + let s = Engine.GetGUIObjectByName("combination[" + i + "]").size; + s.top = +i * 60 + 90; + s.bottom = +i * 60 + 120; + Engine.GetGUIObjectByName("combination[" + i + "]").size = s; + Engine.GetGUIObjectByName("combNb[" + i + "]").caption = sprintf(translate("#%i"), i); + + if (i == this.combinations.length) + this.combinations.push([]); + + let input = Engine.GetGUIObjectByName("combMapping[" + i + "]"); + + let picker = Engine.GetGUIObjectByName("picker[" + i + "]"); + Engine.GetGUIObjectByName("combMappingBtn[" + i + "]").onPress = () => { + this.enteringInput = i; + picker.focus(); + this.render(); + }; + + picker.onKeyChange = keys => { + input.caption = (keys.length ? + formatHotkeyCombination(keys) + translate(" (hold to register)") : + translate("Enter new Hotkey, hold to register.")); + }; + + Engine.GetGUIObjectByName("deleteComb[" + i + "]").onPress = (j => () => { + this.combinations[j] = []; + this.render(); + })(i); + + picker.onCombination = (j => keys => { + this.combinations[j] = keys; + this.enteringInput = -1; + picker.blur(); + + this.render(); + })(i); + } + } + + close() + { + this.window.hidden = true; + for (let i = 0; i < 4; ++i) + Engine.GetGUIObjectByName("picker[" + i + "]").blur(); + } + + render() + { + for (let i = 0; i < 4; ++i) + { + let input = Engine.GetGUIObjectByName("combMapping[" + i + "]"); + if (i == this.enteringInput) + input.caption = translate("Enter new Hotkey, hold to register."); + else + input.caption = formatHotkeyCombination(this.combinations[i]) || "(unused)"; + Engine.GetGUIObjectByName("conflicts[" + i + "]").caption = ""; + + Engine.GetGUIObjectByName("deleteComb[" + i + "]").hidden = !this.combinations[i].length; + + let conflicts = (Engine.GetConflicts(this.combinations[i]) || []) + .filter(name => name != this.name).map(translate); + if (conflicts.length) + Engine.GetGUIObjectByName("conflicts[" + i + "]").caption = + coloredText(translate("May conflict with: "), "255 153 0") + conflicts.join(", "); + } + // Gray out buttons when entering an input so it's obvious clicking on them will do nothing. + Engine.GetGUIObjectByName("hotkeyPickerReset").enabled = this.enteringInput == -1; + Engine.GetGUIObjectByName("hotkeyPickerCancel").enabled = this.enteringInput == -1; + Engine.GetGUIObjectByName("hotkeyPickerSave").enabled = this.enteringInput == -1; + } +} Index: binaries/data/mods/public/gui/hotkeys/HotkeysPage.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/hotkeys/HotkeysPage.js @@ -0,0 +1,184 @@ +class HotkeysPage +{ + constructor() + { + Engine.GetGUIObjectByName("hotkeyList").onMouseLeftDoubleClickItem = () => { + let idx = Engine.GetGUIObjectByName("hotkeyList").selected; + let picker = new HotkeyPicker( + this.onHotkeyPicked.bind(this), + Engine.GetGUIObjectByName("hotkeyList").list_data[idx], + this.hotkeys[Engine.GetGUIObjectByName("hotkeyList").list_data[idx]] + ); + }; + Engine.GetGUIObjectByName("hotkeyFilter").onSelectionChange = () => this.setupHotkeyList(); + + Engine.GetGUIObjectByName("hotkeyTextFilter").onTextEdit = () => this.setupHotkeyList(); + + Engine.GetGUIObjectByName("hotkeyClose").onPress = () => Engine.PopGuiPage(); + Engine.GetGUIObjectByName("hotkeyReset").onPress = () => this.resetUserHotkeys(); + Engine.GetGUIObjectByName("hotkeySave").onPress = () => { + this.saveUserHotkeys(); + }; + + this.setupHotkeyData(); + this.setupFilters(); + this.setupHotkeyList(); + } + + setupFilters() + { + let dropdown = Engine.GetGUIObjectByName("hotkeyFilter"); + let names = []; + for (let cat in this.categories) + names.push(this.categories[cat].label); + dropdown.list = [translate("All Hotkeys")].concat(names); + dropdown.list_data = [-1].concat(Object.keys(this.categories)); + dropdown.selected = 0; + } + + setupHotkeyList() + { + let hotkeyList = Engine.GetGUIObjectByName("hotkeyList"); + hotkeyList.selected = -1; + let textFilter = Engine.GetGUIObjectByName("hotkeyTextFilter").caption; + let dropdown = Engine.GetGUIObjectByName("hotkeyFilter"); + if (dropdown.selected && dropdown.selected !== 0) + { + let category = this.categories[dropdown.list_data[dropdown.selected]]; + // This is inefficient but it seems fast enough. + let hotkeys = category.hotkeys.filter(x => translate(x[0]).indexOf(textFilter) !== -1); + hotkeyList.list_name = hotkeys.map(x => translate(x[0])); + hotkeyList.list_mapping = hotkeys.map(x => formatHotkeyCombinations(x[1])); + hotkeyList.list = hotkeys.map(() => 0); + hotkeyList.list_data = hotkeys.map(x => x[0]); + } + else + { + // TODO SM62+ : refactor using flat() + let flattened = []; + for (let cat in this.categories) + flattened = flattened.concat(this.categories[cat].hotkeys); + flattened = flattened.filter(x => translate(x[0]).indexOf(textFilter) !== -1); + hotkeyList.list_name = flattened.map(x => translate(x[0])); + hotkeyList.list_mapping = flattened.map(x => formatHotkeyCombinations(x[1])); + hotkeyList.list = flattened.map(() => 0); + hotkeyList.list_data = flattened.map(x => x[0]); + } + } + + formatHotkeyCombinations(combinations) + { + return combinations.map(formatHotkeyCombination).join(", "); + } + + + onHotkeyPicked(picker, success) + { + picker.close(); + if (!success) + return; + + // Remove empty combinations which the picker added. + picker.combinations = picker.combinations.filter(x => x.length); + + this.hotkeys[picker.name] = picker.combinations; + // Have to find the correct line. + let panel = Engine.GetGUIObjectByName("hotkeyList"); + for (let cat in this.categories) + { + let idx = this.categories[cat].hotkeys.findIndex(([name, _]) => name == picker.name); + if (idx === -1) + continue; + this.categories[cat].hotkeys[idx][1] = picker.combinations; + } + + this.setupHotkeyList(); + } + + setupHotkeyData() + { + let hotkeydata = Engine.GetHotkeyMap(); + this.hotkeys = hotkeydata; + + let categories = { + "other": { + "label": translate("Other hotkeys"), + "hotkeys": [] + } + }; + let n_categories = 1; + for (let hotkeyName in this.hotkeys) + { + let category = "other"; + let firstdot = hotkeyName.indexOf('.'); + if (firstdot !== -1) + category = hotkeyName.substr(0, firstdot); + if (!(category in categories)) + { + if (n_categories > 18) + category = "other"; + categories[category] = { + "label": category, + "hotkeys": [] + }; + } + categories[category].hotkeys.push([hotkeyName, this.hotkeys[hotkeyName]]); + } + // Remove categories that are too small to deserve a tab. + for (let cat of Object.keys(categories)) + if (categories[cat].hotkeys.length < 3) + { + categories.other.hotkeys = categories.other.hotkeys.concat(categories[cat].hotkeys); + delete categories[cat]; + } + for (let cat in categories) + categories[cat].hotkeys = categories[cat].hotkeys.sort(); + + this.categories = categories; + } + + resetUserHotkeys() + { + messageBox( + 400, 200, + translate("Reset all hotkeys to default values?"), + translate("Confirmation"), + [translate("No"), translate("Yes")], + [ + () => {}, + () => { + for (let cat in this.categories) + this.categories[cat].hotkeys.forEach(([name, _]) => { + Engine.ConfigDB_RemoveValue("user", "hotkey." + name); + }); + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + Engine.ReloadHotkeys(); + this.setupHotkeyData(); + this.setupHotkeyList(); + } + ]); + } + + saveUserHotkeys() + { + for (let cat in this.categories) + { + this.categories[cat].hotkeys.forEach(([name, mapping]) => { + let keymap = mapping.map(comb => comb.sort(hotkeySort).join("+")); + Engine.ConfigDB_RemoveValue("user", "hotkey." + name); + let val = Engine.ConfigDB_GetValue("user", "hotkey." + name); + // val is a string here so I join above. + if (keymap.join(", ") !== val) + Engine.ConfigDB_CreateValues("user", "hotkey." + name, keymap); + }); + } + Engine.ConfigDB_WriteFile("user", "config/user.cfg"); + Engine.ReloadHotkeys(); + } +} + + +function init(data) +{ + let hotkeyPage = new HotkeysPage(data); +} Index: binaries/data/mods/public/gui/hotkeys/hotkeys.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/hotkeys/hotkeys.xml @@ -0,0 +1,112 @@ + + + + +