Browse Source

map keycodes instead of keysyms (compatibility with non-US keyboards)

Fabian Peter Hammerle 4 years ago
parent
commit
cbe1dee16a
5 changed files with 128 additions and 63 deletions
  1. 33 63
      rescriptoon/__init__.py
  2. 23 0
      rescriptoon/_keys.py
  3. 55 0
      rescriptoon/_mapping.py
  4. 6 0
      tests/_keys_test.py
  5. 11 0
      tests/_mapping_test.py

+ 33 - 63
rescriptoon/__init__.py

@@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import copy
+import enum
 import logging
 import os
 import select
@@ -24,8 +24,6 @@ import time
 import typing
 
 import Xlib.display
-from Xlib import XK, X, Xatom
-
 from rescriptoon._actions import (
     LowThrowAction,
     RewriteKeyEventAction,
@@ -33,48 +31,12 @@ from rescriptoon._actions import (
     ToggleOverlayAction,
 )
 from rescriptoon._keys import keysym_to_label
+from rescriptoon._mapping import get_keycode_action_mapping
 from rescriptoon._ui import SystemTrayUnavailable, TrayIcon
+from Xlib import XK, X, Xatom
 
 _TOONTOWN_ENGINE_WINDOW_NAME = "Toontown Rewritten"
 
-_KEYSYM_ACTION_MAPPINGS = {
-    # pylint: disable=no-member; false positive for XK.*
-    XK.XK_w: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=0),
-    XK.XK_a: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=0),
-    XK.XK_s: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=0),
-    XK.XK_d: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=0),
-    XK.XK_Control_L: RewriteKeyEventAction(
-        keysym=XK.XK_Control_L, target_engine_index=0
-    ),
-    XK.XK_v: LowThrowAction(target_engine_index=0),
-    XK.XK_o: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=1),
-    XK.XK_k: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=1),
-    XK.XK_l: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=1),
-    XK.XK_semicolon: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=1),
-    XK.XK_slash: RewriteKeyEventAction(keysym=XK.XK_Control_L, target_engine_index=1),
-    XK.XK_n: LowThrowAction(target_engine_index=1),
-    XK.XK_space: RewriteKeyEventAction(keysym=XK.XK_Control_L),
-    # TODO replace gag_name with enum
-    XK.XK_e: SelectGagAction(
-        gag_name="elephant trunk",
-        target_engine_index=0,
-        column_index=4,
-        factor_y=-0.047,
-    ),
-    XK.XK_i: SelectGagAction(
-        gag_name="elephant trunk",
-        target_engine_index=1,
-        column_index=4,
-        factor_y=-0.047,
-    ),
-    XK.XK_f: SelectGagAction(
-        gag_name="foghorn", target_engine_index=0, column_index=5, factor_y=-0.047
-    ),
-    XK.XK_j: SelectGagAction(
-        gag_name="foghorn", target_engine_index=1, column_index=5, factor_y=-0.047
-    ),
-}
-
 
 def _x_walk_children_windows(
     parent_window: "Xlib.display.Window",
@@ -101,13 +63,13 @@ def _x_wait_for_event(xdisplay, timeout_seconds):
 class Overlay:
     def __init__(self, toggle_keysym: int):
         self._xdisplay = Xlib.display.Display()
-        self._toggle_keysym = toggle_keysym
-        self._keysym_mappings = copy.deepcopy(_KEYSYM_ACTION_MAPPINGS)
-        if self._toggle_keysym in self._keysym_mappings:
+        self._toggle_keycode: int = self._xdisplay.keysym_to_keycode(toggle_keysym)
+        self._keycode_mappings = get_keycode_action_mapping()
+        if self._toggle_keycode in self._keycode_mappings:
             logging.warning(
                 "ignoring mapping for toggle key %s", keysym_to_label(toggle_keysym)
             )
-        self._keysym_mappings[self._toggle_keysym] = ToggleOverlayAction()
+        self._keycode_mappings[self._toggle_keycode] = ToggleOverlayAction()
         self._active_key_registry = {}
         self._enabled = False
         self._engine_windows = None
@@ -139,10 +101,11 @@ class Overlay:
         logging.debug("engine window ids %r", [hex(w.id) for w in self._engine_windows])
         if not self._engine_windows:
             raise Exception("no toontown window found")
-        self._grab_key(self.xdisplay.keysym_to_keycode(self._toggle_keysym),)
+        self._grab_key(self._toggle_keycode)
         print("key bindings:")
-        for keysym, action in self._keysym_mappings.items():
-            print("{}: {}".format(keysym_to_label(keysym), action.description))
+        for keycode, action in self._keycode_mappings.items():
+            keysym: int = self._xdisplay.keycode_to_keysym(keycode, index=0)
+            print("{}: {}".format(keysym_to_label(keysym), action.description,))
         self.enable()
         while self._engine_windows_open:
             while self.xdisplay.pending_events():
@@ -172,41 +135,48 @@ class Overlay:
         ],
     ) -> None:
         self._update_active_key_registry(xkeyevent)
