fixes for graphics tablets, general improvements

pull/14/head
sezanzeb 4 years ago
parent e836f5f984
commit 4884697fa3

@ -42,9 +42,9 @@ def get_data_path(filename=''):
"""
global logged
source_path = None
source = None
try:
source_path = pkg_resources.require('key-mapper')[0].location
source = pkg_resources.require('key-mapper')[0].location
# failed in some ubuntu installations
except pkg_resources.DistributionNotFound:
pass
@ -53,17 +53,16 @@ def get_data_path(filename=''):
# prefix path for data
# https://docs.python.org/3/distutils/setupscript.html?highlight=package_data#installing-additional-files # noqa pylint: disable=line-too-long
data_path = None
data = None
# python3.8/dist-packages python3.7/site-packages, /usr/share,
# /usr/local/share, endless options
if source_path is not None:
if '-packages' not in source_path and 'python' not in source_path:
# probably installed with -e, running from the cloned git source
data_path = os.path.join(source_path, 'data')
if not os.path.exists(data_path):
if not logged:
logger.debug('-e, but data missing at "%s"', data_path)
data_path = None
if source and '-packages' not in source and 'python' not in source:
# probably installed with -e, running from the cloned git source
data = os.path.join(source, 'data')
if not os.path.exists(data):
if not logged:
logger.debug('-e, but data missing at "%s"', data)
data = None
candidates = [
'/usr/share/key-mapper',
@ -71,19 +70,19 @@ def get_data_path(filename=''):
os.path.join(site.USER_BASE, 'share/key-mapper'),
]
if data_path is None:
if data is None:
# try any of the options
for candidate in candidates:
if os.path.exists(candidate):
data_path = candidate
data = candidate
break
if data_path is None:
if data is None:
logger.error('Could not find the application data')
sys.exit(1)
if not logged:
logger.debug('Found data at "%s"', data_path)
logger.debug('Found data at "%s"', data)
logged = True
return os.path.join(data_path, filename)
return os.path.join(data, filename)

@ -25,22 +25,13 @@
import asyncio
import time
import evdev
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
from keymapper.dev.utils import get_max_abs
# other events for ABS include buttons
JOYSTICK = [
evdev.ecodes.ABS_X,
evdev.ecodes.ABS_Y,
evdev.ecodes.ABS_RX,
evdev.ecodes.ABS_RY,
]
# miniscule movements on the joystick should not trigger a mouse wheel event
WHEEL_THRESHOLD = 0.15
@ -49,8 +40,12 @@ def _write(device, ev_type, keycode, value):
"""Inject."""
# if the mouse won't move even though correct stuff is written here, the
# capabilities are probably wrong
device.write(ev_type, keycode, value)
device.syn()
try:
device.write(ev_type, keycode, value)
device.syn()
except OverflowError:
logger.error('OverflowError (%s, %s, %s)', ev_type, keycode, value)
pass
def accumulate(pending, current):
@ -106,6 +101,9 @@ def get_values(abs_state, left_purpose, right_purpose):
async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
"""Keep writing mouse movements based on the gamepad stick position.
Even if no new input event arrived because the joystick remained at
its position, this will keep injecting the mouse movement events.
Parameters
----------
abs_state : [int, int. int, int]
@ -118,11 +116,14 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
mapping : Mapping
the mapping object that configures the current injection
"""
max_value = max_abs(input_device)
max_value = get_max_abs(input_device)
if max_value == 0 or max_value is None:
if max_value in [0, 1, None]:
# not something that was intended for this
return
logger.debug('Max abs of "%s": %s', input_device.name, max_value)
max_speed = ((max_value ** 2) * 2) ** 0.5
# events only take ints, so a movement of 0.3 needs to add
@ -153,6 +154,18 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
right_purpose
)
out_of_bounds = [
val for val in [mouse_x, mouse_y, wheel_x, wheel_y]
if val > max_value
]
if len(out_of_bounds) > 0:
logger.error(
'Encountered inconsistent values: %s, max abs: %s',
out_of_bounds,
max_value
)
return
# mouse movements
if abs(mouse_x) > 0 or abs(mouse_y) > 0:
if non_linearity != 1:
@ -162,8 +175,8 @@ async def ev_abs_mapper(abs_state, input_device, keymapper_device, mapping):
else:
factor = 1
rel_x = mouse_x * factor * pointer_speed / max_value
rel_y = mouse_y * factor * pointer_speed / max_value
rel_x = (mouse_x / max_value) * factor * pointer_speed
rel_y = (mouse_y / max_value) * factor * pointer_speed
pending_x_rel, rel_x = accumulate(pending_x_rel, rel_x)
pending_y_rel, rel_y = accumulate(pending_y_rel, rel_y)
if rel_x != 0:

@ -33,9 +33,9 @@ from evdev.ecodes import EV_KEY, EV_ABS, EV_REL
from keymapper.logger import logger
from keymapper.getdevices import get_devices, map_abs_to_rel
from keymapper.dev.keycode_mapper import handle_keycode, \
should_map_event_as_btn
from keymapper.dev.ev_abs_mapper import ev_abs_mapper, JOYSTICK
from keymapper.dev.keycode_mapper import handle_keycode
from keymapper.dev import utils
from keymapper.dev.ev_abs_mapper import ev_abs_mapper
from keymapper.dev.macros import parse, is_this_a_macro
from keymapper.state import system_mapping
from keymapper.mapping import DISABLE_CODE
@ -261,9 +261,8 @@ class KeycodeInjector:
# to act like the device.
capabilities = input_device.capabilities(absinfo=False)
if len(self._key_to_code) > 0 or len(macros) > 0:
if capabilities.get(EV_KEY) is None:
capabilities[EV_KEY] = []
if (self._key_to_code or macros) and capabilities.get(EV_KEY) is None:
capabilities[EV_KEY] = []
# Furthermore, support all injected keycodes
for code in self._key_to_code.values():
@ -327,6 +326,9 @@ class KeycodeInjector:
Stuff is non-blocking by using asyncio in order to do multiple things
somewhat concurrently.
Use this function as starting point in a process. It creates
the loops needed to read and map events and keeps running them.
"""
# create a new event loop, because somehow running an infinite loop
# that sleeps on iterations (ev_abs_mapper) in one process causes
@ -387,21 +389,25 @@ class KeycodeInjector:
for macro in macros.values():
macro.set_handler(handler)
# keycode injection
coroutine = self._keycode_loop(macros, source, uinput, abs_to_rel)
coroutines.append(coroutine)
# actual reading of events
coroutines.append(self._event_consumer(
macros,
source,
uinput,
abs_to_rel
))
# mouse movement injection
# mouse movement injection based on the results of the
# event consumer
if abs_to_rel:
self.abs_state[0] = 0
self.abs_state[1] = 0
coroutine = ev_abs_mapper(
coroutines.append(ev_abs_mapper(
self.abs_state,
source,
uinput,
self.mapping
)
coroutines.append(coroutine)
))
if len(coroutines) == 0:
logger.error('Did not grab any device')
@ -430,14 +436,14 @@ class KeycodeInjector:
uinput.write(EV_KEY, code, value)
uinput.syn()
async def _keycode_loop(self, macros, source, uinput, abs_to_rel):
"""Inject keycodes for one of the virtual devices.
async def _event_consumer(self, macros, source, uinput, abs_to_rel):
"""Reads input events to inject keycodes or talk to the ev_abs_mapper.
Can be stopped by stopping the asyncio loop.
Parameters
----------
macros : (int, int) -> _Macro
macros : int: _Macro
macro with a handler that writes to the provided uinput
source : evdev.InputDevice
where to read keycodes from
@ -452,7 +458,7 @@ class KeycodeInjector:
)
async for event in source.async_read_loop():
if should_map_event_as_btn(source, event, self.mapping):
if utils.should_map_event_as_btn(source, event, self.mapping):
handle_keycode(
self._key_to_code,
macros,
@ -461,7 +467,9 @@ class KeycodeInjector:
)
continue
if abs_to_rel and event.type == EV_ABS and event.code in JOYSTICK:
is_joystick = event.type == EV_ABS and event.code in utils.JOYSTICK
if abs_to_rel and is_joystick:
# talks to the ev_abs_mapper via the abs_state array
if event.code == evdev.ecodes.ABS_X:
self.abs_state[0] = event.value
elif event.code == evdev.ecodes.ABS_Y:

