Index: binaries/data/config/default.cfg
===================================================================
--- binaries/data/config/default.cfg
+++ binaries/data/config/default.cfg
@@ -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,7 +1,7 @@
## This file documents keynames that can be used in .cfg files for specifying hotkeys
## 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
@@ -19,7 +19,7 @@
(, LeftParen
), RightParen
*, Asterisk
-+, Plus
++, Plus ## This maps to the same key as =
,, Comma
-, Minus
., Period
@@ -34,7 +34,6 @@
7
8
9
-:, Colon
;, Semicolon
<, LessThan
=, Equals
@@ -92,15 +91,15 @@
Numpad Enter, NumEnter
Numpad =, NumEquals
-Arrow Up, UpArrow
-Arrow Down, DownArrow
-Arrow Right, RightArrow
-Arrow Left, LeftArrow
+UpArrow
+DownArrow
+RightArrow
+LeftArrow
Insert, Ins
Home
End
-Page Up, PgUp
-Page Down, PgDn
+PageUp
+PageDown
F1
F2
@@ -149,6 +148,5 @@
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)
+Mouse Wheel Left, MouseX1
+Mouse Wheel Right, MouseX2
Index: binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
@@ -0,0 +1,106 @@
+/**
+ * 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);
+
+ Engine.GetGUIObjectByName("hotkeyPickerDesc").caption = '' +
+ `Click on any mapping to modify it.`;
+
+ this.setupCombinations();
+
+ 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();
+ };
+ Engine.GetGUIObjectByName("hotkeyPickerCancel").onPress = () => {
+ onClose(this, false);
+ }
+ Engine.GetGUIObjectByName("hotkeyPickerAccept").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 = translate(`#${i+1}`);
+
+ if (i == this.combinations.length)
+ this.combinations.push([]);
+
+ let input = Engine.GetGUIObjectByName("combMapping[" + i + "]");
+ input.caption = formatHotkeyCombination(this.combinations[i]);
+
+ let picker = Engine.GetGUIObjectByName("picker[" + i + "]");
+ Engine.GetGUIObjectByName("combMappingBtn[" + i + "]").onPress = () => {
+ if (this.enteringInput)
+ {
+
+ }
+ input.caption = translate("Enter new Hotkey, hold to register.");
+ this.enteringInput = i;
+ picker.focus();
+ };
+
+ 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 = (i => () => {
+ this.combinations[i] = [];
+ input.caption = "";
+ })(i);
+
+ Engine.GetGUIObjectByName("conflicts[" + i + "]").caption = "";
+
+ picker.onCombination = (i => keys => {
+ this.combinations[i] = keys;
+ input.caption = formatHotkeyCombination(this.combinations[i]);
+ picker.blur();
+
+ let conflicts = (Engine.GetConflicts(keys) || [])
+ .filter(name => name != this.name).map(translate);
+ if (conflicts.length)
+ Engine.GetGUIObjectByName("conflicts[" + i + "]").caption =
+ translate(`${coloredText("Conflicts with", "255 153 0")}: ${conflicts.join(", ")}`);
+ })(i);
+ }
+ }
+
+ close()
+ {
+ this.window.hidden = true;
+ for (let i = 0; i < 4; ++i)
+ Engine.GetGUIObjectByName("picker[" + i + "]").blur();
+ }
+
+ renderButtons()
+ {
+ Engine.GetGUIObjectByName("hotkeyPickerAccept").enabled = !!this.combination;
+ Engine.GetGUIObjectByName("hotkeyPickerRepick").enabled = !!this.combination;
+ }
+}
Index: binaries/data/mods/public/gui/hotkeys/HotkeysPage.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/HotkeysPage.js
@@ -0,0 +1,190 @@
+class HotkeysPage
+{
+ constructor()
+ {
+ g_ScancodesMap = Engine.GetScancodeKeyNames();
+
+ Engine.GetGUIObjectByName("hotkeyList").onMouseLeftDoubleClickItem = () => {
+ let idx = Engine.GetGUIObjectByName("hotkeyList").selected;
+ 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("hotkeyCancel").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 => this.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 => this.formatHotkeyCombinations(x[1]));
+ hotkeyList.list = flattened.map(() => 0);
+ hotkeyList.list_data = flattened.map(x => x[0]);
+ }
+ }
+
+ formatHotkeyCombinations(combinations)
+ {
+ return combinations.map(formatHotkeyCombination).join(", ");
+ }
+
+ formatHotkeyCombinationsScancode(combinations)
+ {
+ return combinations.map(formatHotkeyCombinationScancode).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,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hotkey
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ Reset
+
+
+
+ Accept
+
+
+
+
Index: binaries/data/mods/public/gui/hotkeys/page_hotkeys.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/page_hotkeys.xml
@@ -0,0 +1,14 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ common/global.xml
+
+ hotkeys/hotkeys.xml
+
Index: binaries/data/mods/public/gui/hotkeys/utils.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/utils.js
@@ -0,0 +1,21 @@
+/**
+ * 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)
+{
+ let str = comb.sort(hotkeySort).map(hk => g_ScancodesMap[hk]).join("+");
+ return str.replace('\\', '\\\\').replace('[', '\\[');
+}
Index: binaries/data/mods/public/gui/pregame/MainMenuItems.js
===================================================================
--- binaries/data/mods/public/gui/pregame/MainMenuItems.js
+++ binaries/data/mods/public/gui/pregame/MainMenuItems.js
@@ -148,6 +148,13 @@
fireConfigChangeHandlers);
}
},
+ {
+ "caption": translate("Hotkeys"),
+ "tooltip": translate("Adjust hotkeys."),
+ "onPress": () => {
+ Engine.PushGuiPage("hotkeys/page_hotkeys.xml");
+ }
+ },
{
"caption": translate("Language"),
"tooltip": translate("Choose the language of the game."),
Index: binaries/data/mods/public/gui/session/MenuButtons.js
===================================================================
--- binaries/data/mods/public/gui/session/MenuButtons.js
+++ binaries/data/mods/public/gui/session/MenuButtons.js
@@ -174,6 +174,27 @@
}
};
+MenuButtons.prototype.Hotkeys = class
+{
+ constructor(button, pauseControl)
+ {
+ this.button = button;
+ this.button.caption = translate("Hotkeys");
+ this.pauseControl = pauseControl;
+ }
+
+ onPress()
+ {
+ closeOpenDialogs();
+ this.pauseControl.implicitPause();
+
+ Engine.PushGuiPage(
+ "hotkeys/page_hotkeys.xml",
+ {},
+ () => { resumeGame(); });
+ }
+};
+
MenuButtons.prototype.Pause = class
{
constructor(button, pauseControl, playerViewControl)
Index: source/gui/CGUI.cpp
===================================================================
--- source/gui/CGUI.cpp
+++ source/gui/CGUI.cpp
@@ -255,16 +255,12 @@
// Handle keys for input boxes
if (GetFocusedObject())
{
- if ((ev->ev.type == SDL_KEYDOWN &&
- ev->ev.key.keysym.sym != SDLK_ESCAPE &&
- !g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] &&
- !g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) ||
- ev->ev.type == SDL_HOTKEYDOWN ||
- ev->ev.type == SDL_TEXTINPUT ||
- ev->ev.type == SDL_TEXTEDITING)
- {
+ if (ev->ev.type == SDL_MOUSEBUTTONUP || ev->ev.type == SDL_MOUSEBUTTONDOWN ||
+ ev->ev.type == SDL_MOUSEWHEEL ||
+ ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_KEYDOWN ||
+ ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYDOWN ||
+ ev->ev.type == SDL_TEXTINPUT || ev->ev.type == SDL_TEXTEDITING)
ret = GetFocusedObject()->ManuallyHandleEvent(ev);
- }
// else will return IN_PASS because we never used the button.
}
@@ -273,6 +269,7 @@
void CGUI::TickObjects()
{
+ m_BaseObject.RecurseObject(&IGUIObject::IsHiddenOrGhost, &IGUIObject::Tick);
SendEventToAll(EventNameTick);
m_Tooltip.Update(FindObjectUnderMouse(), m_MousePos, *this);
}
Index: source/gui/GUIObjectTypes.h
===================================================================
--- source/gui/GUIObjectTypes.h
+++ source/gui/GUIObjectTypes.h
@@ -21,6 +21,7 @@
#include "gui/ObjectTypes/CChart.h"
#include "gui/ObjectTypes/CCheckBox.h"
#include "gui/ObjectTypes/CDropDown.h"
+#include "gui/ObjectTypes/CHotkeyPicker.h"
#include "gui/ObjectTypes/CImage.h"
#include "gui/ObjectTypes/CInput.h"
#include "gui/ObjectTypes/CList.h"
@@ -39,6 +40,7 @@
AddObjectType("checkbox", &CCheckBox::ConstructObject);
AddObjectType("dropdown", &CDropDown::ConstructObject);
AddObjectType("empty", &CGUIDummyObject::ConstructObject);
+ AddObjectType("hotkeypicker", &CHotkeyPicker::ConstructObject);
AddObjectType("image", &CImage::ConstructObject);
AddObjectType("input", &CInput::ConstructObject);
AddObjectType("list", &CList::ConstructObject);
Index: source/gui/ObjectBases/IGUIObject.h
===================================================================
--- source/gui/ObjectBases/IGUIObject.h
+++ source/gui/ObjectBases/IGUIObject.h
@@ -255,6 +255,12 @@
//@{
public:
+
+ /**
+ * Called on every GUI tick unless the object or one of its parent is hidden/ghost.
+ */
+ virtual void Tick() {};
+
/**
* This function is called with different messages
* for instance when the mouse enters the object.
Index: source/gui/ObjectTypes/CHotkeyPicker.h
===================================================================
--- /dev/null
+++ source/gui/ObjectTypes/CHotkeyPicker.h
@@ -0,0 +1,75 @@
+/* Copyright (C) 2020 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#ifndef INCLUDED_CHOTKEYPICKER
+#define INCLUDED_CHOTKEYPICKER
+
+#include "lib/external_libraries/libsdl.h"
+
+class ScriptInterface;
+
+/**
+ * When in focus, returns all currently pressed keys.
+ * After a set time without changes, it will trigger a "combination" event.
+ *
+ * Used to create new hotkey combinations in-game. Mostly custom.
+ * This object does not draw anything.
+ */
+class CHotkeyPicker : public IGUIObject
+{
+ GUI_OBJECT(CHotkeyPicker)
+
+ friend class ScriptInterface;
+public:
+ CHotkeyPicker(CGUI& pGUI);
+ virtual ~CHotkeyPicker();
+
+ // Do nothing.
+ virtual void Draw() {};
+
+ // Checks if the timer has passed and we need to fire a "combination" event.
+ virtual void Tick();
+
+ // React to blur/focus.
+ virtual void HandleMessage(SGUIMessage& Message);
+
+ // Handle events manually: this is our sole purpose.
+ virtual InReaction ManuallyHandleEvent(const SDL_Event_* ev);
+protected:
+ // Fire an event with m_KeysPressed as argument.
+ void FireEvent(const CStr& event);
+
+ // Time without changes until a "combination" event is sent.
+ float m_TimeToCombination;
+ // Time of the last registered key change.
+ double m_LastKeyChange;
+
+ // Keep track of which keys we are pressing, and precompute their name for JS code.
+ struct Key
+ {
+ // The scancode is used for fast comparisons.
+ SDL_Scancode code;
+ // This is the name ultimately stored in the config file.
+ std::string scancodeName;
+ };
+ std::vector m_KeysPressed;
+
+ static const CStr EventNameCombination;
+ static const CStr EventNameKeyChange;
+};
+
+#endif // INCLUDED_CHOTKEYPICKER
Index: source/gui/ObjectTypes/CHotkeyPicker.cpp
===================================================================
--- /dev/null
+++ source/gui/ObjectTypes/CHotkeyPicker.cpp
@@ -0,0 +1,186 @@
+/* Copyright (C) 2020 Wildfire Games.
+ * This file is part of 0 A.D.
+ *
+ * 0 A.D. is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * 0 A.D. is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with 0 A.D. If not, see .
+ */
+
+#include "precompiled.h"
+
+#include "CHotkeyPicker.h"
+
+#include "lib/timer.h"
+#include "ps/Hotkey.h"
+#include "ps/KeyName.h"
+#include "scriptinterface/ScriptConversions.h"
+
+const CStr CHotkeyPicker::EventNameCombination = "Combination";
+const CStr CHotkeyPicker::EventNameKeyChange = "KeyChange";
+
+// Don't send the scancode, JS doesn't care.
+template<> void ScriptInterface::ToJSVal(JSContext *cx, JS::MutableHandleValue ret, const CHotkeyPicker::Key& val)
+{
+ ScriptInterface::ToJSVal(cx, ret, val.scancodeName);
+}
+
+// Unused, but JSVAL_VECTOR requires it.
+template<> bool ScriptInterface::FromJSVal(JSContext*, const JS::HandleValue, CHotkeyPicker::Key&)
+{
+ LOGWARNING("FromJSVal: Not implemented");
+ return false;
+}
+
+JSVAL_VECTOR(CHotkeyPicker::Key);
+
+CHotkeyPicker::CHotkeyPicker(CGUI& pGUI) : IGUIObject(pGUI), m_TimeToCombination(1.f)
+{
+ RegisterSetting("time_to_combination", m_TimeToCombination);
+ // 8 keys at the same time is probably more than we'll ever need.
+ m_KeysPressed.reserve(8);
+}
+
+CHotkeyPicker::~CHotkeyPicker()
+{
+}
+
+void CHotkeyPicker::FireEvent(const CStr& event)
+{
+ JSContext* cx = m_pGUI.GetScriptInterface()->GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::AutoValueArray<1> args(cx);
+ JS::RootedValue keys(cx);
+ m_pGUI.GetScriptInterface()->ToJSVal(cx, &keys, m_KeysPressed);
+ args[0].set(keys);
+ ScriptEvent(event, args);
+}
+
+void CHotkeyPicker::Tick()
+{
+ if (m_KeysPressed.size() == 0)
+ return;
+
+ double time = timer_Time();
+ if (time - m_LastKeyChange < m_TimeToCombination)
+ return;
+
+ FireEvent(EventNameCombination);
+
+ return;
+}
+
+void CHotkeyPicker::HandleMessage(SGUIMessage& Message)
+{
+ IGUIObject::HandleMessage(Message);
+ switch (Message.type)
+ {
+ case GUIM_GOT_FOCUS:
+ case GUIM_LOST_FOCUS:
+ {
+ m_KeysPressed.clear();
+ m_LastKeyChange = timer_Time();
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+InReaction CHotkeyPicker::ManuallyHandleEvent(const SDL_Event_* ev)
+{
+ switch (ev->ev.type)
+ {
+ // Handle the same mouse events that hotkeys handle
+ case SDL_MOUSEBUTTONUP:
+ case SDL_MOUSEWHEEL:
+ {
+ SDL_Scancode scancode;
+
+ if (ev->ev.type != SDL_MOUSEWHEEL)
+ {
+ // Wait a little bit -> this gets triggered when clicking on a button,
+ // but after the button click is processed, thus immediately triggering...
+ if (timer_Time()-m_LastKeyChange < 0.2)
+ return IN_HANDLED;
+ // This is from hotkeyHandler - not sure what it does in all honesty.
+ if(ev->ev.button.button >= SDL_BUTTON_X1)
+ scancode = static_cast(MOUSE_BASE + (int)ev->ev.button.button + 2);
+ else
+ scancode = static_cast(MOUSE_BASE + (int)ev->ev.button.button);
+ }
+ else
+ {
+ if (ev->ev.wheel.y > 0)
+ scancode = static_cast(MOUSE_WHEELUP);
+ else if (ev->ev.wheel.y < 0)
+ scancode = static_cast(MOUSE_WHEELDOWN);
+ else if (ev->ev.wheel.x > 0)
+ scancode = static_cast(MOUSE_X2);
+ else if (ev->ev.wheel.x < 0)
+ scancode = static_cast(MOUSE_X1);
+ else
+ return IN_HANDLED;
+ }
+ // Don't handle keys and mouse together
+ m_KeysPressed.clear();
+ m_KeysPressed.emplace_back(Key{scancode, FindScancodeName(scancode)});
+ // For mouse events, assume we immediately want to return.
+ FireEvent(EventNameCombination);
+
+ return IN_HANDLED;
+ }
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ {
+ SDL_Scancode scancode = ev->ev.key.keysym.scancode;
+
+ if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT)
+ scancode = static_cast(UNIFIED_SHIFT);
+ else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL)
+ scancode = static_cast(UNIFIED_CTRL);
+ else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT)
+ scancode = static_cast(UNIFIED_ALT);
+ else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI)
+ scancode = static_cast(UNIFIED_SUPER);
+
+ if (ev->ev.type == SDL_KEYDOWN)
+ {
+ std::vector::const_iterator it = \
+ std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&scancode](Key& k) { return k.code == scancode; });
+ // Can happen if multiple keys are mapped the same.
+ if (it != m_KeysPressed.end())
+ return IN_HANDLED;
+ m_KeysPressed.emplace_back(Key{scancode, FindScancodeName(scancode)});
+ }
+ else
+ {
+ std::vector::const_iterator it = \
+ std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&scancode](Key& k) { return k.code == scancode; });
+ // Might happen if a key was down before this object is created.
+ if (it == m_KeysPressed.end())
+ return IN_HANDLED;
+ m_KeysPressed.erase(it);
+ }
+
+ FireEvent(EventNameKeyChange);
+
+ // Register after-JS in case this takes a while (probably not but it doesn't hurt).
+ m_LastKeyChange = timer_Time();
+ return IN_HANDLED;
+ }
+ default:
+ {
+ return IN_PASS;
+ }
+ }
+}
Index: source/gui/Scripting/ScriptFunctions.cpp
===================================================================
--- source/gui/Scripting/ScriptFunctions.cpp
+++ source/gui/Scripting/ScriptFunctions.cpp
@@ -30,6 +30,7 @@
#include "ps/scripting/JSInterface_Console.h"
#include "ps/scripting/JSInterface_Debug.h"
#include "ps/scripting/JSInterface_Game.h"
+#include "ps/scripting/JSInterface_Hotkey.h"
#include "ps/scripting/JSInterface_Main.h"
#include "ps/scripting/JSInterface_Mod.h"
#include "ps/scripting/JSInterface_ModIo.h"
@@ -59,6 +60,7 @@
JSI_GUIManager::RegisterScriptFunctions(scriptInterface);
JSI_Game::RegisterScriptFunctions(scriptInterface);
JSI_GameView::RegisterScriptFunctions(scriptInterface);
+ JSI_Hotkey::RegisterScriptFunctions(scriptInterface);
JSI_L10n::RegisterScriptFunctions(scriptInterface);
JSI_Lobby::RegisterScriptFunctions(scriptInterface);
JSI_Main::RegisterScriptFunctions(scriptInterface);
Index: source/ps/ConfigDB.h
===================================================================
--- source/ps/ConfigDB.h
+++ source/ps/ConfigDB.h
@@ -110,6 +110,8 @@
void SetValueBool(EConfigNamespace ns, const CStr& name, const bool value);
+ void SetValueList(EConfigNamespace ns, const CStr& name, std::vector values);
+
/**
* Remove a config value in the specified namespace.
*/
Index: source/ps/ConfigDB.cpp
===================================================================
--- source/ps/ConfigDB.cpp
+++ source/ps/ConfigDB.cpp
@@ -210,6 +210,18 @@
SetValueString(ns, name, valueString);
}
+void CConfigDB::SetValueList(EConfigNamespace ns, const CStr& name, std::vector values)
+{
+ CHECK_NS(;);
+
+ std::lock_guard s(cfgdb_mutex);
+ TConfigMap::iterator it = m_Map[ns].find(name);
+ if (it == m_Map[ns].end())
+ it = m_Map[ns].insert(m_Map[ns].begin(), make_pair(name, CConfigValueSet(1)));
+
+ it->second = values;
+}
+
void CConfigDB::RemoveValue(EConfigNamespace ns, const CStr& name)
{
CHECK_NS(;);
Index: source/ps/Hotkey.h
===================================================================
--- source/ps/Hotkey.h
+++ source/ps/Hotkey.h
@@ -33,15 +33,43 @@
#include "CStr.h"
#include "lib/input.h"
-#include "lib/external_libraries/libsdl.h" // see note below
-// note: we need the real SDL header - it defines SDL_USEREVENT, which is
-// required for our HOTKEY event type definition. this is OK since
-// hotkey.h is not included from any headers.
+// SDL_Scancode is an enum, we'll use an explicit int to avoid including SDL in this header.
+using SDL_Scancode_ = int;
-const uint SDL_HOTKEYPRESS = SDL_USEREVENT;
-const uint SDL_HOTKEYDOWN = SDL_USEREVENT + 1;
-const uint SDL_HOTKEYUP = SDL_USEREVENT + 2;
+// 0x8000 is SDL_USEREVENT, this is static_asserted in Hotkey.cpp
+// We do this to avoid including SDL in this header.
+const uint SDL_USEREVENT_ = 0x8000;
+const uint SDL_HOTKEYPRESS = SDL_USEREVENT_;
+const uint SDL_HOTKEYDOWN = SDL_USEREVENT_ + 1;
+const uint SDL_HOTKEYUP = SDL_USEREVENT_ + 2;
+
+struct SKey
+{
+ SDL_Scancode_ code; // scancode or MOUSE_ or UNIFIED_ value
+ bool negated; // whether the key must be pressed (false) or unpressed (true)
+
+ bool operator<(const SKey& o) const { return code < o.code && negated < o.negated; }
+ bool operator==(const SKey& o) const { return code == o.code && negated == o.negated; }
+};
+
+// Hotkey data associated with an externally-specified 'primary' keycode
+struct SHotkeyMapping
+{
+ CStr name; // name of the hotkey
+ bool negated; // whether the primary key must be pressed (false) or unpressed (true)
+ std::vector requires; // list of non-primary keys that must also be active
+};
+
+typedef std::vector KeyMapping;
+
+// A mapping of scancodes onto the hotkeys that are associated with that key.
+// (A hotkey triggered by a combination of multiple keys will be in this map
+// multiple times.)
+extern std::map g_HotkeyMap;
+
+// The current pressed status of hotkeys
+extern std::map g_HotkeyStatus;
extern void LoadHotkeys();
extern void UnloadHotkeys();
Index: source/ps/Hotkey.cpp
===================================================================
--- source/ps/Hotkey.cpp
+++ source/ps/Hotkey.cpp
@@ -20,6 +20,7 @@
#include
+#include "lib/external_libraries/libsdl.h"
#include "ps/CConsole.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
@@ -29,30 +30,12 @@
static bool unified[UNIFIED_LAST - UNIFIED_SHIFT];
-struct SKey
-{
- SDL_Keycode code; // keycode or MOUSE_ or UNIFIED_ value
- bool negated; // whether the key must be pressed (false) or unpressed (true)
-};
-
-// Hotkey data associated with an externally-specified 'primary' keycode
-struct SHotkeyMapping
-{
- CStr name; // name of the hotkey
- bool negated; // whether the primary key must be pressed (false) or unpressed (true)
- std::vector requires; // list of non-primary keys that must also be active
-};
-
-typedef std::vector KeyMapping;
-
-// A mapping of keycodes onto the hotkeys that are associated with that key.
-// (A hotkey triggered by a combination of multiple keys will be in this map
-// multiple times.)
-static std::map g_HotkeyMap;
-
-// The current pressed status of hotkeys
+std::map g_HotkeyMap;
std::map g_HotkeyStatus;
+static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum.");
+static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT");
+
// Look up each key binding in the config file and set the mappings for
// all key combinations that trigger it.
static void LoadConfigBindings()
@@ -73,17 +56,16 @@
tokenizer tok(hotkey, sep);
for (tokenizer::iterator it = tok.begin(); it != tok.end(); ++it)
{
+ CStrW t = hotkey.FromUTF8();
// Attempt decode as key name
- int mapping = FindKeyCode(*it);
- if (!mapping)
- mapping = SDL_GetKeyFromName(it->c_str());
- if (!mapping)
+ SDL_Scancode scancode = FindScancode(it->c_str());
+ if (!scancode)
{
LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str());
continue;
}
- SKey key = { (SDL_Keycode)mapping, false };
+ SKey key = { scancode, false };
keyCombination.push_back(key);
}
@@ -107,8 +89,6 @@
void LoadHotkeys()
{
- InitKeyNameMap();
-
LoadConfigBindings();
// Set up the state of the hotkeys given no key is down.
@@ -146,7 +126,7 @@
else if ((int)key.code < MOUSE_LAST && (int)key.code > MOUSE_BASE && g_mouse_buttons[key.code - MOUSE_BASE] == key.negated)
return false;
// Modifier keycodes are between the normal keys and the mouse 'keys'
- else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES) && unified[key.code - UNIFIED_SHIFT] == key.negated)
+ else if ((int)key.code < UNIFIED_LAST && (int)key.code > SDL_NUM_SCANCODES && unified[key.code - UNIFIED_SHIFT] == key.negated)
return false;
else
return true;
@@ -163,13 +143,13 @@
InReaction HotkeyInputHandler(const SDL_Event_* ev)
{
- int keycode = 0;
+ int scancode = SDL_SCANCODE_UNKNOWN;
switch(ev->ev.type)
{
case SDL_KEYDOWN:
case SDL_KEYUP:
- keycode = (int)ev->ev.key.keysym.sym;
+ scancode = ev->ev.key.keysym.scancode;
break;
case SDL_MOUSEBUTTONDOWN:
@@ -177,30 +157,30 @@
// Mousewheel events are no longer buttons, but we want to maintain the order
// expected by g_mouse_buttons for compatibility
if (ev->ev.button.button >= SDL_BUTTON_X1)
- keycode = MOUSE_BASE + (int)ev->ev.button.button + 2;
+ scancode = MOUSE_BASE + (int)ev->ev.button.button + 2;
else
- keycode = MOUSE_BASE + (int)ev->ev.button.button;
+ scancode = MOUSE_BASE + (int)ev->ev.button.button;
break;
case SDL_MOUSEWHEEL:
if (ev->ev.wheel.y > 0)
{
- keycode = MOUSE_WHEELUP;
+ scancode = MOUSE_WHEELUP;
break;
}
else if (ev->ev.wheel.y < 0)
{
- keycode = MOUSE_WHEELDOWN;
+ scancode = MOUSE_WHEELDOWN;
break;
}
else if (ev->ev.wheel.x > 0)
{
- keycode = MOUSE_X2;
+ scancode = MOUSE_X2;
break;
}
else if (ev->ev.wheel.x < 0)
{
- keycode = MOUSE_X1;
+ scancode = MOUSE_X1;
break;
}
return IN_PASS;
@@ -219,33 +199,33 @@
if (phantom.ev.type == SDL_KEYDOWN)
phantom.ev.key.repeat = ev->ev.type == SDL_KEYDOWN ? ev->ev.key.repeat : 0;
- if ((keycode == SDLK_LSHIFT) || (keycode == SDLK_RSHIFT))
+ if (scancode == SDL_SCANCODE_LSHIFT || scancode == SDL_SCANCODE_RSHIFT)
{
- phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_SHIFT;
+ phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SHIFT);
unified[0] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
- else if ((keycode == SDLK_LCTRL) || (keycode == SDLK_RCTRL))
+ else if (scancode == SDL_SCANCODE_LCTRL || scancode == SDL_SCANCODE_RCTRL)
{
- phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_CTRL;
+ phantom.ev.key.keysym.scancode = static_cast(UNIFIED_CTRL);
unified[1] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
- else if ((keycode == SDLK_LALT) || (keycode == SDLK_RALT))
+ else if (scancode == SDL_SCANCODE_LALT || scancode == SDL_SCANCODE_RALT)
{
- phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_ALT;
+ phantom.ev.key.keysym.scancode = static_cast(UNIFIED_ALT);
unified[2] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
- else if ((keycode == SDLK_LGUI) || (keycode == SDLK_RGUI))
+ else if (scancode == SDL_SCANCODE_LGUI || scancode == SDL_SCANCODE_RGUI)
{
- phantom.ev.key.keysym.sym = (SDL_Keycode)UNIFIED_SUPER;
+ phantom.ev.key.keysym.scancode = static_cast(UNIFIED_SUPER);
unified[3] = (phantom.ev.type == SDL_KEYDOWN);
HotkeyInputHandler(&phantom);
}
// Check whether we have any hotkeys registered for this particular keycode
- if (g_HotkeyMap.find(keycode) == g_HotkeyMap.end())
+ if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end())
return (IN_PASS);
// Inhibit the dispatch of hotkey events caused by real keys (not fake mouse button
@@ -253,7 +233,7 @@
bool consoleCapture = false;
- if (g_Console && g_Console->IsActive() && keycode < SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES))
+ if (g_Console && g_Console->IsActive() && scancode < SDL_NUM_SCANCODES)
consoleCapture = true;
// Here's an interesting bit:
@@ -273,7 +253,7 @@
std::vector closestMapNames;
size_t closestMapMatch = 0;
- for (const SHotkeyMapping& hotkey : g_HotkeyMap[keycode])
+ for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode])
{
// If a key has been pressed, and this event triggers on its release, skip it.
// Similarly, if the key's been released and the event triggers on a keypress, skip it.
@@ -329,7 +309,7 @@
// -- KEYUP SECTION --
- for (const SHotkeyMapping& hotkey : g_HotkeyMap[keycode])
+ for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode])
{
// If it's a keydown event, won't cause HotKeyUps in anything that doesn't
// use this key negated => skip them
Index: source/ps/KeyName.h
===================================================================
--- source/ps/KeyName.h
+++ source/ps/KeyName.h
@@ -23,14 +23,16 @@
class CStr8;
-extern void InitKeyNameMap();
-extern CStr8 FindKeyName(int keycode);
-extern int FindKeyCode(const CStr8& keyname);
+extern SDL_Scancode FindScancode(const CStr& keyname);
+// Map a scancode to a locale-independent scancode name.
+extern CStr8 FindScancodeName(SDL_Scancode scancode);
+// Map a scancode to a locale-dependent key name (to show the user).
+extern CStr8 FindKeyName(SDL_Scancode scancode);
enum {
// Start sequential IDs in the right place
- // Pick a code which is greater than any keycodes used by SDL itself
- EXTRA_KEYS_BASE = SDL_SCANCODE_TO_KEYCODE(SDL_NUM_SCANCODES),
+ // Pick a code which is greater than any scancodes used by SDL itself
+ EXTRA_KEYS_BASE = SDL_NUM_SCANCODES,
// 'Keycodes' for the unified modifier keys
UNIFIED_SHIFT,
UNIFIED_CTRL,
Index: source/ps/KeyName.cpp
===================================================================
--- source/ps/KeyName.cpp
+++ source/ps/KeyName.cpp
@@ -24,215 +24,111 @@
#include "lib/external_libraries/libsdl.h"
#include "ps/CStr.h"
-#include