mirror of
https://github.com/sezanzeb/input-remapper
synced 2024-11-12 01:10:38 +00:00
3637204bff
* Tests for the GuiEventHandler * Implement GuiEventHandler * tests for data manager * Implemented data_manager * Remove Ellipsis from type hint * workaround for old pydantic version * workaround for old pydantic version * some more tests for data_manager * Updated Data Manager * move DeviceSelection to its own class * Data Manager no longer listens for events * Moved PresetSelection to its own class * MappingListBox and SelectionLable Listen to the EventHandler * DataManager no longer creates its own data objects in the init * removed global reader object * Changed UI startup * created backend Interface * event_handler debug logs show function which emit a event * some cleanup * added target selector to components * created code editor component * adapted autocompletion & some cleanup * black * connected some buttons to the event_handler * tests for data_manager newest_preset and group * cleanup presets and test_presets * migrated confirm delete dialog * backend tests * controller tests * add python3-gi to ci * more dependencies * and more ... * Github-Actions workaround remove this commit * not so many permission denyed errors in test.yml * Fix #404 (hopefully) * revert Github-Actions workaround * More tests * event_handler allows for event supression * more tests * WIP Implement Key recording * Start and Stop Injection * context no longer stores preset * restructured the RelToBtnHandler * Simplified read_loop * Implement async iterator for ipc.pipe * multiple event actions * helper now implements mapping handlers to read inputs all with async * updated and simplified reader the helper uses the mapping handlers, so the reader now can be much simpler * Fixed race condition in tests * implemented DataBus * Fixed a UIMapping bug where the last_error would not be deleted * added a immutable variant of the UIMapping * updated data_manager to use data_bus * Uptdated tests to use the DataBus * Gui uses DataBus * removed EventHandler * Renamed controller methods * Implemented recording toggle * implemented StatusBar * Sending validation errors to status bar * sending injection status to status bar * proper preset renaming * implemented copy preset in the data manager * implemented copy_preset in controller * fixed a bug where a wron selection lable would update * no longer send invalid data over the bus, if the preset or group changes * Implement create and delete mapping * Allow for frontend specific mapping defaults * implemented autoload toggle * cleanup user_interface * removed editor * Docstings renaming and ordering of methods * more simplifications to user_interface * integrated backend into data_manager * removed active preset * transformation tests * controller tests * fix missing uinputs in gui * moved some tests and implemented basic tests for mapping handlers * docstring reformatting Co-authored-by: Tobi <proxima@sezanzeb.de> * allow for empty groups * docstring * fixed TestGroupFromHelper * some work on integration tests * test for annoying import error in tests * testing if test_user_interface works * I feel lucky * not so lucky * some more tests * fixed but where the group_key was used as folder name * Fixed a bug where state=NO_GRAB would never be read from the injector * allow to stop the recorder * working on integration tests * integration tests * fixed more integration tests * updated coveragerc * no longer attempt to record keys when injecting * event_reader cleans up not finished tasks * More integration tests * All tests pass * renamed data_bus * WIP fixing typing issues * more typing fixes * added keyboard+mouse device to tests * cleanup imports * new read loop because the evdev async read loop can not be cancelled * Added field to modify mapping name * created tests for components * even more component tests * do component tests need a screen? * apparently they do :_( * created release_input switch * Don't record relative axis when movement is slow * show delete dialog above main window * wip basic dialog to edit combination * some gui changes to the combination-editor * Simple implementation of CombinationListbox * renamed attach_to_events method and mark as private * shorter str() for UInputsData * moved logic to generate readable event string from combination to event * new mapping parameter force release timeout this helps with the helper when recording multiple relative axis at once * make it possible to rearange the event_combination * more work on the combination editor * tests for DataManager.load_event * simplyfied test_controller * more controller tests * Implement input threshold in gui * greater range for time dependent unit test * implemented a output-axis selector * data_manager now provides injector state * black * mypy * Updated confirm cancel dialog * created release timeout input * implemented transformation graph * Added sliders for gain, expo and deadzone * fix bug where the system_mapping was overridden in each injector thread * updated slider settings * removed debug statement * explicitly checking output code against None (0 is a valid code) * usage * Allow for multiple axis to be activated by same button * readme * only warn about not implemented mapping-handler don't fail to create event-pipelines * More accurate event names * Allow removal of single events from the input-combination * rename callback to notify_callback * rename event message to selected_event * made read_continuisly private * typing for autocompletion * docstrings for message_broker messages * make components methods and propreties private * gui spacings * removed eval * make some controller functions private * move status message generation from data_manager to controller * parse mapping errors in controller for more helpful messages * remove system_mapping from code editor * More component tests * more tests * mypy * make grab_devices less greedy (partial mitigation for #435) only grab one device if there are multiple which can satisfy the same mapping * accumulate more values in test * docstrings * Updated status messages * comments, docstrings, imports Co-authored-by: Tobi <proxima@sezanzeb.de>
1878 lines
68 KiB
Python
1878 lines
68 KiB
Python
#!/usr/bin/python3
|
|
# -*- coding: utf-8 -*-
|
|
# input-remapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2022 sezanzeb <proxima@sezanzeb.de>
|
|
#
|
|
# This file is part of input-remapper.
|
|
#
|
|
# input-remapper 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.
|
|
#
|
|
# input-remapper 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 input-remapper. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
# the tests file needs to be imported first to make sure patches are loaded
|
|
from contextlib import contextmanager
|
|
from typing import Tuple, List
|
|
|
|
from inputremapper.exceptions import DataManagementError
|
|
from tests.test import (
|
|
get_project_root,
|
|
logger,
|
|
tmp,
|
|
push_events,
|
|
new_event,
|
|
spy,
|
|
cleanup,
|
|
uinput_write_history_pipe,
|
|
MAX_ABS,
|
|
EVENT_READ_TIMEOUT,
|
|
MIN_ABS,
|
|
get_ui_mapping,
|
|
prepare_presets,
|
|
)
|
|
|
|
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,
|
|
KEY_Q,
|
|
ABS_RX,
|
|
EV_REL,
|
|
REL_X,
|
|
ABS_X,
|
|
)
|
|
import json
|
|
from unittest.mock import patch, MagicMock, call
|
|
from importlib.util import spec_from_loader, module_from_spec
|
|
from importlib.machinery import SourceFileLoader
|
|
|
|
import gi
|
|
from inputremapper.input_event import InputEvent
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("GLib", "2.0")
|
|
gi.require_version("GtkSource", "4")
|
|
from gi.repository import Gtk, GLib, Gdk, GtkSource
|
|
|
|
from inputremapper.configs.system_mapping import system_mapping, XMODMAP_FILENAME
|
|
from inputremapper.configs.mapping import UIMapping, Mapping
|
|
from inputremapper.configs.paths import CONFIG_PATH, get_preset_path, get_config_path
|
|
from inputremapper.configs.global_config import global_config, WHEEL, MOUSE, BUTTONS
|
|
from inputremapper.groups import _Groups
|
|
from inputremapper.gui.data_manager import DataManager
|
|
from inputremapper.gui.message_broker import (
|
|
MessageBroker,
|
|
MessageType,
|
|
StatusData,
|
|
CombinationRecorded,
|
|
)
|
|
from inputremapper.gui.components import SelectionLabel, SET_KEY_FIRST
|
|
from inputremapper.gui.reader import Reader
|
|
from inputremapper.gui.controller import Controller
|
|
from inputremapper.gui.helper import RootHelper
|
|
from inputremapper.gui.utils import gtk_iteration
|
|
from inputremapper.gui.user_interface import UserInterface
|
|
from inputremapper.injection.injector import RUNNING, FAILED, UNKNOWN, STOPPED
|
|
from inputremapper.event_combination import EventCombination
|
|
from inputremapper.daemon import Daemon, DaemonProxy
|
|
|
|
|
|
# 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,
|
|
) -> Tuple[UserInterface, Controller, DataManager, MessageBroker, DaemonProxy]:
|
|
"""Start input-remapper-gtk with the command line argument array argv."""
|
|
bin_path = os.path.join(get_project_root(), "bin", "input-remapper-gtk")
|
|
|
|
if not argv:
|
|
argv = ["-d"]
|
|
|
|
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)
|
|
|
|
return (
|
|
module.user_interface,
|
|
module.controller,
|
|
module.data_manager,
|
|
module.message_broker,
|
|
module.daemon,
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def patch_launch():
|
|
"""patch the launch function such that we don't connect to
|
|
the dbus and don't use pkexec to start the helper"""
|
|
original_connect = Daemon.connect
|
|
original_os_system = os.system
|
|
Daemon.connect = Daemon
|
|
|
|
def os_system(cmd):
|
|
# instead of running pkexec, fork instead. This will make
|
|
# the helper aware of all the test patches
|
|
if "pkexec input-remapper-control --command helper" in cmd:
|
|
multiprocessing.Process(target=RootHelper(_Groups()).run).start()
|
|
return 0
|
|
|
|
return original_os_system(cmd)
|
|
|
|
os.system = os_system
|
|
yield
|
|
os.system = original_os_system
|
|
Daemon.connect = original_connect
|
|
|
|
|
|
def clean_up_integration(test):
|
|
test.controller.stop_injecting()
|
|
gtk_iteration()
|
|
test.user_interface.on_gtk_close()
|
|
test.user_interface.window.destroy()
|
|
gtk_iteration()
|
|
cleanup()
|
|
|
|
# do this now, not when all tests are finished
|
|
test.daemon.stop_all()
|
|
if isinstance(test.daemon, Daemon):
|
|
atexit.unregister(test.daemon.stop_all)
|
|
|
|
|
|
class GtkKeyEvent:
|
|
def __init__(self, keyval):
|
|
self.keyval = keyval
|
|
|
|
def get_keyval(self):
|
|
return True, self.keyval
|
|
|
|
|
|
class TestGroupsFromHelper(unittest.TestCase):
|
|
def setUp(self):
|
|
# don't try to connect, return an object instance of it instead
|
|
self.original_connect = Daemon.connect
|
|
Daemon.connect = Daemon
|
|
|
|
# this is already part of the test. we need a bit of patching and hacking
|
|
# because we want to discover the groups as early a possible, to reduce startup
|
|
# time for the application
|
|
self.original_os_system = os.system
|
|
self.helper_started = MagicMock()
|
|
|
|
def os_system(cmd):
|
|
# instead of running pkexec, fork instead. This will make
|
|
# the helper aware of all the test patches
|
|
if "pkexec input-remapper-control --command helper" in cmd:
|
|
self.helper_started() # don't start the helper just log that it was.
|
|
return 0
|
|
|
|
return self.original_os_system(cmd)
|
|
|
|
os.system = os_system
|
|
(
|
|
self.user_interface,
|
|
self.controller,
|
|
self.data_manager,
|
|
self.message_broker,
|
|
self.daemon,
|
|
) = launch()
|
|
|
|
def tearDown(self):
|
|
clean_up_integration(self)
|
|
os.system = self.original_os_system
|
|
Daemon.connect = self.original_connect
|
|
|
|
def test_knows_devices(self):
|
|
# verify that it is working as expected. The gui doesn't have knowledge
|
|
# of groups until the root-helper provides them
|
|
self.data_manager._reader.groups.set_groups([])
|
|
gtk_iteration()
|
|
self.helper_started.assert_called()
|
|
self.assertEqual(len(self.data_manager.get_group_keys()), 0)
|
|
|
|
# start the helper delayed
|
|
multiprocessing.Process(target=RootHelper(_Groups()).run).start()
|
|
# perform some iterations so that the reader ends up reading from the pipes
|
|
# which will make it receive devices.
|
|
for _ in range(10):
|
|
time.sleep(0.02)
|
|
gtk_iteration()
|
|
|
|
self.assertIn("Foo Device 2", self.data_manager.get_group_keys())
|
|
self.assertIn("Foo Device 2", self.data_manager.get_group_keys())
|
|
self.assertIn("Bar Device", self.data_manager.get_group_keys())
|
|
self.assertIn("gamepad", self.data_manager.get_group_keys())
|
|
self.assertEqual(self.data_manager.active_group.name, "Foo Device")
|
|
|
|
|
|
class PatchedConfirmDelete:
|
|
def __init__(self, user_interface: UserInterface, response=Gtk.ResponseType.ACCEPT):
|
|
self.response = response
|
|
self.user_interface = user_interface
|
|
self.patch = None
|
|
|
|
def _confirm_delete_run_patch(self):
|
|
"""A patch for the deletion confirmation that briefly shows the dialog."""
|
|
confirm_cancel_dialog = self.user_interface.confirm_cancel_dialog
|
|
# the emitted signal causes the dialog to close
|
|
GLib.timeout_add(
|
|
100,
|
|
lambda: confirm_cancel_dialog.emit("response", self.response),
|
|
)
|
|
Gtk.MessageDialog.run(confirm_cancel_dialog) # don't recursively call the patch
|
|
return self.response
|
|
|
|
def __enter__(self):
|
|
self.patch = patch.object(
|
|
self.user_interface.get("confirm-cancel"),
|
|
"run",
|
|
self._confirm_delete_run_patch,
|
|
)
|
|
self.patch.__enter__()
|
|
|
|
def __exit__(self, *args, **kwargs):
|
|
self.patch.__exit__(*args, **kwargs)
|
|
|
|
|
|
class GuiTestBase(unittest.TestCase):
|
|
def setUp(self):
|
|
prepare_presets()
|
|
with patch_launch():
|
|
(
|
|
self.user_interface,
|
|
self.controller,
|
|
self.data_manager,
|
|
self.message_broker,
|
|
self.daemon,
|
|
) = launch()
|
|
|
|
get = self.user_interface.get
|
|
self.device_selection: Gtk.ComboBox = get("device_selection")
|
|
self.preset_selection: Gtk.ComboBoxText = get("preset_selection")
|
|
self.selection_label_listbox: Gtk.ListBox = get("selection_label_listbox")
|
|
self.target_selection: Gtk.ComboBox = get("target-selector")
|
|
self.recording_toggle: Gtk.ToggleButton = get("key_recording_toggle")
|
|
self.status_bar: Gtk.Statusbar = get("status_bar")
|
|
self.autoload_toggle: Gtk.Switch = get("preset_autoload_switch")
|
|
self.code_editor: GtkSource.View = get("code_editor")
|
|
|
|
self.delete_preset_btn: Gtk.Button = get("delete_preset")
|
|
self.copy_preset_btn: Gtk.Button = get("copy_preset")
|
|
self.create_preset_btn: Gtk.Button = get("create_preset")
|
|
self.start_injector_btn: Gtk.Button = get("apply_preset")
|
|
self.stop_injector_btn: Gtk.Button = get("apply_system_layout")
|
|
self.rename_btn: Gtk.Button = get("rename-button")
|
|
self.rename_input: Gtk.Entry = get("preset_name_input")
|
|
self.create_mapping_btn: Gtk.Button = get("create_mapping_button")
|
|
self.delete_mapping_btn: Gtk.Button = get("delete-mapping")
|
|
|
|
self.grab_fails = False
|
|
|
|
def grab(_):
|
|
if self.grab_fails:
|
|
raise OSError()
|
|
|
|
evdev.InputDevice.grab = grab
|
|
|
|
global_config._save_config()
|
|
|
|
self.throttle()
|
|
|
|
self.assertIsNotNone(self.data_manager.active_group)
|
|
self.assertIsNotNone(self.data_manager.active_preset)
|
|
|
|
def tearDown(self):
|
|
clean_up_integration(self)
|
|
|
|
self.throttle()
|
|
|
|
def _callTestMethod(self, method):
|
|
"""Retry all tests if they fail.
|
|
|
|
GUI tests suddenly started to lag a lot and fail randomly, and even
|
|
though that improved drastically, sometimes they still do.
|
|
"""
|
|
attempts = 0
|
|
while True:
|
|
attempts += 1
|
|
try:
|
|
method()
|
|
break
|
|
except Exception as e:
|
|
if attempts == 2:
|
|
raise e
|
|
|
|
# try again
|
|
print("Test failed, trying again...")
|
|
self.tearDown()
|
|
self.setUp()
|
|
|
|
def throttle(self, iterations=10):
|
|
"""Give GTK some time to process everything."""
|
|
# tests suddenly started to freeze my computer up completely and tests started
|
|
# to fail. By using this (and by optimizing some redundant calls in the gui) it
|
|
# worked again. EDIT: Might have been caused by my broken/bloated ssd. I'll
|
|
# keep it in some places, since it did make the tests more reliable after all.
|
|
for _ in range(iterations):
|
|
gtk_iteration()
|
|
time.sleep(0.002)
|
|
|
|
def activate_recording_toggle(self):
|
|
logger.info("Activating the recording toggle")
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
|
|
def disable_recording_toggle(self):
|
|
logger.info("Deactivating the recording toggle")
|
|
self.recording_toggle.set_active(False)
|
|
gtk_iteration()
|
|
# should happen automatically:
|
|
self.assertFalse(self.recording_toggle.get_active())
|
|
|
|
def set_focus(self, widget):
|
|
logger.info("Focusing %s", widget)
|
|
|
|
self.user_interface.window.set_focus(widget)
|
|
|
|
self.throttle()
|
|
|
|
def get_selection_labels(self) -> List[SelectionLabel]:
|
|
return self.selection_label_listbox.get_children()
|
|
|
|
def get_status_text(self):
|
|
status_bar = self.user_interface.get("status_bar")
|
|
return status_bar.get_message_area().get_children()[0].get_label()
|
|
|
|
def get_unfiltered_symbol_input_text(self):
|
|
buffer = self.code_editor.get_buffer()
|
|
return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True)
|
|
|
|
def select_mapping(self, i: int):
|
|
"""Select one of the mappings of a preset.
|
|
|
|
Parameters
|
|
----------
|
|
i
|
|
if -1, will select the last row,
|
|
0 will select the uppermost row.
|
|
1 will select the second row, and so on
|
|
"""
|
|
selection_label = self.get_selection_labels()[i]
|
|
self.selection_label_listbox.select_row(selection_label)
|
|
logger.info(
|
|
'Selecting mapping %s "%s"',
|
|
selection_label.combination,
|
|
selection_label.name,
|
|
)
|
|
gtk_iteration()
|
|
return selection_label
|
|
|
|
def add_mapping(self, mapping: Mapping = None):
|
|
self.controller.create_mapping()
|
|
self.controller.load_mapping(EventCombination.empty_combination())
|
|
gtk_iteration()
|
|
if mapping:
|
|
self.controller.update_mapping(**mapping.dict(exclude_defaults=True))
|
|
gtk_iteration()
|
|
|
|
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()
|
|
|
|
|
|
class TestGui(GuiTestBase):
|
|
"""For tests that use the window.
|
|
|
|
Try to modify the configuration only by calling functions of the window.
|
|
"""
|
|
|
|
def test_can_start(self):
|
|
self.assertIsNotNone(self.user_interface)
|
|
self.assertTrue(self.user_interface.window.get_visible())
|
|
|
|
def assert_gui_clean(self):
|
|
selection_labels = self.selection_label_listbox.get_children()
|
|
self.assertEqual(len(selection_labels), 0)
|
|
self.assertEqual(len(self.data_manager.active_preset), 0)
|
|
self.assertEqual(self.preset_selection.get_active_id(), "new preset")
|
|
self.assertEqual(self.recording_toggle.get_label(), "Record Input")
|
|
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
|
|
|
|
def test_initial_state(self):
|
|
self.assertEqual(self.data_manager.active_group.key, "Foo Device")
|
|
self.assertEqual(self.device_selection.get_active_id(), "Foo Device")
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset3")
|
|
self.assertEqual(self.preset_selection.get_active_id(), "preset3")
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination, ((1, 5, 1),)
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination, ((1, 5, 1),)
|
|
)
|
|
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "4")
|
|
self.assertIsNone(self.data_manager.active_mapping.name)
|
|
self.assertTrue(self.data_manager.active_mapping.is_valid())
|
|
self.assertTrue(self.data_manager.active_preset.is_valid())
|
|
# todo
|
|
|
|
def test_set_autoload_refreshes_service_config(self):
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
with spy(self.daemon, "set_config_dir") as set_config_dir:
|
|
self.autoload_toggle.set_active(True)
|
|
gtk_iteration()
|
|
set_config_dir.assert_called_once()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
|
|
def test_autoload_sets_correctly(self):
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
self.autoload_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
self.autoload_toggle.set_active(False)
|
|
gtk_iteration()
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
def test_autoload_is_set_when_changing_preset(self):
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
self.device_selection.set_active_id("Foo Device 2")
|
|
self.preset_selection.set_active_id("preset2")
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
def test_only_one_autoload_per_group(self):
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
self.device_selection.set_active_id("Foo Device 2")
|
|
self.preset_selection.set_active_id("preset2")
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
self.preset_selection.set_active_id("preset3")
|
|
gtk_iteration()
|
|
self.autoload_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.preset_selection.set_active_id("preset2")
|
|
gtk_iteration()
|
|
self.assertFalse(self.data_manager.get_autoload())
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
def test_each_device_can_have_autoload(self):
|
|
self.autoload_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
self.device_selection.set_active_id("Foo Device 2")
|
|
gtk_iteration()
|
|
self.autoload_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
self.device_selection.set_active_id("Foo Device")
|
|
gtk_iteration()
|
|
self.assertTrue(self.data_manager.get_autoload())
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
def test_select_device_without_preset(self):
|
|
# creates a new empty preset when no preset exists for the device
|
|
self.device_selection.set_active_id("Bar Device")
|
|
self.assertEqual(self.preset_selection.get_active_id(), "new preset")
|
|
self.assertEqual(len(self.data_manager.active_preset), 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:
|
|
self.assertEqual(file.read(), "")
|
|
|
|
def test_recording_toggle_labels(self):
|
|
self.assertEqual(self.recording_toggle.get_label(), "Record Input")
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertEqual(self.recording_toggle.get_label(), "Recording ...")
|
|
self.recording_toggle.set_active(False)
|
|
gtk_iteration()
|
|
self.assertEqual(self.recording_toggle.get_label(), "Record Input")
|
|
|
|
def test_recording_label_updates_on_recording_finished(self):
|
|
self.assertEqual(self.recording_toggle.get_label(), "Record Input")
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertEqual(self.recording_toggle.get_label(), "Recording ...")
|
|
self.message_broker.signal(MessageType.recording_finished)
|
|
gtk_iteration()
|
|
self.assertEqual(self.recording_toggle.get_label(), "Record Input")
|
|
self.assertFalse(self.recording_toggle.get_active())
|
|
|
|
def test_events_from_helper_arrive(self):
|
|
# load a device with more capabilities
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
mock1 = MagicMock()
|
|
mock2 = MagicMock()
|
|
self.message_broker.subscribe(MessageType.combination_recorded, mock1)
|
|
self.message_broker.subscribe(MessageType.recording_finished, mock2)
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
|
|
push_events(
|
|
"Foo Device 2",
|
|
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,31,1")],
|
|
)
|
|
self.throttle(20)
|
|
mock1.assert_has_calls(
|
|
(
|
|
call(CombinationRecorded(EventCombination.from_string("1,30,1"))),
|
|
call(
|
|
CombinationRecorded(EventCombination.from_string("1,30,1+1,31,1"))
|
|
),
|
|
),
|
|
any_order=False,
|
|
)
|
|
self.assertEqual(mock1.call_count, 2)
|
|
mock2.assert_not_called()
|
|
|
|
push_events("Foo Device 2", [InputEvent.from_string("1,31,0")])
|
|
self.throttle(20)
|
|
self.assertEqual(mock1.call_count, 2)
|
|
mock2.assert_not_called()
|
|
|
|
push_events("Foo Device 2", [InputEvent.from_string("1,30,0")])
|
|
self.throttle(20)
|
|
self.assertEqual(mock1.call_count, 2)
|
|
mock2.assert_called_once()
|
|
|
|
self.assertFalse(self.recording_toggle.get_active())
|
|
|
|
def test_cannot_create_duplicate_event_combination(self):
|
|
# load a device with more capabilities
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
# update the combination of the active mapping
|
|
self.controller.start_key_recording()
|
|
push_events(
|
|
"Foo Device 2",
|
|
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
|
|
)
|
|
self.throttle(20)
|
|
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.from_string("1,30,1"),
|
|
)
|
|
|
|
# create a new mapping
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
|
|
# try to recorde the same combination
|
|
self.controller.start_key_recording()
|
|
push_events(
|
|
"Foo Device 2",
|
|
[InputEvent.from_string("1,30,1"), InputEvent.from_string("1,30,0")],
|
|
)
|
|
self.throttle(20)
|
|
# should still be the empty mapping
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
|
|
# try to recorde a different combination
|
|
self.controller.start_key_recording()
|
|
push_events("Foo Device 2", [InputEvent.from_string("1,30,1")])
|
|
self.throttle(20)
|
|
# nothing changed yet, as we got the duplicate combination
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
push_events("Foo Device 2", [InputEvent.from_string("1,31,1")])
|
|
self.throttle(20)
|
|
# now the combination is different
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.from_string("1,30,1+1,31,1"),
|
|
)
|
|
|
|
# let's make the combination even longer
|
|
push_events("Foo Device 2", [InputEvent.from_string("1,32,1")])
|
|
self.throttle(20)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
|
|
)
|
|
|
|
# make sure we stop recording by releasing all keys
|
|
push_events(
|
|
"Foo Device 2",
|
|
[
|
|
InputEvent.from_string("1,31,0"),
|
|
InputEvent.from_string("1,30,0"),
|
|
InputEvent.from_string("1,32,0"),
|
|
],
|
|
)
|
|
self.throttle(20)
|
|
|
|
# sending a combination update now should not do anything
|
|
self.message_broker.send(
|
|
CombinationRecorded(EventCombination.from_string("1,35,1"))
|
|
)
|
|
gtk_iteration()
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.from_string("1,30,1+1,31,1+1,32,1"),
|
|
)
|
|
|
|
def test_create_simple_mapping(self):
|
|
# 1. create a mapping
|
|
self.create_mapping_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().name, "Empty Mapping"
|
|
)
|
|
self.assertIsNone(self.data_manager.active_mapping.name)
|
|
|
|
# there are now 2 mappings
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
|
|
self.assertEqual(len(self.data_manager.active_preset), 2)
|
|
|
|
# 2. recorde a combination for that mapping
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
push_events("Foo Device", [InputEvent.from_string("1,30,1")])
|
|
self.throttle(20)
|
|
push_events("Foo Device", [InputEvent.from_string("1,30,0")])
|
|
self.throttle(20)
|
|
|
|
# check the event_combination
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
EventCombination.from_string("1,30,1"),
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.from_string("1,30,1"),
|
|
)
|
|
self.assertEqual(self.selection_label_listbox.get_selected_row().name, "a")
|
|
self.assertIsNone(self.data_manager.active_mapping.name)
|
|
|
|
# 3. set the output symbol
|
|
self.code_editor.get_buffer().set_text("Shift_L")
|
|
gtk_iteration()
|
|
|
|
# the mapping and preset should be valid by now
|
|
self.assertTrue(self.data_manager.active_mapping.is_valid())
|
|
self.assertTrue(self.data_manager.active_preset.is_valid())
|
|
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping,
|
|
Mapping(
|
|
event_combination="1,30,1",
|
|
output_symbol="Shift_L",
|
|
target_uinput="keyboard",
|
|
),
|
|
)
|
|
self.assertEqual(self.target_selection.get_active_id(), "keyboard")
|
|
buffer = self.code_editor.get_buffer()
|
|
self.assertEqual(
|
|
buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True),
|
|
"Shift_L",
|
|
)
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
EventCombination.from_string("1,30,1"),
|
|
)
|
|
|
|
# 4. update target to mouse
|
|
self.target_selection.set_active_id("mouse")
|
|
gtk_iteration()
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping,
|
|
Mapping(
|
|
event_combination="1,30,1",
|
|
output_symbol="Shift_L",
|
|
target_uinput="mouse",
|
|
),
|
|
)
|
|
|
|
def test_show_status(self):
|
|
self.message_broker.send(StatusData(0, "a" * 100))
|
|
gtk_iteration()
|
|
text = self.get_status_text()
|
|
self.assertIn("...", text)
|
|
|
|
self.message_broker.send(StatusData(0, "b"))
|
|
gtk_iteration()
|
|
text = self.get_status_text()
|
|
self.assertNotIn("...", text)
|
|
|
|
def test_hat_switch(self):
|
|
# load a device with more capabilities
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
# it should be possible to add all of them
|
|
ev_1 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
|
|
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
|
|
ev_3 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, -1))
|
|
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0Y, 1))
|
|
|
|
def add_mapping(event, symbol):
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
self.controller.start_key_recording()
|
|
push_events("Foo Device 2", [event, event.modify(value=0)])
|
|
self.throttle(20)
|
|
gtk_iteration()
|
|
self.code_editor.get_buffer().set_text(symbol)
|
|
gtk_iteration()
|
|
|
|
add_mapping(ev_1, "a")
|
|
add_mapping(ev_2, "b")
|
|
add_mapping(ev_3, "c")
|
|
add_mapping(ev_4, "d")
|
|
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(
|
|
EventCombination(ev_1)
|
|
).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(
|
|
EventCombination(ev_2)
|
|
).output_symbol,
|
|
"b",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(
|
|
EventCombination(ev_3)
|
|
).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(
|
|
EventCombination(ev_4)
|
|
).output_symbol,
|
|
"d",
|
|
)
|
|
|
|
def test_combination(self):
|
|
# load a device with more capabilities
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
# it should be possible to write a combination combination
|
|
ev_1 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_A, 1))
|
|
ev_2 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, 1))
|
|
ev_3 = InputEvent.from_tuple((EV_KEY, evdev.ecodes.KEY_C, 1))
|
|
ev_4 = InputEvent.from_tuple((EV_ABS, evdev.ecodes.ABS_HAT0X, -1))
|
|
combination_1 = EventCombination((ev_1, ev_2, ev_3))
|
|
combination_2 = EventCombination((ev_2, ev_1, ev_3))
|
|
|
|
# same as 1, but different D-Pad direction
|
|
combination_3 = EventCombination((ev_1, ev_4, ev_3))
|
|
combination_4 = EventCombination((ev_4, ev_1, ev_3))
|
|
|
|
# same as 1, but the last combination is different
|
|
combination_5 = EventCombination((ev_1, ev_3, ev_2))
|
|
combination_6 = EventCombination((ev_3, ev_1, ev_2))
|
|
|
|
def add_mapping(combi: EventCombination, symbol):
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
self.controller.start_key_recording()
|
|
push_events("Foo Device 2", [event for event in combi])
|
|
push_events("Foo Device 2", [event.modify(value=0) for event in combi])
|
|
self.throttle(20)
|
|
gtk_iteration()
|
|
self.code_editor.get_buffer().set_text(symbol)
|
|
gtk_iteration()
|
|
|
|
add_mapping(combination_1, "a")
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
|
|
|
|
# it won't write the same combination again, even if the
|
|
# first two events are in a different order
|
|
add_mapping(combination_2, "b")
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_3))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_4))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
|
|
|
|
add_mapping(combination_3, "c")
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(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.
|
|
add_mapping(combination_4, "d")
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_5))
|
|
self.assertIsNone(self.data_manager.active_preset.get_mapping(combination_6))
|
|
|
|
add_mapping(combination_5, "e")
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_1).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_2).output_symbol,
|
|
"a",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_3).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_4).output_symbol,
|
|
"c",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_5).output_symbol,
|
|
"e",
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.get_mapping(combination_6).output_symbol,
|
|
"e",
|
|
)
|
|
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
warning_icon = self.user_interface.get("warning_status_icon")
|
|
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
def test_only_one_empty_mapping_possible(self):
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
EventCombination.from_string("1,5,1"),
|
|
)
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
|
|
self.assertEqual(len(self.data_manager.active_preset), 1)
|
|
|
|
self.create_mapping_btn.clicked()
|
|
gtk_iteration()
|
|
self.assertEqual(
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
|
|
self.assertEqual(len(self.data_manager.active_preset), 2)
|
|
|
|
self.create_mapping_btn.clicked()
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
|
|
self.assertEqual(len(self.data_manager.active_preset), 2)
|
|
|
|
def test_selection_labels_sort_alphabetically(self):
|
|
self.controller.load_preset("preset1")
|
|
# contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
|
|
gtk_iteration()
|
|
# we expect (1,2,1 -> a) to be selected because "1" < "Escape"
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
|
|
self.assertIs(
|
|
self.selection_label_listbox.get_row_at_index(0),
|
|
self.selection_label_listbox.get_selected_row(),
|
|
)
|
|
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.message_broker.send(
|
|
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
|
|
)
|
|
gtk_iteration()
|
|
self.message_broker.signal(MessageType.recording_finished)
|
|
gtk_iteration()
|
|
# the combination and the order changed "Escape" < "q"
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
|
|
self.assertIs(
|
|
self.selection_label_listbox.get_row_at_index(1),
|
|
self.selection_label_listbox.get_selected_row(),
|
|
)
|
|
|
|
def test_selection_labels_sort_empty_mapping_to_the_bottom(self):
|
|
# make sure we have a mapping which would sort to the bottom only
|
|
# considering alphanumeric sorting: "q" > "Empty Mapping"
|
|
self.controller.load_preset("preset1")
|
|
gtk_iteration()
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.message_broker.send(
|
|
CombinationRecorded(EventCombination((EV_KEY, KEY_Q, 1)))
|
|
)
|
|
gtk_iteration()
|
|
self.message_broker.signal(MessageType.recording_finished)
|
|
gtk_iteration()
|
|
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
row: SelectionLabel = self.selection_label_listbox.get_selected_row()
|
|
self.assertEqual(row.combination, EventCombination.empty_combination())
|
|
self.assertEqual(row.label.get_text(), "Empty Mapping")
|
|
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
|
|
|
|
def test_select_mapping(self):
|
|
self.controller.load_preset("preset1")
|
|
# contains two mappings (1,1,1 -> b) and (1,2,1 -> a)
|
|
gtk_iteration()
|
|
# we expect (1,2,1 -> a) to be selected because "1" < "Escape"
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
|
|
|
|
# select the second entry in the listbox
|
|
row = self.selection_label_listbox.get_row_at_index(1)
|
|
self.selection_label_listbox.select_row(row)
|
|
gtk_iteration()
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "b")
|
|
|
|
def test_selection_label_uses_name_if_available(self):
|
|
self.controller.load_preset("preset1")
|
|
gtk_iteration()
|
|
row: SelectionLabel = self.selection_label_listbox.get_selected_row()
|
|
self.assertEqual(row.label.get_text(), "1")
|
|
self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
|
|
|
|
self.controller.update_mapping(name="foo")
|
|
gtk_iteration()
|
|
self.assertEqual(row.label.get_text(), "foo")
|
|
self.assertIs(row, self.selection_label_listbox.get_row_at_index(1))
|
|
|
|
# Empty Mapping still sorts to the bottom
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
row = self.selection_label_listbox.get_selected_row()
|
|
self.assertEqual(row.combination, EventCombination.empty_combination())
|
|
self.assertEqual(row.label.get_text(), "Empty Mapping")
|
|
self.assertIs(self.selection_label_listbox.get_row_at_index(2), row)
|
|
|
|
def test_fake_empty_mapping_does_not_sort_to_bottom(self):
|
|
"""If someone chooses to name a mapping "Empty Mapping"
|
|
it is not sorted to the bottom"""
|
|
self.controller.load_preset("preset1")
|
|
gtk_iteration()
|
|
|
|
self.controller.update_mapping(name="Empty Mapping")
|
|
self.throttle() # sorting seems to take a bit
|
|
|
|
# "Empty Mapping" < "Escape" so we still expect this to be the first row
|
|
row = self.selection_label_listbox.get_selected_row()
|
|
self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
|
|
|
|
# now create a real empty mapping
|
|
self.controller.create_mapping()
|
|
self.throttle()
|
|
|
|
# for some reason we no longer can use assertIs maybe a gtk bug?
|
|
# self.assertIs(row, self.selection_label_listbox.get_row_at_index(0))
|
|
|
|
# we expect the fake empty mapping in row 0 and the real one in row 2
|
|
self.selection_label_listbox.select_row(
|
|
self.selection_label_listbox.get_row_at_index(0)
|
|
)
|
|
gtk_iteration()
|
|
self.assertEqual(self.data_manager.active_mapping.name, "Empty Mapping")
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "a")
|
|
|
|
self.selection_label_listbox.select_row(
|
|
self.selection_label_listbox.get_row_at_index(2)
|
|
)
|
|
self.assertIsNone(self.data_manager.active_mapping.name)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
EventCombination.empty_combination(),
|
|
)
|
|
|
|
def test_remove_mapping(self):
|
|
self.controller.load_preset("preset1")
|
|
gtk_iteration()
|
|
self.assertEqual(len(self.data_manager.active_preset), 2)
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 2)
|
|
|
|
with PatchedConfirmDelete(self.user_interface):
|
|
self.delete_mapping_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
self.assertEqual(len(self.data_manager.active_preset), 1)
|
|
self.assertEqual(len(self.selection_label_listbox.get_children()), 1)
|
|
|
|
def test_problematic_combination(self):
|
|
# load a device with more capabilities
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
def add_mapping(combi: EventCombination, symbol):
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
self.controller.start_key_recording()
|
|
push_events("Foo Device 2", [event for event in combi])
|
|
push_events("Foo Device 2", [event.modify(value=0) for event in combi])
|
|
self.throttle(20)
|
|
gtk_iteration()
|
|
self.code_editor.get_buffer().set_text(symbol)
|
|
gtk_iteration()
|
|
|
|
combination = EventCombination(((EV_KEY, KEY_LEFTSHIFT, 1), (EV_KEY, 82, 1)))
|
|
add_mapping(combination, "b")
|
|
text = self.get_status_text()
|
|
self.assertIn("shift", text)
|
|
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
warning_icon = self.user_interface.get("warning_status_icon")
|
|
|
|
self.assertFalse(error_icon.get_visible())
|
|
self.assertTrue(warning_icon.get_visible())
|
|
|
|
def test_rename_and_save(self):
|
|
# only a basic test, TestController and TestDataManager go more in detail
|
|
self.rename_input.set_text("foo")
|
|
self.rename_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
preset_path = f"{CONFIG_PATH}/presets/Foo Device/foo.json"
|
|
self.assertTrue(os.path.exists(preset_path))
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
self.assertFalse(error_icon.get_visible())
|
|
|
|
def save():
|
|
raise PermissionError
|
|
|
|
with patch.object(self.data_manager.active_preset, "save", save):
|
|
self.code_editor.get_buffer().set_text("f")
|
|
gtk_iteration()
|
|
status = self.get_status_text()
|
|
self.assertIn("Permission denied", status)
|
|
|
|
with PatchedConfirmDelete(self.user_interface):
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
self.assertFalse(os.path.exists(preset_path))
|
|
|
|
def test_check_for_unknown_symbols(self):
|
|
status = self.user_interface.get("status_bar")
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
warning_icon = self.user_interface.get("warning_status_icon")
|
|
|
|
self.controller.load_preset("preset1")
|
|
self.throttle()
|
|
self.controller.load_mapping(EventCombination.from_string("1,1,1"))
|
|
gtk_iteration()
|
|
self.controller.update_mapping(output_symbol="foo")
|
|
gtk_iteration()
|
|
self.controller.load_mapping(EventCombination.from_string("1,2,1"))
|
|
gtk_iteration()
|
|
self.controller.update_mapping(output_symbol="qux")
|
|
gtk_iteration()
|
|
|
|
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", "preset1")) as f:
|
|
content = f.read()
|
|
self.assertIn("qux", content)
|
|
self.assertIn("foo", content)
|
|
|
|
self.controller.update_mapping(output_symbol="a")
|
|
gtk_iteration()
|
|
tooltip = status.get_tooltip_text().lower()
|
|
self.assertIn("foo", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
self.controller.load_mapping(EventCombination.from_string("1,1,1"))
|
|
gtk_iteration()
|
|
self.controller.update_mapping(output_symbol="b")
|
|
gtk_iteration()
|
|
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.status_bar
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
warning_icon = self.user_interface.get("warning_status_icon")
|
|
|
|
self.code_editor.get_buffer().set_text("k(1))")
|
|
gtk_iteration()
|
|
tooltip = status.get_tooltip_text().lower()
|
|
self.assertIn("brackets", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
self.code_editor.get_buffer().set_text("k(1)")
|
|
gtk_iteration()
|
|
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(
|
|
self.data_manager.active_mapping.output_symbol,
|
|
"k(1)",
|
|
)
|
|
|
|
def test_check_on_typing(self):
|
|
status = self.user_interface.get("status_bar")
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
warning_icon = self.user_interface.get("warning_status_icon")
|
|
|
|
tooltip = status.get_tooltip_text()
|
|
# nothing wrong yet
|
|
self.assertIsNone(tooltip)
|
|
|
|
# now change the mapping by typing into the field
|
|
buffer = self.code_editor.get_buffer()
|
|
buffer.set_text("sdfgkj()")
|
|
gtk_iteration()
|
|
|
|
# the mapping is immediately validated
|
|
tooltip = status.get_tooltip_text()
|
|
self.assertIn("Unknown function sdfgkj", tooltip)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertFalse(warning_icon.get_visible())
|
|
|
|
self.assertEqual(self.data_manager.active_mapping.output_symbol, "sdfgkj()")
|
|
|
|
def test_select_device(self):
|
|
# simple test to make sure we can switch between devices
|
|
# more detailed tests in TestController and TestDataManager
|
|
self.device_selection.set_active_id("Bar Device")
|
|
gtk_iteration()
|
|
|
|
entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
|
|
self.assertEqual(entries, {"new preset"})
|
|
|
|
self.device_selection.set_active_id("Foo Device")
|
|
gtk_iteration()
|
|
|
|
entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
|
|
self.assertEqual(entries, {"preset1", "preset2", "preset3"})
|
|
|
|
# make sure a preset and mapping was loaded
|
|
self.assertIsNotNone(self.data_manager.active_preset)
|
|
self.assertEqual(
|
|
self.data_manager.active_preset.name, self.preset_selection.get_active_id()
|
|
)
|
|
self.assertIsNotNone(self.data_manager.active_mapping)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.event_combination,
|
|
self.selection_label_listbox.get_selected_row().combination,
|
|
)
|
|
|
|
def test_select_preset(self):
|
|
# simple test to make sure we can switch between presets
|
|
# more detailed tests in TestController and TestDataManager
|
|
self.device_selection.set_active_id("Foo Device 2")
|
|
gtk_iteration()
|
|
self.preset_selection.set_active_id("preset1")
|
|
gtk_iteration()
|
|
|
|
mappings = {
|
|
row.combination for row in self.selection_label_listbox.get_children()
|
|
}
|
|
self.assertEqual(
|
|
mappings,
|
|
{
|
|
EventCombination.from_string("1,1,1"),
|
|
EventCombination.from_string("1,2,1"),
|
|
},
|
|
)
|
|
self.assertFalse(self.autoload_toggle.get_active())
|
|
|
|
self.preset_selection.set_active_id("preset2")
|
|
gtk_iteration()
|
|
|
|
mappings = {
|
|
row.combination for row in self.selection_label_listbox.get_children()
|
|
}
|
|
self.assertEqual(
|
|
mappings,
|
|
{
|
|
EventCombination.from_string("1,3,1"),
|
|
EventCombination.from_string("1,4,1"),
|
|
},
|
|
)
|
|
self.assertTrue(self.autoload_toggle.get_active())
|
|
|
|
def test_copy_preset(self):
|
|
# simple tests to ensure it works
|
|
# more detailed tests in TestController and TestDataManager
|
|
|
|
# check the initial state
|
|
entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
|
|
self.assertEqual(entries, {"preset1", "preset2", "preset3"})
|
|
self.assertEqual(self.preset_selection.get_active_id(), "preset3")
|
|
|
|
self.copy_preset_btn.clicked()
|
|
gtk_iteration()
|
|
entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
|
|
self.assertEqual(entries, {"preset1", "preset2", "preset3", "preset3 copy"})
|
|
self.assertEqual(self.preset_selection.get_active_id(), "preset3 copy")
|
|
|
|
self.copy_preset_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
entries = {entry[0] for entry in self.preset_selection.get_child().get_model()}
|
|
self.assertEqual(
|
|
entries, {"preset1", "preset2", "preset3", "preset3 copy", "preset3 copy 2"}
|
|
)
|
|
|
|
def test_wont_start(self):
|
|
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
|
|
|
|
error_icon = self.user_interface.get("error_status_icon")
|
|
self.controller.load_group("Bar Device")
|
|
|
|
# empty
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
wait()
|
|
text = self.get_status_text()
|
|
self.assertIn("add keys", text)
|
|
self.assertTrue(error_icon.get_visible())
|
|
self.assertNotEqual(self.daemon.get_state("Bar Device"), RUNNING)
|
|
|
|
# device grabbing fails
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
for i in range(2):
|
|
# just pressing apply again will overwrite the previous error
|
|
self.grab_fails = True
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
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.daemon.get_state("Foo Device 2"), RUNNING)
|
|
|
|
# this time work properly
|
|
|
|
self.grab_fails = False
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
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.daemon.get_state("Foo Device 2"), RUNNING)
|
|
|
|
def test_start_with_btn_left(self):
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
self.controller.update_mapping(
|
|
event_combination=EventCombination(InputEvent.btn_left()),
|
|
output_symbol="a",
|
|
)
|
|
gtk_iteration()
|
|
|
|
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
|
|
|
|
# first apply, shows btn_left warning
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
text = self.get_status_text()
|
|
self.assertIn("click", text)
|
|
self.assertEqual(self.daemon.get_state("Foo Device 2"), UNKNOWN)
|
|
|
|
# second apply, overwrites
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
wait()
|
|
self.assertEqual(self.daemon.get_state("Foo Device 2"), 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_cannot_record_keys(self):
|
|
self.controller.load_group("Foo Device 2")
|
|
self.assertNotEqual(self.data_manager.get_state(), RUNNING)
|
|
self.assertNotIn("Stop Injection", self.get_status_text())
|
|
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertTrue(self.recording_toggle.get_active())
|
|
self.controller.stop_key_recording()
|
|
gtk_iteration()
|
|
self.assertFalse(self.recording_toggle.get_active())
|
|
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
# wait for the injector to start
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if "Starting" not in self.get_status_text():
|
|
break
|
|
|
|
self.assertEqual(self.data_manager.get_state(), RUNNING)
|
|
|
|
# the toggle button should reset itself shortly
|
|
self.recording_toggle.set_active(True)
|
|
gtk_iteration()
|
|
self.assertFalse(self.recording_toggle.get_active())
|
|
text = self.get_status_text()
|
|
self.assertIn("Stop Injection", text)
|
|
|
|
def test_start_injecting(self):
|
|
self.controller.load_group("Foo Device 2")
|
|
|
|
with spy(self.daemon, "set_config_dir") as spy1:
|
|
with spy(self.daemon, "start_injecting") as spy2:
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
# correctly uses group.key, not group.name
|
|
spy2.assert_called_once_with("Foo Device 2", "preset3")
|
|
|
|
spy1.assert_called_once_with(get_config_path())
|
|
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if self.data_manager.get_state() == RUNNING:
|
|
break
|
|
|
|
# fail here so we don't block forever
|
|
self.assertEqual(self.data_manager.get_state(), RUNNING)
|
|
|
|
# this is a stupid workaround for the bad test fixtures
|
|
# by switching the group we make sure that the helper no longer listens for
|
|
# events on "Foo Device 2" otherwise we would have two processes
|
|
# (helper and injector) reading the same pipe which can block this test
|
|
# indefinitely
|
|
self.controller.load_group("Foo Device")
|
|
gtk_iteration()
|
|
|
|
push_events(
|
|
"Foo Device 2",
|
|
[
|
|
new_event(evdev.events.EV_KEY, 5, 1),
|
|
new_event(evdev.events.EV_KEY, 5, 0),
|
|
],
|
|
)
|
|
|
|
event = uinput_write_history_pipe[0].recv()
|
|
self.assertEqual(event.type, evdev.events.EV_KEY)
|
|
self.assertEqual(event.code, KEY_A)
|
|
self.assertEqual(event.value, 1)
|
|
|
|
event = uinput_write_history_pipe[0].recv()
|
|
self.assertEqual(event.type, evdev.events.EV_KEY)
|
|
self.assertEqual(event.code, KEY_A)
|
|
self.assertEqual(event.value, 0)
|
|
|
|
# the input-remapper device will not be shown
|
|
self.controller.refresh_groups()
|
|
gtk_iteration()
|
|
|
|
for entry in self.device_selection.get_child().get_model():
|
|
# whichever attribute contains "input-remapper"
|
|
self.assertNotIn("input-remapper", "".join(entry))
|
|
|
|
def test_stop_injecting(self):
|
|
self.controller.load_group("Foo Device 2")
|
|
self.start_injector_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if self.data_manager.get_state() == RUNNING:
|
|
break
|
|
# fail here so we don't block forever
|
|
self.assertEqual(self.data_manager.get_state(), RUNNING)
|
|
|
|
# stupid fixture workaround
|
|
self.controller.load_group("Foo Device")
|
|
gtk_iteration()
|
|
|
|
pipe = uinput_write_history_pipe[0]
|
|
self.assertFalse(pipe.poll())
|
|
|
|
push_events(
|
|
"Foo Device 2",
|
|
[
|
|
new_event(evdev.events.EV_KEY, 5, 1),
|
|
new_event(evdev.events.EV_KEY, 5, 0),
|
|
],
|
|
)
|
|
|
|
time.sleep(0.2)
|
|
self.assertTrue(pipe.poll())
|
|
while pipe.poll():
|
|
pipe.recv()
|
|
|
|
self.controller.load_group("Foo Device 2")
|
|
self.controller.stop_injecting()
|
|
gtk_iteration()
|
|
|
|
for _ in range(10):
|
|
time.sleep(0.1)
|
|
gtk_iteration()
|
|
if self.data_manager.get_state() == STOPPED:
|
|
break
|
|
self.assertEqual(self.data_manager.get_state(), STOPPED)
|
|
|
|
push_events(
|
|
"Foo Device 2",
|
|
[
|
|
new_event(evdev.events.EV_KEY, 5, 1),
|
|
new_event(evdev.events.EV_KEY, 5, 0),
|
|
],
|
|
)
|
|
time.sleep(0.2)
|
|
self.assertFalse(pipe.poll())
|
|
|
|
def test_delete_preset(self):
|
|
# as per test_initial_state we already have preset3 loaded
|
|
|
|
self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
|
|
|
|
with PatchedConfirmDelete(self.user_interface, Gtk.ResponseType.CANCEL):
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
self.assertTrue(os.path.exists(get_preset_path("Foo Device", "preset3")))
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset3")
|
|
self.assertEqual(self.data_manager.active_group.name, "Foo Device")
|
|
|
|
with PatchedConfirmDelete(self.user_interface):
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
self.assertFalse(os.path.exists(get_preset_path("Foo Device", "preset3")))
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset2")
|
|
self.assertEqual(self.data_manager.active_group.name, "Foo Device")
|
|
|
|
def test_refresh_groups(self):
|
|
# sanity check: preset3 should be the newest
|
|
self.assertEqual(self.preset_selection.get_active_id(), "preset3")
|
|
|
|
# select the older one
|
|
self.preset_selection.set_active_id("preset1")
|
|
gtk_iteration()
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset1")
|
|
|
|
# add a device that doesn't exist to the dropdown
|
|
unknown_key = "key-1234"
|
|
self.device_selection.get_child().get_model().insert(
|
|
0, [unknown_key, None, "foo"]
|
|
)
|
|
|
|
self.controller.refresh_groups()
|
|
gtk_iteration()
|
|
self.throttle(100)
|
|
# the newest preset should be selected
|
|
self.assertEqual(self.controller.get_a_preset(), "preset3")
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset3")
|
|
|
|
# the list contains correct entries
|
|
# and the non-existing entry should be removed
|
|
entries = [
|
|
tuple(entry) for entry in self.device_selection.get_child().get_model()
|
|
]
|
|
keys = [entry[0] for entry in self.device_selection.get_child().get_model()]
|
|
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-gaming", "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
|
|
self.data_manager._reader.groups.find(key="Foo Device 2").types = []
|
|
self.data_manager._reader.send_groups()
|
|
gtk_iteration()
|
|
self.assertIn(
|
|
("Foo Device 2", None, "Foo Device 2"),
|
|
[tuple(entry) for entry in self.device_selection.get_child().get_model()],
|
|
)
|
|
|
|
def test_shared_presets(self):
|
|
# devices with the same name (but different key because the key is
|
|
# unique) share the same presets.
|
|
# Those devices would usually be of the same model of keyboard for example
|
|
# Todo: move this to unit tests, there is no point in having the ui around
|
|
self.controller.load_group("Foo Device")
|
|
presets1 = self.data_manager.get_preset_names()
|
|
self.controller.load_group("Foo Device 2")
|
|
gtk_iteration()
|
|
presets2 = self.data_manager.get_preset_names()
|
|
self.controller.load_group("Bar Device")
|
|
gtk_iteration()
|
|
presets3 = self.data_manager.get_preset_names()
|
|
|
|
self.assertEqual(presets1, presets2)
|
|
self.assertNotEqual(presets1, presets3)
|
|
|
|
def test_delete_last_preset(self):
|
|
with PatchedConfirmDelete(self.user_interface):
|
|
# as per test_initial_state we already have preset3 loaded
|
|
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
# the next newest preset should be loaded
|
|
self.assertEqual(self.data_manager.active_preset.name, "preset2")
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
self.delete_preset_btn.clicked()
|
|
# the ui should be clean
|
|
self.assert_gui_clean()
|
|
device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}"
|
|
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
|
|
|
|
self.delete_preset_btn.clicked()
|
|
gtk_iteration()
|
|
# deleting an empty preset als doesn't do weird stuff
|
|
self.assert_gui_clean()
|
|
device_path = f"{CONFIG_PATH}/presets/{self.data_manager.active_group.name}"
|
|
self.assertTrue(os.path.exists(f"{device_path}/new preset.json"))
|
|
|
|
def test_enable_disable_symbol_input(self):
|
|
# load a group without any presets
|
|
self.controller.load_group("Bar Device")
|
|
|
|
# should be disabled by default since no key is recorded yet
|
|
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
|
|
self.assertFalse(self.code_editor.get_sensitive())
|
|
|
|
# create a mapping
|
|
self.controller.create_mapping()
|
|
gtk_iteration()
|
|
|
|
# should still be disabled
|
|
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
|
|
self.assertFalse(self.code_editor.get_sensitive())
|
|
|
|
# enable it by sending a combination
|
|
self.controller.start_key_recording()
|
|
gtk_iteration()
|
|
push_events(
|
|
"Bar Device",
|
|
[
|
|
InputEvent.from_string("1,30,1"),
|
|
InputEvent.from_string("1,30,0"),
|
|
],
|
|
)
|
|
self.throttle(50) # give time for the input to arrive
|
|
|
|
self.assertEqual(self.get_unfiltered_symbol_input_text(), "")
|
|
self.assertTrue(self.code_editor.get_sensitive())
|
|
|
|
# disable it by deleting the mapping
|
|
with PatchedConfirmDelete(self.user_interface):
|
|
self.delete_mapping_btn.clicked()
|
|
gtk_iteration()
|
|
|
|
self.assertEqual(self.get_unfiltered_symbol_input_text(), SET_KEY_FIRST)
|
|
self.assertFalse(self.code_editor.get_sensitive())
|
|
|
|
|
|
class TestAutocompletion(GuiTestBase):
|
|
def press_key(self, keyval):
|
|
event = Gdk.EventKey()
|
|
event.keyval = keyval
|
|
self.user_interface.autocompletion.navigate(None, event)
|
|
|
|
def test_autocomplete_key(self):
|
|
self.controller.update_mapping(output_symbol="")
|
|
gtk_iteration()
|
|
|
|
self.set_focus(self.code_editor)
|
|
|
|
complete_key_name = "Test_Foo_Bar"
|
|
|
|
system_mapping.clear()
|
|
system_mapping._set(complete_key_name, 1)
|
|
system_mapping._set("KEY_A", 30) # we need this for the UIMapping to work
|
|
|
|
# it can autocomplete a combination inbetween other things
|
|
incomplete = "qux_1\n + + qux_2"
|
|
Gtk.TextView.do_insert_at_cursor(self.code_editor, incomplete)
|
|
Gtk.TextView.do_move_cursor(
|
|
self.code_editor,
|
|
Gtk.MovementStep.VISUAL_POSITIONS,
|
|
-8,
|
|
False,
|
|
)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(self.code_editor, "foo")
|
|
self.throttle(100)
|
|
|
|
autocompletion = self.user_interface.autocompletion
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
self.press_key(Gdk.KEY_Down)
|
|
self.press_key(Gdk.KEY_Return)
|
|
gtk_iteration()
|
|
|
|
# the first suggestion should have been selected
|
|
modified_symbol = self.data_manager.active_mapping.output_symbol
|
|
self.assertEqual(modified_symbol, f"qux_1\n + {complete_key_name} + qux_2")
|
|
|
|
# try again, but a whitespace completes the word and so no autocompletion
|
|
# should be shown
|
|
Gtk.TextView.do_insert_at_cursor(self.code_editor, " + foo ")
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
|
|
self.assertFalse(autocompletion.visible)
|
|
|
|
def test_autocomplete_function(self):
|
|
self.controller.update_mapping(output_symbol="")
|
|
gtk_iteration()
|
|
|
|
source_view = self.code_editor
|
|
self.set_focus(source_view)
|
|
|
|
incomplete = "key(KEY_A).\nepea"
|
|
Gtk.TextView.do_insert_at_cursor(source_view, incomplete)
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
|
|
autocompletion = self.user_interface.autocompletion
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
self.press_key(Gdk.KEY_Down)
|
|
self.press_key(Gdk.KEY_Return)
|
|
|
|
# the first suggestion should have been selected
|
|
modified_symbol = self.data_manager.active_mapping.output_symbol
|
|
self.assertEqual(modified_symbol, "key(KEY_A).\nrepeat")
|
|
|
|
def test_close_autocompletion(self):
|
|
self.controller.update_mapping(output_symbol="")
|
|
gtk_iteration()
|
|
|
|
source_view = self.code_editor
|
|
self.set_focus(source_view)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
|
|
autocompletion = self.user_interface.autocompletion
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
self.press_key(Gdk.KEY_Down)
|
|
self.press_key(Gdk.KEY_Escape)
|
|
|
|
self.assertFalse(autocompletion.visible)
|
|
|
|
symbol = self.data_manager.active_mapping.output_symbol
|
|
self.assertEqual(symbol, "KEY_")
|
|
|
|
def test_writing_still_works(self):
|
|
self.controller.update_mapping(output_symbol="")
|
|
gtk_iteration()
|
|
source_view = self.code_editor
|
|
self.set_focus(source_view)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
|
|
|
|
autocompletion = self.user_interface.autocompletion
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
# writing still works while an entry is selected
|
|
self.press_key(Gdk.KEY_Down)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(source_view, "A")
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(source_view, "1234foobar")
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
# no key matches this completion, so it closes again
|
|
self.assertFalse(autocompletion.visible)
|
|
|
|
def test_cycling(self):
|
|
self.controller.update_mapping(output_symbol="")
|
|
gtk_iteration()
|
|
source_view = self.code_editor
|
|
self.set_focus(source_view)
|
|
|
|
Gtk.TextView.do_insert_at_cursor(source_view, "KEY_")
|
|
|
|
autocompletion = self.user_interface.autocompletion
|
|
|
|
time.sleep(0.11)
|
|
gtk_iteration()
|
|
self.assertTrue(autocompletion.visible)
|
|
|
|
self.assertEqual(
|
|
autocompletion.scrolled_window.get_vadjustment().get_value(), 0
|
|
)
|
|
|
|
# cycle to the end of the list because there is no element higher than index 0
|
|
self.press_key(Gdk.KEY_Up)
|
|
self.assertGreater(
|
|
autocompletion.scrolled_window.get_vadjustment().get_value(), 0
|
|
)
|
|
|
|
# go back to the start, because it can't go down further
|
|
self.press_key(Gdk.KEY_Down)
|
|
self.assertEqual(
|
|
autocompletion.scrolled_window.get_vadjustment().get_value(), 0
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|