Index: binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/HotkeyPicker.js
@@ -0,0 +1,61 @@
+/**
+ * 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("+");
+ this.input.onCombination = keys => {
+ this.text.caption = translate(`Mapping: ${keys.join("+")}`);
+ this.input.blur();
+ this.combination = keys;
+
+ 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.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,157 @@
+class HotkeysPage
+{
+ constructor()
+ {
+ this.categories = this.getHotkeyCategories();
+ placeTabButtons(this.categories, 30, 4, index => this.selectPanel(index), ()=>{});
+ 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("hotkeyCancel").onPress = () => Engine.PopGuiPage();
+ Engine.GetGUIObjectByName("hotkeyReset").onPress = () => this.resetUserHotkeys();
+ Engine.GetGUIObjectByName("hotkeySave").onPress = () => {
+ this.saveUserHotkeys();
+ };
+ this.selectPanel(0);
+ }
+
+ 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");
+ let idx = panel.list_name.indexOf(picker.name);
+ if (idx === -1 || this.selectedPanel === -1)
+ return; // Shouldn't happen.
+
+ this.categories[this.selectedPanel].hotkeys[idx][1] = picker.combination;
+ this.setupPanel(this.categories[this.selectedPanel]);
+ }
+
+ selectPanel(index)
+ {
+ this.selectedPanel = index;
+ selectPanel(this.selectedPanel);
+ this.setupPanel(this.categories[this.selectedPanel]);
+ }
+
+ // Variant for tabulation
+ onPanelSetup(index)
+ {
+ this.selectedPanel = index;
+ this.setupPanel(this.categories[this.selectedPanel]);
+ }
+
+ setupPanel(category)
+ {
+ let panel = Engine.GetGUIObjectByName("hotkeyList");
+
+ panel.list_name = category.hotkeys.map(x => x[0]);
+ panel.list_mapping = category.hotkeys.map(x => x[1].sort(this.hotkeySort).join("+"));
+ panel.list = category.hotkeys.map(() => 0);
+ panel.list_data = category.hotkeys.map(() => 0);
+ }
+
+ getHotkeyCategories()
+ {
+ let hotkeys = Engine.GetHotkeyMap();
+ let categories = {
+ "other": {
+ "label": translate("General hotkeys"),
+ "tooltip": translate("Hotkeys that don't fit in another namespace."),
+ "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,
+ "tooltip": sprintf(translate("Hotkeys under the %s namespace"), 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();
+
+ // TODO SM52: Object.values()
+ let ret = [];
+ for (let cat in categories)
+ ret.push(categories[cat]);
+ return ret;
+ }
+
+ resetUserHotkeys()
+ {
+ messageBox(
+ 400, 200,
+ translate("Reset all hotkeys to default values?\nThis will only take place if you click save."),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [
+ () => {},
+ () => {
+ this.categories.forEach(cat => cat.hotkeys.forEach(([name, _]) => {
+ Engine.ConfigDB_RemoveValue("user", "hotkey." + name);
+ }));
+ this.categories = this.getHotkeyCategories();
+ this.selectPanel(this.selectedPanel);
+ }
+ ]);
+ }
+
+ saveUserHotkeys()
+ {
+ this.categories.forEach(cat => 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);
+ g_OnSelectTab = hotkeyPage.onPanelSetup.bind(hotkeyPage);
+}
Index: binaries/data/mods/public/gui/hotkeys/hotkeys.xml
===================================================================
--- /dev/null
+++ binaries/data/mods/public/gui/hotkeys/hotkeys.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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,10 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.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: 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/ConfigDB.cpp
===================================================================
--- source/ps/ConfigDB.cpp
+++ source/ps/ConfigDB.cpp
@@ -414,6 +414,8 @@
pos += sprintf(pos, "%s = ", p.first.c_str());
for (i = 0; i < p.second.size() - 1; ++i)
pos += sprintf(pos, "\"%s\", ", EscapeString(p.second[i]).c_str());
+
+ printf("printing %s\n", p.second[i].c_str());
pos += sprintf(pos, "\"%s\"\n", EscapeString(p.second[i]).c_str());
}
const size_t len = pos - (char*)buf.get();
Index: source/ps/GameSetup/GameSetup.cpp
===================================================================
--- source/ps/GameSetup/GameSetup.cpp
+++ source/ps/GameSetup/GameSetup.cpp
@@ -767,7 +767,7 @@
// wide characters. Peculiarly the string "UTF-8" seems to be acceptable
// despite not being a real locale, and it's conveniently language-agnostic,
// so use that.
- setlocale(LC_CTYPE, "UTF-8");
+ setlocale(LC_ALL, "UTF-8");
#endif
Index: source/ps/Hotkey.h
===================================================================
--- source/ps/Hotkey.h
+++ source/ps/Hotkey.h
@@ -33,15 +33,41 @@
#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)
+};
+
+// 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
@@ -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,99 @@
+/* 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;
+
+
+/**
+ * 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();
+}
+
+void JSI_Hotkey::RegisterScriptFunctions(const ScriptInterface& scriptInterface)
+{
+ scriptInterface.RegisterFunction("GetHotkeyMap");
+ scriptInterface.RegisterFunction("ReloadHotkeys");
+}
Index: source/scriptinterface/ScriptConversions.cpp
===================================================================
--- source/scriptinterface/ScriptConversions.cpp
+++ source/scriptinterface/ScriptConversions.cpp
@@ -158,16 +158,10 @@
template<> bool ScriptInterface::FromJSVal(JSContext* cx, JS::HandleValue v, std::string& out)
{
- JSAutoRequest rq(cx);
- WARN_IF_NOT(v.isString() || v.isNumber(), v); // allow implicit number conversions
- JS::RootedString str(cx, JS::ToString(cx, v));
- if (!str)
- FAIL("Argument must be convertible to a string");
- char* ch = JS_EncodeString(cx, str); // chops off high byte of each char16_t
- if (!ch)
- FAIL("JS_EncodeString failed"); // out of memory
- out.assign(ch, ch + JS_GetStringLength(str));
- JS_free(cx, ch);
+ std::wstring wideout;
+ if (!FromJSVal(cx, v, wideout))
+ return false;
+ out = CStrW(wideout).ToUTF8();
return true;
}
@@ -265,12 +259,7 @@
template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const std::string& val)
{
- JSAutoRequest rq(cx);
- JS::RootedString str(cx, JS_NewStringCopyN(cx, val.c_str(), val.length()));
- if (str)
- ret.setString(str);
- else
- ret.setUndefined();
+ ToJSVal(cx, ret, static_cast(CStr(val).FromUTF8()));
}
template<> void ScriptInterface::ToJSVal(JSContext* cx, JS::MutableHandleValue ret, const wchar_t* const& val)
Index: source/scriptinterface/tests/test_ScriptConversions.h
===================================================================
--- source/scriptinterface/tests/test_ScriptConversions.h
+++ source/scriptinterface/tests/test_ScriptConversions.h
@@ -51,7 +51,7 @@
}
template
- void roundtrip(const T& value, const char* expected)
+ T roundtrip(const T& value, const char* expected)
{
ScriptInterface script("Test", "Test", g_ScriptRuntime);
TS_ASSERT(script.LoadGlobalScripts());
@@ -71,6 +71,7 @@
T v2 = T();
TS_ASSERT(ScriptInterface::FromJSVal(cx, v1, v2));
TS_ASSERT_EQUALS(value, v2);
+ return v2;
}
template
@@ -148,9 +149,9 @@
roundtrip("", "\"\"");
roundtrip("test", "\"test\"");
- roundtrip("тест", "\"\\xD1\\x82\\xD0\\xB5\\xD1\\x81\\xD1\\x82\"");
+ TS_ASSERT_EQUALS(roundtrip("тест", "\"\\u0442\\u0435\\u0441\\u0442\""), "тест");
roundtrip(s1, "\"t\\x00st\"");
- roundtrip(s2, "\"\\xD1\\x82\\x00\\x00\\xD1\\x81\\xD1\\x82\"");
+ TS_ASSERT_EQUALS(roundtrip(s2, "\"\\u0442\\x00\\x00\\u0441\\u0442\""), s2);
roundtrip(L"", "\"\"");
roundtrip(L"test", "\"test\"");
@@ -251,4 +252,37 @@
CFixedVector3D u(fixed::Pi(), fixed::Zero(), fixed::FromInt(2));
call_prototype_function(u, v, "add", "({x:3.1415863037109375, y:3.1415863037109375, z:3})");
}
+
+ void test_strings()
+ {
+ {
+ std::string test("éè!§$-aezi134900°°©©¢¢ÇÇ‘{¶«¡Ç’[å»ÛÁØ");
+ TS_ASSERT_EQUALS(test, roundtrip(test, nullptr));
+ }
+ {
+ std::wstring test(L"éè!§$-aezi134900°°©©¢¢ÇÇ‘{¶«¡Ç’[å»ÛÁØ");
+ TS_ASSERT_EQUALS(test, roundtrip(test, nullptr));
+ }
+ ScriptInterface script("Test", "Test", g_ScriptRuntime);
+ TS_ASSERT(script.LoadGlobalScripts());
+ JSContext* cx = script.GetContext();
+ JSAutoRequest rq(cx);
+ {
+ // Fancier conversion: we store UTF8 and get UTF16 and vice-versa
+ std::string in_utf8("éè!§$-aezi134900°°©©¢¢ÇÇ‘{¶«¡Ç’[å»ÛÁØ");
+ std::wstring in_utf16(L"éè!§$-aezi134900°°©©¢¢ÇÇ‘{¶«¡Ç’[å»ÛÁØ");
+
+ JS::RootedValue v1(cx);
+ ScriptInterface::ToJSVal(cx, &v1, in_utf8);
+
+ std::wstring test_out_utf16;
+ TS_ASSERT(ScriptInterface::FromJSVal(cx, v1, test_out_utf16));
+ TS_ASSERT_EQUALS(test_out_utf16, in_utf16);
+ // Start clean.
+ ScriptInterface::ToJSVal(cx, &v1, in_utf16);
+ std::string test_out_utf8;
+ TS_ASSERT(ScriptInterface::FromJSVal(cx, v1, test_out_utf8));
+ TS_ASSERT_EQUALS(test_out_utf8, in_utf8);
+ }
+ }
};