Automatically set "Use as analog" if output is analog (#627)

pull/373/head
Tobi 1 year ago committed by GitHub
parent d48588bddb
commit 23af936688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -61,6 +61,8 @@ class InputConfig(BaseModel):
# This solves a number of bugs when multiple devices have overlapping capabilities.
# see utils.get_device_hash for the exact hashing function
origin_hash: Optional[DeviceHash] = None
# At which point is an analog input treated as "pressed"
analog_threshold: Optional[int] = None
def __str__(self):
@ -91,7 +93,7 @@ class InputConfig(BaseModel):
@property
def defines_analog_input(self) -> bool:
"""Whether this defines an analog input"""
"""Whether this defines an analog input."""
return not self.analog_threshold and self.type != ecodes.EV_KEY
@property
@ -399,7 +401,7 @@ class InputCombination(Tuple[InputConfig, ...]):
def find_analog_input_config(
self, type_: Optional[int] = None
) -> Optional[InputConfig]:
"""Return the first event that defines an analog input"""
"""Return the first event that defines an analog input."""
for input_config in self:
if input_config.defines_analog_input and (
type_ is None or input_config.type == type_

@ -85,6 +85,13 @@ class KnownUinput(str, enum.Enum):
KEYBOARD_MOUSE = "keyboard + mouse"
class MappingType(str, enum.Enum):
"""What kind of output the mapping produces."""
KEY_MACRO = "key_macro"
ANALOG = "analog"
CombinationChangedCallback = Optional[
Callable[[InputCombination, InputCombination], None]
]
@ -126,7 +133,7 @@ class UIMapping(BaseModel):
output_code: Optional[int] = None # The event code of the mapped event
name: Optional[str] = None
mapping_type: Optional[Literal["key_macro", "analog"]] = None
mapping_type: Optional[MappingType] = None
# if release events will be sent to the forwarded device as soon as a combination
# triggers see also #229
@ -244,6 +251,9 @@ class UIMapping(BaseModel):
REL_HWHEEL_HI_RES,
)
def is_analog_output(self):
return self.mapping_type == MappingType.ANALOG
def set_combination_changed_callback(self, callback: CombinationChangedCallback):
self._combination_changed = callback

@ -138,7 +138,8 @@ class Controller:
self.message_broker.publish(MappingData(**MAPPING_DEFAULTS))
def _on_combination_recorded(self, data: CombinationRecorded):
self.update_combination(data.combination)
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."""
@ -244,8 +245,36 @@ class Controller:
)
self.message_broker.publish(DoStackSwitch(1))
def _auto_use_as_analog(self, combination: InputCombination) -> InputCombination:
"""If output is analog, set the first fitting input to analog."""
if self.data_manager.active_mapping is None:
return combination
if not self.data_manager.active_mapping.is_analog_output():
return combination
if combination.find_analog_input_config():
# something is already set to do that
return combination
for i, input_config in enumerate(combination):
# find the first analog input and set it to "use as analog"
if input_config.type in (EV_ABS, EV_REL):
logger.info("Using %s as analog input", input_config)
# combinations and input_configs are immutable, a new combination
# is created to fit the needs instead
combination_list = list(combination)
combination_list[i] = input_config.modify(analog_threshold=0)
new_combination = InputCombination(combination_list)
return new_combination
return combination
def update_combination(self, combination: InputCombination):
"""Update the input_combination of the active mapping."""
combination = self._auto_use_as_analog(combination)
try:
self.data_manager.update_mapping(input_combination=combination)
self.save()
@ -435,16 +464,16 @@ class Controller:
self.data_manager.load_mapping(input_combination)
self.load_input_config(input_combination[0])
def update_mapping(self, **kwargs):
def update_mapping(self, **changes):
"""Update the active_mapping with the given keywords and values."""
if "mapping_type" in kwargs.keys():
if not (kwargs := self._change_mapping_type(kwargs)):
if "mapping_type" in changes.keys():
if not (changes := self._change_mapping_type(changes)):
# we need to synchronize the gui
self.data_manager.publish_mapping()
self.data_manager.publish_event()
return
self.data_manager.update_mapping(**kwargs)
self.data_manager.update_mapping(**changes)
self.save()
def create_mapping(self):
@ -653,17 +682,17 @@ class Controller:
"""Focus the given component."""
self.gui.window.set_focus(component)
def _change_mapping_type(self, kwargs: Dict[str, Any]):
def _change_mapping_type(self, changes: Dict[str, Any]):
"""Query the user to update the mapping in order to change the mapping type."""
mapping = self.data_manager.active_mapping
if mapping is None:
return kwargs
return changes
if kwargs["mapping_type"] == mapping.mapping_type:
return kwargs
if changes["mapping_type"] == mapping.mapping_type:
return changes
if kwargs["mapping_type"] == "analog":
if changes["mapping_type"] == "analog":
msg = _("You are about to change the mapping to analog.")
if mapping.output_symbol:
msg += _('\nThis will remove "{}" ' "from the text input!").format(
@ -677,20 +706,20 @@ class Controller:
]:
# there is no analog input configured, let's try to autoconfigure it
inputs: List[InputConfig] = list(mapping.input_combination)
for i, e in enumerate(inputs):
if e.type in [EV_ABS, EV_REL]:
inputs[i] = e.modify(analog_threshold=0)
kwargs["input_combination"] = InputCombination(inputs)
for i, input_config in enumerate(inputs):
if input_config.type in [EV_ABS, EV_REL]:
inputs[i] = input_config.modify(analog_threshold=0)
changes["input_combination"] = InputCombination(inputs)
msg += _(
'\nThe input "{}" will be used as analog input.'
).format(e.description())
).format(input_config.description())
break
else:
# not possible to autoconfigure inform the user
msg += _("\nYou need to record an analog input.")
elif not mapping.output_symbol:
return kwargs
return changes
answer = None
@ -700,21 +729,21 @@ class Controller:
self.message_broker.publish(UserConfirmRequest(msg, get_answer))
if answer:
kwargs["output_symbol"] = None
return kwargs
changes["output_symbol"] = None
return changes
else:
return None
if kwargs["mapping_type"] == "key_macro":
if changes["mapping_type"] == "key_macro":
try:
analog_input = tuple(
filter(lambda i: i.defines_analog_input, mapping.input_combination)
)
analog_input = analog_input[0]
except IndexError:
kwargs["output_type"] = None
kwargs["output_code"] = None
return kwargs
changes["output_type"] = None
changes["output_code"] = None
return changes
answer = None
@ -731,10 +760,10 @@ class Controller:
)
)
if answer:
kwargs["output_type"] = None
kwargs["output_code"] = None
return kwargs
changes["output_type"] = None
changes["output_code"] = None
return changes
else:
return None
return kwargs
return changes

@ -529,7 +529,7 @@ class DataManager:
def stop_combination_recording(self):
"""Stop recording user input.
Will send RecordingFinished message if a recording is running.
Will send a recording_finished signal if a recording is running.
"""
self._reader_client.stop_recorder()

@ -181,7 +181,7 @@ class ReaderClient:
def stop_recorder(self) -> None:
"""Stop recording the input.
Will send RecordingFinished message.
Will send recording_finished signals.
"""
logger.debug("Stopping recorder.")
self._send_command(CMD_STOP_READING)

@ -17,15 +17,18 @@
#
# 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.system_mapping import system_mapping
from inputremapper.injection.injector import InjectorState
from tests.lib.logger import logger
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk
@ -49,7 +52,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
from inputremapper.configs.mapping import UIMapping, MappingData
from inputremapper.configs.mapping import UIMapping, MappingData, MappingType, Mapping
from tests.lib.cleanup import quick_cleanup
from tests.lib.stuff import spy
from tests.lib.patches import FakeDaemonProxy
@ -589,6 +592,53 @@ class TestController(unittest.TestCase):
)
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()

Loading…
Cancel
Save