mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-16 06:12:58 +00:00
mapping joysticks like a d-pad
This commit is contained in:
parent
e20f3fbbf9
commit
bc0818a8e5
@ -645,6 +645,7 @@
|
||||
<items>
|
||||
<item id="mouse" translatable="yes">Mouse</item>
|
||||
<item id="wheel" translatable="yes">Wheel</item>
|
||||
<item id="buttons" translatable="yes">Buttons</item>
|
||||
</items>
|
||||
<signal name="changed" handler="on_left_joystick_purpose_changed" swapped="no"/>
|
||||
</object>
|
||||
@ -687,6 +688,7 @@
|
||||
<items>
|
||||
<item id="mouse" translatable="yes">Mouse</item>
|
||||
<item id="wheel" translatable="yes">Wheel</item>
|
||||
<item id="buttons" translatable="yes">Buttons</item>
|
||||
</items>
|
||||
<signal name="changed" handler="on_right_joystick_purpose_changed" swapped="no"/>
|
||||
</object>
|
||||
|
@ -33,6 +33,7 @@ from keymapper.logger import logger
|
||||
|
||||
MOUSE = 'mouse'
|
||||
WHEEL = 'wheel'
|
||||
BUTTONS = 'buttons'
|
||||
|
||||
INITIAL_CONFIG = {
|
||||
'autoload': {},
|
||||
|
@ -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)
|
||||
]
|
||||
max_value = max_abs(input_device)
|
||||
|
||||
if len(absinfos) == 0:
|
||||
return
|
||||
|
||||
max_value = absinfos[0].max
|
||||
|
||||
if max_value == 0:
|
||||
if max_value == 0 or max_value is None:
|
||||
return
|
||||
|
||||
max_speed = ((max_value ** 2) * 2) ** 0.5
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,9 +155,11 @@ 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
|
||||
|
||||
if not (event.value == 0 and event.type == EV_ABS):
|
||||
# avoid gamepad trigger spam
|
||||
logger.spam(
|
||||
'got (%s, %s, %s)',
|
||||
event.type,
|
||||
@ -165,8 +169,8 @@ class _KeycodeReader:
|
||||
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',
|
||||
|
50
keymapper/dev/utils.py
Normal file
50
keymapper/dev/utils.py
Normal file
@ -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:
|
||||
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.
|
||||
|
||||
|
@ -170,7 +170,6 @@ class Window:
|
||||
self.get('gamepad_config').hide()
|
||||
|
||||
self.populate_devices()
|
||||
|
||||
self.select_newest_preset()
|
||||
|
||||
self.timeouts = [
|
||||
|
@ -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,
|
||||
|
@ -249,3 +249,5 @@ class Mapping(ConfigBase):
|
||||
existing = self._mapping.get(permutation)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
return None
|
||||
|
@ -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">91%</text>
|
||||
<text x="82.0" y="14">91%</text>
|
||||
<text x="83.0" y="15" fill="#010101" fill-opacity=".3">92%</text>
|
||||
<text x="82.0" y="14">92%</text>
|
||||
</g>
|
||||
</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] 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
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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,14 +192,56 @@ 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 = {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user