diff --git a/data/key-mapper.glade b/data/key-mapper.glade index c6fb64ac..be015975 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -238,7 +238,8 @@ - go_back + go_back + go_ahead diff --git a/keymapper/X.py b/keymapper/X.py index 14ce5a2f..78d63e07 100644 --- a/keymapper/X.py +++ b/keymapper/X.py @@ -203,8 +203,10 @@ def setxkbmap(device, layout): logger.info('Applying layout "%s" on device %s', layout, device) group = get_devices()[device] - keycodes = None if layout is None else 'key-mapper' - layout = layout or get_system_layout() + if layout is None: + cmd = ['setxkbmap', '-layout', get_system_layout()] + else: + cmd = ['setxkbmap', '-layout', layout, '-keycodes', 'key-mapper'] # apply it to every device that hangs on the same usb port, because I # have no idea how to figure out which one of those 3 devices that are @@ -214,17 +216,9 @@ def setxkbmap(device, layout): # only all virtual devices of the same hardware device continue - cmd = [ - 'setxkbmap', - '-device', str(xinput_id) - ] - if layout is not None: - cmd += ['-layout', layout] - if keycodes is not None: - cmd += ['-keycodes', keycodes] - - logger.debug('Running `%s`', ' '.join(cmd)) - subprocess.run(cmd) + device_cmd = cmd + ['-device', str(xinput_id)] + logger.debug('Running `%s`', ' '.join(device_cmd)) + subprocess.run(device_cmd, capture_output=True) def create_identity_mapping(): diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index 8dff06cb..e3bedc1d 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -36,20 +36,18 @@ from keymapper.linux import keycode_reader CTX_KEYCODE = 2 -class Row: +class Row(Gtk.ListBoxRow): """A single, configurable key mapping.""" + __gtype_name__ = 'ListBoxRow' + def __init__(self, delete_callback, window, keycode=None, character=None): """Construct a row widget.""" - self.widget = None + super().__init__() self.device = window.selected_device self.window = window self.delete_callback = delete_callback self.put_together(keycode, character) - def get_widget(self): - """Return the widget that wraps all the widgets of the row.""" - return self.widget - def get_keycode(self): keycode = self.keycode.get_label() return int(keycode) if keycode else None @@ -111,7 +109,7 @@ class Row: def highlight(self): """Mark this row as changed.""" - self.widget.get_style_context().add_class('changed') + self.get_style_context().add_class('changed') def on_character_input_change(self, entry): keycode = self.get_keycode() @@ -169,11 +167,9 @@ class Row: box.pack_start(delete_button, expand=True, fill=False, padding=0) box.show_all() - row = Gtk.ListBoxRow() - row.add(box) - row.show_all() + self.add(box) + self.show_all() - self.widget = row self.character_input = character_input self.keycode = keycode_input diff --git a/keymapper/gtk/unsaved.py b/keymapper/gtk/unsaved.py index 9763c150..f2848cf1 100644 --- a/keymapper/gtk/unsaved.py +++ b/keymapper/gtk/unsaved.py @@ -34,13 +34,16 @@ CONTINUE = True GO_BACK = False -def unsavedChangesDialog(): +def unsaved_changes_dialog(): + """Blocks until the user decided about an action.""" gladefile = get_data_path('key-mapper.glade') builder = Gtk.Builder() builder.add_from_file(gladefile) dialog = builder.get_object('unsaved_changes') dialog.show() - dialog.run() + response = dialog.run() dialog.hide() - # TODO do something meaningful - return GO_BACK + if response == Gtk.ResponseType.ACCEPT: + return CONTINUE + else: + return GO_BACK diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 7ba21ad1..b3c7fd22 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -35,7 +35,7 @@ from keymapper.presets import get_presets, find_newest_preset, \ from keymapper.logger import logger from keymapper.linux import get_devices, keycode_reader from keymapper.gtk.row import Row -from keymapper.gtk.unsaved import unsavedChangesDialog, GO_BACK +from keymapper.gtk.unsaved import unsaved_changes_dialog, GO_BACK def gtk_iteration(): @@ -220,9 +220,12 @@ class Window: def on_select_device(self, dropdown): """List all presets, create one if none exist yet.""" - if custom_mapping.changed: - if unsavedChangesDialog() == GO_BACK: - return + if dropdown.get_active_id() == self.selected_device: + return + + if custom_mapping.changed and unsaved_changes_dialog() == GO_BACK: + dropdown.set_active_id(self.selected_device) + return device = dropdown.get_active_text() @@ -239,7 +242,7 @@ class Window: def on_create_preset_clicked(self, button): """Create a new preset and select it.""" if custom_mapping.changed: - if unsavedChangesDialog() == GO_BACK: + if unsaved_changes_dialog() == GO_BACK: return new_preset = create_preset(self.selected_device) @@ -249,9 +252,12 @@ class Window: def on_select_preset(self, dropdown): """Show the mappings of the preset.""" - if custom_mapping.changed: - if unsavedChangesDialog() == GO_BACK: - return + if dropdown.get_active_id() == self.selected_preset: + return + + if custom_mapping.changed and unsaved_changes_dialog() == GO_BACK: + dropdown.set_active_id(self.selected_preset) + return self.clear_mapping_table() @@ -269,7 +275,7 @@ class Window: keycode=keycode, character=character ) - key_list.insert(single_key_mapping.get_widget(), -1) + key_list.insert(single_key_mapping, -1) self.add_empty() @@ -279,7 +285,7 @@ class Window: delete_callback=self.on_row_removed ) key_list = self.get('key_list') - key_list.insert(empty.get_widget(), -1) + key_list.insert(empty, -1) def on_row_removed(self, single_key_mapping): """Stuff to do when a row was removed @@ -290,7 +296,7 @@ class Window: """ key_list = self.get('key_list') # https://stackoverflow.com/a/30329591/4417769 - key_list.remove(single_key_mapping.get_widget()) + key_list.remove(single_key_mapping) def save_config(self): """Write changes to disk""" diff --git a/tests/test.py b/tests/test.py index 0c8e6c8e..881a5668 100644 --- a/tests/test.py +++ b/tests/test.py @@ -55,6 +55,10 @@ linux._devices = { } linux.get_devices = lambda: linux._devices +# don't block tests +from keymapper.gtk import unsaved +unsaved.unsaved_changes_dialog = lambda: unsaved.CONTINUE + from keymapper.logger import update_verbosity # some class function stubs. @@ -73,7 +77,7 @@ if __name__ == "__main__": # in all of the available tests like unittest.main() does..., # so provide both options. if len(modules) > 0: - # for example `tests/test.py ConfigTest.testFirstLine` + # for example `tests/test.py integration.Integration.test_can_start` testsuite = unittest.defaultTestLoader.loadTestsFromNames( [f'testcases.{module}' for module in modules] ) diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index 9cf3266c..c95b14b5 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -124,6 +124,9 @@ class Integration(unittest.TestCase): def get_active_text(self): return self.name + def get_active_id(self): + return self.name + # created on start because the first device is selected and some empty # preset prepared. self.assertTrue(os.path.exists(f'{USERS_SYMBOLS}/device_1/new_preset'))