Index: binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
@@ -0,0 +1,69 @@
+/**
+ * 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, mapping)
+ {
+ this.name = name;
+
+ this.window = Engine.GetGUIObjectByName("hotkeyPicker");
+ this.window.hidden = false;
+
+ Engine.GetGUIObjectByName("hotkeyPickerDesc").caption = '' +
+ `Entering hotkey for ${name}.\nCurrent hotkey mapping is ${mapping}`;
+
+ this.text = Engine.GetGUIObjectByName("hotkeyPickerText");
+ this.text.caption = translate("Start pressing keys to pick a hotkey.");
+ this.input = Engine.GetGUIObjectByName("hotkeyPickerInput");
+ this.input.focus();
+
+ this.combination = null;
+
+ this.input.onKeyChange = keys => this.text.caption = keys.join("+") + translate("\n(Hold for one second to register)");
+ this.input.onCombination = keys => {
+ this.text.caption = translate(`Mapping: ${keys.join("+")}`);
+ this.input.blur();
+ this.combination = keys;
+
+ let conflicts = (Engine.GetConflicts(this.combination) || [])
+ .map(translate).filter(name => name != this.name);
+ if (conflicts.length)
+ Engine.GetGUIObjectByName("hotkeyPickerConflicts").caption =
+ translate(`${coloredText("Conflicts with", "255 153 0")}: ${conflicts.join(", ")}`);
+
+ this.renderButtons();
+ };
+
+ this.renderButtons();
+
+ Engine.GetGUIObjectByName("hotkeyPickerCancel").onPress = () => {
+ this.combination = null;
+ onClose(this);
+ };
+ Engine.GetGUIObjectByName("hotkeyPickerAccept").onPress = () => onClose(this);
+ Engine.GetGUIObjectByName("hotkeyPickerRepick").onPress = () => {
+ this.input.focus();
+ this.combination = null;
+ this.text.caption = translate("Start pressing keys to pick a hotkey.");
+ Engine.GetGUIObjectByName("hotkeyPickerConflicts").caption = "";
+ this.renderButtons();
+ };
+
+ this.combination = null;
+ this.input.focus();
+ }
+
+ close()
+ {
+ this.window.hidden = true;
+ this.input.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,186 @@
+class HotkeysPage
+{
+ constructor()
+ {
+ Engine.GetGUIObjectByName("hotkeyList").onMouseLeftDoubleClickItem = () => {
+ let idx = Engine.GetGUIObjectByName("hotkeyList").selected;
+ new HotkeyPicker(
+ this.onHotkeyPicked.bind(this),
+ Engine.GetGUIObjectByName("hotkeyList").list_name[idx],
+ Engine.GetGUIObjectByName("hotkeyList").list_mapping[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.categories = this.getHotkeyCategories();
+ 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 absurdly 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.formatHotkeyCombination(x[1]));
+ hotkeyList.list = hotkeys.map(() => 0);
+ hotkeyList.list_data = hotkeys.map(() => 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.formatHotkeyCombination(x[1]));
+ hotkeyList.list = flattened.map(() => 0);
+ hotkeyList.list_data = flattened.map(() => 0);
+ }
+ }
+
+ formatHotkeyCombination(comb)
+ {
+ let str = comb.sort(this.hotkeySort).join("+");
+ return str.replace('\\', '\\\\').replace('[', '\\[');
+ }
+
+ hotkeySort(a, b)
+ {
+ const specialKeys = ["Shift", "Alt", "Ctrl", "Super", "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 });
+ }
+
+ onHotkeyPicked(picker)
+ {
+ picker.close();
+ if (!picker.combination)
+ return;
+ let panel = Engine.GetGUIObjectByName("hotkeyList");
+ for (let cat in this.categories)
+ {
+ let idx = this.categories[cat].hotkeys.findIndex(([name, mapping]) => name == picker.name);
+ if (idx === -1)
+ continue;
+ this.categories[cat].hotkeys[idx][1] = picker.combination;
+ }
+
+ this.setupHotkeyList();
+ }
+
+ getHotkeyCategories()
+ {
+ let hotkeys = Engine.GetHotkeyMap();
+ let categories = {
+ "other": {
+ "label": translate("Other hotkeys"),
+ "hotkeys": []
+ }
+ };
+ let n_categories = 1;
+ for (let hotkeyName in 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, hotkeys[hotkeyName]]);
+ }
+ // Remove categories that are too small to deserve a tab.
+ for (let cat of Object.keys(categories))
+ if (categories[cat].hotkeys.length < 6)
+ {
+ 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();
+
+ return 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.categories = this.getHotkeyCategories();
+ this.setupHotkeyList();
+ }
+ ]);
+ }
+
+ saveUserHotkeys()
+ {
+ for (let cat in this.categories)
+ {
+ this.categories[cat].hotkeys.forEach(([name, mapping]) => {
+ let keymap = mapping.sort(this.hotkeySort).join("+");
+ Engine.ConfigDB_RemoveValue("user", "hotkey." + name);
+ let val = Engine.ConfigDB_GetValue("user", "hotkey." + name);
+ if (keymap !== val)
+ Engine.ConfigDB_CreateValue("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,97 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hotkey
+
+
+
+
+
+
+
+
+
+ Start pressing keys to pick a hotkey.
+
+
+
+
+
+ Cancel
+
+
+
+ Pick new
+
+
+
+ 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/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,79 @@
+/* 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"
+
+/**
+ * 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)
+
+protected:
+ struct Key;
+
+ friend void ScriptInterface::ToJSVal>(JSContext *cx, JS::MutableHandleValue ret, const std::vector &val);
+ friend bool ScriptInterface::FromJSVal>(JSContext *cx, JS::HandleValue v, std::vector& out);
+
+ friend void ScriptInterface::ToJSVal(JSContext *cx, JS::MutableHandleValue ret, const Key& val);
+ friend bool ScriptInterface::FromJSVal(JSContext *cx, const JS::HandleValue val, Key& ret);
+
+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
+ {
+ SDL_Keycode code;
+ std::string name;
+ };
+ 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,177 @@
+/* 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, CStr(val.name).FromUTF8());
+}
+
+// Provided only because JSVAL_VECTOR requires it.
+template<> bool ScriptInterface::FromJSVal(JSContext *cx, const JS::HandleValue val, CHotkeyPicker::Key& ret)
+{
+ return ScriptInterface::FromJSVal(cx, val, ret.name);
+}
+
+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_Keycode keyCode;
+
+ 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)
+ keyCode = MOUSE_BASE + (int)ev->ev.button.button + 2;
+ else
+ keyCode = MOUSE_BASE + (int)ev->ev.button.button;
+ }
+ else
+ {
+ if (ev->ev.wheel.y > 0)
+ keyCode = MOUSE_WHEELUP;
+ else if (ev->ev.wheel.y < 0)
+ keyCode = MOUSE_WHEELDOWN;
+ else if (ev->ev.wheel.x > 0)
+ keyCode = MOUSE_X2;
+ else if (ev->ev.wheel.x < 0)
+ keyCode = MOUSE_X1;
+ else
+ return IN_HANDLED;
+ }
+ // Don't handle keys and mouse together
+ m_KeysPressed.clear();
+ CStr keyName = FindKeyName(keyCode);
+ m_KeysPressed.emplace_back(Key{SDL_Scancode{}, keyName});
+ // For mouse events, assume we immediately want to return.
+ FireEvent(EventNameCombination);
+
+ return IN_HANDLED;
+ }
+ case SDL_KEYDOWN:
+ case SDL_KEYUP:
+ {
+ SDL_Keycode keyCode = ev->ev.key.keysym.sym;
+ if (ev->ev.type == SDL_KEYDOWN)
+ {
+ std::vector::const_iterator it = \
+ std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&keyCode](Key& k) { return k.code == keyCode; });
+ // Can happen if multiple keys are mapped the same.
+ if (it != m_KeysPressed.end())
+ return IN_HANDLED;
+ CStr keyName = FindKeyName(keyCode);
+ m_KeysPressed.emplace_back(Key{keyCode, std::move(keyName)});
+ }
+ else
+ {
+ std::vector::const_iterator it = \
+ std::find_if(m_KeysPressed.begin(), m_KeysPressed.end(), [&keyCode](Key& k) { return k.code == keyCode; });
+ // 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/Hotkey.h
===================================================================
--- source/ps/Hotkey.h
+++ source/ps/Hotkey.h
@@ -33,15 +33,44 @@
#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.
+// Static_asserted to equal the real SDL_Keycode in Hotkey.cpp
+// We do this to avoid including SDL in this header.
+using SDL_Keycode_ = 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_Keycode_ code; // keycode 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 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.)
+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_same::value, "SDL_Keycode_ is not the same type as the real SDL_Keycode");
+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,10 +56,19 @@
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 the setting is a default setting, interpret it as a QWERTY scancode
+ // and map that automatically to whatever the local keyboard would use instead.
+ if (g_ConfigDB.GetValueNamespace(CFG_USER, configPair.first) < CFG_USER)
+ {
+ SDL_Scancode scanCode = SDL_GetScancodeFromName(it->c_str());
+ if (scanCode != SDL_SCANCODE_UNKNOWN)
+ mapping = SDL_GetKeyFromScancode(scanCode);
+ }
+
if (!mapping)
{
LOGWARNING("Hotkey mapping used invalid key '%s'", hotkey.c_str());
Index: source/ps/KeyName.cpp
===================================================================
--- source/ps/KeyName.cpp
+++ source/ps/KeyName.cpp
@@ -163,15 +163,15 @@
{ SDLK_SCROLLLOCK, "Scroll Lock", "ScrlLock" },
- { SDLK_RSHIFT, "Right Shift", "RightShift" },
- { SDLK_LSHIFT, "Left Shift", "LeftShift" },
- { SDLK_RCTRL, "Right Ctrl", "RightCtrl" },
- { SDLK_LCTRL, "Left Ctrl", "LeftCtrl" },
- { SDLK_RALT, "Right Alt", "RightAlt" },
- { SDLK_LALT, "Left Alt", "LeftAlt" },
+ { SDLK_RSHIFT, "Shift", "Right Shift" },
+ { SDLK_LSHIFT, "Shift", "Left Shift" },
+ { SDLK_RCTRL, "Ctrl", "Right Ctrl" },
+ { SDLK_LCTRL, "Ctrl", "Left Ctrl" },
+ { SDLK_RALT, "Alt", "Right Alt" },
+ { SDLK_LALT, "Alt", "Left Alt" },
- { SDLK_LGUI, "Left Super", "LeftWin" }, /* "Windows" keys */
- { SDLK_RGUI, "Right Super", "RightWin" },
+ { SDLK_LGUI, "Super", "Left Super" }, /* "Windows" keys */
+ { SDLK_RGUI, "Super", "Right Super" },
{ SDLK_MODE, "Alt Gr", "AltGr" },
@@ -202,6 +202,7 @@
void InitKeyNameMap()
{
+ keymap.clear();
for (const SKeycodeMapping* it = keycodeMapping; it->keycode != 0; ++it)
{
keymap.insert(std::pair(CStr(it->keyname).LowerCase(), it->keycode));
@@ -223,6 +224,10 @@
it = keymap.find(keyname.LowerCase());
if (it != keymap.end())
return it->second;
+ // Fallback to SDL.
+ SDL_Keycode keycode = SDL_GetKeyFromName(keyname.LowerCase().c_str());
+ if (keycode != SDLK_UNKNOWN)
+ return keycode;
return 0;
}
@@ -232,6 +237,11 @@
if (it->keycode == keycode)
return CStr(it->keyname);
+ // Fallback to SDL.
+ CStr name(SDL_GetKeyName(keycode));
+ if (name != "")
+ return name;
+
return CStr("Unknown");
}
Index: source/ps/scripting/JSInterface_Hotkey.h
===================================================================
--- /dev/null
+++ source/ps/scripting/JSInterface_Hotkey.h
@@ -0,0 +1,28 @@
+/* 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_JSI_HOTKEY
+#define INCLUDED_JSI_HOTKEY
+
+#include "scriptinterface/ScriptInterface.h"
+
+namespace JSI_Hotkey
+{
+ void RegisterScriptFunctions(const ScriptInterface& ScriptInterface);
+}
+
+#endif // INCLUDED_JSI_HOTKEY
Index: source/ps/scripting/JSInterface_Hotkey.cpp
===================================================================
--- /dev/null
+++ source/ps/scripting/JSInterface_Hotkey.cpp
@@ -0,0 +1,148 @@
+/* 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 "JSInterface_Hotkey.h"
+
+#include
+#include
+
+#include "lib/external_libraries/libsdl.h"
+#include "ps/Hotkey.h"
+#include "ps/KeyName.h"
+#include "scriptinterface/ScriptConversions.h"
+
+// extern std::map g_HotkeyMap;
+// extern std::map g_HotkeyStatus;
+
+//JSVAL_VECTOR(std::wstring);
+
+/**
+ * Convert an unordered map to a JS object, mapping keys to values.
+ * Assumes T to have a c_str() method that returns a const char*
+ * NB: this is unordered since no particular effort is made to preserve order.
+ * TODO: this could be moved to ScriptConversions.cpp if the need arises.
+ */
+template
+static void ToJSVal_unordered_map(JSContext* cx, JS::MutableHandleValue ret, const std::unordered_map& val)
+{
+ JSAutoRequest rq(cx);
+ JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+ if (!obj)
+ {
+ ret.setUndefined();
+ return;
+ }
+ for (const std::pair& item : val)
+ {
+ JS::RootedValue el(cx);
+ ScriptInterface::ToJSVal(cx, &el, item.second);
+ JS_SetProperty(cx, obj, item.first.c_str(), el);
+ }
+ ret.setObject(*obj);
+}
+
+template<>
+void ScriptInterface::ToJSVal>>(JSContext* cx, JS::MutableHandleValue ret, const std::unordered_map>& val)
+{
+ ToJSVal_unordered_map(cx, ret, val);
+}
+
+JS::Value GetHotkeyMap(ScriptInterface::CxPrivate* pCxPrivate)
+{
+ JSContext* cx = pCxPrivate->pScriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ JS::RootedValue hotkeyMap(cx);
+
+ std::unordered_map> hotkeys;
+ for (const std::pair& key : g_HotkeyMap)
+ for (const SHotkeyMapping& mapping : key.second)
+ {
+ if (hotkeys.count(mapping.name) != 0)
+ continue;
+ std::vector& keymap = hotkeys[mapping.name];
+ keymap.push_back(CStr(FindKeyName(key.first)).FromUTF8());
+ for (const SKey& secondary_key : mapping.requires)
+ keymap.push_back(CStr(FindKeyName(secondary_key.code)).FromUTF8());
+ }
+
+ pCxPrivate->pScriptInterface->ToJSVal(cx, &hotkeyMap, hotkeys);
+
+ return hotkeyMap;
+}
+
+void ReloadHotkeys(ScriptInterface::CxPrivate* UNUSED(pCxPrivate))
+{
+ UnloadHotkeys();
+ LoadHotkeys();
+}
+
+JS::Value GetConflicts(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue combination)
+{
+ ScriptInterface* scriptInterface = pCxPrivate->pScriptInterface;
+ JSContext* cx = scriptInterface->GetContext();
+ JSAutoRequest rq(cx);
+
+ std::vector keys;
+ if (!scriptInterface->FromJSVal(cx, combination, keys))
+ {
+ LOGERROR("Invalid hotkey combination");
+ return JS::NullValue();
+ }
+
+ if (keys.empty())
+ return JS::NullValue();
+
+ // Pick a random code as a starting point of the hotkeys (they are all equivalent).
+ SDL_Keycode_ startCode = FindKeyCode(keys.back());
+
+ std::map::const_iterator it = g_HotkeyMap.find(startCode);
+ if (it == g_HotkeyMap.end())
+ return JS::NullValue();
+
+ // Create a sorted vector with the remaining keys.
+ keys.pop_back();
+
+ std::set codes;
+ for (const std::string& key : keys)
+ codes.insert(SKey{ FindKeyCode(key), false });
+
+ std::vector conflicts;
+ // This isn't very efficient, but we shouldn't iterate too many hotkeys
+ // since we at least have one matching key.
+ for (const SHotkeyMapping& keymap : it->second)
+ {
+ std::set match(keymap.requires.begin(), keymap.requires.end());
+ if (codes == match)
+ conflicts.emplace_back(keymap.name);
+ }
+ if (conflicts.empty())
+ return JS::NullValue();
+
+ JS::RootedValue ret(cx);
+ scriptInterface->ToJSVal(cx, &ret, conflicts);
+ return ret;
+}
+
+void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
+{
+ scriptInterface.RegisterFunction("GetHotkeyMap");
+ scriptInterface.RegisterFunction("ReloadHotkeys");
+ scriptInterface.RegisterFunction("GetConflicts");
+}