mapping joysticks like a d-pad

xkb
sezanzeb 4 years ago committed by sezanzeb
parent 06995ea3dc
commit 62d79e8aa7

@ -645,6 +645,7 @@
<items> <items>
<item id="mouse" translatable="yes">Mouse</item> <item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item> <item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
</items> </items>
<signal name="changed" handler="on_left_joystick_purpose_changed" swapped="no"/> <signal name="changed" handler="on_left_joystick_purpose_changed" swapped="no"/>
</object> </object>
@ -687,6 +688,7 @@
<items> <items>
<item id="mouse" translatable="yes">Mouse</item> <item id="mouse" translatable="yes">Mouse</item>
<item id="wheel" translatable="yes">Wheel</item> <item id="wheel" translatable="yes">Wheel</item>
<item id="buttons" translatable="yes">Buttons</item>
</items> </items>
<signal name="changed" handler="on_right_joystick_purpose_changed" swapped="no"/> <signal name="changed" handler="on_right_joystick_purpose_changed" swapped="no"/>
</object> </object>

@ -33,6 +33,7 @@ from keymapper.logger import logger
MOUSE = 'mouse' MOUSE = 'mouse'
WHEEL = 'wheel' WHEEL = 'wheel'
BUTTONS = 'buttons'
INITIAL_CONFIG = { INITIAL_CONFIG = {
'autoload': {}, 'autoload': {},

@ -26,10 +26,11 @@ import asyncio
import time import time
import evdev 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.logger import logger
from keymapper.config import MOUSE, WHEEL from keymapper.config import MOUSE, WHEEL
from keymapper.dev.utils import max_abs
# other events for ABS include buttons # other events for ABS include buttons
@ -117,20 +118,9 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
mapping : Mapping mapping : Mapping
the mapping object that configures the current injection the mapping object that configures the current injection
""" """
# since input_device.absinfo(EV_ABS).max is too new for ubuntu, max_value = max_abs(input_device)
# 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
if max_value == 0: if max_value == 0 or max_value is None:
return return
max_speed = ((max_value ** 2) * 2) ** 0.5 max_speed = ((max_value ** 2) * 2) ** 0.5

@ -162,6 +162,11 @@ class KeycodeInjector:
continue continue
for permutation in key.get_permutations(): 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 key_to_code[permutation.keys] = target_code
return key_to_code return key_to_code
@ -447,6 +452,15 @@ class KeycodeInjector:
) )
async for event in source.async_read_loop(): 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 abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
if event.code == evdev.ecodes.ABS_X: if event.code == evdev.ecodes.ABS_X:
self.abs_state[0] = event.value self.abs_state[0] = event.value
@ -458,15 +472,6 @@ class KeycodeInjector:
self.abs_state[3] = event.value self.abs_state[3] = event.value
continue continue
if should_map_event_as_btn(event.type, event.code):
handle_keycode(
self._key_to_code,
macros,
event,
uinput
)
continue
# forward the rest # forward the rest
uinput.write(event.type, event.code, event.value) uinput.write(event.type, event.code, event.value)
# this already includes SYN events, so need to syn here again # this already includes SYN events, so need to syn here again

