diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index f4d81fe3..5aa981fb 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -55,24 +55,26 @@ class Row(Gtk.ListBoxRow): character = self.character_input.get_text() return character if character else None - def start_watching_keycodes(self, *args): - """Start to periodically check if a keycode has been pressed. + def highlight(self): + """Mark this row as changed.""" + self.get_style_context().add_class('changed') - This is different from just listening for text input events - (as in Gtk.Entry), since keys may not write characters into the form - because they are not mapped. Furthermore their keycode is needed, - not their mapped character.""" - keycode_reader.clear() + def unhighlight(self): + """Mark this row as unchanged.""" + self.get_style_context().remove_class('changed') - def iterate(): - self.check_newest_keycode() - return self.keycode.is_focus() and self.window.window.is_active() + def on_character_input_change(self, entry): + keycode = self.get_keycode() + character = self.get_character() - GLib.timeout_add(1000 / 30, iterate) + self.highlight() - def check_newest_keycode(self): + if keycode is not None: + custom_mapping.change(None, keycode, character) + + def on_key_pressed(self, button, event): """Check if a keycode has been pressed and if so, display it.""" - new_keycode = keycode_reader.read() + new_keycode = event.get_keycode()[1] previous_keycode = self.get_keycode() character = self.get_character() @@ -106,25 +108,8 @@ class Row(Gtk.ListBoxRow): # else, the keycode has changed, the character is set, all good 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(None, keycode, character) - def put_together(self, keycode, character): - """Create all GTK widgets.""" + """Create all child GTK widgets and connect their signals.""" delete_button = Gtk.EventBox() delete_button.add(Gtk.Image.new_from_icon_name( 'window-close', @@ -141,8 +126,8 @@ class Row(Gtk.ListBoxRow): if keycode is not None: keycode_input.set_label(str(keycode)) keycode_input.connect( - 'focus-in-event', - self.start_watching_keycodes + 'key-press-event', + self.on_key_pressed ) # make the togglebutton go back to its normal state when doing # something else in the UI diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 1f818558..68fc8fb8 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -106,7 +106,7 @@ class Window: self.select_newest_preset() - GLib.timeout_add(100, self.check_add_row) + self.timeout = GLib.timeout_add(100, self.check_add_row) def get(self, name): """Get a widget from the window""" @@ -114,6 +114,7 @@ class Window: def on_close(self, *_): """Safely close the application.""" + GLib.source_remove(self.timeout) Gtk.main_quit() def check_add_row(self): diff --git a/keymapper/linux.py b/keymapper/linux.py index f8d64c62..c88c40ec 100644 --- a/keymapper/linux.py +++ b/keymapper/linux.py @@ -47,6 +47,8 @@ def can_grab(path): class KeycodeReader: """Keeps reading keycodes in the background for the UI to use. + This was written before I figured out there is get_keycode in GLib. + A new arriving keycode indicates that a button was pressed, so the UI can keep checking for a new keycode on this object and act like the keycode went right to the input box. @@ -96,9 +98,6 @@ class KeycodeReader: # 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 xev - # TODO check if 280 and above works on wayland and - # if not, prevent anything > 255. adjust - # the maximum of keycodes before trying newest_keycode = event.code + 8 return newest_keycode diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index 02d3b35c..56e51556 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -83,8 +83,6 @@ class Integration(unittest.TestCase): self.window = launch() def tearDown(self): - # before calling destroy to break everything (happened with - # check_add_row), make an iteration to clear all pending events. gtk_iteration() self.window.on_close() self.window.window.destroy() @@ -111,12 +109,14 @@ class Integration(unittest.TestCase): def test_rows(self): """Comprehensive test for rows.""" - def read(): - """Always return a different keycode for each row.""" - # + 7 because keycodes usually start at 8 - return len(self.window.get('key_list').get_children()) + 7 + class FakeEvent: + def __init__(self, keycode): + self.keycode = keycode - def change_empty_row(character): + def get_keycode(self): + return [False, 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 time.sleep(0.2) @@ -129,14 +129,9 @@ class Integration(unittest.TestCase): self.assertIsNone(row.keycode.get_label()) self.assertEqual(row.character_input.get_text(), '') - # focus the keycode to trigger reading the fake keycode - self.window.window.set_focus(row.keycode) - time.sleep(0.2) - gtk_iteration() + row.on_key_pressed(None, FakeEvent(keycode)) - # it should be filled using the `read` patch - self.assertEqual(int(row.keycode.get_label()), len(rows) + 7) - self.window.window.set_focus(None) + self.assertEqual(int(row.keycode.get_label()), keycode) # set the character to make the new row complete row.character_input.set_text(character) @@ -145,45 +140,44 @@ class Integration(unittest.TestCase): return row - with patch.object(keycode_reader, 'read', read): - # add two rows by modifiying the one empty row that exists - change_empty_row('a') - change_empty_row('b') + # add two rows by modifiying the one empty row that exists + change_empty_row(10, 'a') + change_empty_row(11, 'b') - # one empty row added automatically again - time.sleep(0.2) - gtk_iteration() - # sleep one more time because it's funny to watch the ui - # during the test, how rows turn blue and stuff - time.sleep(0.2) - self.assertEqual(len(self.get_rows()), 3) - - self.assertEqual(custom_mapping.get(8), 'a') - self.assertEqual(custom_mapping.get(9), 'b') - self.assertTrue(custom_mapping.changed) - - self.window.on_save_preset_clicked(None) - for row in self.get_rows(): - self.assertNotIn( - 'changed', - row.get_style_context().list_classes() - ) - self.assertFalse(custom_mapping.changed) - - # now change the first row and it should turn blue, - # but the other should remain unhighlighted - row = self.get_rows()[0] - row.character_input.set_text('c') - self.assertIn('changed', row.get_style_context().list_classes()) - for row in self.get_rows()[1:]: - self.assertNotIn( - 'changed', - row.get_style_context().list_classes() - ) - - self.assertEqual(custom_mapping.get(8), 'c') - self.assertEqual(custom_mapping.get(9), 'b') - self.assertTrue(custom_mapping.changed) + # one empty row added automatically again + time.sleep(0.2) + gtk_iteration() + # sleep one more time because it's funny to watch the ui + # during the test, how rows turn blue and stuff + time.sleep(0.2) + self.assertEqual(len(self.get_rows()), 3) + + self.assertEqual(custom_mapping.get(10), 'a') + self.assertEqual(custom_mapping.get(11), 'b') + self.assertTrue(custom_mapping.changed) + + self.window.on_save_preset_clicked(None) + for row in self.get_rows(): + self.assertNotIn( + 'changed', + row.get_style_context().list_classes() + ) + self.assertFalse(custom_mapping.changed) + + # now change the first row and it should turn blue, + # but the other should remain unhighlighted + row = self.get_rows()[0] + row.character_input.set_text('c') + self.assertIn('changed', row.get_style_context().list_classes()) + for row in self.get_rows()[1:]: + self.assertNotIn( + 'changed', + row.get_style_context().list_classes() + ) + + self.assertEqual(custom_mapping.get(10), 'c') + self.assertEqual(custom_mapping.get(11), 'b') + self.assertTrue(custom_mapping.changed) def test_rename_and_save(self): custom_mapping.change(None, 14, 'a')