@ -24,16 +24,11 @@
import itertools
import asyncio
import math
from evdev.ecodes import EV_KEY, EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY
from evdev.ecodes import EV_KEY, EV_ABS
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
@ -58,49 +53,6 @@ active_macros = {}
unreleased = {}
# 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.
"""
if event.type == EV_KEY:
return True
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
def is_key_down(event):
"""Is this event a key press."""
return event.value != 0
@ -163,6 +115,10 @@ def log(key, msg, *args):
def handle_keycode(key_to_code, macros, event, uinput):
"""Write mapped keycodes, forward unmapped ones and manage macros.
As long as the provided event is mapped it will handle it, it won't
check any type, code or capability anymore. Otherwise it forwards
it as it is.
Parameters
----------
key_to_code : dict
@ -181,15 +137,12 @@ def handle_keycode(key_to_code, macros, event, uinput):
# no need to forward or map them.
return
# normalize event numbers to one of -1, 0, +1. Otherwise mapping
# trigger values that are between 1 and 255 is not possible, because
# they might skip the 1 when pressed fast enough.
# The key used to index the mappings `key_to_code` and `macros`
key = (event.type, event.code, sign(event.value))
key = (event.type, event.code, event.value)
# the tuple of the actual input event. Used to forward the event if it is
# not mapped, and to index unreleased and active_macros
event_tuple = (event.type, event.code, sign(event.value))
event_tuple = (event.type, event.code, event.value)
type_code = (event.type, event.code)
# the triggering key-down has to be the last element in combination, all

