diff --git a/data/key-mapper.glade b/data/key-mapper.glade
index cff5e825..43d3a6dc 100644
--- a/data/key-mapper.glade
+++ b/data/key-mapper.glade
@@ -645,6 +645,7 @@
- Mouse
- Wheel
+ - Buttons
@@ -687,6 +688,7 @@
- Mouse
- Wheel
+ - Buttons
diff --git a/keymapper/config.py b/keymapper/config.py
index 94eae4fb..f56e3458 100644
--- a/keymapper/config.py
+++ b/keymapper/config.py
@@ -33,6 +33,7 @@ from keymapper.logger import logger
MOUSE = 'mouse'
WHEEL = 'wheel'
+BUTTONS = 'buttons'
INITIAL_CONFIG = {
'autoload': {},
diff --git a/keymapper/dev/ev_abs_mapper.py b/keymapper/dev/ev_abs_mapper.py
index bac3f762..1efea74a 100644
--- a/keymapper/dev/ev_abs_mapper.py
+++ b/keymapper/dev/ev_abs_mapper.py
@@ -26,10 +26,11 @@ import asyncio
import time
import evdev
-from evdev.ecodes import EV_ABS, EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
+from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger
from keymapper.config import MOUSE, WHEEL
+from keymapper.dev.utils import max_abs
# other events for ABS include buttons
@@ -117,20 +118,9 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
mapping : Mapping
the mapping object that configures the current injection
"""
- # since input_device.absinfo(EV_ABS).max is too new for ubuntu,
- # figure out the max value via the capabilities
- absinfos = [
- entry[1] for entry in
- input_device.capabilities(absinfo=True)[EV_ABS]
- if isinstance(entry, tuple) and isinstance(entry[1], evdev.AbsInfo)
- ]
-
- if len(absinfos) == 0:
- return
-
- max_value = absinfos[0].max
+ max_value = max_abs(input_device)
- if max_value == 0:
+ if max_value == 0 or max_value is None:
return
max_speed = ((max_value ** 2) * 2) ** 0.5
diff --git a/keymapper/dev/injector.py b/keymapper/dev/injector.py
index 3f946fd5..77685fee 100644
--- a/keymapper/dev/injector.py
+++ b/keymapper/dev/injector.py
@@ -162,6 +162,11 @@ class KeycodeInjector:
continue
for permutation in key.get_permutations():
+ if permutation.keys[-1][-1] not in [-1, 1]:
+ logger.error(
+ 'Expected values to be -1 or 1 at this point: %s',
+ permutation.keys
+ )
key_to_code[permutation.keys] = target_code
return key_to_code
@@ -447,6 +452,15 @@ class KeycodeInjector:
)
async for event in source.async_read_loop():
+ if should_map_event_as_btn(source, event, self.mapping):
+ handle_keycode(
+ self._key_to_code,
+ macros,
+ event,
+ uinput
+ )
+ continue
+
if abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
if event.code == evdev.ecodes.ABS_X:
self.abs_state[0] = event.value
@@ -458,15 +472,6 @@ class KeycodeInjector:
self.abs_state[3] = event.value
continue
- if should_map_event_as_btn(event.type, event.code):
- handle_keycode(
- self._key_to_code,
- macros,
- event,
- uinput
- )
- continue
-
# forward the rest
uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again
diff --git a/keymapper/dev/keycode_mapper.py b/keymapper/dev/keycode_mapper.py
index 931d6f14..7c06ee2d 100644
--- a/keymapper/dev/keycode_mapper.py
+++ b/keymapper/dev/keycode_mapper.py
@@ -24,13 +24,16 @@
import itertools
import asyncio
+import math
-from evdev.ecodes import EV_KEY, EV_ABS
+from evdev.ecodes import EV_KEY, EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY
from keymapper.logger import logger, is_debug
from keymapper.util import sign
from keymapper.mapping import DISABLE_CODE
+from keymapper.config import BUTTONS
from keymapper.dev.ev_abs_mapper import JOYSTICK
+from keymapper.dev.utils import max_abs
# maps mouse buttons to macro instances that have been executed. They may
@@ -55,25 +58,44 @@ active_macros = {}
unreleased = {}
-def should_map_event_as_btn(ev_type, code):
+# a third of a quarter circle
+JOYSTICK_BUTTON_THRESHOLD = math.sin((math.pi / 2) / 3 * 1)
+
+
+# TODO test intuos again
+
+
+def should_map_event_as_btn(device, event, mapping):
"""Does this event describe a button.
+ If it does, this function will make sure its value is one of [-1, 0, 1],
+ so that it matches the possible values in a mapping object.
+
Especially important for gamepad events, some of the buttons
require special rules.
-
- Parameters
- ----------
- ev_type : int
- one of evdev.events
- code : int
- linux keycode
"""
- if ev_type == EV_KEY:
+ if event.type == EV_KEY:
return True
- if ev_type == EV_ABS:
- is_mousepad = 47 <= code <= 61
- if not is_mousepad and code not in JOYSTICK:
+ is_mousepad = event.type == EV_ABS and 47 <= event.code <= 61
+ if is_mousepad:
+ return False
+
+ if event.type == EV_ABS:
+ if event.code in JOYSTICK:
+ l_purpose = mapping.get('gamepad.joystick.left_purpose')
+ r_purpose = mapping.get('gamepad.joystick.right_purpose')
+ threshold = max_abs(device) * JOYSTICK_BUTTON_THRESHOLD
+ triggered = abs(event.value) > threshold
+
+ if event.code in [ABS_X, ABS_Y] and l_purpose == BUTTONS:
+ event.value = sign(event.value) if triggered else 0
+ return True
+
+ if event.code in [ABS_RX, ABS_RY] and r_purpose == BUTTONS:
+ event.value = sign(event.value) if triggered else 0
+ return True
+ else:
return True
return False
@@ -217,8 +239,9 @@ def handle_keycode(key_to_code, macros, event, uinput):
else:
log(key, 'releasing %s', target_code)
write(uinput, (target_type, target_code, 0))
- else:
- # disabled keys can still be used in combinations btw
+ elif event.type != EV_ABS:
+ # ABS events might be spammed like crazy every time the position
+ # slightly changes
log(key, 'unexpected key up')
# everything that can be released is released now
diff --git a/keymapper/dev/reader.py b/keymapper/dev/reader.py
index c097cd30..41ae5be0 100644
--- a/keymapper/dev/reader.py
+++ b/keymapper/dev/reader.py
@@ -25,13 +25,15 @@
import sys
import select
import multiprocessing
+import threading
import evdev
-from evdev.events import EV_KEY, EV_ABS
+from evdev.ecodes import EV_KEY, EV_ABS
from keymapper.logger import logger
from keymapper.util import sign
from keymapper.key import Key
+from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices, refresh_devices
from keymapper.dev.keycode_mapper import should_map_event_as_btn
@@ -128,13 +130,13 @@ class _KeycodeReader:
pipe = multiprocessing.Pipe()
self._pipe = pipe
- self._process = multiprocessing.Process(target=self._read_worker)
+ self._process = threading.Thread(target=self._read_worker)
self._process.start()
- def _consume_event(self, event):
+ def _consume_event(self, event, device):
"""Write the event code into the pipe if it is a key-down press."""
# value: 1 for down, 0 for up, 2 for hold.
- if self._pipe[1].closed:
+ if self._pipe is None or self._pipe[1].closed:
logger.debug('Pipe closed, reader stops.')
sys.exit(0)
@@ -153,20 +155,22 @@ class _KeycodeReader:
# which breaks the current workflow.
return
- if not should_map_event_as_btn(event.type, event.code):
+ if not should_map_event_as_btn(device, event, custom_mapping):
return
- logger.spam(
- 'got (%s, %s, %s)',
- event.type,
- event.code,
- event.value
- )
+ if not (event.value == 0 and event.type == EV_ABS):
+ # avoid gamepad trigger spam
+ logger.spam(
+ 'got (%s, %s, %s)',
+ event.type,
+ event.code,
+ event.value
+ )
self._pipe[1].send(event)
def _read_worker(self):
- """Process that reads keycodes and buffers them into a pipe."""
- # using a process that blocks instead of read_one made it easier
+ """Thread that reads keycodes and buffers them into a pipe."""
+ # using a thread that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices}
@@ -175,7 +179,7 @@ class _KeycodeReader:
while True:
ready = select.select(rlist, [], [])[0]
for fd in ready:
- readable = rlist[fd]
+ readable = rlist[fd] # a device or a pipe
if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv()
if msg == CLOSE:
@@ -185,7 +189,7 @@ class _KeycodeReader:
try:
for event in rlist[fd].read():
- self._consume_event(event)
+ self._consume_event(event, readable)
except OSError:
logger.debug(
'Device "%s" disappeared from the reader',
diff --git a/keymapper/dev/utils.py b/keymapper/dev/utils.py
new file mode 100644
index 00000000..12b01204
--- /dev/null
+++ b/keymapper/dev/utils.py
@@ -0,0 +1,50 @@
+#!/usr/bin/python3
+# -*- coding: utf-8 -*-
+# key-mapper - GUI for device specific keyboard mappings
+# Copyright (C) 2020 sezanzeb
+#
+# This file is part of key-mapper.
+#
+# key-mapper 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 3 of the License, or
+# (at your option) any later version.
+#
+# key-mapper 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 key-mapper. If not, see .
+
+
+"""Utility functions for all other modules in keymapper.dev"""
+
+
+import evdev
+from evdev.ecodes import EV_ABS
+
+
+def max_abs(device):
+ """Figure out the maximum value of EV_ABS events of that device.
+
+ Like joystick movements or triggers.
+ """
+ # since input_device.absinfo(EV_ABS).max is too new for (some?) ubuntus,
+ # figure out the max value via the capabilities
+ capabilities = device.capabilities(absinfo=True)
+
+ if EV_ABS not in capabilities:
+ return None
+
+ absinfos = [
+ entry[1] for entry in
+ capabilities[EV_ABS]
+ if isinstance(entry, tuple) and isinstance(entry[1], evdev.AbsInfo)
+ ]
+
+ if len(absinfos) == 0:
+ return None
+
+ return absinfos[0].max
diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py
index 657d14c3..e5416df7 100644
--- a/keymapper/gtk/row.py
+++ b/keymapper/gtk/row.py
@@ -62,6 +62,7 @@ def to_string(key):
if ev_type != evdev.ecodes.EV_KEY:
direction = {
+ # D-Pad
(evdev.ecodes.ABS_HAT0X, -1): 'L',
(evdev.ecodes.ABS_HAT0X, 1): 'R',
(evdev.ecodes.ABS_HAT0Y, -1): 'U',
@@ -74,6 +75,15 @@ def to_string(key):
(evdev.ecodes.ABS_HAT2X, 1): 'R',
(evdev.ecodes.ABS_HAT2Y, -1): 'U',
(evdev.ecodes.ABS_HAT2Y, 1): 'D',
+ # joystick
+ (evdev.ecodes.ABS_X, 1): 'R',
+ (evdev.ecodes.ABS_X, -1): 'L',
+ (evdev.ecodes.ABS_Y, 1): 'D',
+ (evdev.ecodes.ABS_Y, -1): 'U',
+ (evdev.ecodes.ABS_RX, 1): 'R',
+ (evdev.ecodes.ABS_RX, -1): 'L',
+ (evdev.ecodes.ABS_RY, 1): 'D',
+ (evdev.ecodes.ABS_RY, -1): 'U',
}.get((code, value))
if direction is not None:
key_name += f' {direction}'
@@ -115,13 +125,15 @@ class Row(Gtk.ListBoxRow):
def release(self):
"""Tell the row that no keys are currently pressed down."""
- if self.state == HOLDING:
+ if self.state == HOLDING and self.get_key() is not None:
# A key was pressed and then released.
# Switch to the character. idle_add this so that the
# keycode event won't write into the character input as well.
window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input))
+ self.state = IDLE
+
def get_key(self):
"""Get the Key object from the left column.
diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py
index d0232c57..ab025136 100755
--- a/keymapper/gtk/window.py
+++ b/keymapper/gtk/window.py
@@ -170,7 +170,6 @@ class Window:
self.get('gamepad_config').hide()
self.populate_devices()
-
self.select_newest_preset()
self.timeouts = [
diff --git a/keymapper/key.py b/keymapper/key.py
index 1ff8cbdc..9755701a 100644
--- a/keymapper/key.py
+++ b/keymapper/key.py
@@ -32,7 +32,7 @@ def verify(key):
if not isinstance(key, tuple) or len(key) != 3:
raise ValueError(f'Expected key to be a 3-tuple, but got {key}')
if sum([not isinstance(value, int) for value in key]) != 0:
- raise ValueError(f'Can only use numbers, but got {key}')
+ raise ValueError(f'Can only use integers, but got {key}')
# having shift in combinations modifies the configured output,
diff --git a/keymapper/mapping.py b/keymapper/mapping.py
index a1fbc52b..52d17ea9 100644
--- a/keymapper/mapping.py
+++ b/keymapper/mapping.py
@@ -249,3 +249,5 @@ class Mapping(ConfigBase):
existing = self._mapping.get(permutation)
if existing is not None:
return existing
+
+ return None
diff --git a/readme/coverage.svg b/readme/coverage.svg
index c6421901..d6e67f4f 100644
--- a/readme/coverage.svg
+++ b/readme/coverage.svg
@@ -17,7 +17,7 @@
coverage
- 91%
- 91%
+ 92%
+ 92%
\ No newline at end of file
diff --git a/readme/development.md b/readme/development.md
index ec412f42..6188561b 100644
--- a/readme/development.md
+++ b/readme/development.md
@@ -27,7 +27,8 @@ requests.
- [x] start the daemon in such a way to not require usermod
- [x] mapping a combined button press to a key
- [x] add "disable" as mapping option
-- [ ] mapping joystick directions as buttons, making it act like a D-Pad
+- [x] mapping joystick directions as buttons, making it act like a D-Pad
+- [ ] mapping mouse wheel events to buttons
- [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] configure locale for preset to provide a different set of possible keys
- [ ] user-friendly way to map btn_left
diff --git a/tests/test.py b/tests/test.py
index 37bf420f..e8903386 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -71,6 +71,16 @@ uinput_write_history = []
uinput_write_history_pipe = multiprocessing.Pipe()
pending_events = {}
+
+def read_write_history_pipe():
+ """convert the write history from the pipe to some easier to manage list"""
+ history = []
+ while uinput_write_history_pipe[0].poll():
+ event = uinput_write_history_pipe[0].recv()
+ history.append((event.type, event.code, event.value))
+ return history
+
+
# key-mapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored.
phys_1 = 'usb-0000:03:00.0-1/input2'
@@ -197,6 +207,9 @@ class InputEvent:
self.sec = int(timestamp)
self.usec = timestamp % 1 * 1000000
+ def __str__(self):
+ return f'InputEvent{self.t}'
+
def patch_paths():
from keymapper import paths
@@ -222,6 +235,9 @@ def patch_select():
if len(pending_events.get(thing, [])) > 0:
ret.append(thing)
+ # avoid a fast iterating infinite loop in the reader
+ time.sleep(0.01)
+
return [ret, [], []]
select.select = new_select
@@ -242,6 +258,13 @@ class InputDevice:
self.name = fixtures[path]['name']
self.fd = self.name
+ def log(self, key, msg):
+ print(
+ f'\033[90m' # color
+ f'{msg} "{self.name}" "{self.phys}" {key}'
+ '\033[0m' # end style
+ )
+
def absinfo(self, *args):
raise Exception('Ubuntus version of evdev doesn\'t support .absinfo')
@@ -264,6 +287,7 @@ class InputDevice:
return None
event = pending_events[self.name].pop(0)
+ self.log(event, 'read_one')
return event
def read_loop(self):
@@ -272,7 +296,9 @@ class InputDevice:
return
while len(pending_events[self.name]) > 0:
- yield pending_events[self.name].pop(0)
+ result = pending_events[self.name].pop(0)
+ self.log(result, 'read_loop')
+ yield result
time.sleep(EVENT_READ_TIMEOUT)
async def async_read_loop(self):
@@ -281,7 +307,9 @@ class InputDevice:
return
while len(pending_events[self.name]) > 0:
- yield pending_events[self.name].pop(0)
+ result = pending_events[self.name].pop(0)
+ self.log(result, 'async_read_loop')
+ yield result
await asyncio.sleep(0.01)
def capabilities(self, absinfo=True):
@@ -375,6 +403,9 @@ def cleanup():
task.cancel()
os.system('pkill -f key-mapper-service')
+
+ time.sleep(0.05)
+
if os.path.exists(tmp):
shutil.rmtree(tmp)
@@ -436,7 +467,6 @@ def main():
print()
unittest.TextTestResult.startTest = start_test
-
unittest.TextTestRunner(verbosity=2).run(testsuite)
diff --git a/tests/testcases/test_daemon.py b/tests/testcases/test_daemon.py
index 196b4890..f9dc5557 100644
--- a/tests/testcases/test_daemon.py
+++ b/tests/testcases/test_daemon.py
@@ -148,6 +148,7 @@ class TestDaemon(unittest.TestCase):
self.daemon = Daemon()
preset_path = get_preset_path(device, preset)
+ self.assertFalse(uinput_write_history_pipe[0].poll())
self.daemon.start_injecting(device, preset_path)
self.assertTrue(self.daemon.is_injecting(device))
@@ -161,6 +162,13 @@ class TestDaemon(unittest.TestCase):
self.daemon.stop_injecting(device)
self.assertFalse(self.daemon.is_injecting(device))
+ time.sleep(0.2)
+ try:
+ self.assertFalse(uinput_write_history_pipe[0].poll())
+ except AssertionError:
+ print(uinput_write_history_pipe[0].recv())
+ raise
+
"""injection 2"""
# -1234 will be normalized to -1 by the injector
@@ -168,9 +176,6 @@ class TestDaemon(unittest.TestCase):
InputEvent(*ev_2, -1234)
]
- time.sleep(0.2)
- self.assertFalse(uinput_write_history_pipe[0].poll())
-
path = get_preset_path(device, preset)
self.daemon.start_injecting(device, path)
diff --git a/tests/testcases/test_ev_abs_mapper.py b/tests/testcases/test_ev_abs_mapper.py
index d950b3c8..26d06408 100644
--- a/tests/testcases/test_ev_abs_mapper.py
+++ b/tests/testcases/test_ev_abs_mapper.py
@@ -27,6 +27,7 @@ from evdev.ecodes import EV_REL, REL_X, REL_Y, REL_WHEEL, REL_HWHEEL
from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.config import config
from keymapper.mapping import Mapping
+from keymapper.dev.utils import max_abs
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
@@ -59,6 +60,10 @@ class TestEvAbsMapper(unittest.TestCase):
def tearDown(self):
cleanup()
+ def test_max_abs(self):
+ self.assertEqual(max_abs(InputDevice('/dev/input/event30')), MAX_ABS)
+ self.assertIsNone(max_abs(InputDevice('/dev/input/event10')))
+
def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome."""
clear_write_history()
diff --git a/tests/testcases/test_getdevices.py b/tests/testcases/test_getdevices.py
index e46e66fa..41cb6e17 100644
--- a/tests/testcases/test_getdevices.py
+++ b/tests/testcases/test_getdevices.py
@@ -118,5 +118,6 @@ class TestGetDevices(unittest.TestCase):
EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY
}))
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/testcases/test_injector.py b/tests/testcases/test_injector.py
index 07d508ce..e8a328c7 100644
--- a/tests/testcases/test_injector.py
+++ b/tests/testcases/test_injector.py
@@ -24,19 +24,19 @@ import time
import copy
import evdev
-from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X
+from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, ABS_RX, ABS_Y
from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, KeycodeInjector, is_in_capabilities
from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME
-from keymapper.config import config
+from keymapper.config import config, BUTTONS
from keymapper.key import Key
from keymapper.dev.macros import parse
from tests.test import InputEvent, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
- MAX_ABS, cleanup
+ MAX_ABS, cleanup, read_write_history_pipe
class TestInjector(unittest.TestCase):
@@ -393,10 +393,7 @@ class TestInjector(unittest.TestCase):
self.injector._msg_pipe[1].send(1234)
# convert the write history to some easier to manage list
- history = []
- while uinput_write_history_pipe[0].poll():
- event = uinput_write_history_pipe[0].recv()
- history.append((event.type, event.code, event.value))
+ history = read_write_history_pipe()
# 1 event before the combination was triggered (+1 for release)
# 4 events for the macro
@@ -448,6 +445,69 @@ class TestInjector(unittest.TestCase):
numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after)
+ def test_joysticks_as_buttons(self):
+ y_up = (EV_ABS, ABS_Y, -MAX_ABS)
+ y_release = (EV_ABS, ABS_Y, MAX_ABS // 4)
+
+ rx_right = (EV_ABS, ABS_RX, MAX_ABS)
+ rx_release = (EV_ABS, ABS_RX, -MAX_ABS // 4)
+
+ custom_mapping.change(Key(*y_up[:2], -1), 'w')
+ custom_mapping.change(Key(*rx_right[:2], 1), 'k(d)')
+
+ system_mapping.clear()
+ code_w = 71
+ code_d = 74
+ system_mapping._set('w', code_w)
+ system_mapping._set('d', code_d)
+
+ def do_stuff():
+ if self.injector is not None:
+ self.injector.stop_injecting()
+ time.sleep(0.1)
+ while uinput_write_history_pipe[0].poll():
+ uinput_write_history_pipe[0].recv()
+
+ pending_events['gamepad'] = [
+ InputEvent(*y_up),
+ InputEvent(*rx_right),
+ InputEvent(*y_release),
+ InputEvent(*rx_release),
+ ]
+
+ self.injector = KeycodeInjector('gamepad', custom_mapping)
+ self.injector.start_injecting()
+ uinput_write_history_pipe[0].poll(timeout=1)
+ time.sleep(EVENT_READ_TIMEOUT * 10)
+ return read_write_history_pipe()
+
+ """purpose != buttons"""
+
+ history = do_stuff()
+ self.assertEqual(history.count((EV_KEY, code_w, 1)), 0)
+ self.assertEqual(history.count((EV_KEY, code_d, 1)), 0)
+ self.assertEqual(history.count((EV_KEY, code_w, 0)), 0)
+ self.assertEqual(history.count((EV_KEY, code_d, 0)), 0)
+
+ """left purpose buttons"""
+
+ custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
+ history = do_stuff()
+ self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
+ self.assertEqual(history.count((EV_KEY, code_d, 1)), 0)
+ self.assertEqual(history.count((EV_KEY, code_w, 0)), 1)
+ self.assertEqual(history.count((EV_KEY, code_d, 0)), 0)
+
+ """right purpose buttons"""
+
+ custom_mapping.remove('gamepad.joystick.right_purpose')
+ config.set('gamepad.joystick.right_purpose', BUTTONS)
+ history = do_stuff()
+ self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
+ self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)
+ self.assertEqual(history.count((EV_KEY, code_w, 0)), 1)
+ self.assertEqual(history.count((EV_KEY, code_d, 0)), 1)
+
def test_store_permutations_for_macros(self):
mapping = Mapping()
ev_1 = (EV_KEY, 41, 1)
diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py
index 49831114..5fd2a4bd 100644
--- a/tests/testcases/test_integration.py
+++ b/tests/testcases/test_integration.py
@@ -231,9 +231,11 @@ class TestIntegration(unittest.TestCase):
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_9, 1)), '9')
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON')
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)), 'ABS_HAT0X L')
- self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)), 'ABS_HAT0X R')
+ self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)), 'ABS_HAT0Y U')
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.BTN_A, 1)), 'BTN_A')
self.assertEqual(to_string(Key(EV_KEY, 1234, 1)), 'unknown')
+ self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_X, 1)), 'ABS_X R')
+ self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_RY, 1)), 'ABS_RY D')
# combinations
self.assertEqual(to_string(Key(
@@ -334,7 +336,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key))
# make the window consume the keycode
- time.sleep(0.05)
+ time.sleep(0.06)
gtk_iteration()
# holding down
@@ -347,7 +349,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
# make the window consume the keycode
- time.sleep(0.05)
+ time.sleep(0.06)
gtk_iteration()
# released
@@ -367,6 +369,8 @@ class TestIntegration(unittest.TestCase):
css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes)
self.assertEqual(row.state, IDLE)
+ # it won't switch the focus to the character input
+ self.assertTrue(row.keycode_input.is_focus())
return row
if char and code_first:
diff --git a/tests/testcases/test_keycode_mapper.py b/tests/testcases/test_keycode_mapper.py
index 0aee8245..8c3dc82e 100644
--- a/tests/testcases/test_keycode_mapper.py
+++ b/tests/testcases/test_keycode_mapper.py
@@ -31,11 +31,11 @@ from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
active_macros, handle_keycode, unreleased, subsets, log
from keymapper.state import system_mapping
from keymapper.dev.macros import parse
-from keymapper.config import config
+from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import InputEvent, UInput, uinput_write_history, \
- cleanup
+ cleanup, InputDevice, MAX_ABS
def wait(func, timeout=1.0):
@@ -192,19 +192,61 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
def test_should_map_event_as_btn(self):
- self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X))
- self.assertTrue(should_map_event_as_btn(EV_KEY, KEY_A))
- self.assertFalse(should_map_event_as_btn(EV_ABS, ABS_X))
- self.assertFalse(should_map_event_as_btn(EV_REL, REL_X))
+ device = InputDevice('/dev/input/event30')
+ mapping = Mapping()
- self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_SLOT))
- self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_TOOL_Y))
- self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_POSITION_X))
+ # the function name is so horribly long
+ do = should_map_event_as_btn
+
+ """D-Pad"""
+ self.assertTrue(do(device, InputEvent(EV_ABS, ABS_HAT0X, 1), mapping))
+ self.assertTrue(do(device, InputEvent(EV_ABS, ABS_HAT0X, -1), mapping))
+
+ """regular keys"""
+
+ self.assertTrue(do(device, InputEvent(EV_KEY, KEY_A, 1), mapping))
+ self.assertFalse(do(device, InputEvent(EV_ABS, ABS_X, 1), mapping))
+ self.assertFalse(do(device, InputEvent(EV_REL, REL_X, 1), mapping))
+
+ """mousepad events"""
+
+ self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_SLOT, 1), mapping))
+ self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_TOOL_Y, 1), mapping))
+ self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_MT_POSITION_X, 1), mapping))
+
+ """joysticks"""
+
+ self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_RX, 1234), mapping))
+ self.assertFalse(do(device, InputEvent(EV_ABS, ecodes.ABS_Y, -1), mapping))
+
+ mapping.set('gamepad.joystick.left_purpose', BUTTONS)
+
+ event = InputEvent(EV_ABS, ecodes.ABS_RX, MAX_ABS)
+ self.assertFalse(do(device, event, mapping))
+ self.assertEqual(event.value, MAX_ABS)
+ event = InputEvent(EV_ABS, ecodes.ABS_Y, -MAX_ABS)
+ self.assertTrue(do(device, event, mapping))
+ self.assertEqual(event.value, -1)
+ event = InputEvent(EV_ABS, ecodes.ABS_X, -MAX_ABS / 4)
+ self.assertTrue(do(device, event, mapping))
+ self.assertEqual(event.value, 0)
+
+ config.set('gamepad.joystick.right_purpose', BUTTONS)
+
+ event = InputEvent(EV_ABS, ecodes.ABS_RX, MAX_ABS)
+ self.assertTrue(do(device, event, mapping))
+ self.assertEqual(event.value, 1)
+ event = InputEvent(EV_ABS, ecodes.ABS_Y, MAX_ABS)
+ self.assertTrue(do(device, event, mapping))
+ self.assertEqual(event.value, 1)
+ event = InputEvent(EV_ABS, ecodes.ABS_X, MAX_ABS / 4)
+ self.assertTrue(do(device, event, mapping))
+ self.assertEqual(event.value, 0)
def test_handle_keycode(self):
_key_to_code = {
((EV_KEY, 1, 1),): 101,
- ((EV_KEY, 2, 1),): 102
+ ((EV_KEY, 2, 1),): 102
}
uinput = UInput()
diff --git a/tests/testcases/test_reader.py b/tests/testcases/test_reader.py
index 627f4681..be4063a2 100644
--- a/tests/testcases/test_reader.py
+++ b/tests/testcases/test_reader.py
@@ -24,11 +24,14 @@ import time
import multiprocessing
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_COMMA, \
- BTN_LEFT, BTN_TOOL_DOUBLETAP, ABS_Z
+ BTN_LEFT, BTN_TOOL_DOUBLETAP, ABS_Z, ABS_Y
from keymapper.dev.reader import keycode_reader
+from keymapper.state import custom_mapping
+from keymapper.config import BUTTONS, MOUSE
-from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT, cleanup
+from tests.test import InputEvent, pending_events, EVENT_READ_TIMEOUT, \
+ cleanup, MAX_ABS
CODE_1 = 100
@@ -88,6 +91,28 @@ class TestReader(unittest.TestCase):
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 3)
+ def test_reads_joysticks(self):
+ # if their purpose is "buttons"
+ custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
+ pending_events['gamepad'] = [
+ InputEvent(EV_ABS, ABS_Y, MAX_ABS)
+ ]
+ keycode_reader.start_reading('gamepad')
+ wait(keycode_reader._pipe[0].poll, 0.5)
+ self.assertEqual(keycode_reader.read(), (EV_ABS, ABS_Y, 1))
+ self.assertEqual(keycode_reader.read(), None)
+ self.assertEqual(len(keycode_reader._unreleased), 1)
+
+ keycode_reader._unreleased = {}
+ custom_mapping.set('gamepad.joystick.left_purpose', MOUSE)
+ pending_events['gamepad'] = [
+ InputEvent(EV_ABS, ABS_Y, MAX_ABS)
+ ]
+ keycode_reader.start_reading('gamepad')
+ time.sleep(0.1)
+ self.assertEqual(keycode_reader.read(), None)
+ self.assertEqual(len(keycode_reader._unreleased), 0)
+
def test_ignore_btn_left(self):
# click events are ignored because overwriting them would render the
# mouse useless, but a mouse is needed to stop the injection