diff --git a/data/input-remapper.glade b/data/input-remapper.glade index 6fe351611..b620dda53 100644 --- a/data/input-remapper.glade +++ b/data/input-remapper.glade @@ -1798,7 +1798,7 @@ Macros allow multiple characters to be written with a single key-press. Informat True False - Map this input to an Analog Axis + Map this input to an Analog Axis. Only possible for analog inputs and mice. 0.5 18 True diff --git a/inputremapper/configs/mapping.py b/inputremapper/configs/mapping.py index ae009e761..eb2514355 100644 --- a/inputremapper/configs/mapping.py +++ b/inputremapper/configs/mapping.py @@ -34,6 +34,8 @@ ) from packaging import version +from inputremapper.logging.logger import logger + try: from pydantic.v1 import ( BaseModel, @@ -71,8 +73,9 @@ OutputSymbolVariantError, MacroButTypeOrCodeSetError, SymbolAndCodeMismatchError, - MissingMacroOrKeyError, + WrongMappingTypeForKeyError, MissingOutputAxisError, + MissingMacroOrKeyError, ) from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_types import MessageType @@ -152,7 +155,9 @@ class UIMapping(BaseModel): target_uinput: Optional[Union[str, KnownUinput]] = None # Either `output_symbol` or `output_type` and `output_code` is required + # Only set if output is "Key or Macro": output_symbol: Optional[str] = None # The symbol or macro string if applicable + # "Analog Axis" or if preset edited manually to inject a code instead of a symbol: output_type: Optional[int] = None # The event type of the mapped event output_code: Optional[int] = None # The event code of the mapped event @@ -322,11 +327,20 @@ def validate_mapping_type(cls, values): output_code = values.get("output_code") output_symbol = values.get("output_symbol") - if output_type is not None and output_code is not None and not output_symbol: - values["mapping_type"] = "analog" + if output_type is not None and output_symbol is not None: + # This is currently only possible when someone edits the preset file by + # hand. A key-output mapping without an output_symbol, but type and code + # instead, is valid as well. + logger.debug("Both output_type and output_symbol are set") + + if output_type != EV_KEY and output_code is not None and not output_symbol: + values["mapping_type"] = MappingType.ANALOG.value if output_type is None and output_code is None and output_symbol: - values["mapping_type"] = "key_macro" + values["mapping_type"] = MappingType.KEY_MACRO.value + + if output_type == EV_KEY: + values["mapping_type"] = MappingType.KEY_MACRO.value return values @@ -463,17 +477,27 @@ def output_matches_input(cls, values: Dict[str, Any]) -> Dict[str, Any]: And vice versa.""" assert isinstance(values.get("input_combination"), InputCombination) combination: InputCombination = values["input_combination"] - analog_input_config = combination.find_analog_input_config() - use_as_analog = analog_input_config is not None + analog_input_config = combination.find_analog_input_config() + defines_analog_input = analog_input_config is not None output_type = values.get("output_type") + output_code = values.get("output_code") + mapping_type = values.get("mapping_type") output_symbol = values.get("output_symbol") + output_key_set = output_symbol or (output_type == EV_KEY and output_code) + + if mapping_type is None: + # Empty mapping most likely + return values + + if not defines_analog_input and mapping_type != MappingType.KEY_MACRO.value: + raise WrongMappingTypeForKeyError() - if not use_as_analog and not output_symbol and output_type != EV_KEY: + if not defines_analog_input and not output_key_set: raise MissingMacroOrKeyError() if ( - use_as_analog + defines_analog_input and output_type not in (EV_ABS, EV_REL) and output_symbol != DISABLE_NAME ): diff --git a/inputremapper/configs/validation_errors.py b/inputremapper/configs/validation_errors.py index 349eec6e5..7a6706ec4 100644 --- a/inputremapper/configs/validation_errors.py +++ b/inputremapper/configs/validation_errors.py @@ -97,9 +97,14 @@ def __init__(self, symbol, code): ) +class WrongMappingTypeForKeyError(ValueError): + def __init__(self): + super().__init__(f"Wrong mapping_type for key input") + + class MissingMacroOrKeyError(ValueError): def __init__(self): - super().__init__("missing macro or key") + super().__init__("Missing macro or key") class MissingOutputAxisError(ValueError): diff --git a/inputremapper/gui/components/editor.py b/inputremapper/gui/components/editor.py index 80ec4818d..c342fedbf 100644 --- a/inputremapper/gui/components/editor.py +++ b/inputremapper/gui/components/editor.py @@ -37,12 +37,13 @@ BTN_EXTRA, BTN_SIDE, ) - from gi.repository import Gtk, GtkSource, Gdk -from inputremapper.configs.mapping import MappingData from inputremapper.configs.input_config import InputCombination, InputConfig +from inputremapper.configs.keyboard_layout import keyboard_layout, XKB_KEYCODE_OFFSET +from inputremapper.configs.mapping import MappingData, MappingType from inputremapper.groups import DeviceType +from inputremapper.gui.components.output_type_names import OutputTypeNames from inputremapper.gui.controller import Controller from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_broker import ( @@ -57,7 +58,6 @@ from inputremapper.gui.utils import HandlerDisabled, Colors from inputremapper.injection.mapping_handlers.axis_transform import Transformation from inputremapper.input_event import InputEvent -from inputremapper.configs.keyboard_layout import keyboard_layout, XKB_KEYCODE_OFFSET from inputremapper.utils import get_evdev_constant_name Capabilities = Dict[int, List] @@ -1037,12 +1037,12 @@ def __init__( self._message_broker.subscribe(MessageType.mapping, self._on_mapping_message) def _set_active(self, mapping_type: Literal["key_macro", "analog"]): - if mapping_type == "analog": - self._stack.set_visible_child_name("Analog Axis") + if mapping_type == MappingType.ANALOG.value: + self._stack.set_visible_child_name(OutputTypeNames.analog_axis) active = self._analog_toggle inactive = self._key_macro_toggle else: - self._stack.set_visible_child_name("Key or Macro") + self._stack.set_visible_child_name(OutputTypeNames.key_or_macro) active = self._key_macro_toggle inactive = self._analog_toggle @@ -1053,11 +1053,11 @@ def _set_active(self, mapping_type: Literal["key_macro", "analog"]): def _on_mapping_message(self, mapping: MappingData): # fist check the actual mapping - if mapping.mapping_type == "analog": - self._set_active("analog") + if mapping.mapping_type == MappingType.ANALOG.value: + self._set_active(MappingType.ANALOG.value) - if mapping.mapping_type == "key_macro": - self._set_active("key_macro") + if mapping.mapping_type == MappingType.KEY_MACRO.value: + self._set_active(MappingType.KEY_MACRO.value) def _on_gtk_toggle(self, btn: Gtk.ToggleButton): # get_active returns the new toggle state already @@ -1070,9 +1070,9 @@ def _on_gtk_toggle(self, btn: Gtk.ToggleButton): return if btn is self._key_macro_toggle: - self._controller.update_mapping(mapping_type="key_macro") + self._controller.update_mapping(mapping_type=MappingType.KEY_MACRO.value) else: - self._controller.update_mapping(mapping_type="analog") + self._controller.update_mapping(mapping_type=MappingType.ANALOG.value) class TransformationDrawArea: diff --git a/inputremapper/gui/components/output_type_names.py b/inputremapper/gui/components/output_type_names.py new file mode 100644 index 000000000..1407692c3 --- /dev/null +++ b/inputremapper/gui/components/output_type_names.py @@ -0,0 +1,3 @@ +class OutputTypeNames: + analog_axis = "Analog Axis" + key_or_macro = "Key or Macro" diff --git a/inputremapper/gui/controller.py b/inputremapper/gui/controller.py index 5b8e9fb87..25bb9030c 100644 --- a/inputremapper/gui/controller.py +++ b/inputremapper/gui/controller.py @@ -31,6 +31,7 @@ Callable, List, Any, + Tuple, ) from evdev.ecodes import EV_KEY, EV_REL, EV_ABS @@ -40,15 +41,20 @@ from inputremapper.configs.mapping import ( MappingData, UIMapping, + MappingType, +) +from inputremapper.configs.paths import PathUtils +from inputremapper.configs.validation_errors import ( + pydantify, + MissingMacroOrKeyError, MacroButTypeOrCodeSetError, SymbolAndCodeMismatchError, MissingOutputAxisError, - MissingMacroOrKeyError, + WrongMappingTypeForKeyError, OutputSymbolVariantError, ) -from inputremapper.configs.paths import PathUtils -from inputremapper.configs.validation_errors import pydantify from inputremapper.exceptions import DataManagementError +from inputremapper.gui.components.output_type_names import OutputTypeNames from inputremapper.gui.data_manager import DataManager, DEFAULT_PRESET_NAME from inputremapper.gui.gettext import _ from inputremapper.gui.messages.message_broker import ( @@ -80,7 +86,11 @@ class Controller: """Implements the behaviour of the gui.""" - def __init__(self, message_broker: MessageBroker, data_manager: DataManager): + def __init__( + self, + message_broker: MessageBroker, + data_manager: DataManager, + ) -> None: self.message_broker = message_broker self.data_manager = data_manager self.gui: Optional[UserInterface] = None @@ -148,27 +158,35 @@ def _on_combination_recorded(self, data: CombinationRecorded): combination = self._auto_use_as_analog(data.combination) self.update_combination(combination) - def _publish_mapping_errors_as_status_msg(self, *__): - """Send mapping ValidationErrors to the MessageBroker.""" + def _format_status_bar_validation_errors(self) -> Optional[Tuple[str, str]]: if not self.data_manager.active_preset: - return + return None if self.data_manager.active_preset.is_valid(): self.message_broker.publish(StatusData(CTX_MAPPING)) - return + return None + + mappings = list(self.data_manager.active_preset) - for mapping in self.data_manager.active_preset: - if not mapping.get_error(): + # Move the selected (active) mapping to the front, so that it is checked first. + active_mapping = self.data_manager.active_mapping + if active_mapping is not None: + mappings.remove(active_mapping) + mappings.insert(0, active_mapping) + + for mapping in mappings: + if not mapping.has_input_defined(): + # Empty mapping, nothing recorded yet so nothing can be configured, + # therefore there isn't anything to validate. continue position = mapping.format_name() error_strings = self._get_ui_error_strings(mapping) - tooltip = "" + if len(error_strings) == 0: - # shouldn't be possible to get to this point - logger.error("Expected an error") - return - elif len(error_strings) > 1: + continue + + if len(error_strings) > 1: msg = _('%d Mapping errors at "%s", hover for info') % ( len(error_strings), position, @@ -178,11 +196,22 @@ def _publish_mapping_errors_as_status_msg(self, *__): msg = f'"{position}": {error_strings[0]}' tooltip = error_strings[0] - self.show_status( - CTX_MAPPING, - msg.replace("\n", " "), - tooltip, - ) + return msg.replace("\n", " "), tooltip + + return None + + def _publish_mapping_errors_as_status_msg(self, *__) -> None: + """Send mapping ValidationErrors to the MessageBroker.""" + validation_result = self._format_status_bar_validation_errors() + + if validation_result is None: + return + + self.show_status( + CTX_MAPPING, + validation_result[0], + validation_result[1], + ) @staticmethod def format_error_message(mapping, error_type, error_message: str) -> str: @@ -224,21 +253,23 @@ def format_error_message(mapping, error_type, error_message: str) -> str: ) return error_message - if ( - pydantify(MissingMacroOrKeyError) in error_type - and mapping.output_symbol is None - ): + if pydantify(WrongMappingTypeForKeyError) in error_type: error_message = _( - "The input specifies a key or macro input, but no macro or key is " - "programmed." + "The input specifies a key, but the output type is not " + f'"{OutputTypeNames.key_or_macro}".' ) + if mapping.output_type in (EV_ABS, EV_REL): error_message += _( "\nIf you mean to create an analog axis mapping go to the " 'advanced input configuration and set an input to "Use as Analog".' ) + return error_message + if pydantify(MissingMacroOrKeyError) in error_type: + return _("Missing macro or key") + return error_message @staticmethod @@ -643,16 +674,17 @@ def start_injecting(self): self.message_broker.unsubscribe(self.show_injector_result) self.show_status( CTX_APPLY, - _("Failed to apply preset %s") % self.data_manager.active_preset.name, + _('Failed to apply preset "%s"') % self.data_manager.active_preset.name, ) - def show_injector_result(self, msg: InjectorStateMessage): + def show_injector_result(self, msg: InjectorStateMessage) -> None: """Show if the injection was successfully started.""" self.message_broker.unsubscribe(self.show_injector_result) state = msg.state - def running(): - msg = _("Applied preset %s") % self.data_manager.active_preset.name + def running() -> None: + assert self.data_manager.active_preset is not None + msg = _('Applied preset "%s"') % self.data_manager.active_preset.name if self.data_manager.active_preset.dangerously_mapped_btn_left(): msg += _(", CTRL + DEL to stop") self.show_status(CTX_APPLY, msg) @@ -660,22 +692,34 @@ def running(): 'Group "%s" is currently mapped', self.data_manager.active_group.key ) + def no_grab() -> None: + assert self.data_manager.active_preset is not None + msg = ( + _('Failed to apply preset "%s"') % self.data_manager.active_preset.name + ) + tooltip = ( + "Maybe your preset doesn't contain anything that is sent by the " + "device or another device is already grabbing it" + ) + + # InjectorState.NO_GRAB also happens when all mappings have validation + # errors. In that case, we can show something more useful. + validation_result = self._format_status_bar_validation_errors() + if validation_result is not None: + msg = f"{msg}. {validation_result[0]}" + tooltip = validation_result[1] + + self.show_status(CTX_ERROR, msg, tooltip) + assert self.data_manager.active_preset # make mypy happy state_calls: Dict[InjectorState, Callable] = { InjectorState.RUNNING: running, - InjectorState.FAILED: partial( + InjectorState.ERROR: partial( self.show_status, CTX_ERROR, - _("Failed to apply preset %s") % self.data_manager.active_preset.name, - ), - InjectorState.NO_GRAB: partial( - self.show_status, - CTX_ERROR, - "The device was not grabbed", - "Either another application is already grabbing it, " - "your preset doesn't contain anything that is sent by the " - "device or your preset contains errors", + _('Error applying preset "%s"') % self.data_manager.active_preset.name, ), + InjectorState.NO_GRAB: no_grab, InjectorState.UPGRADE_EVDEV: partial( self.show_status, CTX_ERROR, @@ -712,7 +756,10 @@ def show_result(msg: InjectorStateMessage): self.message_broker.unsubscribe(show_result) def show_status( - self, ctx_id: int, msg: Optional[str] = None, tooltip: Optional[str] = None + self, + ctx_id: int, + msg: Optional[str] = None, + tooltip: Optional[str] = None, ): """Send a status message to the ui to show it in the status-bar.""" self.message_broker.publish(StatusData(ctx_id, msg, tooltip)) @@ -753,7 +800,7 @@ def _change_mapping_type(self, changes: Dict[str, Any]): if changes["mapping_type"] == mapping.mapping_type: return changes - if changes["mapping_type"] == "analog": + if changes["mapping_type"] == MappingType.ANALOG.value: msg = _("You are about to change the mapping to analog.") if mapping.output_symbol: msg += _('\nThis will remove "{}" ' "from the text input!").format( @@ -795,7 +842,7 @@ def get_answer(answer_: bool): else: return None - if changes["mapping_type"] == "key_macro": + if changes["mapping_type"] == MappingType.KEY_MACRO.value: try: analog_input = tuple( filter(lambda i: i.defines_analog_input, mapping.input_combination) diff --git a/inputremapper/gui/data_manager.py b/inputremapper/gui/data_manager.py index 25a38c1ba..97739c9ed 100644 --- a/inputremapper/gui/data_manager.py +++ b/inputremapper/gui/data_manager.py @@ -570,7 +570,7 @@ def start_injecting(self) -> bool: self.do_when_injector_state( { InjectorState.RUNNING, - InjectorState.FAILED, + InjectorState.ERROR, InjectorState.NO_GRAB, InjectorState.UPGRADE_EVDEV, }, diff --git a/inputremapper/injection/injector.py b/inputremapper/injection/injector.py index 3744e2690..3fd870010 100644 --- a/inputremapper/injection/injector.py +++ b/inputremapper/injection/injector.py @@ -62,7 +62,7 @@ class InjectorCommand(str, enum.Enum): class InjectorState(str, enum.Enum): UNKNOWN = "UNKNOWN" STARTING = "STARTING" - FAILED = "FAILED" + ERROR = "FAILED" RUNNING = "RUNNING" STOPPED = "STOPPED" NO_GRAB = "NO_GRAB" @@ -174,7 +174,7 @@ def get_state(self) -> InjectorState: if state in (InjectorState.STARTING, InjectorState.RUNNING) and not alive: # we thought it is running (maybe it was when get_state was previously), # but the process is not alive. It probably crashed - state = InjectorState.FAILED + state = InjectorState.ERROR logger.error("Injector was unexpectedly found stopped") logger.debug( @@ -409,8 +409,8 @@ def run(self) -> None: self._devices = self.group.get_devices() - # InputConfigs may not contain the origin_hash information, this will try to make a - # good guess if the origin_hash information is missing or invalid. + # InputConfigs may not contain the origin_hash information, this will try to + # make a good guess if the origin_hash information is missing or invalid. self._update_preset() # grab devices as early as possible. If events appear that won't get diff --git a/tests/integration/test_components.py b/tests/integration/test_components.py index 5dd880006..50cb2b46b 100644 --- a/tests/integration/test_components.py +++ b/tests/integration/test_components.py @@ -905,11 +905,12 @@ def test_placeholder(self): def focus(): self.gui.grab_focus() - gtk_iteration(5) + # Do as many iterations as needed to make it work. + gtk_iteration(15) def unfocus(): window.set_focus(None) - gtk_iteration(5) + gtk_iteration(15) # clears the input when we enter the editor widget focus() diff --git a/tests/integration/test_gui.py b/tests/integration/test_gui.py index 0885038ac..b19a8966a 100644 --- a/tests/integration/test_gui.py +++ b/tests/integration/test_gui.py @@ -1489,40 +1489,56 @@ def save(): self.assertFalse(os.path.exists(preset_path)) def test_check_for_unknown_symbols(self): + first_input = InputCombination([InputConfig(type=1, code=1)]) + second_input = InputCombination([InputConfig(type=1, code=2)]) 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(20) - self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)])) + + # Switch to the first mapping, and change it + self.controller.load_mapping(first_input) gtk_iteration() self.controller.update_mapping(output_symbol="foo") gtk_iteration() - self.controller.load_mapping(InputCombination([InputConfig(type=1, code=2)])) + + # Switch to the second mapping, and change it + self.controller.load_mapping(second_input) gtk_iteration() self.controller.update_mapping(output_symbol="qux") gtk_iteration() + # The tooltip should show the error of the currently selected mapping 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 + # So switching to the other mapping changes the tooltip + self.controller.load_mapping(first_input) + gtk_iteration() + tooltip = status.get_tooltip_text().lower() + self.assertIn("foo", tooltip) + + # It will still save it though with open(PathUtils.get_preset_path("Foo Device", "preset1")) as f: content = f.read() self.assertIn("qux", content) self.assertIn("foo", content) + # Fix the current active mapping. + # It should show the error of the other mapping now. self.controller.update_mapping(output_symbol="a") gtk_iteration() tooltip = status.get_tooltip_text().lower() - self.assertIn("foo", tooltip) + self.assertIn("qux", tooltip) self.assertTrue(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible()) - self.controller.load_mapping(InputCombination([InputConfig(type=1, code=1)])) + # Fix the other mapping as well. No tooltip should be shown afterward. + self.controller.load_mapping(second_input) gtk_iteration() self.controller.update_mapping(output_symbol="b") gtk_iteration() @@ -1531,6 +1547,18 @@ def test_check_for_unknown_symbols(self): self.assertFalse(error_icon.get_visible()) self.assertFalse(warning_icon.get_visible()) + def test_no_validation_tooltip_for_empty_mappings(self): + self.controller.load_preset("preset1") + self.throttle(20) + + status = self.user_interface.get("status_bar") + self.assertIsNone(status.get_tooltip_text()) + + self.controller.create_mapping() + gtk_iteration() + self.assertTrue(self.controller.is_empty_mapping()) + self.assertIsNone(status.get_tooltip_text()) + def test_check_macro_syntax(self): status = self.status_bar error_icon = self.user_interface.get("error_status_icon") @@ -1702,7 +1730,7 @@ def wait(): self.assertFalse(error_icon.get_visible()) wait() text = self.get_status_text() - self.assertIn("not grabbed", text) + self.assertIn("Failed to apply preset", text) self.assertTrue(error_icon.get_visible()) self.assertNotEqual( self.daemon.get_state("Foo Device 2"), InjectorState.RUNNING diff --git a/tests/unit/test_controller.py b/tests/unit/test_controller.py index b8d435179..0d594aee5 100644 --- a/tests/unit/test_controller.py +++ b/tests/unit/test_controller.py @@ -50,7 +50,7 @@ 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, UInput, FrontendUInput +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 @@ -999,7 +999,7 @@ def f(data): calls[-1], StatusData( CTX_APPLY, - _("Failed to apply preset %s") % self.data_manager.active_preset.name, + _('Failed to apply preset "%s"') % self.data_manager.active_preset.name, ), ) @@ -1048,17 +1048,17 @@ def f(data): self.controller.start_injecting() gtk_iteration(50) - self.assertEqual(calls[-1].msg, _("Applied preset %s") % "preset2") + self.assertEqual(calls[-1].msg, _('Applied preset "%s"') % "preset2") - mock.return_value = InjectorState.FAILED + mock.return_value = InjectorState.ERROR self.controller.start_injecting() gtk_iteration(50) - self.assertEqual(calls[-1].msg, _("Failed to apply preset %s") % "preset2") + 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, "The device was not grabbed") + self.assertEqual(calls[-1].msg, _('Failed to apply preset "%s"') % "preset2") mock.return_value = InjectorState.UPGRADE_EVDEV self.controller.start_injecting() diff --git a/tests/unit/test_injector.py b/tests/unit/test_injector.py index de698abe3..253733b53 100644 --- a/tests/unit/test_injector.py +++ b/tests/unit/test_injector.py @@ -107,7 +107,7 @@ def tearDown(self): time.sleep(0.2) self.assertIn( self.injector.get_state(), - (InjectorState.STOPPED, InjectorState.FAILED, InjectorState.NO_GRAB), + (InjectorState.STOPPED, InjectorState.ERROR, InjectorState.NO_GRAB), ) self.injector = None evdev.InputDevice.grab = self.grab diff --git a/tests/unit/test_mapping.py b/tests/unit/test_mapping.py index fe389ebc6..7a4514a78 100644 --- a/tests/unit/test_mapping.py +++ b/tests/unit/test_mapping.py @@ -29,6 +29,7 @@ REL_WHEEL, REL_WHEEL_HI_RES, KEY_1, + KEY_ESC, ) try: @@ -36,7 +37,7 @@ except ImportError: from pydantic import ValidationError -from inputremapper.configs.mapping import Mapping, UIMapping +from inputremapper.configs.mapping import Mapping, UIMapping, MappingType from inputremapper.configs.keyboard_layout import keyboard_layout, DISABLE_NAME from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.gui.messages.message_broker import MessageType @@ -185,19 +186,20 @@ def test_init_fails(self): # missing output symbol del cfg["output_symbol"] test(**cfg) - cfg["output_code"] = 1 + cfg["output_code"] = KEY_ESC test(**cfg) - cfg["output_type"] = 1 + cfg["output_type"] = EV_KEY Mapping(**cfg) - # matching type, code and symbol + # matching type, code and symbol. This cannot be done via the ui, and requires + # manual editing of the preset file. a = keyboard_layout.get("a") cfg["output_code"] = a cfg["output_symbol"] = "a" cfg["output_type"] = EV_KEY Mapping(**cfg) - # macro + type and code + # macro cfg["output_symbol"] = "key(a)" test(**cfg) cfg["output_symbol"] = "a" @@ -330,6 +332,30 @@ def test_init_fails(self): cfg["input_combination"] = [{"type": 3, "code": 1, "analog_threshold": -1}] test(**cfg) + def test_automatically_detects_mapping_type(self): + cfg = { + "input_combination": [{"type": 1, "code": 2}], + "target_uinput": "keyboard", + "output_symbol": "a", + } + self.assertEqual(Mapping(**cfg).mapping_type, MappingType.KEY_MACRO.value) + + cfg = { + "input_combination": [{"type": 1, "code": 2}], + "target_uinput": "keyboard", + "output_type": EV_KEY, + "output_code": KEY_ESC, + } + self.assertEqual(Mapping(**cfg).mapping_type, MappingType.KEY_MACRO.value) + + cfg = { + "input_combination": [{"type": EV_REL, "code": REL_X}], + "target_uinput": "keyboard", + "output_type": EV_REL, + "output_code": REL_X, + } + self.assertEqual(Mapping(**cfg).mapping_type, MappingType.ANALOG.value) + def test_revalidate_at_assignment(self): cfg = { "input_combination": [{"type": 1, "code": 1}],