@ -28,14 +28,13 @@ import multiprocessing
import threading
import evdev
from evdev.ecodes import EV_KEY, EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS, ABS_MISC
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
from keymapper.dev import utils
CLOSE = 1
@ -45,14 +44,21 @@ PRIORITIES = {
EV_ABS: 50,
}
FILTER_THRESHOLD = 0.01
def prioritize(events):
"""Return the event that is most likely desired to be mapped.
High absolute values (down) over low values (up), KEY over ABS.
KEY over ABS and everything over ABS_MISC.
"""
events = [
event for event in events
if event is not None
]
return sorted(events, key=lambda e: (
PRIORITIES[e.type],
not (e.type == EV_ABS and e.code == ABS_MISC),
abs(e.value)
))[-1]
@ -155,7 +161,7 @@ class _KeycodeReader:
# which breaks the current workflow.
return
if not should_map_event_as_btn(device, event, custom_mapping):
if not utils.should_map_event_as_btn(device, event, custom_mapping):
return
if not (event.value == 0 and event.type == EV_ABS):
@ -233,25 +239,31 @@ class _KeycodeReader:
# no duplicate down events (gamepad triggers)
continue
time = event.sec + event.usec / 1000000
delta = time - newest_time
if delta < FILTER_THRESHOLD:
if prioritize([newest_event, event]) != event:
# two events happened very close, probably some weird
# spam from the device. The wacom intuos 5 adds an
# ABS_MISC event to every button press, filter that out
logger.spam(
'Ignoring event (%s, %s, %s)',
event.type, event.code, event.value
)
continue
# the previous event is ignored
previous_without_value = (newest_event.type, newest_event.code)
if previous_without_value in self._unreleased:
del self._unreleased[previous_without_value]
self._unreleased[without_value] = (
event.type,
event.code,
sign(event.value)
event.value
)
time = event.sec + event.usec / 1000000
delta = time - newest_time
if delta < 0.01 and prioritize([newest_event, event]) != event:
# two events happened very close, probably some weird
# spam from the device. The wacom intuos 5 adds an
# ABS_MISC event to every button press, filter that out
logger.spam(
'Ignoring event (%s, %s, %s)',
event.type, event.code, event.value
)
continue
newest_event = event
newest_time = time

