diff --git a/README.md b/README.md index 736d407a..0c6e8cd9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ sudo python3 setup.py install && python3 tests/test.py - [x] generate a file for /usr/share/X11/xkb/symbols/ for each preset - [x] load that file with `setxkbmap` - [x] keep the system defaults for unmapped buttons -- [ ] highlight changes and alert before discarding unsaved changes +- [x] highlight changes and alert before discarding unsaved changes - [ ] automatically load the preset (on startup?, udev on mouse connect?) - [ ] make it work on wayland - [ ] add to the AUR, provide .deb and .appimage files diff --git a/keymapper/gtk/row.py b/keymapper/gtk/row.py index c272590d..f4d81fe3 100644 --- a/keymapper/gtk/row.py +++ b/keymapper/gtk/row.py @@ -26,7 +26,6 @@ import gi gi.require_version('Gtk', '3.0') gi.require_version('GLib', '2.0') from gi.repository import Gtk, GLib -import cairo as Cairo from keymapper.mapping import custom_mapping from keymapper.logger import logger diff --git a/keymapper/gtk/window.py b/keymapper/gtk/window.py index 872da70d..1f818558 100755 --- a/keymapper/gtk/window.py +++ b/keymapper/gtk/window.py @@ -118,12 +118,16 @@ class Window: def check_add_row(self): """Ensure that one empty row is available at all times.""" - rows = len(self.get('key_list').get_children()) + num_rows = len(self.get('key_list').get_children()) # verify that all mappings are displayed - assert rows >= len(custom_mapping) + if num_rows < len(custom_mapping): + raise AssertionError( + f'custom_mapping contains {len(custom_mapping)} rows, ' + f'but only {num_rows} are displayed' + ) - if rows == len(custom_mapping): + if num_rows == len(custom_mapping): self.add_empty() return True diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index c95b14b5..02d3b35c 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -34,6 +34,7 @@ from gi.repository import Gtk from keymapper.mapping import custom_mapping from keymapper.paths import USERS_SYMBOLS, HOME_PATH, KEYCODES_PATH +from keymapper.linux import keycode_reader from test import tmp @@ -82,11 +83,17 @@ 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() gtk_iteration() shutil.rmtree('/tmp/key-mapper-test') + def get_rows(self): + return self.window.get('key_list').get_children() + def test_can_start(self): self.assertIsNotNone(self.window) self.assertTrue(self.window.window.get_visible()) @@ -102,6 +109,82 @@ class Integration(unittest.TestCase): rows = len(self.window.get('key_list').get_children()) self.assertEqual(rows, 2) + 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 + + def change_empty_row(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) + gtk_iteration() + + # find the empty row + rows = self.get_rows() + row = rows[-1] + self.assertNotIn('changed', row.get_style_context().list_classes()) + 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() + + # it should be filled using the `read` patch + self.assertEqual(int(row.keycode.get_label()), len(rows) + 7) + self.window.window.set_focus(None) + + # set the character to make the new row complete + row.character_input.set_text(character) + + self.assertIn('changed', row.get_style_context().list_classes()) + + 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') + + # 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) + def test_rename_and_save(self): custom_mapping.change(None, 14, 'a') self.assertEqual(self.window.selected_preset, 'new preset')