-        keysym_in = self.xdisplay.keycode_to_keysym(xkeyevent.detail, index=0,)
+        keycode_in = xkeyevent.detail
         try:
-            action = self._keysym_mappings[keysym_in]
+            action = self._keycode_mappings[keycode_in]
         except KeyError:
-            logging.warning("received key event of unmapped keysym %d", keysym_in)
+            keysym_in = self.xdisplay.keycode_to_keysym(keycode_in, index=0,)
+            logging.warning(
+                "received key event of unmapped key %d (%s)",
+                keycode_in,
+                keysym_to_label(keysym_in),
+            )
             return
         action.execute(self, xkeyevent)
 
     @property
-    def _toggle_keysym_name(self) -> str:
-        return XK.keysym_to_string(self._toggle_keysym)
+    def _toggle_keysym(self) -> int:
+        return self._xdisplay.keycode_to_keysym(self._toggle_keycode, index=0)
+
+    @property
+    def _toggle_key_label(self) -> str:
+        return keysym_to_label(self._toggle_keysym)
 
     def enable(self) -> None:
-        for keysym in self._keysym_mappings.keys():
-            if keysym != self._toggle_keysym:
-                self._grab_key(self.xdisplay.keysym_to_keycode(keysym),)
+        for keycode in self._keycode_mappings.keys():
+            if keycode != self._toggle_keycode:
+                self._grab_key(keycode)
         self._enabled = True
         self._draw_tray_icon()
         logging.info(
-            "rescriptoon is now enabled. press %s to disable.",
-            self._toggle_keysym_name,
+            "rescriptoon is now enabled. press %s to disable.", self._toggle_key_label,
         )
 
     def _disable(self) -> None:
-        for keysym in self._keysym_mappings.keys():
-            if keysym != self._toggle_keysym:
-                self._ungrab_key(self.xdisplay.keysym_to_keycode(keysym),)
+        for keycode in self._keycode_mappings.keys():
+            if keycode != self._toggle_keycode:
+                self._ungrab_key(keycode)
         self._enabled = False
         self._draw_tray_icon()
 
     def disable(self) -> None:
         self._disable()
         logging.info(
-            "rescriptoon is now disabled. press %s to enable.",
-            self._toggle_keysym_name,
+            "rescriptoon is now disabled. press %s to enable.", self._toggle_key_label,
         )
 
     @property

+ 23 - 0
rescriptoon/_keys.py

@@ -16,10 +16,33 @@ You should have received a copy of the GNU General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
+import enum
 import typing
 
 from Xlib import XK
 