@ -22,11 +22,94 @@
"""Utility functions for all other modules in keymapper.dev"""
import math
import evdev
from evdev.ecodes import EV_ABS
from evdev.ecodes import EV_KEY, EV_ABS, ABS_X, ABS_Y, ABS_RX, ABS_RY, \
EV_REL, REL_WHEEL, REL_HWHEEL
from keymapper.logger import logger
from keymapper.config import BUTTONS
# other events for ABS include buttons
JOYSTICK = [
evdev.ecodes.ABS_X,
evdev.ecodes.ABS_Y,
evdev.ecodes.ABS_RX,
evdev.ecodes.ABS_RY,
]
# a third of a quarter circle
JOYSTICK_BUTTON_THRESHOLD = math.sin((math.pi / 2) / 3 * 1)
def sign(value):
"""Get the sign of the value, or 0 if 0."""
if value > 0:
return 1
if value < 0:
return -1
return 0
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 if needed.
If a new kind of event should be mappable to buttons, this is the place
to add it.
def max_abs(device):
Especially important for gamepad events, some of the buttons
require special rules.
"""
if event.type == EV_KEY:
return True
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')
max_abs = get_max_abs(device)
if max_abs is None:
logger.error(
'Got %s, but max_abs is %s',
(event.type, event.code, event.value), max_abs
)
return False
threshold = max_abs * 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:
# normalize event numbers to one of -1, 0, +1. Otherwise mapping
# trigger values that are between 1 and 255 is not possible,
# because they might skip the 1 when pressed fast enough.
event.value = sign(event.value)
return True
return False
def get_max_abs(device):
"""Figure out the maximum value of EV_ABS events of that device.
Like joystick movements or triggers.
@ -45,6 +128,9 @@ def max_abs(device):
]
if len(absinfos) == 0:
logger.error('Failed to get max abs of "%s"')
return None
return absinfos[0].max
max_abs = absinfos[0].max
return max_abs

@ -58,6 +58,13 @@ def map_abs_to_rel(capabilities):
# check for some random mousepad capability
return False
if evdev.ecodes.BTN_TOOL_BRUSH in capabilities.get(EV_KEY, []):
# a graphics tablet, not a gamepad
return False
if evdev.ecodes.BTN_STYLUS in capabilities.get(EV_KEY, []):
# another graphics tablet test
return False
if evdev.ecodes.ABS_X in abs_capabilities:
# can be a joystick or a mousepad (already handled), so it's
# a joystick

@ -353,10 +353,10 @@ class Window:
# they have already been read.
key = keycode_reader.read()
if isinstance(focused, Gtk.ToggleButton):
if not keycode_reader.are_keys_pressed():
row.release()
return True
keys_pressed = keycode_reader.are_keys_pressed()
if isinstance(focused, Gtk.ToggleButton) and not keys_pressed:
row.release()
return True
if key is None:
return True
@ -519,8 +519,7 @@ class Window:
@with_selected_device
def on_create_preset_clicked(self, _):
"""Create a new preset and select it."""
if custom_mapping.changed:
if unsaved_changes_dialog() == GO_BACK:
if custom_mapping.changed and unsaved_changes_dialog() == GO_BACK:
return
try:

@ -1,33 +0,0 @@
#!/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."""
def sign(value):
"""Get the sign of the value, or 0 if 0."""
if value > 0:
return 1
if value < 0:
return -1
return 0

@ -17,7 +17,7 @@
<text x="32.5" y="14">coverage</text>
</g>
<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">92%</text>
<text x="82.0" y="14">92%</text>
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">91%</text>
<text x="82.0" y="14">91%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

@ -198,15 +198,17 @@ class InputEvent:
self.code = code
self.value = value
# tuple shorthand
self.t = (type, code, value)
if timestamp is None:
timestamp = time.time()
self.sec = int(timestamp)
self.usec = timestamp % 1 * 1000000
@property
def t(self):
# tuple shorthand
return self.type, self.code, self.value
def __str__(self):
return f'InputEvent{self.t}'

