supports keycodes from mouse-buttons (such as middle-mouse) as well

pull/14/head
sezanzeb 4 years ago
parent 4a423455e9
commit 8f3b9cb475

@ -25,10 +25,11 @@
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GLib', '2.0')
from gi.repository import Gtk
from gi.repository import Gtk, GLib
from keymapper.state import custom_mapping
from keymapper.logger import logger
from keymapper.reader import keycode_reader
CTX_KEYCODE = 2
@ -44,6 +45,10 @@ class Row(Gtk.ListBoxRow):
self.device = window.selected_device
self.window = window
self.delete_callback = delete_callback
self.character_input = None
self.keycode = None
self.put_together(keycode, character)
def get_keycode(self):
@ -54,30 +59,23 @@ class Row(Gtk.ListBoxRow):
character = self.character_input.get_text()
return character if character else None
def highlight(self):
"""Mark this row as changed."""
self.get_style_context().add_class('changed')
def start_watching_keycodes(self, *args):
"""Start to periodically check if a keycode has been pressed.
def unhighlight(self):
"""Mark this row as unchanged."""
self.get_style_context().remove_class('changed')
This also includes e.g. middle mouse buttons, as opposed to
get_keycode from gdk events.
"""
keycode_reader.clear()
def on_character_input_change(self, entry):
keycode = self.get_keycode()
character = self.get_character()
def iterate():
self.check_newest_keycode()
return self.keycode.is_focus() and self.window.window.is_active()
self.highlight()
GLib.timeout_add(1000 / 30, iterate)
if keycode is not None:
custom_mapping.change(
previous_keycode=None,
new_keycode=keycode,
character=character
)
def on_key_pressed(self, button, event):
def check_newest_keycode(self):
"""Check if a keycode has been pressed and if so, display it."""
new_keycode = event.get_keycode()[1]
new_keycode = keycode_reader.read()
previous_keycode = self.get_keycode()
character = self.get_character()
@ -95,6 +93,7 @@ class Row(Gtk.ListBoxRow):
logger.info(msg)
self.window.get('status_bar').push(CTX_KEYCODE, msg)
return
# it's legal to display the keycode
self.window.get('status_bar').remove_all(CTX_KEYCODE)
self.keycode.set_label(str(new_keycode))
@ -109,11 +108,28 @@ class Row(Gtk.ListBoxRow):
return
# else, the keycode has changed, the character is set, all good
custom_mapping.change(
previous_keycode=previous_keycode,
new_keycode=new_keycode,
character=character
)
custom_mapping.change(previous_keycode, new_keycode, character)
def highlight(self):
"""Mark this row as changed."""
self.get_style_context().add_class('changed')
def unhighlight(self):
"""Mark this row as unchanged."""
self.get_style_context().remove_class('changed')
def on_character_input_change(self, entry):
keycode = self.get_keycode()
character = self.get_character()
self.highlight()
if keycode is not None:
custom_mapping.change(
previous_keycode=None,
new_keycode=keycode,
character=character
)
def put_together(self, keycode, character):
"""Create all child GTK widgets and connect their signals."""
@ -130,12 +146,16 @@ class Row(Gtk.ListBoxRow):
delete_button.set_margin_end(5)
keycode_input = Gtk.ToggleButton()
if keycode is not None:
keycode_input.set_label(str(keycode))
# to capture regular keyboard keys or extra-mouse keys
keycode_input.connect(
'key-press-event',
self.on_key_pressed
'focus-in-event',
self.start_watching_keycodes
)
# make the togglebutton go back to its normal state when doing
# something else in the UI
keycode_input.connect(

@ -36,6 +36,7 @@ from keymapper.injector import KeycodeInjector
from keymapper.getdevices import get_devices
from keymapper.gtk.row import Row
from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK
from keymapper.reader import keycode_reader
def gtk_iteration():
@ -279,6 +280,9 @@ class Window:
self.selected_preset = None
self.populate_presets()
GLib.idle_add(
lambda: keycode_reader.start_reading(self.selected_device)
)
def on_create_preset_clicked(self, button):
"""Create a new preset and select it."""

@ -0,0 +1,84 @@
#!/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/>.
"""Keeps reading keycodes in the background for the UI to use."""
import evdev
from keymapper.logger import logger
from keymapper.getdevices import get_devices
class KeycodeReader:
"""Keeps reading keycodes in the background for the UI to use.
When a button was pressed, the newest keycode can be obtained from this
object. GTK has get_keycode for keyboard keys, but KeycodeReader also
has knowledge of buttons like the middle-mouse button.
"""
def __init__(self):
self.virtual_devices = []
def clear(self):
"""Next time when reading don't return the previous keycode."""
# read all of them to clear the buffer or whatever
for virtual_device in self.virtual_devices:
while virtual_device.read_one():
pass
def start_reading(self, device):
"""Tell the evdev lib to start looking for keycodes.
If read is called without prior start_reading, no keycodes
will be available.
"""
paths = get_devices()[device]['paths']
logger.debug(
'Starting reading keycodes for %s on %s',
device,
', '.join(paths)
)
# Watch over each one of the potentially multiple devices per hardware
self.virtual_devices = [
evdev.InputDevice(path)
for path in paths
]
def read(self):
"""Get the newest key or None if none was pressed."""
newest_keycode = None
for virtual_device in self.virtual_devices:
while True:
event = virtual_device.read_one()
if event is None:
break
if event.type == evdev.ecodes.EV_KEY and event.value == 1:
# value: 1 for down, 0 for up, 2 for hold.
# this happens to report key codes that are 8 lower
# than the ones reported by evtest and used in xkb files
newest_keycode = event.code + 8
return newest_keycode
keycode_reader = KeycodeReader()

@ -115,7 +115,8 @@ class Event:
type : int
one of evdev.ecodes.EV_*
code : int
keyboard event code as known to linux. E.g. 2 for the '1' button
keyboard event code as known to linux. E.g. 2 for the '1' button,
which would be 10 in xkb
value : int
1 for down, 0 for up, 2 for hold
"""
@ -142,6 +143,16 @@ def patch_evdev():
def grab(self):
pass
def read_one(self):
if pending_events.get(self.name) is None:
return None
if len(pending_events[self.name]) == 0:
return None
event = pending_events[self.name].pop(0)
return event
def read_loop(self):
"""Read all prepared events at once."""
if pending_events.get(self.name) is None:
@ -210,7 +221,6 @@ if __name__ == "__main__":
originalStartTest = unittest.TextTestResult.startTest
def startTest(self, test):
originalStartTest(self, test)
print()
unittest.TextTestResult.startTest = startTest
testrunner = unittest.TextTestRunner(verbosity=2).run(testsuite)

@ -131,11 +131,11 @@ class TestInjector(unittest.TestCase):
# the second arg of those event objects is 8 lower than the
# keycode used in X and in the mappings
pending_events['device 2'] = [
Event(evdev.events.EV_KEY, 1, 0),
Event(evdev.events.EV_KEY, 1, 1),
Event(evdev.events.EV_KEY, 1, 0),
# ignored because unknown to the system
Event(evdev.events.EV_KEY, 2, 0),
Event(evdev.events.EV_KEY, 2, 1),
Event(evdev.events.EV_KEY, 2, 0),
# just pass those over without modifying
Event(3124, 3564, 6542),
]
@ -152,11 +152,11 @@ class TestInjector(unittest.TestCase):
self.assertEqual(uinput_write_history[0].type, evdev.events.EV_KEY)
self.assertEqual(uinput_write_history[0].code, 92)
self.assertEqual(uinput_write_history[0].value, 0)
self.assertEqual(uinput_write_history[0].value, 1)
self.assertEqual(uinput_write_history[1].type, evdev.events.EV_KEY)
self.assertEqual(uinput_write_history[1].code, 92)
self.assertEqual(uinput_write_history[1].value, 1)
self.assertEqual(uinput_write_history[1].value, 0)
self.assertEqual(uinput_write_history[2].type, 3124)
self.assertEqual(uinput_write_history[2].code, 3564)

@ -23,6 +23,7 @@ import sys
import time
import os
import unittest
import evdev
from unittest.mock import patch
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader
@ -35,7 +36,7 @@ from gi.repository import Gtk
from keymapper.state import custom_mapping
from keymapper.paths import CONFIG
from test import tmp
from test import tmp, pending_events, Event
def gtk_iteration():
@ -108,13 +109,6 @@ class Integration(unittest.TestCase):
def test_rows(self):
"""Comprehensive test for rows."""
class FakeEvent:
def __init__(self, keycode):
self.keycode = keycode
def get_keycode(self):
return [True, self.keycode]
def change_empty_row(keycode, character):
"""Modify the one empty row that always exists."""
# wait for the window to create a new empty row if needed
@ -128,7 +122,14 @@ class Integration(unittest.TestCase):
self.assertIsNone(row.keycode.get_label())
self.assertEqual(row.character_input.get_text(), '')
row.on_key_pressed(None, FakeEvent(keycode))
self.window.window.set_focus(row.keycode)
pending_events[self.window.selected_device] = [
Event(evdev.events.EV_KEY, keycode - 8, 1)
]
time.sleep(0.1)
gtk_iteration()
self.assertEqual(int(row.keycode.get_label()), keycode)

Loading…
Cancel
Save