diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index 5d6ab7ef..a025db88 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -28,7 +28,7 @@ from argparse import ArgumentParser import gi gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') -from gi.repository import Gtk, Gdk +from gi.repository import Gtk, Gdk, GLib from keymapper.data import get_data_path from keymapper.X import create_setxkbmap_config, apply_preset, \ @@ -36,7 +36,7 @@ from keymapper.X import create_setxkbmap_config, apply_preset, \ from keymapper.presets import get_presets, find_newest_preset, \ delete_preset, rename_preset from keymapper.logger import logger, update_verbosity, log_info -from keymapper.linux import get_devices +from keymapper.linux import get_devices, KeycodeReader window = None @@ -45,29 +45,46 @@ window = None # TODO check for sudo rights +def gtk_iteration(): + """Iterate while events are pending.""" + while Gtk.events_pending(): + Gtk.main_iteration() + + class SingleKeyMapping: """A single, configurable key mapping.""" - def __init__(self, delete_callback, key_code=None, character=None): + def __init__( + self, device, delete_callback, keycode_reader, + key_code=None, character=None, + ): """Construct a row and add it to the list in the GUI.""" self.widget = None + self.device = device self.delete_callback = delete_callback + self.keycode_reader = keycode_reader self.put_together(key_code, character) def get_widget(self): """Return the widget that wraps all the widgets of the row.""" return self.widget - def get_code(self): - return int(self.key_code.get_text()) + def start_watching_key_codes(self, *args): + print('start_watching_key_codes') + self.keycode_reader.clear() + GLib.timeout_add(100, self.get_newest_keycode) - def get_character(self): - return self.character.get_text() + def get_newest_keycode(self): + code = self.keycode_reader.read() + if code is not None: + self.key_code.set_label(str(code)) + return self.key_code.is_focus() def put_together(self, key_code, character): """Create all GTK widgets.""" delete_button = Gtk.EventBox() delete_button.add(Gtk.Image.new_from_icon_name( - 'window-close', Gtk.IconSize.BUTTON + 'window-close', + Gtk.IconSize.BUTTON )) delete_button.connect( 'button-press-event', @@ -76,13 +93,19 @@ class SingleKeyMapping: delete_button.set_margin_start(5) delete_button.set_margin_end(5) - key_code_input = Gtk.Entry() - key_code_input.set_alignment(0.5) - key_code_input.set_width_chars(4) - key_code_input.set_has_frame(False) - key_code_input.set_input_purpose(Gtk.InputPurpose.NUMBER) + key_code_input = Gtk.ToggleButton() if key_code is not None: - key_code_input.set_text(key_code) + key_code_input.set_label(key_code) + key_code_input.connect( + 'focus-in-event', + self.start_watching_key_codes + ) + # make the togglebutton go back to its normal state when doing + # something else in the UI + key_code_input.connect( + 'focus-out-event', + lambda *args: key_code_input.set_active(False) + ) character_input = Gtk.Entry() character_input.set_alignment(0.5) @@ -117,6 +140,8 @@ class Window: self.selected_preset = None self.mappings = [] + self.keycode_reader = KeycodeReader(gtk_iteration) + gladefile = get_data_path('key-mapper.glade') builder = Gtk.Builder() builder.add_from_file(gladefile) @@ -131,8 +156,11 @@ class Window: self.select_newest_preset() + print(1) css_provider = Gtk.CssProvider() + print(2) css_provider.load_from_path(get_data_path('style.css')) + print(3) Gtk.StyleContext.add_provider_for_screen( Gdk.Screen.get_default(), css_provider, @@ -145,6 +173,7 @@ class Window: def on_close(self, *_): """Safely close the application.""" + self.keycode_reader.stop_reading() Gtk.main_quit() def select_newest_preset(self): @@ -196,10 +225,20 @@ class Window: def read_mapping(row): box = row.get_children()[0] columns = box.get_children() - # TODO test if columns[0] is a number - # and if one of them is empty + code = columns[0].get_label() + + # validate + character = columns[1].get_text() + if code == '' or character == '': + return + try: + code = int(code) + except ValueError: + return + + # add to mapping mappings.append(( - int(columns[0].get_text()), + int(columns[0].get_label()), columns[1].get_text() )) @@ -243,6 +282,9 @@ class Window: self.mappings = [] self.populate_presets() + GLib.idle_add( + lambda: self.keycode_reader.start_reading(self.selected_device) + ) def on_create_preset_clicked(self, button): """Create a new preset and select it.""" @@ -267,8 +309,11 @@ class Window: key_list = self.get('key_list') for mapping in mappings: mapping = SingleKeyMapping( + self.selected_device, self.on_row_removed, - mapping[0], mapping[1] + self.keycode_reader, + mapping[0], + mapping[1] ) key_list.insert(mapping.get_widget(), -1) @@ -277,7 +322,11 @@ class Window: self.add_empty() def add_empty(self): - empty = SingleKeyMapping(self.on_row_removed) + empty = SingleKeyMapping( + self.selected_device, + self.on_row_removed, + self.keycode_reader + ) key_list = self.get('key_list') key_list.insert(empty.get_widget(), -1) diff --git a/keymapper/X.py b/keymapper/X.py index 5c57320b..e0d3b4bc 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -43,13 +43,6 @@ from keymapper.presets import get_presets from keymapper.linux import get_devices, can_grab -def get_keycode(device, letter): - """Get the keycode that is configured for the given letter.""" - # TODO I have no idea how to do this - # in /usr/share/X11/xkb/keycodes the mapping is made - return '' - - def ensure_symlink(): """Make sure the symlink exists. @@ -135,15 +128,6 @@ def apply_preset(device, preset): # only all virtual devices of the same hardware device continue - """# get the path in /dev for that - path = [ - path for name, path - in zip(group['devices'], group['paths']) - if name == xinput_name - ][0] - if not can_grab(path): - logger.error('Something else is')""" - symbols = '/usr/share/X11/xkb/symbols/' layout_path = get_usr_path(device, preset) with open(layout_path, 'r') as f: diff --git a/keymapper/linux.py b/keymapper/linux.py index 3dfedd6a..a6fac23a 100644 --- a/keymapper/linux.py +++ b/keymapper/linux.py @@ -22,9 +22,11 @@ """Device stuff that is independent from the display server.""" -import evdev +import re import subprocess +import evdev + from keymapper.logger import logger @@ -43,6 +45,66 @@ def can_grab(path): return p.returncode == 1 +class KeycodeReader: + def __init__(self, iterate): + self.iterate = iterate + self.keep_reading = False + self.currently_reading = False + self.newest_keycode = None + + def clear(self): + """Next time when reading don't return the previous keycode.""" + self.newest_keycode = None + + def start_reading(self, device): + """Start a loop that keeps reading keycodes. + + This keeps the main loop running, however, it is blocking for the + function that calls this until stop_reading is called from somewhere + else. + """ + # stop the current loop + if self.currently_reading: + self.stop_reading() + while self.currently_reading: + self.iterate() + + # start the next one + logger.debug('Starting reading keycodes for %s', device) + self.keep_reading = True + self.currently_reading = True + + # all the virtual devices of the hardware. + # Watch over each one of them + paths = _devices[device]['paths'] + virtual_devices = [ + evdev.InputDevice(path) + for path in paths[:1] + ] + + while self.keep_reading: + for virtual_device in virtual_devices: + event = virtual_device.read_one() + if event is not None and event.type == evdev.ecodes.EV_KEY: + # this happens to report key codes that are 8 lower + # than the ones reported by xev + self.newest_keycode = event.code + 8 + self.iterate() + + # done + logger.debug('Stopped reading keycodes for %s', device) + self.currently_reading = False + + def stop_reading(self): + """Stop the loop that keeps reading keycodes.""" + self.keep_reading = False + self.newest_keycode = None + + def read(self): + """Get the newest key.""" + return self.newest_keycode + + def get_devices(): """Group devices and get relevant infos per group.