@ -0,0 +1,98 @@
#!/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/>.
import unittest
from evdev import ecodes
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, KEY_A, ABS_X, \
EV_REL, REL_X, REL_WHEEL, REL_HWHEEL
from keymapper.config import config, BUTTONS
from keymapper.mapping import Mapping
from keymapper.dev import utils
from tests.test import InputEvent, InputDevice, MAX_ABS
class TestDevUtils(unittest.TestCase):
def test_max_abs(self):
self.assertEqual(utils.get_max_abs(InputDevice('/dev/input/event30')), MAX_ABS)
self.assertIsNone(utils.get_max_abs(InputDevice('/dev/input/event10')))
def test_should_map_event_as_btn(self):
device = InputDevice('/dev/input/event30')
mapping = Mapping()
# the function name is so horribly long
do = utils.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))
"""Mouse movements"""
self.assertFalse(do(device, InputEvent(EV_REL, REL_WHEEL, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_REL, REL_WHEEL, -1), mapping))
self.assertFalse(do(device, InputEvent(EV_REL, REL_HWHEEL, 1), mapping))
self.assertFalse(do(device, InputEvent(EV_REL, REL_HWHEEL, -1), mapping))
self.assertFalse(do(device, InputEvent(EV_REL, REL_X, -1), mapping))
"""regular keys and buttons"""
self.assertTrue(do(device, InputEvent(EV_KEY, KEY_A, 1), mapping))
self.assertTrue(do(device, InputEvent(EV_ABS, ABS_HAT0X, -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)

@ -27,7 +27,6 @@ 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, \
@ -60,10 +59,6 @@ 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()

@ -117,6 +117,14 @@ class TestGetDevices(unittest.TestCase):
self.assertFalse(map_abs_to_rel({
EV_KEY: [evdev.ecodes.ABS_X] # intentionally ABS_X (0) on EV_KEY
}))
self.assertFalse(map_abs_to_rel({
EV_ABS: [evdev.ecodes.ABS_X],
EV_KEY: [evdev.ecodes.BTN_TOOL_BRUSH]
}))
self.assertFalse(map_abs_to_rel({
EV_ABS: [evdev.ecodes.ABS_X],
EV_KEY: [evdev.ecodes.BTN_STYLUS]
}))
if __name__ == "__main__":

@ -24,19 +24,23 @@ import time
import copy
import evdev
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, ABS_RX, ABS_Y
from evdev.ecodes import EV_REL, EV_KEY, EV_ABS, ABS_HAT0X, BTN_LEFT, KEY_A
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, BUTTONS
from keymapper.config import config
from keymapper.key import Key
from keymapper.dev.macros import parse
from keymapper.dev import utils
from tests.test import InputEvent, pending_events, fixtures, \
EVENT_READ_TIMEOUT, uinput_write_history_pipe, \
MAX_ABS, cleanup, read_write_history_pipe
MAX_ABS, cleanup, read_write_history_pipe, InputDevice
original_smeab = utils.should_map_event_as_btn
class TestInjector(unittest.TestCase):
@ -59,6 +63,8 @@ class TestInjector(unittest.TestCase):
evdev.InputDevice.grab = grab_fail_twice
def tearDown(self):
utils.should_map_event_as_btn = original_smeab
if self.injector is not None:
self.injector.stop_injecting()
self.injector = None
@ -211,22 +217,36 @@ class TestInjector(unittest.TestCase):
path = self.new_gamepad
gamepad_template = copy.deepcopy(fixtures['/dev/input/event30'])
fixtures[path] = {
'name': 'gamepad 2',
'phys': 'abcd',
'info': '1234',
'name': 'gamepad 2', 'phys': 'abcd', 'info': '1234',
'capabilities': gamepad_template['capabilities']
}
del fixtures[path]['capabilities'][EV_KEY]
device, abs_to_rel = self.injector._prepare_device(path)
self.assertNotIn(EV_KEY, device.capabilities())
capabilities = self.injector._modify_capabilities(
{},
device,
abs_to_rel
{}, device, abs_to_rel
)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
"""gamepad with a btn_mouse capability existing"""
path = self.new_gamepad
gamepad_template = copy.deepcopy(fixtures['/dev/input/event30'])
fixtures[path] = {
'name': 'gamepad 3', 'phys': 'abcd', 'info': '1234',
'capabilities': gamepad_template['capabilities']
}
fixtures[path]['capabilities'][EV_KEY].append(BTN_LEFT)
fixtures[path]['capabilities'][EV_KEY].append(KEY_A)
device, abs_to_rel = self.injector._prepare_device(path)
capabilities = self.injector._modify_capabilities(
{}, device, abs_to_rel
)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
self.assertIn(evdev.ecodes.KEY_A, capabilities[EV_KEY])
"""gamepad with existing key capabilities, but not btn_mouse"""
path = '/dev/input/event30'
@ -234,11 +254,10 @@ class TestInjector(unittest.TestCase):
self.assertIn(EV_KEY, device.capabilities())
self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY])
capabilities = self.injector._modify_capabilities(
{},
device,
abs_to_rel
{}, device, abs_to_rel
)
self.assertIn(EV_KEY, capabilities)
self.assertGreater(len(capabilities), 1)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
def test_skip_unused_device(self):
@ -296,7 +315,6 @@ class TestInjector(unittest.TestCase):
pointer_speed = 80
config.set('gamepad.joystick.pointer_speed', pointer_speed)
# same for ABS, 0 for x, 1 for y
rel_x = evdev.ecodes.REL_X
rel_y = evdev.ecodes.REL_Y
@ -445,15 +463,21 @@ 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)
def test_any_funky_event_as_button(self):
# as long as should_map_event_as_btn says it should be a button,
# it will be.
EV_TYPE = 4531
CODE_1 = 754
CODE_2 = 4139
w_down = (EV_TYPE, CODE_1, -1)
w_up = (EV_TYPE, CODE_1, 0)
rx_right = (EV_ABS, ABS_RX, MAX_ABS)
rx_release = (EV_ABS, ABS_RX, -MAX_ABS // 4)
d_down = (EV_TYPE, CODE_2, 1)
d_up = (EV_TYPE, CODE_2, 0)
custom_mapping.change(Key(*y_up[:2], -1), 'w')
custom_mapping.change(Key(*rx_right[:2], 1), 'k(d)')
custom_mapping.change(Key(*w_down[:2], -1), 'w')
custom_mapping.change(Key(*d_down[:2], 1), 'k(d)')
system_mapping.clear()
code_w = 71
@ -463,25 +487,32 @@ class TestInjector(unittest.TestCase):
def do_stuff():
if self.injector is not None:
# discard the previous injector
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),
InputEvent(*w_down),
InputEvent(*d_down),
InputEvent(*w_up),
InputEvent(*d_up),
]
self.injector = KeycodeInjector('gamepad', custom_mapping)
# the injector will otherwise skip the device because
# the capabilities don't contain EV_TYPE
input = InputDevice('/dev/input/event30')
self.injector._prepare_device = lambda *args: (input, False)
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"""
"""no"""
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 0)
@ -489,19 +520,9 @@ class TestInjector(unittest.TestCase):
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"""
"""yes"""
custom_mapping.remove('gamepad.joystick.right_purpose')
config.set('gamepad.joystick.right_purpose', BUTTONS)
utils.should_map_event_as_btn = lambda *args: True
history = do_stuff()
self.assertEqual(history.count((EV_KEY, code_w, 1)), 1)
self.assertEqual(history.count((EV_KEY, code_d, 1)), 1)

