Ajoute la configuration des associations application/preset, le service de suivi du focus, l'intégration GUI et les fichiers d'installation nécessaires. Ajoute les tests unitaires ciblés des bindings, du service de focus, des migrations et des composants GUI associés. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1698 lines
63 KiB
Python
1698 lines
63 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# input-remapper - GUI for device specific keyboard mappings
|
|
# Copyright (C) 2025 sezanzeb <b8x45ygc9@mozmail.com>
|
|
#
|
|
# 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/>.
|
|
|
|
import os.path
|
|
import unittest
|
|
from typing import List
|
|
from unittest.mock import patch, MagicMock, call
|
|
|
|
import gi
|
|
from evdev.ecodes import EV_ABS, ABS_X, ABS_Y, ABS_RX
|
|
|
|
from inputremapper.configs.keyboard_layout import keyboard_layout
|
|
from inputremapper.injection.injector import InjectorState
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
from gi.repository import Gtk
|
|
|
|
from inputremapper.configs.input_config import InputCombination, InputConfig
|
|
from inputremapper.groups import _Groups
|
|
from inputremapper.gui.messages.message_broker import (
|
|
MessageBroker,
|
|
MessageType,
|
|
)
|
|
from inputremapper.gui.messages.message_data import (
|
|
GroupsData,
|
|
GroupData,
|
|
PresetData,
|
|
StatusData,
|
|
CombinationRecorded,
|
|
CombinationUpdate,
|
|
UserConfirmRequest,
|
|
)
|
|
from inputremapper.gui.reader_client import ReaderClient
|
|
from inputremapper.gui.utils import CTX_ERROR, CTX_APPLY, gtk_iteration
|
|
from inputremapper.gui.gettext import _
|
|
from inputremapper.injection.global_uinputs import GlobalUInputs, FrontendUInput
|
|
from inputremapper.configs.mapping import UIMapping, MappingData, Mapping
|
|
from tests.lib.spy import spy
|
|
from tests.lib.patches import FakeDaemonProxy
|
|
from tests.lib.fixtures import fixtures, prepare_presets
|
|
from inputremapper.configs.global_config import GlobalConfig
|
|
from inputremapper.gui.controller import Controller, MAPPING_DEFAULTS
|
|
from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME
|
|
from inputremapper.configs.paths import PathUtils
|
|
from inputremapper.configs.preset import Preset
|
|
from tests.lib.test_setup import test_setup
|
|
|
|
|
|
@test_setup
|
|
class TestController(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
super().setUp()
|
|
self.message_broker = MessageBroker()
|
|
uinputs = GlobalUInputs(FrontendUInput)
|
|
uinputs.prepare_all()
|
|
self.data_manager = DataManager(
|
|
self.message_broker,
|
|
GlobalConfig(),
|
|
ReaderClient(self.message_broker, _Groups()),
|
|
FakeDaemonProxy(),
|
|
uinputs,
|
|
keyboard_layout,
|
|
)
|
|
self.user_interface = MagicMock()
|
|
self.controller = Controller(self.message_broker, self.data_manager)
|
|
self.controller.set_gui(self.user_interface)
|
|
|
|
def test_should_get_newest_group(self):
|
|
"""get_a_group should the newest group."""
|
|
with patch.object(
|
|
self.data_manager, "get_newest_group_key", MagicMock(return_value="foo")
|
|
):
|
|
self.assertEqual(self.controller.get_a_group(), "foo")
|
|
|
|
def test_should_get_any_group(self):
|
|
"""get_a_group should return a valid group."""
|
|
with patch.object(
|
|
self.data_manager,
|
|
"get_newest_group_key",
|
|
MagicMock(side_effect=FileNotFoundError),
|
|
):
|
|
fixture_keys = [fixture.group_key or fixture.name for fixture in fixtures]
|
|
self.assertIn(self.controller.get_a_group(), fixture_keys)
|
|
|
|
def test_should_get_newest_preset(self):
|
|
"""get_a_group should the newest group."""
|
|
with patch.object(
|
|
self.data_manager, "get_newest_preset_name", MagicMock(return_value="bar")
|
|
):
|
|
self.data_manager.load_group("Foo Device")
|
|
self.assertEqual(self.controller.get_a_preset(), "bar")
|
|
|
|
def test_should_get_any_preset(self):
|
|
"""get_a_preset should return a new preset if none exist."""
|
|
self.data_manager.load_group("Foo Device")
|
|
# the default name
|
|
self.assertEqual(self.controller.get_a_preset(), "new preset")
|
|
|
|
def test_on_init_should_provide_uinputs(self):
|
|
calls = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.uinputs, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
self.assertEqual(
|
|
["keyboard", "gamepad", "mouse", "keyboard + mouse"],
|
|
list(calls[-1].uinputs.keys()),
|
|
)
|
|
|
|
def test_on_init_should_provide_groups(self):
|
|
calls: List[GroupsData] = []
|
|
|
|
def f(groups):
|
|
calls.append(groups)
|
|
|
|
self.message_broker.subscribe(MessageType.groups, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
self.assertEqual(
|
|
["Foo Device", "Foo Device 2", "Bar Device", "gamepad", "Qux/[Device]?"],
|
|
list(calls[-1].groups.keys()),
|
|
)
|
|
|
|
def test_on_init_should_provide_a_group(self):
|
|
calls: List[GroupData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.group, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
self.assertGreaterEqual(len(calls), 1)
|
|
|
|
def test_on_init_should_provide_a_preset(self):
|
|
calls: List[PresetData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.preset, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
self.assertGreaterEqual(len(calls), 1)
|
|
|
|
def test_on_init_should_provide_a_mapping(self):
|
|
"""Only if there is one."""
|
|
prepare_presets()
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
self.assertTrue(calls[-1].is_valid())
|
|
|
|
def test_on_init_should_provide_a_default_mapping(self):
|
|
"""If there is no real preset available"""
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.message_broker.signal(MessageType.init)
|
|
for m in calls:
|
|
self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
|
|
|
|
def test_on_load_group_should_provide_preset(self):
|
|
with patch.object(self.data_manager, "load_preset") as mock:
|
|
self.controller.load_group("Foo Device")
|
|
mock.assert_called_once()
|
|
|
|
def test_on_load_group_should_provide_mapping(self):
|
|
"""If there is one"""
|
|
prepare_presets()
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.controller.load_group(group_key="Foo Device 2")
|
|
self.assertTrue(calls[-1].is_valid())
|
|
|
|
def test_on_load_group_should_provide_default_mapping(self):
|
|
"""If there is none."""
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
|
|
self.controller.load_group(group_key="Foo Device")
|
|
for m in calls:
|
|
self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
|
|
|
|
def test_on_load_preset_should_provide_mapping(self):
|
|
"""If there is one."""
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.controller.load_preset(name="preset2")
|
|
self.assertTrue(calls[-1].is_valid())
|
|
|
|
def test_on_load_preset_should_provide_default_mapping(self):
|
|
"""If there is none."""
|
|
Preset(PathUtils.get_preset_path("Foo Device", "bar")).save()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.controller.load_preset(name="bar")
|
|
for m in calls:
|
|
self.assertEqual(m, UIMapping(**MAPPING_DEFAULTS))
|
|
|
|
def test_on_delete_preset_asks_for_confirmation(self):
|
|
prepare_presets()
|
|
self.message_broker.signal(MessageType.init)
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, mock)
|
|
self.controller.delete_preset()
|
|
mock.assert_called_once()
|
|
|
|
def test_deletes_preset_when_confirmed(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda msg: msg.respond(True)
|
|
)
|
|
self.controller.delete_preset()
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
def test_does_not_delete_preset_when_not_confirmed(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.user_interface.confirm_delete.configure_mock(
|
|
return_value=Gtk.ResponseType.CANCEL
|
|
)
|
|
self.controller.delete_preset()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
def test_copy_preset(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset()
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy"))
|
|
)
|
|
|
|
def test_copy_preset_should_add_number(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy"
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy 2"
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2"))
|
|
)
|
|
|
|
def test_copy_preset_should_increment_existing_number(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy"
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy 2"
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy 3"
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 3"))
|
|
)
|
|
|
|
def test_copy_preset_should_not_append_copy_twice(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy"
|
|
self.controller.copy_preset() # creates "preset2 copy 2" not "preset2 copy copy"
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2"))
|
|
)
|
|
|
|
def test_copy_preset_should_not_append_copy_to_copy_with_number(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy"
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.copy_preset() # creates "preset2 copy 2"
|
|
self.controller.copy_preset() # creates "preset2 copy 3" not "preset2 copy 2 copy"
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 copy 3"))
|
|
)
|
|
|
|
def test_rename_preset(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertFalse(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo")))
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.rename_preset(new_name="foo")
|
|
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo")))
|
|
|
|
def test_rename_preset_sanitized(self):
|
|
Preset(PathUtils.get_preset_path("Qux/[Device]?", "bla")).save()
|
|
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Qux/[Device]?", "bla"))
|
|
)
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Qux/[Device]?", "blubb"))
|
|
)
|
|
|
|
self.data_manager.load_group("Qux/[Device]?")
|
|
self.data_manager.load_preset("bla")
|
|
self.controller.rename_preset(new_name="foo:/bar")
|
|
|
|
# all functions expect the true name, which is also shown to the user, but on
|
|
# the file system it always uses sanitized names.
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Qux/[Device]?", "foo__bar"))
|
|
)
|
|
|
|
# since the name is never stored in an un-sanitized way, this can't work
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Qux/[Device]?", "foo:/bar"))
|
|
)
|
|
|
|
path = os.path.join(
|
|
PathUtils.config_path(), "presets", "Qux_[Device]_", "foo__bar.json"
|
|
)
|
|
self.assertTrue(os.path.exists(path))
|
|
|
|
# using the sanitized name in function calls works as well
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Qux_[Device]_", "foo__bar"))
|
|
)
|
|
|
|
def test_rename_preset_should_pick_available_name(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3"))
|
|
)
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3 2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.rename_preset(new_name="preset3")
|
|
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3"))
|
|
)
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset3 2"))
|
|
)
|
|
|
|
def test_rename_preset_should_not_rename_to_empty_name(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.rename_preset(new_name="")
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
def test_rename_preset_should_not_update_same_name(self):
|
|
"""When the new name is the same as the current name."""
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.rename_preset(new_name="preset2")
|
|
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "preset2 2"))
|
|
)
|
|
|
|
def test_on_add_preset_uses_default_name(self):
|
|
self.assertFalse(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", DEFAULT_PRESET_NAME))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
|
|
self.controller.add_preset()
|
|
self.assertTrue(
|
|
os.path.exists(PathUtils.get_preset_path("Foo Device", "new preset"))
|
|
)
|
|
|
|
def test_on_add_preset_uses_provided_name(self):
|
|
self.assertFalse(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo")))
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
|
|
self.controller.add_preset(name="foo")
|
|
self.assertTrue(os.path.exists(PathUtils.get_preset_path("Foo Device", "foo")))
|
|
|
|
def test_on_add_preset_shows_permission_error_status(self):
|
|
self.data_manager.load_group("Foo Device 2")
|
|
|
|
msg = None
|
|
|
|
def f(data):
|
|
nonlocal msg
|
|
msg = data
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
mock = MagicMock(side_effect=PermissionError)
|
|
with patch("inputremapper.configs.preset.Preset.save", mock):
|
|
self.controller.add_preset("foo")
|
|
|
|
mock.assert_called()
|
|
self.assertIsNotNone(msg)
|
|
self.assertIn("Permission denied", msg.msg)
|
|
|
|
def test_on_update_mapping(self):
|
|
"""Update_mapping should call data_manager.update_mapping.
|
|
|
|
This ensures mapping_changed is emitted.
|
|
"""
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
combination=InputCombination([InputConfig(type=1, code=4)])
|
|
)
|
|
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(
|
|
name="foo",
|
|
output_symbol="f",
|
|
release_timeout=0.3,
|
|
)
|
|
mock.assert_called_once()
|
|
|
|
def test_create_mapping_will_load_the_created_mapping(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
|
|
calls: List[MappingData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.controller.create_mapping()
|
|
|
|
self.assertEqual(calls[-1], UIMapping(**MAPPING_DEFAULTS))
|
|
|
|
def test_create_mapping_should_not_create_multiple_empty_mappings(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.controller.create_mapping() # create a first empty mapping
|
|
|
|
calls = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.mapping, f)
|
|
self.message_broker.subscribe(MessageType.preset, f)
|
|
|
|
self.controller.create_mapping() # try to create a second one
|
|
self.assertEqual(len(calls), 0)
|
|
|
|
def test_delete_mapping_asks_for_confirmation(self):
|
|
prepare_presets()
|
|
self.message_broker.signal(MessageType.init)
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, mock)
|
|
self.controller.delete_mapping()
|
|
mock.assert_called_once()
|
|
|
|
def test_deletes_mapping_when_confirmed(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda msg: msg.respond(True)
|
|
)
|
|
self.controller.delete_mapping()
|
|
self.controller.save()
|
|
|
|
preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
preset.load()
|
|
self.assertIsNone(
|
|
preset.get_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
)
|
|
|
|
def test_does_not_delete_mapping_when_not_confirmed(self):
|
|
prepare_presets()
|
|
self.assertTrue(
|
|
os.path.isfile(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
self.user_interface.confirm_delete.configure_mock(
|
|
return_value=Gtk.ResponseType.CANCEL
|
|
)
|
|
|
|
self.controller.delete_mapping()
|
|
self.controller.save()
|
|
|
|
preset = Preset(PathUtils.get_preset_path("Foo Device", "preset2"))
|
|
preset.load()
|
|
self.assertIsNotNone(
|
|
preset.get_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
)
|
|
|
|
def test_should_update_combination(self):
|
|
"""When combination is free."""
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
self.controller.update_combination(
|
|
InputCombination([InputConfig(type=1, code=10)])
|
|
)
|
|
self.assertEqual(
|
|
calls[0],
|
|
CombinationUpdate(
|
|
InputCombination([InputConfig(type=1, code=3)]),
|
|
InputCombination([InputConfig(type=1, code=10)]),
|
|
),
|
|
)
|
|
|
|
def test_should_not_update_combination(self):
|
|
"""When combination is already used."""
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
self.controller.update_combination(
|
|
InputCombination([InputConfig(type=1, code=4)])
|
|
)
|
|
self.assertEqual(len(calls), 0)
|
|
|
|
def test_sets_input_to_analog(self):
|
|
prepare_presets()
|
|
|
|
input_config = InputConfig(type=EV_ABS, code=ABS_RX)
|
|
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.active_preset.add(
|
|
Mapping(
|
|
input_combination=InputCombination([input_config]),
|
|
output_type=EV_ABS,
|
|
output_code=ABS_X,
|
|
target_uinput="gamepad",
|
|
)
|
|
)
|
|
self.data_manager.load_mapping(InputCombination([input_config]))
|
|
|
|
self.controller.start_key_recording()
|
|
self.message_broker.publish(
|
|
CombinationRecorded(
|
|
InputCombination(
|
|
[
|
|
InputConfig(
|
|
type=EV_ABS,
|
|
code=ABS_Y,
|
|
analog_threshold=50,
|
|
),
|
|
InputConfig(
|
|
type=EV_ABS,
|
|
code=ABS_RX,
|
|
analog_threshold=60,
|
|
),
|
|
]
|
|
)
|
|
)
|
|
)
|
|
|
|
# the analog_threshold is removed automatically, otherwise the mapping doesn't
|
|
# make sense because only analog inputs can map to analog outputs.
|
|
# This is indicated by is_analog_output being true.
|
|
self.assertTrue(self.controller.data_manager.active_mapping.is_analog_output())
|
|
|
|
# only the first input is modified
|
|
active_mapping = self.controller.data_manager.active_mapping
|
|
self.assertEqual(active_mapping.input_combination[0].analog_threshold, None)
|
|
self.assertEqual(active_mapping.input_combination[1].analog_threshold, 60)
|
|
|
|
def test_key_recording_disables_gui_shortcuts(self):
|
|
self.message_broker.signal(MessageType.init)
|
|
self.user_interface.disconnect_shortcuts.assert_not_called()
|
|
self.controller.start_key_recording()
|
|
self.user_interface.disconnect_shortcuts.assert_called_once()
|
|
|
|
def test_key_recording_enables_gui_shortcuts_when_finished(self):
|
|
self.message_broker.signal(MessageType.init)
|
|
self.controller.start_key_recording()
|
|
|
|
self.user_interface.connect_shortcuts.assert_not_called()
|
|
self.message_broker.signal(MessageType.recording_finished)
|
|
self.user_interface.connect_shortcuts.assert_called_once()
|
|
|
|
def test_key_recording_enables_gui_shortcuts_when_stopped(self):
|
|
self.message_broker.signal(MessageType.init)
|
|
self.controller.start_key_recording()
|
|
|
|
self.user_interface.connect_shortcuts.assert_not_called()
|
|
self.controller.stop_key_recording()
|
|
self.user_interface.connect_shortcuts.assert_called_once()
|
|
|
|
def test_recording_messages(self):
|
|
mock1 = MagicMock()
|
|
mock2 = MagicMock()
|
|
self.message_broker.subscribe(MessageType.recording_started, mock1)
|
|
self.message_broker.subscribe(MessageType.recording_finished, mock2)
|
|
|
|
self.message_broker.signal(MessageType.init)
|
|
self.controller.start_key_recording()
|
|
|
|
mock1.assert_called_once()
|
|
mock2.assert_not_called()
|
|
|
|
self.controller.stop_key_recording()
|
|
|
|
mock1.assert_called_once()
|
|
mock2.assert_called_once()
|
|
|
|
def test_key_recording_updates_mapping_combination(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
|
|
self.controller.start_key_recording()
|
|
self.message_broker.publish(
|
|
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
|
|
)
|
|
self.assertEqual(
|
|
calls[0],
|
|
CombinationUpdate(
|
|
InputCombination([InputConfig(type=1, code=3)]),
|
|
InputCombination([InputConfig(type=1, code=10)]),
|
|
),
|
|
)
|
|
self.message_broker.publish(
|
|
CombinationRecorded(
|
|
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
|
|
)
|
|
)
|
|
self.assertEqual(
|
|
calls[1],
|
|
CombinationUpdate(
|
|
InputCombination([InputConfig(type=1, code=10)]),
|
|
InputCombination(InputCombination.from_tuples((1, 10), (1, 3))),
|
|
),
|
|
)
|
|
|
|
def test_no_key_recording_when_not_started(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
|
|
self.message_broker.publish(
|
|
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
|
|
)
|
|
self.assertEqual(len(calls), 0)
|
|
|
|
def test_key_recording_stops_when_finished(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
|
|
self.controller.start_key_recording()
|
|
self.message_broker.publish(
|
|
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
|
|
)
|
|
self.message_broker.signal(MessageType.recording_finished)
|
|
self.message_broker.publish(
|
|
CombinationRecorded(
|
|
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
|
|
)
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1) # only the first was processed
|
|
|
|
def test_key_recording_stops_when_stopped(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(InputCombination([InputConfig(type=1, code=3)]))
|
|
|
|
calls: List[CombinationUpdate] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.combination_update, f)
|
|
|
|
self.controller.start_key_recording()
|
|
self.message_broker.publish(
|
|
CombinationRecorded(InputCombination([InputConfig(type=1, code=10)]))
|
|
)
|
|
self.controller.stop_key_recording()
|
|
self.message_broker.publish(
|
|
CombinationRecorded(
|
|
InputCombination(InputCombination.from_tuples((1, 10), (1, 3)))
|
|
)
|
|
)
|
|
|
|
self.assertEqual(len(calls), 1) # only the first was processed
|
|
|
|
def test_start_injecting_shows_status_when_preset_empty(self):
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.create_preset("foo")
|
|
self.data_manager.load_preset("foo")
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
|
|
def f2():
|
|
raise AssertionError("Injection started unexpectedly")
|
|
|
|
self.data_manager.start_injecting = f2
|
|
self.controller.start_injecting()
|
|
|
|
self.assertEqual(
|
|
calls[-1], StatusData(CTX_ERROR, _("You need to add mappings first"))
|
|
)
|
|
|
|
def test_start_injecting_warns_about_btn_left(self):
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.create_preset("foo")
|
|
self.data_manager.load_preset("foo")
|
|
self.data_manager.create_mapping()
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination([InputConfig.btn_left()]),
|
|
target_uinput="keyboard",
|
|
output_symbol="a",
|
|
)
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
|
|
def f2():
|
|
raise AssertionError("Injection started unexpectedly")
|
|
|
|
self.data_manager.start_injecting = f2
|
|
self.controller.start_injecting()
|
|
|
|
self.assertEqual(calls[-1].ctx_id, CTX_ERROR)
|
|
self.assertIn("BTN_LEFT", calls[-1].tooltip)
|
|
|
|
def test_start_injecting_starts_with_btn_left_on_second_try(self):
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.create_preset("foo")
|
|
self.data_manager.load_preset("foo")
|
|
self.data_manager.create_mapping()
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination([InputConfig.btn_left()]),
|
|
target_uinput="keyboard",
|
|
output_symbol="a",
|
|
)
|
|
|
|
with patch.object(self.data_manager, "start_injecting") as mock:
|
|
self.controller.start_injecting()
|
|
mock.assert_not_called()
|
|
self.controller.start_injecting()
|
|
mock.assert_called_once()
|
|
|
|
def test_start_injecting_starts_with_btn_left_when_mapped_to_other_button(self):
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.create_preset("foo")
|
|
self.data_manager.load_preset("foo")
|
|
self.data_manager.create_mapping()
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination([InputConfig.btn_left()]),
|
|
target_uinput="keyboard",
|
|
output_symbol="a",
|
|
)
|
|
self.data_manager.create_mapping()
|
|
self.data_manager.load_mapping(InputCombination.empty_combination())
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination([InputConfig(type=1, code=5)]),
|
|
target_uinput="mouse",
|
|
output_symbol="BTN_LEFT",
|
|
)
|
|
|
|
mock = MagicMock(return_value=True)
|
|
self.data_manager.start_injecting = mock
|
|
self.controller.start_injecting()
|
|
mock.assert_called()
|
|
|
|
def test_start_injecting_shows_status(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
mock = MagicMock(return_value=True)
|
|
self.data_manager.start_injecting = mock
|
|
self.controller.start_injecting()
|
|
|
|
mock.assert_called()
|
|
self.assertEqual(calls[0], StatusData(CTX_APPLY, _("Starting injection...")))
|
|
|
|
def test_start_injecting_shows_failure_status(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
mock = MagicMock(return_value=False)
|
|
self.data_manager.start_injecting = mock
|
|
self.controller.start_injecting()
|
|
|
|
mock.assert_called()
|
|
self.assertEqual(
|
|
calls[-1],
|
|
StatusData(
|
|
CTX_APPLY,
|
|
_('Failed to apply preset "%s"') % self.data_manager.active_preset.name,
|
|
),
|
|
)
|
|
|
|
def test_start_injecting_adds_listener_to_update_injector_status(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
|
|
with patch.object(self.message_broker, "subscribe") as mock:
|
|
self.controller.start_injecting()
|
|
mock.assert_called_once_with(
|
|
MessageType.injector_state, self.controller.show_injector_result
|
|
)
|
|
|
|
def test_stop_injecting_shows_status(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
mock = MagicMock(return_value=InjectorState.STOPPED)
|
|
self.data_manager.get_state = mock
|
|
self.controller.stop_injecting()
|
|
gtk_iteration(50)
|
|
|
|
mock.assert_called()
|
|
self.assertEqual(calls[-1], StatusData(CTX_APPLY, _("Stopped the injection")))
|
|
|
|
def test_show_injection_result(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
|
|
mock = MagicMock(return_value=InjectorState.RUNNING)
|
|
self.data_manager.get_state = mock
|
|
calls: List[StatusData] = []
|
|
|
|
def f(data):
|
|
calls.append(data)
|
|
|
|
self.message_broker.subscribe(MessageType.status_msg, f)
|
|
|
|
self.controller.start_injecting()
|
|
gtk_iteration(50)
|
|
self.assertEqual(calls[-1].msg, _('Applied preset "%s"') % "preset2")
|
|
|
|
mock.return_value = InjectorState.ERROR
|
|
self.controller.start_injecting()
|
|
gtk_iteration(50)
|
|
self.assertEqual(calls[-1].msg, _('Error applying preset "%s"') % "preset2")
|
|
|
|
mock.return_value = InjectorState.NO_GRAB
|
|
self.controller.start_injecting()
|
|
gtk_iteration(50)
|
|
self.assertEqual(calls[-1].msg, _('Failed to apply preset "%s"') % "preset2")
|
|
|
|
mock.return_value = InjectorState.UPGRADE_EVDEV
|
|
self.controller.start_injecting()
|
|
gtk_iteration(50)
|
|
self.assertEqual(calls[-1].msg, "Upgrade python-evdev")
|
|
|
|
def test_close(self):
|
|
mock_save = MagicMock()
|
|
listener = MagicMock()
|
|
self.message_broker.subscribe(MessageType.terminate, listener)
|
|
self.data_manager.save = mock_save
|
|
|
|
self.controller.close()
|
|
mock_save.assert_called()
|
|
listener.assert_called()
|
|
|
|
def test_set_autoload_refreshes_service_config(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
|
|
with patch.object(self.data_manager, "refresh_service_config_path") as mock:
|
|
self.controller.set_autoload(True)
|
|
mock.assert_called()
|
|
|
|
def test_move_event_up(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination(
|
|
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
|
|
)
|
|
)
|
|
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=2), "up"
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))),
|
|
)
|
|
# now nothing changes
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=2), "up"
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 2), (1, 1), (1, 3))),
|
|
)
|
|
|
|
def test_move_event_down(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination(
|
|
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
|
|
)
|
|
)
|
|
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=2), "down"
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))),
|
|
)
|
|
# now nothing changes
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=2), "down"
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 1), (1, 3), (1, 2))),
|
|
)
|
|
|
|
def test_move_event_in_combination_of_len_1(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=3), "down"
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 3))),
|
|
)
|
|
|
|
def test_move_event_loads_it_again(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination(
|
|
InputCombination.from_tuples((1, 1), (1, 2), (1, 3))
|
|
)
|
|
)
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.controller.move_input_config_in_combination(
|
|
InputConfig(type=1, code=2), "down"
|
|
)
|
|
mock.assert_called_once_with(InputConfig(type=1, code=2))
|
|
|
|
def test_update_event(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=3))
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.controller.update_input_config(InputConfig(type=1, code=10))
|
|
mock.assert_called_once_with(InputConfig(type=1, code=10))
|
|
|
|
def test_update_event_reloads_mapping_and_event_when_update_fails(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=3))
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.message_broker.subscribe(MessageType.mapping, mock)
|
|
calls = [
|
|
call(self.data_manager.active_mapping.get_bus_message()),
|
|
call(InputConfig(type=1, code=3)),
|
|
]
|
|
self.controller.update_input_config(
|
|
InputConfig(type=1, code=4)
|
|
) # already exists
|
|
mock.assert_has_calls(calls, any_order=False)
|
|
|
|
def test_remove_event_does_nothing_when_mapping_not_loaded(self):
|
|
with spy(self.data_manager, "update_mapping") as mock:
|
|
self.controller.remove_event()
|
|
mock.assert_not_called()
|
|
|
|
def test_remove_event_removes_active_event(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 3), (1, 4))),
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=4))
|
|
|
|
self.controller.remove_event()
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 3))),
|
|
)
|
|
|
|
def test_remove_event_loads_a_event(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
|
|
)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((1, 3), (1, 4))),
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=4))
|
|
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.controller.remove_event()
|
|
mock.assert_called_once_with(InputConfig(type=1, code=3))
|
|
|
|
def test_remove_event_reloads_mapping_and_event_when_update_fails(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (1, 4))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=3))
|
|
|
|
# removing "1,3,1" will throw a key error because a mapping with combination
|
|
# "1,4,1" already exists in preset
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.message_broker.subscribe(MessageType.mapping, mock)
|
|
calls = [
|
|
call(self.data_manager.active_mapping.get_bus_message()),
|
|
call(InputConfig(type=1, code=3)),
|
|
]
|
|
self.controller.remove_event()
|
|
mock.assert_has_calls(calls, any_order=False)
|
|
|
|
def test_set_event_as_analog_saves(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((3, 0, 10))
|
|
)
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((3, 0, 10)))
|
|
)
|
|
self.data_manager.load_input_config(
|
|
InputConfig(type=3, code=0, analog_threshold=10)
|
|
)
|
|
|
|
with patch.object(self.data_manager, "save") as mock:
|
|
self.controller.set_event_as_analog(False)
|
|
mock.assert_called_once()
|
|
|
|
with patch.object(self.data_manager, "save") as mock:
|
|
self.controller.set_event_as_analog(True)
|
|
mock.assert_called_once()
|
|
|
|
def test_set_event_as_analog_sets_input_to_analog(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((3, 0, 10))
|
|
)
|
|
self.data_manager.load_input_config(
|
|
InputConfig(type=3, code=0, analog_threshold=10)
|
|
)
|
|
|
|
self.controller.set_event_as_analog(True)
|
|
self.assertEqual(
|
|
self.data_manager.active_mapping.input_combination,
|
|
InputCombination(InputCombination.from_tuples((3, 0))),
|
|
)
|
|
|
|
def test_set_event_as_analog_adds_rel_threshold(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((2, 0))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=2, code=0))
|
|
|
|
self.controller.set_event_as_analog(False)
|
|
combinations = [
|
|
InputCombination(InputCombination.from_tuples((2, 0, 1))),
|
|
InputCombination(InputCombination.from_tuples((2, 0, -1))),
|
|
]
|
|
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
|
|
|
|
def test_set_event_as_analog_adds_abs_threshold(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((3, 0))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=3, code=0))
|
|
|
|
self.controller.set_event_as_analog(False)
|
|
combinations = [
|
|
InputCombination(InputCombination.from_tuples((3, 0, 10))),
|
|
InputCombination(InputCombination.from_tuples((3, 0, -10))),
|
|
]
|
|
self.assertIn(self.data_manager.active_mapping.input_combination, combinations)
|
|
|
|
def test_set_event_as_analog_reloads_mapping_and_event_when_key_event(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=1, code=3))
|
|
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.message_broker.subscribe(MessageType.mapping, mock)
|
|
calls = [
|
|
call(self.data_manager.active_mapping.get_bus_message()),
|
|
call(InputConfig(type=1, code=3)),
|
|
]
|
|
self.controller.set_event_as_analog(True)
|
|
mock.assert_has_calls(calls, any_order=False)
|
|
|
|
def test_set_event_as_analog_reloads_when_setting_to_analog_fails(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((3, 0, 10))
|
|
)
|
|
self.data_manager.load_input_config(
|
|
InputConfig(type=3, code=0, analog_threshold=10)
|
|
)
|
|
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.message_broker.subscribe(MessageType.mapping, mock)
|
|
calls = [
|
|
call(self.data_manager.active_mapping.get_bus_message()),
|
|
call(InputConfig(type=3, code=0, analog_threshold=10)),
|
|
]
|
|
with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
|
|
self.controller.set_event_as_analog(True)
|
|
mock.assert_has_calls(calls, any_order=False)
|
|
|
|
def test_set_event_as_analog_reloads_when_setting_to_key_fails(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((3, 0))
|
|
)
|
|
self.data_manager.load_input_config(InputConfig(type=3, code=0))
|
|
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.selected_event, mock)
|
|
self.message_broker.subscribe(MessageType.mapping, mock)
|
|
calls = [
|
|
call(self.data_manager.active_mapping.get_bus_message()),
|
|
call(InputConfig(type=3, code=0)),
|
|
]
|
|
with patch.object(self.data_manager, "update_mapping", side_effect=KeyError):
|
|
self.controller.set_event_as_analog(False)
|
|
mock.assert_has_calls(calls, any_order=False)
|
|
|
|
def test_update_mapping_type_will_ask_user_when_output_symbol_is_set(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
request: UserConfirmRequest = None
|
|
|
|
def f(r: UserConfirmRequest):
|
|
nonlocal request
|
|
request = r
|
|
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, f)
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
self.assertIn('This will remove "a" from the text input', request.msg)
|
|
|
|
def test_update_mapping_type_will_notify_user_to_recorde_analog_input(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(output_symbol=None)
|
|
request: UserConfirmRequest = None
|
|
|
|
def f(r: UserConfirmRequest):
|
|
nonlocal request
|
|
request = r
|
|
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, f)
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
self.assertIn("You need to record an analog input.", request.msg)
|
|
|
|
def test_update_mapping_type_will_tell_user_which_input_is_used_as_analog(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
|
|
output_symbol=None,
|
|
)
|
|
request: UserConfirmRequest = None
|
|
|
|
def f(r: UserConfirmRequest):
|
|
nonlocal request
|
|
request = r
|
|
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, f)
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
self.assertIn('The input "Y Down 1" will be used as analog input.', request.msg)
|
|
|
|
def test_update_mapping_type_will_will_autoconfigure_the_input(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
|
|
output_symbol=None,
|
|
)
|
|
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda r: r.respond(True)
|
|
)
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
mock.assert_called_once_with(
|
|
mapping_type="analog",
|
|
output_symbol=None,
|
|
input_combination=InputCombination(
|
|
InputCombination.from_tuples((1, 3), (2, 1))
|
|
),
|
|
)
|
|
|
|
def test_update_mapping_type_will_abort_when_user_denys(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda r: r.respond(False)
|
|
)
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
mock.assert_not_called()
|
|
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
|
|
output_symbol=None,
|
|
mapping_type="analog",
|
|
)
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(mapping_type="key_macro")
|
|
mock.assert_not_called()
|
|
|
|
def test_update_mapping_type_will_delete_output_symbol_when_user_confirms(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda r: r.respond(True)
|
|
)
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
mock.assert_called_once_with(mapping_type="analog", output_symbol=None)
|
|
|
|
def test_update_mapping_will_ask_user_to_set_trigger_threshold(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
|
|
output_symbol=None,
|
|
mapping_type="analog",
|
|
)
|
|
request: UserConfirmRequest = None
|
|
|
|
def f(r: UserConfirmRequest):
|
|
nonlocal request
|
|
request = r
|
|
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, f)
|
|
self.controller.update_mapping(mapping_type="key_macro")
|
|
self.assertIn('and set a "Trigger Threshold" for "Y".', request.msg)
|
|
|
|
def test_update_mapping_update_to_analog_without_asking(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
|
|
output_symbol=None,
|
|
)
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, mock)
|
|
self.controller.update_mapping(mapping_type="analog")
|
|
mock.assert_not_called()
|
|
|
|
def test_update_mapping_update_to_key_macro_without_asking(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1, 1)),
|
|
mapping_type="analog",
|
|
output_symbol=None,
|
|
)
|
|
mock = MagicMock()
|
|
self.message_broker.subscribe(MessageType.user_confirm_request, mock)
|
|
self.controller.update_mapping(mapping_type="key_macro")
|
|
mock.assert_not_called()
|
|
|
|
def test_update_mapping_will_remove_output_type_and_code(self):
|
|
prepare_presets()
|
|
self.data_manager.load_group("Foo Device 2")
|
|
self.data_manager.load_preset("preset2")
|
|
self.data_manager.load_mapping(
|
|
InputCombination(InputCombination.from_tuples((1, 3)))
|
|
)
|
|
self.data_manager.update_mapping(
|
|
input_combination=InputCombination.from_tuples((1, 3), (2, 1)),
|
|
output_symbol=None,
|
|
mapping_type="analog",
|
|
)
|
|
self.message_broker.subscribe(
|
|
MessageType.user_confirm_request, lambda r: r.respond(True)
|
|
)
|
|
with patch.object(self.data_manager, "update_mapping") as mock:
|
|
self.controller.update_mapping(mapping_type="key_macro")
|
|
mock.assert_called_once_with(
|
|
mapping_type="key_macro",
|
|
output_type=None,
|
|
output_code=None,
|
|
)
|
|
|
|
|
|
@test_setup
|
|
class TestControllerAppBindings(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.message_broker = MessageBroker()
|
|
self.data_manager = MagicMock()
|
|
self.controller = Controller(self.message_broker, self.data_manager)
|
|
self.user_interface = MagicMock()
|
|
self.controller.set_gui(self.user_interface)
|
|
|
|
def test_load_app_bindings(self):
|
|
self.controller.load_app_bindings()
|
|
self.data_manager.publish_app_bindings.assert_called_once()
|
|
|
|
def test_set_app_binding_enabled_true_starts_service(self):
|
|
with patch(
|
|
"inputremapper.gui.controller.ensure_focus_service_running"
|
|
) as ensure, patch(
|
|
"inputremapper.gui.controller.GLib.timeout_add"
|
|
) as timeout_add:
|
|
self.controller.set_app_binding_enabled(True)
|
|
|
|
ensure.assert_called_once()
|
|
self.data_manager.set_app_binding_enabled.assert_called_once_with(True)
|
|
# a delayed status refresh is scheduled
|
|
timeout_add.assert_called_once()
|
|
|
|
def test_set_app_binding_enabled_false_does_not_start_service(self):
|
|
with patch(
|
|
"inputremapper.gui.controller.ensure_focus_service_running"
|
|
) as ensure, patch(
|
|
"inputremapper.gui.controller.GLib.timeout_add"
|
|
) as timeout_add:
|
|
self.controller.set_app_binding_enabled(False)
|
|
|
|
ensure.assert_not_called()
|
|
self.data_manager.set_app_binding_enabled.assert_called_once_with(False)
|
|
timeout_add.assert_not_called()
|
|
|
|
def test_refresh_app_bindings_once(self):
|
|
result = self.controller._refresh_app_bindings_once()
|
|
self.assertFalse(result) # GLib: do not repeat
|
|
self.data_manager.publish_app_bindings.assert_called_once()
|
|
|
|
def test_update_app_bindings(self):
|
|
from inputremapper.configs.app_binding import AppBinding
|
|
|
|
bindings = [AppBinding(app_id="firefox")]
|
|
self.controller.update_app_bindings(bindings)
|
|
self.data_manager.set_app_bindings.assert_called_once_with(bindings)
|
|
|
|
def test_update_app_bindings_permission_error(self):
|
|
self.data_manager.set_app_bindings.side_effect = PermissionError("nope")
|
|
with patch.object(self.controller, "show_status") as show_status:
|
|
self.controller.update_app_bindings([])
|
|
show_status.assert_called_once()
|
|
self.assertEqual(show_status.call_args[0][0], CTX_ERROR)
|
|
|
|
def test_get_presets_for_group(self):
|
|
self.data_manager.get_presets_for_group.return_value = ("a", "b")
|
|
result = self.controller.get_presets_for_group("Foo Device")
|
|
self.assertEqual(result, ("a", "b"))
|
|
self.data_manager.get_presets_for_group.assert_called_once_with("Foo Device")
|
|
|
|
def test_start_and_stop_app_detection(self):
|
|
self.controller.start_app_detection()
|
|
self.data_manager.start_app_detection.assert_called_once()
|
|
self.controller.stop_app_detection()
|
|
self.data_manager.stop_app_detection.assert_called_once()
|