diff --git a/bin/key-mapper-gtk b/bin/key-mapper-gtk index d8e5fda0..5d6ab7ef 100755 --- a/bin/key-mapper-gtk +++ b/bin/key-mapper-gtk @@ -31,7 +31,8 @@ gi.require_version('GLib', '2.0') from gi.repository import Gtk, Gdk from keymapper.data import get_data_path -from keymapper.X import create_setxkbmap_config, apply_preset, create_preset +from keymapper.X import create_setxkbmap_config, apply_preset, \ + create_preset, get_mappings from keymapper.presets import get_presets, find_newest_preset, \ delete_preset, rename_preset from keymapper.logger import logger, update_verbosity, log_info @@ -46,48 +47,72 @@ window = None class SingleKeyMapping: """A single, configurable key mapping.""" - def __init__(self, delete_callback): + def __init__(self, delete_callback, key_code=None, character=None): """Construct a row and add it to the list in the GUI.""" + self.widget = None self.delete_callback = delete_callback - self.put_together() + self.put_together(key_code, character) - def get_widgets(self): + def get_widget(self): """Return the widget that wraps all the widgets of the row.""" - return self.widgets + return self.widget - def put_together(self): + def get_code(self): + return int(self.key_code.get_text()) + + def get_character(self): + return self.character.get_text() + + 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 )) - delete_button.connect('button-press-event', self.on_delete_button_clicked) + delete_button.connect( + 'button-press-event', + self.on_delete_button_clicked + ) delete_button.set_margin_start(5) delete_button.set_margin_end(5) - key_code = Gtk.Entry() - key_code.set_alignment(0.5) - key_code.set_width_chars(4) - key_code.set_has_frame(False) + 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) + if key_code is not None: + key_code_input.set_text(key_code) + + character_input = Gtk.Entry() + character_input.set_alignment(0.5) + character_input.set_width_chars(4) + character_input.set_has_frame(False) + if character is not None: + character_input.set_text(character) + + row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + row.set_homogeneous(True) + row.pack_start(key_code_input, expand=True, fill=True, padding=0) + row.pack_start(character_input, expand=True, fill=True, padding=0) + row.pack_start(delete_button, expand=True, fill=False, padding=0) - original_key = Gtk.Entry() - original_key.set_alignment(0.5) - original_key.set_width_chars(4) - original_key.set_has_frame(False) + row.show_all() + # in order to get this object when iterating over the listbox + row.logic = self - self.widgets = (key_code, original_key, delete_button) + self.widget = row + self.character_input = character_input + self.key_code = key_code_input def on_delete_button_clicked(self, *args): """Destroy the row and remove it from the config.""" - for widget in self.widgets: - widget.destroy() self.delete_callback(self) class Window: """User Interface.""" def __init__(self): - self.rows = 0 self.selected_device = None self.selected_preset = None self.mappings = [] @@ -161,20 +186,25 @@ class Window: def clear_mapping_table(self): """Remove all rows from the mappings table.""" key_list = self.get('key_list') - for i in range(self.rows): - key_list.remove_row(i + 1) - self.rows = 0 + key_list.forall(key_list.remove) def update_mappings(self): """Construct the mapping from the inputs without saving or applying.""" key_list = self.get('key_list') mappings = [] - for i in range(self.rows): - code = key_list.get_child_at(1, i + 1).get_text() - character = key_list.get_child_at(2, i + 1).get_text() - if code == '' or character == '': - continue - mappings.append((int(code), character)) + + 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 + mappings.append(( + int(columns[0].get_text()), + columns[1].get_text() + )) + + key_list.forall(read_mapping) + logger.debug('Constructed mappings: %s', mappings) self.mappings = mappings def on_save_preset_clicked(self, button): @@ -223,25 +253,33 @@ class Window: def on_select_preset(self, dropdown): """Show the mappings of the preset.""" + self.clear_mapping_table() + preset = dropdown.get_active_text() logger.debug('Selecting preset "%s"', preset) self.selected_preset = preset - self.mappings = [] + mappings = get_mappings( + self.selected_device, + self.selected_preset + ) - # TODO show all mapped keys from config - single_key_mapping = SingleKeyMapping(self.on_row_removed) key_list = self.get('key_list') - row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) - row.set_homogeneous(True) - widgets = single_key_mapping.get_widgets() - row.pack_start(widgets[0], expand=True, fill=True, padding=0) - row.pack_start(widgets[1], expand=True, fill=True, padding=0) - row.pack_start(widgets[2], expand=True, fill=True, padding=0) - key_list.insert(row, -1) - key_list.show_all() + for mapping in mappings: + mapping = SingleKeyMapping( + self.on_row_removed, + mapping[0], mapping[1] + ) + key_list.insert(mapping.get_widget(), -1) - self.clear_mapping_table() + self.mappings = mappings + + self.add_empty() + + def add_empty(self): + empty = SingleKeyMapping(self.on_row_removed) + key_list = self.get('key_list') + key_list.insert(empty.get_widget(), -1) def on_row_removed(self, mapping): """Stuff to do when a row was removed @@ -250,17 +288,14 @@ class Window: ---------- mapping : SingleKeyMapping """ + key_list = self.get('key_list') + # https://stackoverflow.com/a/30329591/4417769 + key_list.remove(mapping.get_widget().get_parent()) # shrink the window down as much as possible, otherwise it # will increase with each added mapping but won't go back when they # are removed. window = self.get('window') window.resize(window.get_size()[0], 1) - # note, that the grid row still exist, it just shrank down to 0 - # because there are no contents. - self.rows -= 1 - if self.rows == 0: - # add back an empty row - self.on_add_key_clicked() def update_config(self): """Write changes to disk""" diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 879489cd..dda82022 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -2,59 +2,12 @@ - - True - True - asdf - 0.5 - - - True - True - - - True - False - 2 - - - True - False - 10 - 5 - True - Key - 10 - - - 0 - 0 - - - - - True - False - 10 - 5 - True - Mapping - 10 - - - 1 - 0 - - - - 450 False Key Mapper False + mouse diff --git a/data/style.css b/data/style.css index 8ebd8d2f..e3a29144 100644 --- a/data/style.css +++ b/data/style.css @@ -2,4 +2,13 @@ list entry { background-color: transparent; } +.button_container { + padding: 5px; + background-color: @content_view_bg; +} + +.button_container > * { + background-color: transparent; +} + /* @theme_bg_color, @theme_fg_color */ \ No newline at end of file diff --git a/keymapper/X.py b/keymapper/X.py index 219c60ee..5c57320b 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -256,3 +256,16 @@ def get_xinput_id_mapping(): names = [name for name in names if name != ''] ids = [int(id) for id in ids if id != ''] return zip(names, ids) + + +def get_mappings(device, preset): + """Parse the X config to get a current mapping. + + Returns tuples of (keycode, character) + """ + with open(get_home_path(device, preset), 'r') as f: + # from "key <12> { [ 1 ] };" extract 12 and 1, + # avoid lines that start with special characters (might be comments) + result = re.findall(r'\n\s+?key <(.+?)>.+?\[\s+(\w+)', f.read()) + logger.debug('Found %d mappings in this preset', len(result)) + return result diff --git a/keymapper/paths.py b/keymapper/paths.py index da07355d..ba0da842 100644 --- a/keymapper/paths.py +++ b/keymapper/paths.py @@ -46,7 +46,9 @@ KEYCODES_PATH = '/usr/share/X11/xkb/keycodes/key-mapper' def get_home_path(device, preset=None): """Get the path to the config file in /usr.""" + device = device.strip() if preset is not None: + preset = preset.strip() return os.path.join(CONFIG_PATH, device, preset).replace(' ', '_') else: return os.path.join(CONFIG_PATH, device.replace(' ', '_')) @@ -57,7 +59,9 @@ def get_usr_path(device, preset=None): If preset is omitted, returns the folder for the device. """ + device = device.strip() if preset is not None: + preset = preset.strip() return os.path.join(SYMBOLS_PATH, device, preset).replace(' ', '_') else: return os.path.join(SYMBOLS_PATH, device.replace(' ', '_'))