+
+class USKeyCode(enum.Enum):
+    w = 25
+    e = 26
+    i = 31
+    o = 32
+    ctrl_left = 37
+    a = 38
+    s = 39
+    d = 40
+    f = 41
+    j = 44
+    k = 45
+    l = 46
+    semicolon = 47
+    grave = 49
+    v = 55
+    n = 57
+    slash = 61
+    space = 65
+
+
 _KEYSYM_LABELS = {
     # pylint: disable=no-member; false positive
     XK.XK_Control_L: "left ctrl",

+ 55 - 0
rescriptoon/_mapping.py

@@ -0,0 +1,55 @@
+import typing
+
+from Xlib import XK
+
+from rescriptoon._actions import LowThrowAction, RewriteKeyEventAction, SelectGagAction
+from rescriptoon._keys import USKeyCode
+
+_DEFAULT_KEYCODE_ACTION_MAPPING = {
+    # pylint: disable=no-member; false positive for XK.*
+    USKeyCode.w: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=0),
+    USKeyCode.a: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=0),
+    USKeyCode.s: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=0),
+    USKeyCode.d: RewriteKeyEventAction(keysym=XK.XK_Right, target_engine_index=0),
+    USKeyCode.ctrl_left: RewriteKeyEventAction(
+        keysym=XK.XK_Control_L, target_engine_index=0
+    ),
+    USKeyCode.v: LowThrowAction(target_engine_index=0),
+    USKeyCode.o: RewriteKeyEventAction(keysym=XK.XK_Up, target_engine_index=1),
+    USKeyCode.k: RewriteKeyEventAction(keysym=XK.XK_Left, target_engine_index=1),
+    USKeyCode.l: RewriteKeyEventAction(keysym=XK.XK_Down, target_engine_index=1),
+    USKeyCode.semicolon: RewriteKeyEventAction(
+        keysym=XK.XK_Right, target_engine_index=1
+    ),
+    USKeyCode.slash: RewriteKeyEventAction(
+        keysym=XK.XK_Control_L, target_engine_index=1
+    ),
+    USKeyCode.n: LowThrowAction(target_engine_index=1),
+    USKeyCode.space: RewriteKeyEventAction(keysym=XK.XK_Control_L),
+    # TODO replace gag_name with enum
+    USKeyCode.e: SelectGagAction(
+        gag_name="elephant trunk",
+        target_engine_index=0,
+        column_index=4,
+        factor_y=-0.047,
+    ),
+    USKeyCode.i: SelectGagAction(
+        gag_name="elephant trunk",
+        target_engine_index=1,
+        column_index=4,
+        factor_y=-0.047,
+    ),
+    USKeyCode.f: SelectGagAction(
+        gag_name="foghorn", target_engine_index=0, column_index=5, factor_y=-0.047
+    ),
+    USKeyCode.j: SelectGagAction(
+        gag_name="foghorn", target_engine_index=1, column_index=5, factor_y=-0.047
+    ),
+}
+
+
+def get_keycode_action_mapping() -> typing.Dict:
+    return {
+        us_keycode.value: action
+        for us_keycode, action in _DEFAULT_KEYCODE_ACTION_MAPPING.items()
+    }

+ 6 - 0
tests/_keys_test.py

@@ -24,6 +24,12 @@ from Xlib import XK
 import rescriptoon._keys
 
 
+def test_us_keycode():
+    us_keycode = rescriptoon._keys.USKeyCode.w
+    assert us_keycode.name == "w"
+    assert us_keycode.value == 25
+
+
 @pytest.mark.parametrize(
     ("keysym", "label"),
     [

+ 11 - 0
tests/_mapping_test.py

@@ -0,0 +1,11 @@
+# pylint: disable=protected-access
+from rescriptoon._actions import RewriteKeyEventAction
+from rescriptoon._mapping import get_keycode_action_mapping
+from Xlib import XK
+
+
+def test_get_keycode_action_mapping():
+    mapping = get_keycode_action_mapping()
+    assert isinstance(mapping[25], RewriteKeyEventAction)
+    # pylint: disable=no-member; false positive for XK.*
+    assert mapping[25]._keysym == XK.XK_Up