@ -39,7 +39,7 @@ from gi.repository import Gtk, Gdk
from keymapper.state import custom_mapping, system_mapping, XMODMAP_FILENAME
from keymapper.paths import CONFIG_PATH, get_preset_path
from keymapper.config import config, WHEEL, MOUSE
from keymapper.dev.reader import keycode_reader
from keymapper.dev.reader import keycode_reader, FILTER_THRESHOLD
from keymapper.gtk.row import to_string, HOLDING, IDLE
from keymapper.dev import permissions
from keymapper.key import Key
@ -334,6 +334,7 @@ class TestIntegration(unittest.TestCase):
# press down all the keys of a combination
for sub_key in key:
keycode_reader._pipe[1].send(InputEvent(*sub_key))
time.sleep(FILTER_THRESHOLD * 2)
# make the window consume the keycode
time.sleep(0.06)

@ -23,19 +23,17 @@ import unittest
import asyncio
import time
from evdev import ecodes
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_A, ABS_X, \
EV_REL, REL_X, BTN_TL
from evdev.ecodes import EV_KEY, EV_ABS, ABS_HAT0X, ABS_HAT0Y, KEY_A, BTN_TL
from keymapper.dev.keycode_mapper import should_map_event_as_btn, \
active_macros, handle_keycode, unreleased, subsets, log
from keymapper.dev.keycode_mapper import active_macros, handle_keycode,\
unreleased, subsets, log
from keymapper.state import system_mapping
from keymapper.dev.macros import parse
from keymapper.config import config, BUTTONS
from keymapper.config import config
from keymapper.mapping import Mapping, DISABLE_CODE
from tests.test import InputEvent, UInput, uinput_write_history, \
cleanup, InputDevice, MAX_ABS
cleanup
def wait(func, timeout=1.0):
@ -191,58 +189,6 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(uinput_write_history[2].t, ev_3)
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 51, 0))
def test_should_map_event_as_btn(self):
device = InputDevice('/dev/input/event30')
mapping = Mapping()
# 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,
@ -781,37 +727,8 @@ class TestKeycodeMapper(unittest.TestCase):
self.assertEqual(history.count((code_2, 1)), 10)
self.assertEqual(history.count((code_2, 0)), 10)
def test_normalize(self):
# -1234 to -1, 5678 to 1, 0 to 0
key_1 = (EV_KEY, BTN_TL)
ev_1 = (*key_1, 5678)
ev_2 = (*key_1, 0)
# doesn't really matter if it makes sense, the A key reports
# negative values now.
key_2 = (EV_KEY, KEY_A)
ev_3 = (*key_2, -1234)
ev_4 = (*key_2, 0)
_key_to_code = {
((*key_1, 1),): 41,
((*key_2, -1),): 42
}
uinput = UInput()
handle_keycode(_key_to_code, {}, InputEvent(*ev_1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_2), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_3), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*ev_4), uinput)
self.assertEqual(len(uinput_write_history), 4)
self.assertEqual(uinput_write_history[0].t, (EV_KEY, 41, 1))
self.assertEqual(uinput_write_history[1].t, (EV_KEY, 41, 0))
self.assertEqual(uinput_write_history[2].t, (EV_KEY, 42, 1))
self.assertEqual(uinput_write_history[3].t, (EV_KEY, 42, 0))
def test_filter_trigger_spam(self):
# test_filter_duplicates
trigger = (EV_KEY, BTN_TL)
_key_to_code = {
@ -823,16 +740,16 @@ class TestKeycodeMapper(unittest.TestCase):
"""positive"""
for i in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, i), uinput)
for _ in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, 1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*trigger, 0), uinput)
self.assertEqual(len(uinput_write_history), 2)
"""negative"""
for i in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, -i), uinput)
for _ in range(1, 20):
handle_keycode(_key_to_code, {}, InputEvent(*trigger, -1), uinput)
handle_keycode(_key_to_code, {}, InputEvent(*trigger, 0), uinput)
self.assertEqual(len(uinput_write_history), 4)

