diff --git a/data/key-mapper.glade b/data/key-mapper.glade index 466dbecc..8538dcc3 100644 --- a/data/key-mapper.glade +++ b/data/key-mapper.glade @@ -121,13 +121,12 @@ True True True - Shortcut: shift + del + Shortcut: ctrl + del To give your keys back their original mapping. end gtk-redo-icon True - - + False @@ -1442,6 +1441,123 @@ See the <a href="https://www.gnu.org/licenses/gpl-3.0.html">GNU General Pu 1 + + + True + False + 5 + 5 + 5 + 5 + 6 + vertical + 6 + + + True + False + Shortcuts only work while keys are not being recorded and the gui is in focus. + True + 0 + + + False + True + 0 + + + + + + True + False + 18 + + + True + False + ctrl + del + 0 + + + 0 + 0 + + + + + True + False + closes the application + 0 + + + 1 + 1 + + + + + True + False + ctrl + q + 0 + + + 0 + 1 + + + + + True + False + ctrl + r + 0 + + + 0 + 2 + + + + + True + False + refreshes the device list + 0 + + + 1 + 2 + + + + + True + False + stops the injection + 0 + + + 1 + 0 + + + + + False + False + 3 + + + + + Shortcuts + Shortcuts + 2 + + diff --git a/keymapper/gui/helper.py b/keymapper/gui/helper.py index 701d2bef..3a6c3365 100644 --- a/keymapper/gui/helper.py +++ b/keymapper/gui/helper.py @@ -38,11 +38,12 @@ from evdev.ecodes import EV_KEY, EV_ABS from keymapper.ipc.pipe import Pipe from keymapper.logger import logger -from keymapper.getdevices import get_devices +from keymapper.getdevices import get_devices, refresh_devices from keymapper import utils TERMINATE = 'terminate' +GET_DEVICES = 'get_devices' def is_helper_running(): @@ -67,11 +68,7 @@ class RootHelper: self._results = Pipe('/tmp/key-mapper/results') self._commands = Pipe('/tmp/key-mapper/commands') - # the ui needs the devices first - self._results.send({ - 'type': 'devices', - 'message': get_devices() - }) + self._send_devices() self.device_name = None self._pipe = multiprocessing.Pipe() @@ -82,6 +79,13 @@ class RootHelper: self._handle_commands() self._start_reading() + def _send_devices(self): + """Send the get_devices datastructure to the gui.""" + self._results.send({ + 'type': 'devices', + 'message': get_devices() + }) + def _handle_commands(self): """Handle all unread commands.""" # wait for something to do @@ -93,6 +97,9 @@ class RootHelper: if cmd == TERMINATE: logger.debug('Helper terminates') sys.exit(0) + if cmd == GET_DEVICES: + refresh_devices() + self._send_devices() elif cmd in get_devices(): self.device_name = cmd else: @@ -114,7 +121,14 @@ class RootHelper: logger.error('device_name is None') return - group = get_devices()[device_name] + group = get_devices().get(device_name) + + if group is None: + # a device possibly disappeared due to refresh_devices + self.device_name = None + logger.error('%s disappeared', device_name) + return + virtual_devices = [] # Watch over each one of the potentially multiple devices per # hardware diff --git a/keymapper/gui/reader.py b/keymapper/gui/reader.py index 6e577f35..e40e01e0 100644 --- a/keymapper/gui/reader.py +++ b/keymapper/gui/reader.py @@ -32,7 +32,7 @@ from keymapper.logger import logger from keymapper.key import Key from keymapper.getdevices import set_devices from keymapper.ipc.pipe import Pipe -from keymapper.gui.helper import TERMINATE +from keymapper.gui.helper import TERMINATE, GET_DEVICES from keymapper import utils from keymapper.state import custom_mapping from keymapper.getdevices import get_devices, GAMEPAD @@ -89,9 +89,10 @@ class Reader: if message_type == 'devices': # result of get_devices in the helper - logger.debug('Received %d devices', len(message_body)) - set_devices(message_body) - self._devices_updated = True + if message_body != get_devices(): + logger.debug('Received %d devices', len(message_body)) + set_devices(message_body) + self._devices_updated = True return None if message_type == 'event': @@ -195,6 +196,10 @@ class Reader: logger.debug('Sending close msg to helper') self._commands.send(TERMINATE) + def get_devices(self): + """Ask the helper for new devices.""" + self._commands.send(GET_DEVICES) + def clear(self): """Next time when reading don't return the previous keycode.""" logger.debug('Clearing reader') diff --git a/keymapper/gui/window.py b/keymapper/gui/window.py index 960814b5..e1f383be 100755 --- a/keymapper/gui/window.py +++ b/keymapper/gui/window.py @@ -235,13 +235,25 @@ class Window: This has nothing to do with the keycode reader. """ + _, focused = self.get_focused_row() + if isinstance(focused, Gtk.ToggleButton): + return + gdk_keycode = event.get_keyval()[1] if gdk_keycode in [Gdk.KEY_Control_L, Gdk.KEY_Control_R]: self.ctrl = True - if gdk_keycode == Gdk.KEY_q and self.ctrl: - self.on_close() + if self.ctrl: + # shortcuts + if gdk_keycode == Gdk.KEY_q: + self.on_close() + + if gdk_keycode == Gdk.KEY_r: + reader.get_devices() + + if gdk_keycode == Gdk.KEY_Delete: + self.on_restore_defaults_clicked() def key_release(self, _, event): """To execute shortcuts. @@ -345,6 +357,7 @@ class Window: device_selection = self.get('device_selection') with HandlerDisabled(device_selection, self.on_select_device): + print('clearing device_store') self.device_store.clear() for device in devices: types = devices[device]['types'] @@ -443,7 +456,7 @@ class Window: self.show_status( CTX_WARNING, 'ctrl, alt and shift may not combine properly', - 'Your system will probably reinterpret combinations ' + + 'Your system might reinterpret combinations ' + 'with those after they are injected, and by doing so ' + 'break them.' ) @@ -454,7 +467,7 @@ class Window: return True @with_selected_device - def on_apply_system_layout_clicked(self, _): + def on_restore_defaults_clicked(self, *_): """Stop injecting the mapping.""" self.dbus.stop_injecting(self.selected_device) self.show_status(CTX_APPLY, 'Applied the system default') diff --git a/readme/pylint.svg b/readme/pylint.svg index 4aeb9d5c..df8fd8a5 100644 --- a/readme/pylint.svg +++ b/readme/pylint.svg @@ -17,7 +17,7 @@ pylint - 9.83 - 9.83 + 9.82 + 9.82 \ No newline at end of file diff --git a/readme/usage.md b/readme/usage.md index af8d40ee..56e25141 100644 --- a/readme/usage.md +++ b/readme/usage.md @@ -96,8 +96,9 @@ Bear in mind that anti-cheat software might detect macros in games. ## UI Shortcuts -- `shift` + `del` stops the injection (only works while the gui is in focus) +- `ctrl` + `del` stops the injection (only works while the gui is in focus) - `ctrl` + `q` closes the application +- `ctrl` + `r` refreshes the device list ## Key Names diff --git a/tests/test.py b/tests/test.py index e728a0e2..466003ab 100644 --- a/tests/test.py +++ b/tests/test.py @@ -364,9 +364,8 @@ class InputDevice: time.sleep(EVENT_READ_TIMEOUT) try: event = pending_events[self.group][1].recv() - except UnpicklingError as error: + except (UnpicklingError, EOFError): # failed in tests sometimes - print(error) return None self.log(event, 'read_one') @@ -523,7 +522,7 @@ def quick_cleanup(log=True): try: while pending_events[device][1].poll(): pending_events[device][1].recv() - except EOFError: + except (UnpicklingError, EOFError): # it broke, set up a new pipe pending_events[device] = None setup_pipe(device) diff --git a/tests/testcases/test_integration.py b/tests/testcases/test_integration.py index ad004759..3ed3af23 100644 --- a/tests/testcases/test_integration.py +++ b/tests/testcases/test_integration.py @@ -109,7 +109,7 @@ def clean_up_integration(test): if hasattr(test, 'original_on_close'): test.window.on_close = test.original_on_close - test.window.on_apply_system_layout_clicked(None) + test.window.on_restore_defaults_clicked(None) gtk_iteration() test.window.on_close() test.window.window.destroy() @@ -125,6 +125,14 @@ def clean_up_integration(test): original_on_select_preset = Window.on_select_preset +class GtkKeyEvent: + def __init__(self, keyval): + self.keyval = keyval + + def get_keyval(self): + return True, self.keyval + + class TestGetDevicesFromHelper(unittest.TestCase): @classmethod def setUpClass(cls): @@ -236,13 +244,6 @@ class TestIntegration(unittest.TestCase): self.assertTrue(self.window.window.get_visible()) def test_ctrl_q(self): - class Event: - def __init__(self, keyval): - self.keyval = keyval - - def get_keyval(self): - return True, self.keyval - closed = False def on_close(): @@ -251,22 +252,43 @@ class TestIntegration(unittest.TestCase): self.window.on_close = on_close - self.window.key_press(self.window, Event(Gdk.KEY_Control_L)) - self.window.key_press(self.window, Event(Gdk.KEY_a)) - self.window.key_release(self.window, Event(Gdk.KEY_Control_L)) - self.window.key_release(self.window, Event(Gdk.KEY_a)) - self.window.key_press(self.window, Event(Gdk.KEY_b)) - self.window.key_press(self.window, Event(Gdk.KEY_q)) - self.window.key_release(self.window, Event(Gdk.KEY_q)) - self.window.key_release(self.window, Event(Gdk.KEY_b)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_a)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_a)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_b)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_q)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_q)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_b)) self.assertFalse(closed) - self.window.key_press(self.window, Event(Gdk.KEY_Control_L)) - self.window.key_press(self.window, Event(Gdk.KEY_q)) + # while keys are being recorded no shortcut should work + rows = self.get_rows() + row = rows[-1] + self.window.window.set_focus(row.keycode_input) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_q)) + self.assertFalse(closed) + + self.window.window.set_focus(None) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_q)) self.assertTrue(closed) - self.window.key_release(self.window, Event(Gdk.KEY_Control_L)) - self.window.key_release(self.window, Event(Gdk.KEY_q)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_q)) + + def test_ctrl_r(self): + with patch.object(reader, 'get_devices') as reader_get_devices_patch: + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_r)) + reader_get_devices_patch.assert_called_once() + + def test_ctrl_del(self): + with patch.object(self.window.dbus, 'stop_injecting') as stop_injecting: + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Control_L)) + self.window.key_press(self.window, GtkKeyEvent(Gdk.KEY_Delete)) + stop_injecting.assert_called_once() def test_show_device_mapping_status(self): # this function may not return True, otherwise the timeout @@ -1293,7 +1315,7 @@ class TestIntegration(unittest.TestCase): write_history = [pipe.recv()] # stop - self.window.on_apply_system_layout_clicked(None) + self.window.on_restore_defaults_clicked(None) # try to receive a few of the events time.sleep(0.2) diff --git a/tests/testcases/test_reader.py b/tests/testcases/test_reader.py index d25c8de1..01f2f1df 100644 --- a/tests/testcases/test_reader.py +++ b/tests/testcases/test_reader.py @@ -33,7 +33,7 @@ from keymapper.state import custom_mapping from keymapper.config import BUTTONS, MOUSE from keymapper.key import Key from keymapper.gui.helper import RootHelper -from keymapper.getdevices import set_devices +from keymapper.getdevices import set_devices, get_devices, refresh_devices from tests.test import new_event, push_events, send_event_to_reader, \ EVENT_READ_TIMEOUT, START_READING_DELAY, quick_cleanup, MAX_ABS @@ -63,6 +63,7 @@ class TestReader(unittest.TestCase): quick_cleanup() if self.helper is not None: self.helper.join() + refresh_devices() def create_helper(self): # this will cause pending events to be copied over to the helper @@ -506,6 +507,26 @@ class TestReader(unittest.TestCase): reader.read() self.assertTrue(reader.are_new_devices_available()) + # a bit weird, but it assumes the gui handled that and returns + # false afterwards + self.assertFalse(reader.are_new_devices_available()) + + # send the same devices again + reader._get_event({ + 'type': 'devices', + 'message': get_devices() + }) + self.assertFalse(reader.are_new_devices_available()) + + # send changed devices + message = {**get_devices()} + del message['device 1'] + reader._get_event({ + 'type': 'devices', + 'message': message + }) + self.assertTrue(reader.are_new_devices_available()) + self.assertFalse(reader.are_new_devices_available()) if __name__ == "__main__":