@ -24,13 +24,16 @@
import itertools import itertools
import asyncio 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.logger import logger, is_debug
from keymapper.util import sign from keymapper.util import sign
from keymapper.mapping import DISABLE_CODE from keymapper.mapping import DISABLE_CODE
from keymapper.config import BUTTONS
from keymapper.dev.ev_abs_mapper import JOYSTICK 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 # maps mouse buttons to macro instances that have been executed. They may
@ -55,25 +58,44 @@ active_macros = {}
unreleased = {} 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. """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 Especially important for gamepad events, some of the buttons
require special rules. 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 return True
if ev_type == EV_ABS: is_mousepad = event.type == EV_ABS and 47 <= event.code <= 61
is_mousepad = 47 <= code <= 61 if is_mousepad:
if not is_mousepad and code not in JOYSTICK: 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 True
return False return False
@ -217,8 +239,9 @@ def handle_keycode(key_to_code, macros, event, uinput):
else: else:
log(key, 'releasing %s', target_code) log(key, 'releasing %s', target_code)
write(uinput, (target_type, target_code, 0)) write(uinput, (target_type, target_code, 0))
else: elif event.type != EV_ABS:
# disabled keys can still be used in combinations btw # ABS events might be spammed like crazy every time the position
# slightly changes
log(key, 'unexpected key up') log(key, 'unexpected key up')
# everything that can be released is released now # everything that can be released is released now

@ -25,13 +25,15 @@
import sys import sys
import select import select
import multiprocessing import multiprocessing
import threading
import evdev 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.logger import logger
from keymapper.util import sign from keymapper.util import sign
from keymapper.key import Key from keymapper.key import Key
from keymapper.state import custom_mapping
from keymapper.getdevices import get_devices, refresh_devices from keymapper.getdevices import get_devices, refresh_devices
from keymapper.dev.keycode_mapper import should_map_event_as_btn from keymapper.dev.keycode_mapper import should_map_event_as_btn
@ -128,13 +130,13 @@ class _KeycodeReader:
pipe = multiprocessing.Pipe() pipe = multiprocessing.Pipe()
self._pipe = pipe self._pipe = pipe
self._process = multiprocessing.Process(target=self._read_worker) self._process = threading.Thread(target=self._read_worker)
self._process.start() 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.""" """Write the event code into the pipe if it is a key-down press."""
# value: 1 for down, 0 for up, 2 for hold. # 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.') logger.debug('Pipe closed, reader stops.')
sys.exit(0) sys.exit(0)
@ -153,20 +155,22 @@ class _KeycodeReader:
# which breaks the current workflow. # which breaks the current workflow.
return return
if not should_map_event_as_btn(event.type, event.code): if not should_map_event_as_btn(device, event, custom_mapping):
return return
logger.spam( if not (event.value == 0 and event.type == EV_ABS):
'got (%s, %s, %s)', # avoid gamepad trigger spam
event.type, logger.spam(
event.code, 'got (%s, %s, %s)',
event.value event.type,
) event.code,
event.value
)
self._pipe[1].send(event) self._pipe[1].send(event)
def _read_worker(self): def _read_worker(self):
"""Process that reads keycodes and buffers them into a pipe.""" """Thread that reads keycodes and buffers them into a pipe."""
# using a process that blocks instead of read_one made it easier # using a thread that blocks instead of read_one made it easier
# to debug via the logs, because the UI was not polling properly # to debug via the logs, because the UI was not polling properly
# at some point which caused logs for events not to be written. # at some point which caused logs for events not to be written.
rlist = {device.fd: device for device in self.virtual_devices} rlist = {device.fd: device for device in self.virtual_devices}
@ -175,7 +179,7 @@ class _KeycodeReader:
while True: while True:
ready = select.select(rlist, [], [])[0] ready = select.select(rlist, [], [])[0]
for fd in ready: for fd in ready:
readable = rlist[fd] readable = rlist[fd] # a device or a pipe
if isinstance(readable, multiprocessing.connection.Connection): if isinstance(readable, multiprocessing.connection.Connection):
msg = readable.recv() msg = readable.recv()
if msg == CLOSE: if msg == CLOSE:
@ -185,7 +189,7 @@ class _KeycodeReader:
try: try:
for event in rlist[fd].read(): for event in rlist[fd].read():
self._consume_event(event) self._consume_event(event, readable)
except OSError: except OSError:
logger.debug( logger.debug(
'Device "%s" disappeared from the reader', 'Device "%s" disappeared from the reader',

@ -0,0 +1,50 @@
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# key-mapper - GUI for device specific keyboard mappings
# Copyright (C) 2020 sezanzeb <proxima@hip70890b.de>
#
# 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 <https://www.gnu.org/licenses/>.
"""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

@ -62,6 +62,7 @@ def to_string(key):
if ev_type != evdev.ecodes.EV_KEY: if ev_type != evdev.ecodes.EV_KEY:
direction = { direction = {
# D-Pad
(evdev.ecodes.ABS_HAT0X, -1): 'L', (evdev.ecodes.ABS_HAT0X, -1): 'L',
(evdev.ecodes.ABS_HAT0X, 1): 'R', (evdev.ecodes.ABS_HAT0X, 1): 'R',
(evdev.ecodes.ABS_HAT0Y, -1): 'U', (evdev.ecodes.ABS_HAT0Y, -1): 'U',
@ -74,6 +75,15 @@ def to_string(key):
(evdev.ecodes.ABS_HAT2X, 1): 'R', (evdev.ecodes.ABS_HAT2X, 1): 'R',
(evdev.ecodes.ABS_HAT2Y, -1): 'U', (evdev.ecodes.ABS_HAT2Y, -1): 'U',
(evdev.ecodes.ABS_HAT2Y, 1): 'D', (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)) }.get((code, value))
if direction is not None: if direction is not None:
key_name += f' {direction}' key_name += f' {direction}'
@ -115,13 +125,15 @@ class Row(Gtk.ListBoxRow):
def release(self): def release(self):
"""Tell the row that no keys are currently pressed down.""" """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. # A key was pressed and then released.
# Switch to the character. idle_add this so that the # Switch to the character. idle_add this so that the
# keycode event won't write into the character input as well. # keycode event won't write into the character input as well.
window = self.window.window window = self.window.window
GLib.idle_add(lambda: window.set_focus(self.character_input)) GLib.idle_add(lambda: window.set_focus(self.character_input))
self.state = IDLE
def get_key(self): def get_key(self):
"""Get the Key object from the left column. """Get the Key object from the left column.

@ -170,7 +170,6 @@ class Window:
self.get('gamepad_config').hide() self.get('gamepad_config').hide()
self.populate_devices() self.populate_devices()
self.select_newest_preset() self.select_newest_preset()
self.timeouts = [ self.timeouts = [

@ -32,7 +32,7 @@ def verify(key):
if not isinstance(key, tuple) or len(key) != 3: if not isinstance(key, tuple) or len(key) != 3:
raise ValueError(f'Expected key to be a 3-tuple, but got {key}') raise ValueError(f'Expected key to be a 3-tuple, but got {key}')
if sum([not isinstance(value, int) for value in key]) != 0: 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, # having shift in combinations modifies the configured output,

@ -249,3 +249,5 @@ class Mapping(ConfigBase):
existing = self._mapping.get(permutation) existing = self._mapping.get(permutation)
if existing is not None: if existing is not None:
return existing return existing
return None

@ -17,7 +17,7 @@
<text x="32.5" y="14">coverage</text> <text x="32.5" y="14">coverage</text>
</g> </g>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">91%</text> <text x="83.0" y="15" fill="#010101" fill-opacity=".3">92%</text>
<text x="82.0" y="14">91%</text> <text x="82.0" y="14">92%</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -27,7 +27,8 @@ requests.
- [x] start the daemon in such a way to not require usermod - [x] start the daemon in such a way to not require usermod
- [x] mapping a combined button press to a key - [x] mapping a combined button press to a key
- [x] add "disable" as mapping option - [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) - [ ] automatically load presets when devices get plugged in after login (udev)
- [ ] configure locale for preset to provide a different set of possible keys - [ ] configure locale for preset to provide a different set of possible keys
- [ ] user-friendly way to map btn_left - [ ] user-friendly way to map btn_left

@ -71,6 +71,16 @@ uinput_write_history = []
uinput_write_history_pipe = multiprocessing.Pipe() uinput_write_history_pipe = multiprocessing.Pipe()
pending_events = {} 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 # key-mapper is only interested in devices that have EV_KEY, add some
# random other stuff to test that they are ignored. # random other stuff to test that they are ignored.
phys_1 = 'usb-0000:03:00.0-1/input2' phys_1 = 'usb-0000:03:00.0-1/input2'
@ -197,6 +207,9 @@ class InputEvent:
self.sec = int(timestamp) self.sec = int(timestamp)
self.usec = timestamp % 1 * 1000000 self.usec = timestamp % 1 * 1000000
def __str__(self):
return f'InputEvent{self.t}'
def patch_paths(): def patch_paths():
from keymapper import paths from keymapper import paths
@ -222,6 +235,9 @@ def patch_select():
if len(pending_events.get(thing, [])) > 0: if len(pending_events.get(thing, [])) > 0:
ret.append(thing) ret.append(thing)
# avoid a fast iterating infinite loop in the reader
time.sleep(0.01)
return [ret, [], []] return [ret, [], []]
select.select = new_select select.select = new_select
@ -242,6 +258,13 @@ class InputDevice:
self.name = fixtures[path]['name'] self.name = fixtures[path]['name']
self.fd = self.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): def absinfo(self, *args):
raise Exception('Ubuntus version of evdev doesn\'t support .absinfo') raise Exception('Ubuntus version of evdev doesn\'t support .absinfo')
@ -264,6 +287,7 @@ class InputDevice:
return None return None
event = pending_events[self.name].pop(0) event = pending_events[self.name].pop(0)
self.log(event, 'read_one')
return event return event
def read_loop(self): def read_loop(self):
@ -272,7 +296,9 @@ class InputDevice:
return return
while len(pending_events[self.name]) > 0: 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) time.sleep(EVENT_READ_TIMEOUT)
async def async_read_loop(self): async def async_read_loop(self):
@ -281,7 +307,9 @@ class InputDevice:
return return
while len(pending_events[self.name]) > 0: 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) await asyncio.sleep(0.01)
def capabilities(self, absinfo=True): def capabilities(self, absinfo=True):
@ -375,6 +403,9 @@ def cleanup():
task.cancel() task.cancel()
os.system('pkill -f key-mapper-service') os.system('pkill -f key-mapper-service')
time.sleep(0.05)
if os.path.exists(tmp): if os.path.exists(tmp):
shutil.rmtree(tmp) shutil.rmtree(tmp)
@ -436,7 +467,6 @@ def main():
print() print()
unittest.TextTestResult.startTest = start_test unittest.TextTestResult.startTest = start_test
unittest.TextTestRunner(verbosity=2).run(testsuite) unittest.TextTestRunner(verbosity=2).run(testsuite)

@ -148,6 +148,7 @@ class TestDaemon(unittest.TestCase):
self.daemon = Daemon() self.daemon = Daemon()
preset_path = get_preset_path(device, preset) preset_path = get_preset_path(device, preset)
self.assertFalse(uinput_write_history_pipe[0].poll())
self.daemon.start_injecting(device, preset_path) self.daemon.start_injecting(device, preset_path)
self.assertTrue(self.daemon.is_injecting(device)) self.assertTrue(self.daemon.is_injecting(device))
@ -161,6 +162,13 @@ class TestDaemon(unittest.TestCase):
self.daemon.stop_injecting(device) self.daemon.stop_injecting(device)
self.assertFalse(self.daemon.is_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""" """injection 2"""
# -1234 will be normalized to -1 by the injector # -1234 will be normalized to -1 by the injector
@ -168,9 +176,6 @@ class TestDaemon(unittest.TestCase):
InputEvent(*ev_2, -1234) InputEvent(*ev_2, -1234)
] ]
time.sleep(0.2)
self.assertFalse(uinput_write_history_pipe[0].poll())
path = get_preset_path(device, preset) path = get_preset_path(device, preset)
self.daemon.start_injecting(device, path) self.daemon.start_injecting(device, path)

@ -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.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.config import config from keymapper.config import config
from keymapper.mapping import Mapping from keymapper.mapping import Mapping
from keymapper.dev.utils import max_abs
from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL from keymapper.dev.ev_abs_mapper import MOUSE, WHEEL
from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \ from tests.test import InputDevice, UInput, MAX_ABS, clear_write_history, \
@ -59,6 +60,10 @@ class TestEvAbsMapper(unittest.TestCase):
def tearDown(self): def tearDown(self):
cleanup() 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): def do(self, a, b, c, d, expectation):
"""Present fake values to the loop and observe the outcome.""" """Present fake values to the loop and observe the outcome."""
clear_write_history() clear_write_history()

@ -118,5 +118,6 @@ class TestGetDevices(unittest.TestCase):
EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY
})) }))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

@ -24,19 +24,19 @@ import time
import copy import copy
import evdev 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, \ from keymapper.dev.injector import is_numlock_on, set_numlock, \
ensure_numlock, KeycodeInjector, is_in_capabilities ensure_numlock, KeycodeInjector, is_in_capabilities
from keymapper.state import custom_mapping, system_mapping from keymapper.state import custom_mapping, system_mapping
from keymapper.mapping import Mapping, DISABLE_CODE, DISABLE_NAME 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.key import Key
from keymapper.dev.macros import parse from keymapper.dev.macros import parse
from tests.test import InputEvent, pending_events, fixtures, \ from tests.test import InputEvent, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \ EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, cleanup MAX_ABS, cleanup, read_write_history_pipe
class TestInjector(unittest.TestCase): class TestInjector(unittest.TestCase):
@ -393,10 +393,7 @@ class TestInjector(unittest.TestCase):
self.injector._msg_pipe[1].send(1234) self.injector._msg_pipe[1].send(1234)
# convert the write history to some easier to manage list # convert the write history to some easier to manage list
history = [] history = read_write_history_pipe()
while uinput_write_history_pipe[0].poll():
event = uinput_write_history_pipe[0].recv()
history.append((event.type, event.code, event.value))
# 1 event before the combination was triggered (+1 for release) # 1 event before the combination was triggered (+1 for release)
# 4 events for the macro # 4 events for the macro
@ -448,6 +445,69 @@ class TestInjector(unittest.TestCase):
numlock_after = is_numlock_on() numlock_after = is_numlock_on()
self.assertEqual(numlock_before, numlock_after) 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): def test_store_permutations_for_macros(self):
mapping = Mapping() mapping = Mapping()
ev_1 = (EV_KEY, 41, 1) ev_1 = (EV_KEY, 41, 1)

@ -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_9, 1)), '9')
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_SEMICOLON, 1)), 'SEMICOLON') 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 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, evdev.ecodes.BTN_A, 1)), 'BTN_A')
self.assertEqual(to_string(Key(EV_KEY, 1234, 1)), 'unknown') 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 # combinations
self.assertEqual(to_string(Key( self.assertEqual(to_string(Key(
@ -334,7 +336,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key)) keycode_reader._pipe[1].send(InputEvent(*sub_key))
# make the window consume the keycode # make the window consume the keycode
time.sleep(0.05) time.sleep(0.06)
gtk_iteration() gtk_iteration()
# holding down # holding down
@ -347,7 +349,7 @@ class TestIntegration(unittest.TestCase):
keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0)) keycode_reader._pipe[1].send(InputEvent(*sub_key[:2], 0))
# make the window consume the keycode # make the window consume the keycode
time.sleep(0.05) time.sleep(0.06)
gtk_iteration() gtk_iteration()
# released # released
@ -367,6 +369,8 @@ class TestIntegration(unittest.TestCase):
css_classes = row.get_style_context().list_classes() css_classes = row.get_style_context().list_classes()
self.assertNotIn('changed', css_classes) self.assertNotIn('changed', css_classes)
self.assertEqual(row.state, IDLE) self.assertEqual(row.state, IDLE)
# it won't switch the focus to the character input
self.assertTrue(row.keycode_input.is_focus())
return row return row
if char and code_first: if char and code_first:

@ -31,11 +31,11 @@ from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
active_macros, handle_keycode, unreleased, subsets, log active_macros, handle_keycode, unreleased, subsets, log
from keymapper.state import system_mapping from keymapper.state import system_mapping
from keymapper.dev.macros import parse 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 keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import InputEvent, UInput, uinput_write_history, \ from tests.test import InputEvent, UInput, uinput_write_history, \
cleanup cleanup, InputDevice, MAX_ABS
def wait(func, timeout=1.0): 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)) self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
def test_should_map_event_as_btn(self): def test_should_map_event_as_btn(self):
self.assertTrue(should_map_event_as_btn(EV_ABS, ABS_HAT0X)) device = InputDevice('/dev/input/event30')
self.assertTrue(should_map_event_as_btn(EV_KEY, KEY_A)) mapping = Mapping()
self.assertFalse(should_map_event_as_btn(EV_ABS, ABS_X))
self.assertFalse(should_map_event_as_btn(EV_REL, REL_X))
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_SLOT)) # the function name is so horribly long
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_TOOL_Y)) do = should_map_event_as_btn
self.assertFalse(should_map_event_as_btn(EV_ABS, ecodes.ABS_MT_POSITION_X))
"""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): def test_handle_keycode(self):
_key_to_code = { _key_to_code = {
((EV_KEY, 1, 1),): 101, ((EV_KEY, 1, 1),): 101,
((EV_KEY, 2, 1),): 102 ((EV_KEY, 2, 1),): 102
} }
uinput = UInput() uinput = UInput()

@ -24,11 +24,14 @@ import time
import multiprocessing import multiprocessing
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_COMMA, \ 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.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 CODE_1 = 100
@ -88,6 +91,28 @@ class TestReader(unittest.TestCase):
self.assertEqual(keycode_reader.read(), None) self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 3) 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): def test_ignore_btn_left(self):
# click events are ignored because overwriting them would render the # click events are ignored because overwriting them would render the
# mouse useless, but a mouse is needed to stop the injection # mouse useless, but a mouse is needed to stop the injection

Loading…
Cancel
Save