mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-16 06:12:58 +00:00
1621 lines
62 KiB
Python
1621 lines
62 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
# key-mapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2021 sezanzeb <proxima@sezanzeb.de>
|
|
#
|
|
# This file is part of key-mapper.
|
|
#
|
|
# key-mapper is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# key-mapper is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with key-mapper. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
import sys
|
|
import time
|
|
import atexit
|
|
import os
|
|
import unittest
|
|
import multiprocessing
|
|
import evdev
|
|
from evdev.ecodes import (
|
|
EV_KEY,
|
|
EV_ABS,
|
|
KEY_LEFTSHIFT,
|
|
KEY_A,
|
|
ABS_RX,
|
|
EV_REL,
|
|
REL_X,
|
|
ABS_X,
|
|
)
|
|
import json
|
|
from unittest.mock import patch
|
|
from importlib.util import spec_from_loader, module_from_spec
|
|
from importlib.machinery import SourceFileLoader
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk, Gdk
|
|
|
|
from keymapper.system_mapping import system_mapping, XMODMAP_FILENAME
|
|
from keymapper.gui.custom_mapping import custom_mapping
|
|
from keymapper.paths import CONFIG_PATH, get_preset_path, get_config_path
|
|
from keymapper.config import config, WHEEL, MOUSE, BUTTONS
|
|
from keymapper.gui.reader import reader
|
|
from keymapper.injection.injector import RUNNING, FAILED, UNKNOWN
|
|
from keymapper.gui.row import Row, to_string, HOLDING, IDLE
|
|
from keymapper.gui.window import Window
|
|
from keymapper.key import Key
|
|
from keymapper.daemon import Daemon
|
|
from keymapper.groups import groups
|
|
from keymapper.gui.helper import RootHelper
|
|
|
|
from tests.test import (
|
|
tmp,
|
|
push_events,
|
|
new_event,
|
|
spy,
|
|
cleanup,
|
|
uinput_write_history_pipe,
|
|
MAX_ABS,
|
|
EVENT_READ_TIMEOUT,
|
|
send_event_to_reader,
|
|
MIN_ABS,
|
|
)
|
|
|
|
|
|
def gtk_iteration():
|
|
"""Iterate while events are pending."""
|
|
while Gtk.events_pending():
|
|
Gtk.main_iteration()
|
|
|
|
|
|
# iterate a few times when Gtk.main() is called, but don't block
|
|
# there and just continue to the tests while the UI becomes
|
|
# unresponsive
|
|
Gtk.main = gtk_iteration
|
|
|
|
# doesn't do much except avoid some Gtk assertion error, whatever:
|
|
Gtk.main_quit = lambda: None
|
|
|
|
|
|
def launch(argv=None):
|
|
"""Start key-mapper-gtk with the command line argument array argv."""
|
|
bin_path = os.path.join(os.getcwd(), "bin", "key-mapper-gtk")
|
|
|
|
if not argv:
|
|
argv = ["-d"]
|
|
|
|
with patch("keymapper.gui.window.Window.setup_timeouts", lambda *args: None):
|
|
with patch.object(sys, "argv", [""] + [str(arg) for arg in argv]):
|
|
loader = SourceFileLoader("__main__", bin_path)
|
|
spec = spec_from_loader("__main__", loader)
|
|
module = module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
|
|
gtk_iteration()
|
|
|
|
# otherwise a new handler is added with each call to launch, which
|
|
# spams tons of garbage when all tests finish
|
|
atexit.unregister(module.stop)
|
|
|
|
# to avoid triggering any timeouts while the module loads, patch it and
|
|
# do it afterwards. Because some tests don't want them to be triggered
|
|
# yet and test the windows initial state. This is only a problem on
|
|
# slow computers that take long for the window import.
|
|
module.window.setup_timeouts()
|
|
|
|
return module.window
|
|
|
|
|
|
class FakeDeviceDropdown(Gtk.ComboBoxText):
|
|
def __init__(self, group):
|
|
if type(group) == str:
|
|
group = groups.find(key=group)
|
|
|
|
self.group = group
|
|
|
|
def get_active_text(self):
|
|
return self.group.name
|
|
|
|
def get_active_id(self):
|
|
return self.group.key
|
|
|
|
def set_active_id(self, key):
|
|
self.group = groups.find(key=key)
|
|
|
|
|
|
class FakePresetDropdown(Gtk.ComboBoxText):
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
def get_active_text(self):
|
|
return self.name
|
|
|
|
def get_active_id(self):
|
|
return self.name
|
|
|
|
def set_active_id(self, name):
|
|
self.name = name
|
|
|
|
|
|
def clean_up_integration(test):
|
|
if hasattr(test, "original_on_close"):
|
|
test.window.on_close = test.original_on_close
|
|
|
|
test.window.on_restore_defaults_clicked(None)
|
|
gtk_iteration()
|
|
test.window.on_close()
|
|
test.window.window.destroy()
|
|
gtk_iteration()
|
|
cleanup()
|
|
|
|
# do this now, not when all tests are finished
|
|
test.window.dbus.stop_all()
|
|
if isinstance(test.window.dbus, Daemon):
|
|
atexit.unregister(test.window.dbus.stop_all)
|
|
|
|
|
|
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 TestGroupsFromHelper(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.injector = None
|
|
cls.grab = evdev.InputDevice.grab
|
|
|
|
# don't try to connect, return an object instance of it instead
|
|
cls.original_connect = Daemon.connect
|
|
Daemon.connect = Daemon
|
|
|
|
cls.original_os_system = os.system
|
|
|
|
def os_system(cmd):
|
|
# instead of running pkexec, fork instead. This will make
|
|
# the helper aware of all the test patches
|
|
if "pkexec key-mapper-control --command helper" in cmd:
|
|
# the forked process should get the initial groups
|
|
groups.refresh()
|
|
multiprocessing.Process(target=RootHelper).start()
|
|
# the gui an empty dict, because it doesn't know any devices
|
|
# without the help of the privileged helper
|
|
groups.set_groups({})
|
|
return 0
|
|
|
|
return cls.original_os_system(cmd)
|
|
|
|
os.system = os_system
|
|
|
|
def setUp(self):
|
|
self.window = launch()
|
|
# verify that the ui doesn't have knowledge of any device yet
|
|
print("test self.assertIsNone(self.window.group)")
|
|
self.assertIsNone(self.window.group)
|
|
self.assertEqual(len(groups), 0)
|
|
|
|
def tearDown(self):
|
|
clean_up_integration(self)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
os.system = cls.original_os_system
|
|
Daemon.connect = cls.original_connect
|
|
|
|
@patch("keymapper.gui.window.Window.on_select_preset")
|
|
def test_knows_devices(self, on_select_preset_patch):
|
|
# verify that it is working as expected
|
|
gtk_iteration()
|
|
self.assertIsNone(self.window.group)
|
|
self.assertIsNone(self.window.preset_name)
|
|
self.assertEqual(len(groups), 0)
|
|
on_select_preset_patch.assert_not_called()
|
|
|
|
# perform some iterations so that the gui ends up running
|
|
# consume_newest_keycode, which will make it receive devices.
|
|
# Restore patch, otherwise gtk complains when disabling handlers
|
|
Window.on_select_preset = original_on_select_preset
|
|
for _ in range(10):
|
|
time.sleep(0.01)
|
|
gtk_iteration()
|
|
|
|
self.assertIsNotNone(groups.find(key="Foo Device 2"))
|
|
self.assertIsNotNone(groups.find(name="Bar Device"))
|
|
self.assertIsNotNone(groups.find(name="gamepad"))
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
|
|
|
|
class TestIntegration(unittest.TestCase):
|
|
"""For tests that use the window.
|
|
|
|
Try to modify the configuration only by calling functions of the window.
|
|
"""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.injector = None
|
|
cls.grab = evdev.InputDevice.grab
|
|
|
|
def start_processes(self):
|
|
"""Avoid running pkexec which requires user input, and fork in
|
|
order to pass the fixtures to the helper and daemon process.
|
|
"""
|
|
multiprocessing.Process(target=RootHelper).start()
|
|
self.dbus = Daemon()
|
|
|
|
Window.start_processes = start_processes
|
|
|
|
def setUp(self):
|
|
self.window = launch()
|
|
self.original_on_close = self.window.on_close
|
|
|
|
self.grab_fails = False
|
|
|
|
def grab(_):
|
|
if self.grab_fails:
|
|
raise OSError()
|
|
|
|
evdev.InputDevice.grab = grab
|
|
|
|
config.save_config()
|
|
|
|
def tearDown(self):
|
|
clean_up_integration(self)
|
|
|
|
def get_rows(self):
|
|
return self.window.get("key_list").get_children()
|
|
|
|
def get_status_text(self):
|
|
status_bar = self.window.get("status_bar")
|
|
return status_bar.get_message_area().get_children()[0].get_label()
|
|
|
|
def test_can_start(self):
|
|
self.assertIsNotNone(self.window)
|
|
self.assertTrue(self.window.window.get_visible())
|
|
|
|
def test_ctrl_q(self):
|
|
closed = False
|
|
|
|
def on_close():
|
|
nonlocal closed
|
|
closed = True
|
|
|
|
self.window.on_close = on_close
|
|
|
|
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)
|
|
|
|
# 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, GtkKeyEvent(Gdk.KEY_Control_L))
|
|
self.window.key_release(self.window, GtkKeyEvent(Gdk.KEY_q))
|
|
|
|
def test_ctrl_r(self):
|
|
with patch.object(reader, "refresh_groups") 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
|
|
# runs forever
|
|
self.assertFalse(self.window.show_device_mapping_status())
|
|
|
|
def test_autoload(self):
|
|
with spy(self.window.dbus, "set_config_dir") as set_config_dir:
|
|
self.window.on_autoload_switch(None, False)
|
|
set_config_dir.assert_called_once()
|
|
|
|
self.assertFalse(
|
|
config.is_autoloaded(self.window.group.key, self.window.preset_name)
|
|
)
|
|
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device 2"))
|
|
gtk_iteration()
|
|
self.assertFalse(self.window.get("preset_autoload_switch").get_active())
|
|
|
|
# select a preset for the first device
|
|
self.window.get("preset_autoload_switch").set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(self.window.get("preset_autoload_switch").get_active())
|
|
self.assertEqual(self.window.group.key, "Foo Device 2")
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
self.assertTrue(config.is_autoloaded(self.window.group.key, "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Bar Device", "new preset"))
|
|
self.assertListEqual(
|
|
list(config.iterate_autoload_presets()), [("Foo Device 2", "new preset")]
|
|
)
|
|
|
|
# create a new preset, the switch should be correctly off and the
|
|
# config not changed.
|
|
self.window.on_create_preset_clicked(None)
|
|
gtk_iteration()
|
|
self.assertEqual(self.window.preset_name, "new preset 2")
|
|
self.assertFalse(self.window.get("preset_autoload_switch").get_active())
|
|
self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Foo Device", "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Foo Device", "new preset 2"))
|
|
self.assertFalse(config.is_autoloaded("Foo Device 2", "new preset 2"))
|
|
|
|
# select a preset for the second device
|
|
self.window.on_select_device(FakeDeviceDropdown("Bar Device"))
|
|
self.window.get("preset_autoload_switch").set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Foo Device", "new preset"))
|
|
self.assertTrue(config.is_autoloaded("Bar Device", "new preset"))
|
|
self.assertListEqual(
|
|
list(config.iterate_autoload_presets()),
|
|
[("Foo Device 2", "new preset"), ("Bar Device", "new preset")],
|
|
)
|
|
|
|
# disable autoloading for the second device
|
|
self.window.get("preset_autoload_switch").set_active(False)
|
|
gtk_iteration()
|
|
self.assertTrue(config.is_autoloaded("Foo Device 2", "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Foo Device", "new preset"))
|
|
self.assertFalse(config.is_autoloaded("Bar Device", "new preset"))
|
|
self.assertListEqual(
|
|
list(config.iterate_autoload_presets()), [("Foo Device 2", "new preset")]
|
|
)
|
|
|
|
def test_select_device(self):
|
|
# creates a new empty preset when no preset exists for the device
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device"))
|
|
custom_mapping.change(Key(EV_KEY, 50, 1), "q")
|
|
custom_mapping.change(Key(EV_KEY, 51, 1), "u")
|
|
custom_mapping.change(Key(EV_KEY, 52, 1), "x")
|
|
self.assertEqual(len(custom_mapping), 3)
|
|
self.window.on_select_device(FakeDeviceDropdown("Bar Device"))
|
|
self.assertEqual(len(custom_mapping), 0)
|
|
# it creates the file for that right away. It may have been possible
|
|
# to write it such that it doesn't (its empty anyway), but it does,
|
|
# so use that to test it in more detail.
|
|
path = get_preset_path("Bar Device", "new preset")
|
|
self.assertTrue(os.path.exists(path))
|
|
with open(path, "r") as file:
|
|
preset = json.load(file)
|
|
self.assertEqual(len(preset["mapping"]), 0)
|
|
|
|
def test_permission_error_on_create_preset_clicked(self):
|
|
def save(_=None):
|
|
raise PermissionError
|
|
|
|
with patch.object(custom_mapping, "save", save):
|
|
self.window.on_create_preset_clicked(None)
|
|
status = self.get_status_text()
|
|
self.assertIn("Permission denied", status)
|
|
|
|
def test_show_injection_result_failure(self):
|
|
def get_state(_=None):
|
|
return FAILED
|
|
|
|
with patch.object(self.window.dbus, "get_state", get_state):
|
|
self.window.show_injection_result()
|
|
text = self.get_status_text()
|
|
self.assertIn("Failed", text)
|
|
|
|
def test_row_keycode_to_string(self):
|
|
# not an integration test, but I have all the row tests here already
|
|
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.KEY_A, 1)), "a")
|
|
self.assertEqual(
|
|
to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)), "DPad Left"
|
|
)
|
|
self.assertEqual(to_string(Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)), "DPad Up")
|
|
self.assertEqual(to_string(Key(EV_KEY, evdev.ecodes.BTN_A, 1)), "Button A")
|
|
self.assertEqual(to_string(Key(EV_KEY, 1234, 1)), "unknown")
|
|
self.assertEqual(
|
|
to_string(Key(EV_ABS, evdev.ecodes.ABS_X, 1)), "Joystick Right"
|
|
)
|
|
self.assertEqual(
|
|
to_string(Key(EV_ABS, evdev.ecodes.ABS_RY, 1)), "Joystick 2 Down"
|
|
)
|
|
self.assertEqual(
|
|
to_string(Key(EV_REL, evdev.ecodes.REL_HWHEEL, 1)), "Wheel Right"
|
|
)
|
|
self.assertEqual(
|
|
to_string(Key(EV_REL, evdev.ecodes.REL_WHEEL, -1)), "Wheel Down"
|
|
)
|
|
|
|
# combinations
|
|
self.assertEqual(
|
|
to_string(
|
|
Key(
|
|
(EV_KEY, evdev.ecodes.BTN_A, 1),
|
|
(EV_KEY, evdev.ecodes.BTN_B, 1),
|
|
(EV_KEY, evdev.ecodes.BTN_C, 1),
|
|
)
|
|
),
|
|
"Button A + Button B + Button C",
|
|
)
|
|
|
|
def test_row_simple(self):
|
|
rows = self.window.get("key_list").get_children()
|
|
self.assertEqual(len(rows), 1)
|
|
|
|
row = rows[0]
|
|
|
|
row.set_new_key(None)
|
|
self.assertIsNone(row.get_key())
|
|
self.assertEqual(len(custom_mapping), 0)
|
|
self.assertEqual(row.keycode_input.get_label(), "click here")
|
|
|
|
row.set_new_key(Key(EV_KEY, 30, 1))
|
|
self.assertEqual(len(custom_mapping), 0)
|
|
self.assertEqual(row.get_key(), (EV_KEY, 30, 1))
|
|
# this is KEY_A in linux/input-event-codes.h,
|
|
# but KEY_ is removed from the text
|
|
self.assertEqual(row.keycode_input.get_label(), "a")
|
|
|
|
row.set_new_key(Key(EV_KEY, 30, 1))
|
|
self.assertEqual(len(custom_mapping), 0)
|
|
self.assertEqual(row.get_key(), (EV_KEY, 30, 1))
|
|
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.window.get("key_list").get_children()), 1)
|
|
|
|
row.symbol_input.set_text("Shift_L")
|
|
self.assertEqual(len(custom_mapping), 1)
|
|
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.window.get("key_list").get_children()), 2)
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 30, 1)), "Shift_L")
|
|
self.assertEqual(row.get_symbol(), "Shift_L")
|
|
self.assertEqual(row.get_key(), (EV_KEY, 30, 1))
|
|
|
|
def sleep(self, num_events):
|
|
for _ in range(num_events * 2):
|
|
time.sleep(EVENT_READ_TIMEOUT)
|
|
gtk_iteration()
|
|
|
|
time.sleep(1 / 30) # one window iteration
|
|
gtk_iteration()
|
|
|
|
def test_row_not_focused(self):
|
|
# focus anything that is not the row,
|
|
# no keycode should be inserted into it
|
|
self.window.window.set_focus(self.window.get("preset_name_input"))
|
|
send_event_to_reader(new_event(1, 61, 1))
|
|
self.window.consume_newest_keycode()
|
|
|
|
rows = self.get_rows()
|
|
self.assertEqual(len(rows), 1)
|
|
row = rows[0]
|
|
|
|
# the empty row has this key not set
|
|
self.assertIsNone(row.get_key())
|
|
|
|
# focus the text input instead
|
|
self.window.window.set_focus(row.symbol_input)
|
|
send_event_to_reader(new_event(1, 61, 1))
|
|
self.window.consume_newest_keycode()
|
|
|
|
# still nothing set
|
|
self.assertIsNone(row.get_key())
|
|
|
|
def test_show_status(self):
|
|
self.window.show_status(0, "a" * 100)
|
|
text = self.get_status_text()
|
|
self.assertIn("...", text)
|
|
|
|
self.window.show_status(0, "b")
|
|
text = self.get_status_text()
|
|
self.assertNotIn("...", text)
|
|
|
|
def change_empty_row(self, key, char, code_first=True, expect_success=True):
|
|
"""Modify the one empty row that always exists.
|
|
|
|
Utility function for other tests.
|
|
|
|
Parameters
|
|
----------
|
|
key : Key or None
|
|
code_first : boolean
|
|
If True, the code is entered and then the symbol.
|
|
If False, the symbol is entered first.
|
|
expect_success : boolean
|
|
If this change on the empty row is going to result in a change
|
|
in the mapping eventually. False if this change is going to
|
|
cause a duplicate.
|
|
"""
|
|
self.assertIsNone(reader.get_unreleased_keys())
|
|
|
|
changed = custom_mapping.changed
|
|
|
|
# wait for the window to create a new empty row if needed
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
|
|
# find the empty row
|
|
rows = self.get_rows()
|
|
row = rows[-1]
|
|
self.assertIsNone(row.get_key())
|
|
self.assertEqual(row.symbol_input.get_text(), "")
|
|
self.assertEqual(row._state, IDLE)
|
|
|
|
if char and not code_first:
|
|
# set the symbol to make the new row complete
|
|
self.assertIsNone(row.get_symbol())
|
|
row.symbol_input.set_text(char)
|
|
self.assertEqual(row.get_symbol(), char)
|
|
|
|
if row.keycode_input.is_focus():
|
|
self.assertEqual(row.keycode_input.get_label(), "press key")
|
|
else:
|
|
self.assertEqual(row.keycode_input.get_label(), "click here")
|
|
|
|
self.window.window.set_focus(row.keycode_input)
|
|
gtk_iteration()
|
|
gtk_iteration()
|
|
self.assertIsNone(row.get_key())
|
|
self.assertEqual(row.keycode_input.get_label(), "press key")
|
|
|
|
if key:
|
|
# modifies the keycode in the row not by writing into the input,
|
|
# but by sending an event. press down all the keys of a combination
|
|
for sub_key in key:
|
|
|
|
send_event_to_reader(new_event(*sub_key))
|
|
# this will be consumed all at once, since no gt_iteration
|
|
# is done
|
|
|
|
# make the window consume the keycode
|
|
self.sleep(len(key))
|
|
|
|
# holding down
|
|
self.assertIsNotNone(reader.get_unreleased_keys())
|
|
self.assertGreater(len(reader.get_unreleased_keys()), 0)
|
|
self.assertEqual(row._state, HOLDING)
|
|
self.assertTrue(row.keycode_input.is_focus())
|
|
|
|
# release all the keys
|
|
for sub_key in key:
|
|
send_event_to_reader(new_event(*sub_key[:2], 0))
|
|
|
|
# wait for the window to consume the keycode
|
|
self.sleep(len(key))
|
|
|
|
# released
|
|
self.assertIsNone(reader.get_unreleased_keys())
|
|
self.assertEqual(row._state, IDLE)
|
|
|
|
if expect_success:
|
|
self.assertEqual(row.get_key(), key)
|
|
self.assertEqual(row.keycode_input.get_label(), to_string(key))
|
|
self.assertFalse(row.keycode_input.is_focus())
|
|
self.assertEqual(len(reader._unreleased), 0)
|
|
|
|
if not expect_success:
|
|
self.assertIsNone(row.get_key())
|
|
self.assertIsNone(row.get_symbol())
|
|
self.assertEqual(row._state, IDLE)
|
|
# it won't switch the focus to the symbol input
|
|
self.assertTrue(row.keycode_input.is_focus())
|
|
self.assertEqual(custom_mapping.changed, changed)
|
|
return row
|
|
|
|
if char and code_first:
|
|
# set the symbol to make the new row complete
|
|
self.assertIsNone(row.get_symbol())
|
|
row.symbol_input.set_text(char)
|
|
self.assertEqual(row.get_symbol(), char)
|
|
|
|
# unfocus them to trigger some final logic
|
|
self.window.window.set_focus(None)
|
|
correct_case = system_mapping.correct_case(char)
|
|
self.assertEqual(row.get_symbol(), correct_case)
|
|
self.assertFalse(custom_mapping.changed)
|
|
|
|
return row
|
|
|
|
def test_clears_unreleased_on_focus_change(self):
|
|
ev_1 = Key(EV_KEY, 41, 1)
|
|
|
|
# focus
|
|
self.window.window.set_focus(self.get_rows()[0].keycode_input)
|
|
send_event_to_reader(new_event(*ev_1.keys[0]))
|
|
reader.read()
|
|
self.assertEqual(reader.get_unreleased_keys(), ev_1)
|
|
|
|
# unfocus
|
|
# doesn't call reader.clear
|
|
# because otherwise the super key cannot be mapped
|
|
self.window.window.set_focus(None)
|
|
self.assertEqual(reader.get_unreleased_keys(), ev_1)
|
|
|
|
# focus different row
|
|
self.window.add_empty()
|
|
self.window.window.set_focus(self.get_rows()[1].keycode_input)
|
|
self.assertEqual(reader.get_unreleased_keys(), None)
|
|
|
|
def test_rows(self):
|
|
"""Comprehensive test for rows."""
|
|
system_mapping.clear()
|
|
system_mapping._set("Foo_BAR", 41)
|
|
system_mapping._set("B", 42)
|
|
system_mapping._set("c", 43)
|
|
system_mapping._set("d", 44)
|
|
|
|
# how many rows there should be in the end
|
|
num_rows_target = 3
|
|
|
|
ev_1 = Key(EV_KEY, 10, 1)
|
|
ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
|
|
|
|
"""edit"""
|
|
|
|
# add two rows by modifiying the one empty row that exists.
|
|
# Insert lowercase, it should be corrected to uppercase as stored
|
|
# in system_mapping
|
|
self.change_empty_row(ev_1, "foo_bar", code_first=False)
|
|
self.change_empty_row(ev_2, "k(b).k(c)")
|
|
|
|
# one empty row added automatically again
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.get_rows()), num_rows_target)
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(ev_1), "Foo_BAR")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_2), "k(b).k(c)")
|
|
|
|
"""edit first row"""
|
|
|
|
row = self.get_rows()[0]
|
|
row.symbol_input.set_text("c")
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(ev_1), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_2), "k(b).k(c)")
|
|
self.assertTrue(custom_mapping.changed)
|
|
|
|
"""add duplicate"""
|
|
|
|
# try to add a duplicate keycode, it should be ignored
|
|
self.change_empty_row(ev_2, "d", expect_success=False)
|
|
self.assertEqual(custom_mapping.get_symbol(ev_2), "k(b).k(c)")
|
|
# and the number of rows shouldn't change
|
|
self.assertEqual(len(self.get_rows()), num_rows_target)
|
|
|
|
def test_hat0x(self):
|
|
# it should be possible to add all of them
|
|
ev_1 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
|
|
ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
|
|
ev_3 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, -1)
|
|
ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0Y, 1)
|
|
|
|
self.change_empty_row(ev_1, "a")
|
|
self.change_empty_row(ev_2, "b")
|
|
self.change_empty_row(ev_3, "c")
|
|
self.change_empty_row(ev_4, "d")
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(ev_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_2), "b")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_3), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_4), "d")
|
|
|
|
# and trying to add them as duplicate rows will be ignored for each
|
|
# of them
|
|
self.change_empty_row(ev_1, "e", expect_success=False)
|
|
self.change_empty_row(ev_2, "f", expect_success=False)
|
|
self.change_empty_row(ev_3, "g", expect_success=False)
|
|
self.change_empty_row(ev_4, "h", expect_success=False)
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(ev_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_2), "b")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_3), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(ev_4), "d")
|
|
|
|
def test_combination(self):
|
|
# it should be possible to write a key combination
|
|
ev_1 = Key(EV_KEY, evdev.ecodes.KEY_A, 1)
|
|
ev_2 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, 1)
|
|
ev_3 = Key(EV_KEY, evdev.ecodes.KEY_C, 1)
|
|
ev_4 = Key(EV_ABS, evdev.ecodes.ABS_HAT0X, -1)
|
|
combination_1 = Key(ev_1, ev_2, ev_3)
|
|
combination_2 = Key(ev_2, ev_1, ev_3)
|
|
|
|
# same as 1, but different D-Pad direction
|
|
combination_3 = Key(ev_1, ev_4, ev_3)
|
|
combination_4 = Key(ev_4, ev_1, ev_3)
|
|
|
|
# same as 1, but the last key is different
|
|
combination_5 = Key(ev_1, ev_3, ev_2)
|
|
combination_6 = Key(ev_3, ev_1, ev_2)
|
|
|
|
self.change_empty_row(combination_1, "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_2), "a")
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_3))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_4))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_5))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_6))
|
|
|
|
# it won't write the same combination again, even if the
|
|
# first two events are in a different order
|
|
self.change_empty_row(combination_2, "b", expect_success=False)
|
|
self.assertEqual(custom_mapping.get_symbol(combination_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_2), "a")
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_3))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_4))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_5))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_6))
|
|
|
|
self.change_empty_row(combination_3, "c")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_2), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_3), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_4), "c")
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_5))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_6))
|
|
|
|
# same as with combination_2, the existing combination_3 blocks
|
|
# combination_4 because they have the same keys and end in the
|
|
# same key.
|
|
self.change_empty_row(combination_4, "d", expect_success=False)
|
|
self.assertEqual(custom_mapping.get_symbol(combination_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_2), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_3), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_4), "c")
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_5))
|
|
self.assertIsNone(custom_mapping.get_symbol(combination_6))
|
|
|
|
self.change_empty_row(combination_5, "e")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_1), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_2), "a")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_3), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_4), "c")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_5), "e")
|
|
self.assertEqual(custom_mapping.get_symbol(combination_6), "e")
|
|
|
|
error_icon = self.window.get("error_status_icon")
|
|
warning_icon = self.window.get("warning_status_icon")
|
|
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
def test_remove_row(self):
|
|
"""Comprehensive test for rows 2."""
|
|
# sleeps are added to be able to visually follow and debug the test.
|
|
# add two rows by modifiying the one empty row that exists
|
|
row_1 = self.change_empty_row(Key(EV_KEY, 10, 1), "a")
|
|
row_2 = self.change_empty_row(Key(EV_KEY, 11, 1), "b")
|
|
row_3 = self.change_empty_row(None, "c")
|
|
|
|
# no empty row added because one is unfinished
|
|
time.sleep(0.2)
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.get_rows()), 3)
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 11, 1)), "b")
|
|
|
|
def remove(row, code, char, num_rows_after):
|
|
"""Remove a row by clicking the delete button.
|
|
|
|
Parameters
|
|
----------
|
|
row : Row
|
|
code : int or None
|
|
keycode of the mapping that is displayed by this row
|
|
char : string or None
|
|
ouptut of the mapping that is displayed by this row
|
|
num_rows_after : int
|
|
after deleting, how many rows are expected to still be there
|
|
"""
|
|
if code is not None and char is not None:
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, code, 1)), char)
|
|
|
|
self.assertEqual(row.get_symbol(), char)
|
|
if code is None:
|
|
self.assertIsNone(row.get_key())
|
|
else:
|
|
self.assertEqual(row.get_key(), Key(EV_KEY, code, 1))
|
|
|
|
row.on_delete_button_clicked()
|
|
time.sleep(0.2)
|
|
gtk_iteration()
|
|
|
|
# if a reference to the row is held somewhere and it is
|
|
# accidentally used again, make sure to not provide any outdated
|
|
# information that is supposed to be deleted
|
|
self.assertIsNone(row.get_key())
|
|
self.assertIsNone(row.get_symbol())
|
|
if code is not None:
|
|
self.assertIsNone(custom_mapping.get_symbol(Key(EV_KEY, code, 1)))
|
|
self.assertEqual(len(self.get_rows()), num_rows_after)
|
|
|
|
remove(row_1, 10, "a", 2)
|
|
remove(row_2, 11, "b", 1)
|
|
# there is no empty row at the moment, so after removing that one,
|
|
# which is the only row, one empty row will be there. So the number
|
|
# of rows won't change.
|
|
remove(row_3, None, "c", 1)
|
|
|
|
def test_problematic_combination(self):
|
|
combination = Key((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1))
|
|
self.change_empty_row(combination, "b")
|
|
text = self.get_status_text()
|
|
self.assertIn("shift", text)
|
|
|
|
error_icon = self.window.get("error_status_icon")
|
|
warning_icon = self.window.get("warning_status_icon")
|
|
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertTrue(warning_icon.get_visible())
|
|
|
|
def test_rename_and_save(self):
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
self.assertFalse(config.is_autoloaded("Foo Device", "new preset"))
|
|
|
|
custom_mapping.change(Key(EV_KEY, 14, 1), "a", None)
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
self.window.save_preset()
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 14, 1)), "a")
|
|
config.set_autoload_preset("Foo Device", "new preset")
|
|
self.assertTrue(config.is_autoloaded("Foo Device", "new preset"))
|
|
|
|
custom_mapping.change(Key(EV_KEY, 14, 1), "b", None)
|
|
self.window.get("preset_name_input").set_text("asdf")
|
|
self.window.save_preset()
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "asdf")
|
|
preset_path = f"{CONFIG_PATH}/presets/Foo Device/asdf.json"
|
|
self.assertTrue(os.path.exists(preset_path))
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 14, 1)), "b")
|
|
# after renaming the preset it is still set to autoload
|
|
self.assertTrue(config.is_autoloaded("Foo Device", "asdf"))
|
|
|
|
error_icon = self.window.get("error_status_icon")
|
|
self.assertFalse(error_icon.get_visible())
|
|
|
|
# otherwise save won't do anything
|
|
custom_mapping.change(Key(EV_KEY, 14, 1), "c", None)
|
|
self.assertTrue(custom_mapping.changed)
|
|
|
|
def save(_):
|
|
raise PermissionError
|
|
|
|
with patch.object(custom_mapping, "save", save):
|
|
self.window.save_preset()
|
|
status = self.get_status_text()
|
|
self.assertIn("Permission denied", status)
|
|
|
|
with patch.object(
|
|
self.window, "show_confirm_delete", lambda: Gtk.ResponseType.ACCEPT
|
|
):
|
|
self.window.on_delete_preset_clicked(None)
|
|
self.assertFalse(os.path.exists(preset_path))
|
|
|
|
def test_rename_create_switch(self):
|
|
# after renaming a preset and saving it, new presets
|
|
# start with "new preset" again
|
|
custom_mapping.change(Key(EV_KEY, 14, 1), "a", None)
|
|
self.window.get("preset_name_input").set_text("asdf")
|
|
self.window.save_preset()
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(len(custom_mapping), 1)
|
|
self.assertEqual(self.window.preset_name, "asdf")
|
|
|
|
self.window.on_create_preset_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
self.assertIsNone(custom_mapping.get_symbol(Key(EV_KEY, 14, 1)))
|
|
self.window.save_preset()
|
|
|
|
# selecting the first one again loads the saved mapping
|
|
self.window.on_select_preset(FakePresetDropdown("asdf"))
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 14, 1)), "a")
|
|
config.set_autoload_preset("Foo Device", "new preset")
|
|
|
|
# renaming a preset to an existing name appends a number
|
|
self.window.on_select_preset(FakePresetDropdown("new preset"))
|
|
self.window.get("preset_name_input").set_text("asdf")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "asdf 2")
|
|
# and that added number is correctly used in the autoload
|
|
# configuration as well
|
|
self.assertTrue(config.is_autoloaded("Foo Device", "asdf 2"))
|
|
|
|
self.assertEqual(self.window.get("preset_name_input").get_text(), "")
|
|
|
|
# renaming the current preset to itself doesn't append a number and
|
|
# it doesn't do anything on the file system
|
|
def _raise(*_):
|
|
# should not get called
|
|
raise AssertionError
|
|
|
|
with patch.object(os, "rename", _raise):
|
|
self.window.get("preset_name_input").set_text("asdf 2")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "asdf 2")
|
|
|
|
self.window.get("preset_name_input").set_text("")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "asdf 2")
|
|
|
|
def test_avoids_redundant_saves(self):
|
|
custom_mapping.change(Key(EV_KEY, 14, 1), "abcd", None)
|
|
|
|
custom_mapping.changed = False
|
|
self.window.save_preset()
|
|
|
|
with open(get_preset_path("Foo Device", "new preset")) as f:
|
|
content = f.read()
|
|
self.assertNotIn("abcd", content)
|
|
|
|
custom_mapping.changed = True
|
|
self.window.save_preset()
|
|
|
|
with open(get_preset_path("Foo Device", "new preset")) as f:
|
|
content = f.read()
|
|
self.assertIn("abcd", content)
|
|
|
|
def test_check_for_unknown_symbols(self):
|
|
status = self.window.get("status_bar")
|
|
error_icon = self.window.get("error_status_icon")
|
|
warning_icon = self.window.get("warning_status_icon")
|
|
|
|
custom_mapping.change(Key(EV_KEY, 71, 1), "qux", None)
|
|
custom_mapping.change(Key(EV_KEY, 72, 1), "foo", None)
|
|
self.window.save_preset()
|
|
tooltip = status.get_tooltip_text().lower()
|
|
self.assertIn("qux", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
# it will still save it though
|
|
with open(get_preset_path("Foo Device", "new preset")) as f:
|
|
content = f.read()
|
|
self.assertIn("qux", content)
|
|
self.assertIn("foo", content)
|
|
|
|
custom_mapping.change(Key(EV_KEY, 71, 1), "a", None)
|
|
self.window.save_preset()
|
|
tooltip = status.get_tooltip_text().lower()
|
|
self.assertIn("foo", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
custom_mapping.change(Key(EV_KEY, 72, 1), "b", None)
|
|
self.window.save_preset()
|
|
tooltip = status.get_tooltip_text()
|
|
self.assertIsNone(tooltip)
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
def test_check_macro_syntax(self):
|
|
status = self.window.get("status_bar")
|
|
error_icon = self.window.get("error_status_icon")
|
|
warning_icon = self.window.get("warning_status_icon")
|
|
|
|
custom_mapping.change(Key(EV_KEY, 9, 1), "k(1))", None)
|
|
self.window.save_preset()
|
|
tooltip = status.get_tooltip_text().lower()
|
|
self.assertIn("brackets", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
custom_mapping.change(Key(EV_KEY, 9, 1), "k(1)", None)
|
|
self.window.save_preset()
|
|
tooltip = (status.get_tooltip_text() or "").lower()
|
|
self.assertNotIn("brackets", tooltip)
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
self.assertEqual(custom_mapping.get_symbol(Key(EV_KEY, 9, 1)), "k(1)")
|
|
|
|
def test_select_device_and_preset(self):
|
|
# created on start because the first device is selected and some empty
|
|
# preset prepared.
|
|
self.assertTrue(
|
|
os.path.exists(f"{CONFIG_PATH}/presets/Foo Device/new preset.json")
|
|
)
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
|
|
# create another one
|
|
self.window.on_create_preset_clicked(None)
|
|
gtk_iteration()
|
|
self.assertTrue(
|
|
os.path.exists(f"{CONFIG_PATH}/presets/Foo Device/new preset.json")
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(f"{CONFIG_PATH}/presets/Foo Device/new preset 2.json")
|
|
)
|
|
self.assertEqual(self.window.preset_name, "new preset 2")
|
|
|
|
self.window.on_select_preset(FakePresetDropdown("new preset"))
|
|
gtk_iteration()
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
|
|
self.assertListEqual(
|
|
sorted(os.listdir(f"{CONFIG_PATH}/presets/Foo Device")),
|
|
sorted(["new preset.json", "new preset 2.json"]),
|
|
)
|
|
|
|
# now try to change the name
|
|
self.window.get("preset_name_input").set_text("abc 123")
|
|
gtk_iteration()
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
self.assertFalse(
|
|
os.path.exists(f"{CONFIG_PATH}/presets/Foo Device/abc 123.json")
|
|
)
|
|
custom_mapping.change(Key(EV_KEY, 10, 1), "1", None)
|
|
self.window.save_preset()
|
|
self.window.on_rename_button_clicked(None)
|
|
gtk_iteration()
|
|
self.assertEqual(self.window.preset_name, "abc 123")
|
|
self.assertTrue(
|
|
os.path.exists(f"{CONFIG_PATH}/presets/Foo Device/abc 123.json")
|
|
)
|
|
self.assertListEqual(
|
|
sorted(os.listdir(os.path.join(CONFIG_PATH, "presets"))),
|
|
sorted(["Foo Device"]),
|
|
)
|
|
self.assertListEqual(
|
|
sorted(os.listdir(f"{CONFIG_PATH}/presets/Foo Device")),
|
|
sorted(["abc 123.json", "new preset 2.json"]),
|
|
)
|
|
|
|
def test_copy_preset(self):
|
|
key_list = self.window.get("key_list")
|
|
self.change_empty_row(Key(EV_KEY, 81, 1), "a")
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
self.window.save_preset()
|
|
self.assertEqual(len(key_list.get_children()), 2)
|
|
|
|
# should be cleared when creating a new preset
|
|
custom_mapping.set("a.b", 3)
|
|
self.assertEqual(custom_mapping.get("a.b"), 3)
|
|
|
|
self.window.on_create_preset_clicked(None)
|
|
|
|
# the preset should be empty, only one empty row present
|
|
self.assertEqual(len(key_list.get_children()), 1)
|
|
self.assertIsNone(custom_mapping.get("a.b"), 3)
|
|
|
|
# add one new row again and a setting
|
|
self.change_empty_row(Key(EV_KEY, 81, 1), "b")
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
self.window.save_preset()
|
|
self.assertEqual(len(key_list.get_children()), 2)
|
|
custom_mapping.set(["foo", "bar"], 2)
|
|
|
|
# this time it should be copied
|
|
self.window.on_copy_preset_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "new preset 2 copy")
|
|
self.assertEqual(len(key_list.get_children()), 2)
|
|
self.assertEqual(key_list.get_children()[0].get_symbol(), "b")
|
|
self.assertEqual(custom_mapping.get(["foo", "bar"]), 2)
|
|
|
|
# make another copy
|
|
self.window.on_copy_preset_clicked(None)
|
|
self.assertEqual(self.window.preset_name, "new preset 2 copy 2")
|
|
self.assertEqual(len(key_list.get_children()), 2)
|
|
self.assertEqual(key_list.get_children()[0].get_symbol(), "b")
|
|
self.assertEqual(len(custom_mapping), 1)
|
|
self.assertEqual(custom_mapping.get("foo.bar"), 2)
|
|
|
|
def test_gamepad_config(self):
|
|
# set some stuff in the beginning, otherwise gtk fails to
|
|
# do handler_unblock_by_func, which makes no sense at all.
|
|
# but it ONLY fails on right_joystick_purpose for some reason,
|
|
# unblocking the left one works just fine. I should open a bug report
|
|
# on gtk or something probably.
|
|
self.window.get("left_joystick_purpose").set_active_id(BUTTONS)
|
|
self.window.get("right_joystick_purpose").set_active_id(BUTTONS)
|
|
self.window.get("joystick_mouse_speed").set_value(1)
|
|
custom_mapping.changed = False
|
|
|
|
# select a device that is not a gamepad
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device"))
|
|
self.assertFalse(self.window.get("gamepad_config").is_visible())
|
|
self.assertFalse(custom_mapping.changed)
|
|
|
|
# select a gamepad
|
|
self.window.on_select_device(FakeDeviceDropdown("gamepad"))
|
|
self.assertTrue(self.window.get("gamepad_config").is_visible())
|
|
self.assertFalse(custom_mapping.changed)
|
|
|
|
# set stuff
|
|
gtk_iteration()
|
|
self.window.get("left_joystick_purpose").set_active_id(WHEEL)
|
|
self.window.get("right_joystick_purpose").set_active_id(WHEEL)
|
|
joystick_mouse_speed = 5
|
|
self.window.get("joystick_mouse_speed").set_value(joystick_mouse_speed)
|
|
|
|
# it should be stored in custom_mapping, which overwrites the
|
|
# global config
|
|
config.set("gamepad.joystick.left_purpose", MOUSE)
|
|
config.set("gamepad.joystick.right_purpose", MOUSE)
|
|
config.set("gamepad.joystick.pointer_speed", 50)
|
|
self.assertTrue(custom_mapping.changed)
|
|
left_purpose = custom_mapping.get("gamepad.joystick.left_purpose")
|
|
right_purpose = custom_mapping.get("gamepad.joystick.right_purpose")
|
|
pointer_speed = custom_mapping.get("gamepad.joystick.pointer_speed")
|
|
self.assertEqual(left_purpose, WHEEL)
|
|
self.assertEqual(right_purpose, WHEEL)
|
|
self.assertEqual(pointer_speed, 2 ** joystick_mouse_speed)
|
|
|
|
# select a device that is not a gamepad again
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device"))
|
|
self.assertFalse(self.window.get("gamepad_config").is_visible())
|
|
self.assertFalse(custom_mapping.changed)
|
|
|
|
def test_wont_start(self):
|
|
error_icon = self.window.get("error_status_icon")
|
|
preset_name = "foo preset"
|
|
group_name = "Bar Device"
|
|
self.window.preset_name = preset_name
|
|
self.window.group = groups.find(name=group_name)
|
|
|
|
# empty
|
|
|
|
custom_mapping.empty()
|
|
self.window.save_preset()
|
|
self.window.on_apply_preset_clicked(None)
|
|
text = self.get_status_text()
|
|
self.assertIn("add keys", text)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertNotEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
|
|
# not empty, but keys are held down
|
|
|
|
custom_mapping.change(Key(EV_KEY, KEY_A, 1), "a")
|
|
self.window.save_preset()
|
|
send_event_to_reader(new_event(EV_KEY, KEY_A, 1))
|
|
reader.read()
|
|
self.assertEqual(len(reader._unreleased), 1)
|
|
self.assertFalse(self.window.unreleased_warn)
|
|
self.window.on_apply_preset_clicked(None)
|
|
text = self.get_status_text()
|
|
self.assertIn("release", text)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertNotEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
self.assertTrue(self.window.unreleased_warn)
|
|
self.assertEqual(self.window.get("apply_system_layout").get_opacity(), 0.4)
|
|
|
|
# device grabbing fails
|
|
|
|
def wait():
|
|
"""Wait for the injector process to finish doing stuff."""
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if "Starting" not in self.get_status_text():
|
|
return
|
|
|
|
for i in range(2):
|
|
# just pressing apply again will overwrite the previous error
|
|
self.grab_fails = True
|
|
self.window.on_apply_preset_clicked(None)
|
|
self.assertFalse(self.window.unreleased_warn)
|
|
text = self.get_status_text()
|
|
# it takes a little bit of time
|
|
self.assertIn("Starting injection", text)
|
|
self.assertFalse(error_icon.get_visible())
|
|
wait()
|
|
text = self.get_status_text()
|
|
self.assertIn("not grabbed", text)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertNotEqual(
|
|
self.window.dbus.get_state(self.window.group.key), RUNNING
|
|
)
|
|
|
|
# for the second try, release the key. that should also work
|
|
send_event_to_reader(new_event(EV_KEY, KEY_A, 0))
|
|
reader.read()
|
|
self.assertEqual(len(reader._unreleased), 0)
|
|
|
|
# this time work properly
|
|
|
|
self.grab_fails = False
|
|
custom_mapping.save(get_preset_path(group_name, preset_name))
|
|
self.window.on_apply_preset_clicked(None)
|
|
text = self.get_status_text()
|
|
self.assertIn("Starting injection", text)
|
|
self.assertFalse(error_icon.get_visible())
|
|
wait()
|
|
text = self.get_status_text()
|
|
self.assertIn("Applied", text)
|
|
text = self.get_status_text()
|
|
self.assertNotIn("CTRL + DEL", text) # only shown if btn_left mapped
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
|
|
# because this test managed to reproduce some minor bug:
|
|
self.assertNotIn("mapping", custom_mapping._config)
|
|
|
|
def test_wont_start_2(self):
|
|
preset_name = "foo preset"
|
|
group_name = "Bar Device"
|
|
self.window.preset_name = preset_name
|
|
self.window.group = groups.find(name=group_name)
|
|
|
|
def wait():
|
|
"""Wait for the injector process to finish doing stuff."""
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if "Starting" not in self.get_status_text():
|
|
return
|
|
|
|
# btn_left mapped
|
|
custom_mapping.change(Key.btn_left(), "a")
|
|
self.window.save_preset()
|
|
|
|
# and key held down
|
|
send_event_to_reader(new_event(EV_KEY, KEY_A, 1))
|
|
reader.read()
|
|
self.assertEqual(len(reader._unreleased), 1)
|
|
self.assertFalse(self.window.unreleased_warn)
|
|
|
|
# first apply, shows btn_left warning
|
|
self.window.on_apply_preset_clicked(None)
|
|
text = self.get_status_text()
|
|
self.assertIn("click", text)
|
|
self.assertEqual(self.window.dbus.get_state(self.window.group.key), UNKNOWN)
|
|
|
|
# second apply, shows unreleased warning
|
|
self.window.on_apply_preset_clicked(None)
|
|
text = self.get_status_text()
|
|
self.assertIn("release", text)
|
|
self.assertEqual(self.window.dbus.get_state(self.window.group.key), UNKNOWN)
|
|
|
|
# third apply, overwrites both warnings
|
|
self.window.on_apply_preset_clicked(None)
|
|
wait()
|
|
self.assertEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
text = self.get_status_text()
|
|
# because btn_left is mapped, shows help on how to stop
|
|
# injecting via the keyboard
|
|
self.assertIn("CTRL + DEL", text)
|
|
|
|
def test_can_modify_mapping(self):
|
|
preset_name = "foo preset"
|
|
group_name = "Bar Device"
|
|
self.window.preset_name = preset_name
|
|
self.window.group = groups.find(name=group_name)
|
|
|
|
self.assertNotEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
self.window.can_modify_mapping()
|
|
text = self.get_status_text()
|
|
self.assertNotIn("Restore Defaults", text)
|
|
|
|
custom_mapping.change(Key(EV_KEY, KEY_A, 1), "b")
|
|
custom_mapping.save(get_preset_path(group_name, preset_name))
|
|
self.window.on_apply_preset_clicked(None)
|
|
|
|
# wait for the injector to start
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if "Starting" not in self.get_status_text():
|
|
return
|
|
|
|
self.assertEqual(self.window.dbus.get_state(self.window.group.key), RUNNING)
|
|
|
|
# the mapping cannot be changed anymore
|
|
self.window.can_modify_mapping()
|
|
text = self.get_status_text()
|
|
self.assertIn("Restore Defaults", text)
|
|
|
|
def test_start_injecting(self):
|
|
keycode_from = 9
|
|
keycode_to = 200
|
|
|
|
self.change_empty_row(Key(EV_KEY, keycode_from, 1), "a")
|
|
system_mapping.clear()
|
|
system_mapping._set("a", keycode_to)
|
|
|
|
push_events(
|
|
"Foo Device 2",
|
|
[
|
|
new_event(evdev.events.EV_KEY, keycode_from, 1),
|
|
new_event(evdev.events.EV_KEY, keycode_from, 0),
|
|
],
|
|
)
|
|
|
|
# injecting for group.key will look at paths containing group.name
|
|
custom_mapping.save(get_preset_path("Foo Device", "foo preset"))
|
|
|
|
# use only the manipulated system_mapping
|
|
if os.path.exists(os.path.join(tmp, XMODMAP_FILENAME)):
|
|
os.remove(os.path.join(tmp, XMODMAP_FILENAME))
|
|
|
|
# select the second Foo device
|
|
self.window.group = groups.find(key="Foo Device 2")
|
|
|
|
with spy(self.window.dbus, "set_config_dir") as spy1:
|
|
self.window.preset_name = "foo preset"
|
|
|
|
with spy(self.window.dbus, "start_injecting") as spy2:
|
|
self.window.on_apply_preset_clicked(None)
|
|
# correctly uses group.key, not group.name
|
|
spy2.assert_called_once_with("Foo Device 2", "foo preset")
|
|
|
|
spy1.assert_called_once_with(get_config_path())
|
|
|
|
# the integration tests will cause the injection to be started as
|
|
# processes, as intended. Luckily, recv will block until the events
|
|
# are handled and pushed.
|
|
|
|
# Note, that appending events to pending_events won't work anymore
|
|
# from here on because the injector processes memory cannot be
|
|
# modified from here.
|
|
|
|
event = uinput_write_history_pipe[0].recv()
|
|
self.assertEqual(event.type, evdev.events.EV_KEY)
|
|
self.assertEqual(event.code, keycode_to)
|
|
self.assertEqual(event.value, 1)
|
|
|
|
event = uinput_write_history_pipe[0].recv()
|
|
self.assertEqual(event.type, evdev.events.EV_KEY)
|
|
self.assertEqual(event.code, keycode_to)
|
|
self.assertEqual(event.value, 0)
|
|
|
|
# the key-mapper device will not be shown
|
|
groups.refresh()
|
|
self.window.populate_devices()
|
|
for entry in self.window.device_store:
|
|
# whichever attribute contains "key-mapper"
|
|
self.assertNotIn("key-mapper", "".join(entry))
|
|
|
|
def test_gamepad_purpose_mouse_and_button(self):
|
|
self.window.on_select_device(FakeDeviceDropdown("gamepad"))
|
|
self.window.get("right_joystick_purpose").set_active_id(MOUSE)
|
|
self.window.get("left_joystick_purpose").set_active_id(BUTTONS)
|
|
self.window.get("joystick_mouse_speed").set_value(6)
|
|
gtk_iteration()
|
|
speed = custom_mapping.get("gamepad.joystick.pointer_speed")
|
|
custom_mapping.set("gamepad.joystick.non_linearity", 1)
|
|
self.assertEqual(speed, 2 ** 6)
|
|
|
|
# don't consume the events in the reader, they are used to test
|
|
# the injection
|
|
reader.terminate()
|
|
time.sleep(0.1)
|
|
|
|
push_events(
|
|
"gamepad",
|
|
[new_event(EV_ABS, ABS_RX, MIN_ABS), new_event(EV_ABS, ABS_X, MAX_ABS)]
|
|
* 100,
|
|
)
|
|
|
|
custom_mapping.change(Key(EV_ABS, ABS_X, 1), "a")
|
|
self.window.save_preset()
|
|
|
|
gtk_iteration()
|
|
|
|
self.window.on_apply_preset_clicked(None)
|
|
time.sleep(0.3)
|
|
|
|
history = []
|
|
while uinput_write_history_pipe[0].poll():
|
|
history.append(uinput_write_history_pipe[0].recv().t)
|
|
|
|
count_mouse = history.count((EV_REL, REL_X, -speed))
|
|
count_button = history.count((EV_KEY, KEY_A, 1))
|
|
self.assertGreater(count_mouse, 1)
|
|
self.assertEqual(count_button, 1)
|
|
self.assertEqual(count_button + count_mouse, len(history))
|
|
|
|
self.assertIn("gamepad", self.window.dbus.injectors)
|
|
|
|
def test_stop_injecting(self):
|
|
keycode_from = 16
|
|
keycode_to = 90
|
|
|
|
self.change_empty_row(Key(EV_KEY, keycode_from, 1), "t")
|
|
system_mapping.clear()
|
|
system_mapping._set("t", keycode_to)
|
|
|
|
# not all of those events should be processed, since that takes some
|
|
# time due to time.sleep in the fakes and the injection is stopped.
|
|
push_events("Bar Device", [new_event(1, keycode_from, 1)] * 100)
|
|
|
|
custom_mapping.save(get_preset_path("Bar Device", "foo preset"))
|
|
|
|
self.window.group = groups.find(name="Bar Device")
|
|
self.window.preset_name = "foo preset"
|
|
self.window.on_apply_preset_clicked(None)
|
|
|
|
pipe = uinput_write_history_pipe[0]
|
|
# block until the first event is available, indicating that
|
|
# the injector is ready
|
|
write_history = [pipe.recv()]
|
|
|
|
# stop
|
|
self.window.on_restore_defaults_clicked(None)
|
|
|
|
# try to receive a few of the events
|
|
time.sleep(0.2)
|
|
while pipe.poll():
|
|
write_history.append(pipe.recv())
|
|
|
|
len_before = len(write_history)
|
|
self.assertLess(len(write_history), 50)
|
|
|
|
# since the injector should not be running anymore, no more events
|
|
# should be received after waiting even more time
|
|
time.sleep(0.2)
|
|
while pipe.poll():
|
|
write_history.append(pipe.recv())
|
|
self.assertEqual(len(write_history), len_before)
|
|
|
|
def test_delete_preset(self):
|
|
custom_mapping.change(Key(EV_KEY, 71, 1), "a", None)
|
|
self.window.get("preset_name_input").set_text("asdf")
|
|
self.window.on_rename_button_clicked(None)
|
|
gtk_iteration()
|
|
self.assertEqual(self.window.preset_name, "asdf")
|
|
self.assertEqual(len(custom_mapping), 1)
|
|
self.window.save_preset()
|
|
self.assertTrue(os.path.exists(get_preset_path("Foo Device", "asdf")))
|
|
|
|
with patch.object(
|
|
self.window, "show_confirm_delete", lambda: Gtk.ResponseType.CANCEL
|
|
):
|
|
self.window.on_delete_preset_clicked(None)
|
|
self.assertTrue(os.path.exists(get_preset_path("Foo Device", "asdf")))
|
|
self.assertEqual(self.window.preset_name, "asdf")
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
|
|
with patch.object(
|
|
self.window, "show_confirm_delete", lambda: Gtk.ResponseType.ACCEPT
|
|
):
|
|
self.window.on_delete_preset_clicked(None)
|
|
self.assertFalse(os.path.exists(get_preset_path("Foo Device", "asdf")))
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
self.assertEqual(self.window.group.name, "Foo Device")
|
|
|
|
def test_populate_devices(self):
|
|
preset_selection = self.window.get("preset_selection")
|
|
|
|
# create two presets
|
|
self.window.get("preset_name_input").set_text("preset 1")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(preset_selection.get_active_id(), "preset 1")
|
|
|
|
# to make sure the next preset has a slightly higher timestamp
|
|
time.sleep(0.1)
|
|
self.window.on_create_preset_clicked(None)
|
|
self.window.get("preset_name_input").set_text("preset 2")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.assertEqual(preset_selection.get_active_id(), "preset 2")
|
|
|
|
# select the older one
|
|
preset_selection.set_active_id("preset 1")
|
|
self.assertEqual(self.window.preset_name, "preset 1")
|
|
|
|
# add a device that doesn't exist to the dropdown
|
|
unknown_key = "key-1234"
|
|
self.window.device_store.insert(0, [unknown_key, None, "foo"])
|
|
|
|
self.window.populate_devices()
|
|
# the newest preset should be selected
|
|
self.assertEqual(self.window.preset_name, "preset 2")
|
|
|
|
# the list contains correct entries
|
|
# and the non-existing entry should be removed
|
|
entries = [tuple(entry) for entry in self.window.device_store]
|
|
keys = [entry[0] for entry in self.window.device_store]
|
|
self.assertNotIn(unknown_key, keys)
|
|
self.assertIn("Foo Device", keys)
|
|
self.assertIn(("Foo Device", "input-keyboard", "Foo Device"), entries)
|
|
self.assertIn(("Foo Device 2", "input-mouse", "Foo Device 2"), entries)
|
|
self.assertIn(("Bar Device", "input-keyboard", "Bar Device"), entries)
|
|
self.assertIn(("gamepad", "input-gaming", "gamepad"), entries)
|
|
|
|
# it won't crash due to "list index out of range"
|
|
# when `types` is an empty list. Won't show an icon
|
|
groups.find(key="Foo Device 2").types = []
|
|
self.window.populate_devices()
|
|
self.assertIn(
|
|
("Foo Device 2", None, "Foo Device 2"),
|
|
[tuple(entry) for entry in self.window.device_store],
|
|
)
|
|
|
|
def test_screw_up_rows(self):
|
|
# add a row that is not present in custom_mapping
|
|
key_list = self.window.get("key_list")
|
|
key_list.forall(key_list.remove)
|
|
for i in range(5):
|
|
broken = Row(window=self.window, delete_callback=lambda: None)
|
|
broken.set_new_key(Key(1, i, 1))
|
|
broken.symbol_input.set_text("a")
|
|
key_list.insert(broken, -1)
|
|
custom_mapping.empty()
|
|
|
|
# the ui has 5 rows, the custom_mapping 0. mismatch
|
|
num_rows_before = len(key_list.get_children())
|
|
self.assertEqual(len(custom_mapping), 0)
|
|
self.assertEqual(num_rows_before, 5)
|
|
|
|
# it returns true to keep the glib timeout going
|
|
self.assertTrue(self.window.check_add_row())
|
|
# it still adds a new empty row and won't break
|
|
num_rows_after = len(key_list.get_children())
|
|
self.assertEqual(num_rows_after, num_rows_before + 1)
|
|
|
|
rows = key_list.get_children()
|
|
self.assertEqual(rows[0].get_symbol(), "a")
|
|
self.assertEqual(rows[1].get_symbol(), "a")
|
|
self.assertEqual(rows[2].get_symbol(), "a")
|
|
self.assertEqual(rows[3].get_symbol(), "a")
|
|
self.assertEqual(rows[4].get_symbol(), "a")
|
|
self.assertEqual(rows[5].get_symbol(), None)
|
|
|
|
def test_shared_presets(self):
|
|
# devices with the same name (but different key because the key is
|
|
# unique) share the same presets
|
|
key_list = self.window.get("key_list")
|
|
|
|
# 1. create a preset
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device 2"))
|
|
self.window.on_create_preset_clicked(None)
|
|
self.change_empty_row(Key(3, 2, 1), "qux")
|
|
self.window.get("preset_name_input").set_text("asdf")
|
|
self.window.on_rename_button_clicked(None)
|
|
self.window.save_preset()
|
|
self.assertIn("asdf.json", os.listdir(get_preset_path("Foo Device")))
|
|
|
|
# 2. switch to the other device, there should be no preset named asdf
|
|
self.window.on_select_device(FakeDeviceDropdown("Bar Device"))
|
|
self.assertEqual(self.window.preset_name, "new preset")
|
|
self.assertNotIn("asdf.json", os.listdir(get_preset_path("Bar Device")))
|
|
self.assertIsNone(key_list.get_children()[0].get_symbol(), None)
|
|
|
|
# 3. switch to the device with the same name
|
|
self.window.on_select_device(FakeDeviceDropdown("Foo Device"))
|
|
# the newest preset is asdf, it should be automatically selected
|
|
self.assertEqual(self.window.preset_name, "asdf")
|
|
self.assertEqual(key_list.get_children()[0].get_symbol(), "qux")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|