@ -24,7 +24,7 @@ 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, ABS_Y
BTN_LEFT, BTN_TOOL_DOUBLETAP, ABS_Z, ABS_Y, ABS_MISC
from keymapper.dev.reader import keycode_reader
from keymapper.state import custom_mapping
@ -244,22 +244,25 @@ class TestReader(unittest.TestCase):
self.assertEqual(len(keycode_reader._unreleased), 1)
def test_prioritizing_2_normalize(self):
# furthermore, 1234 is 1 in the reader, because it probably is some
# sort of continuous trigger or joystick value
pending_events['device 1'] = [
# a value of 1234 becomes 1 in the reader in order to properly map
# it. Value like that are usually some sort of continuous trigger
# value and normal for some ev_abs events.
custom_mapping.set('gamepad.joystick.left_purpose', BUTTONS)
pending_events['gamepad'] = [
InputEvent(EV_ABS, ABS_HAT0X, 1, 1234.0000),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0000), # ignored
InputEvent(EV_KEY, KEY_COMMA, 1234, 1235.0010),
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0020), # ignored
InputEvent(EV_ABS, ABS_HAT0X, 1, 1235.0030) # ignored
InputEvent(EV_ABS, ABS_MISC, 1, 1235.0000), # ignored
InputEvent(EV_ABS, ABS_Y, MAX_ABS, 1235.0010),
InputEvent(EV_ABS, ABS_MISC, 1, 1235.0020), # ignored
InputEvent(EV_ABS, ABS_MISC, 1, 1235.0030) # ignored
# this time, don't release anything. the combination should
# ignore stuff as well.
]
keycode_reader.start_reading('device 1')
keycode_reader.start_reading('gamepad')
time.sleep(0.5)
wait(keycode_reader._pipe[0].poll, 0.5)
self.assertEqual(keycode_reader.read(), (
(EV_ABS, ABS_HAT0X, 1),
(EV_KEY, KEY_COMMA, 1)
(EV_ABS, ABS_Y, 1)
))
self.assertEqual(keycode_reader.read(), None)
self.assertEqual(len(keycode_reader._unreleased), 2)

Loading…
Cancel
Save