small gamepad improvements

pull/14/head
sezanzeb 4 years ago
parent 3c502348e1
commit bdebee3ce9

@ -14,6 +14,7 @@
</object>
<object class="GtkDialog" id="error_dialog">
<property name="can-focus">False</property>
<property name="border-width">10</property>
<property name="title" translatable="yes">Key Mapper</property>
<property name="modal">True</property>
<property name="window-position">center</property>
@ -30,6 +31,7 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="margin-top">10</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="close_error_dialog">
@ -63,9 +65,8 @@
<object class="GtkImage" id="error-image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-end">10</property>
<property name="yalign">0</property>
<property name="xpad">12</property>
<property name="ypad">12</property>
<property name="icon-name">dialog-error</property>
<property name="icon_size">6</property>
</object>
@ -137,6 +138,7 @@
</object>
<object class="GtkDialog" id="unsaved_changes">
<property name="can-focus">False</property>
<property name="border-width">10</property>
<property name="title" translatable="yes">Key Mapper</property>
<property name="modal">True</property>
<property name="window-position">center</property>
@ -153,6 +155,7 @@
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="margin-top">10</property>
<property name="layout-style">end</property>
<child>
<object class="GtkButton" id="go_back">
@ -199,10 +202,9 @@
<object class="GtkImage" id="error-image1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-end">10</property>
<property name="yalign">0</property>
<property name="xpad">12</property>
<property name="ypad">12</property>
<property name="icon-name">dialog-error</property>
<property name="icon-name">dialog-warning</property>
<property name="icon_size">6</property>
</object>
<packing>
@ -770,6 +772,19 @@
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImage" id="warning_status_icon">
<property name="can-focus">False</property>
<property name="margin-left">10</property>
<property name="margin-start">10</property>
<property name="icon-name">dialog-warning</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage" id="error_status_icon">
<property name="can-focus">False</property>
@ -779,7 +794,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
@ -798,7 +813,7 @@
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
<child>
@ -809,7 +824,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
<child>
@ -824,7 +839,7 @@
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
<property name="position">4</property>
</packing>
</child>
</object>

@ -46,6 +46,8 @@ WHEEL_THRESHOLD = 0.3
def _write(device, ev_type, keycode, value):
"""Inject."""
# if the mouse won't move even though correct stuff is written here, the
# capabilities are probably wrong
device.write(ev_type, keycode, value)
device.syn()

@ -257,10 +257,12 @@ class KeycodeInjector:
evdev.ecodes.REL_WHEEL,
]
keys = capabilities.get(EV_KEY)
if keys is None or len(keys) == 0:
# for reasons I don't know, it is also required to have
# any keyboard button in capabilities. Maybe they intended to
# check for the mouse-button code.
if keys is None:
capabilities[EV_KEY] = []
if ecodes.BTN_MOUSE not in capabilities[EV_KEY]:
# to be able to move the cursor, this key capability is
# needed
capabilities[EV_KEY] = [ecodes.BTN_MOUSE]
# just like what python-evdev does in from_device

@ -49,7 +49,7 @@ def check_group(group):
if not in_group:
msg = (
'Some devices may not be visible without being in the '
'Some devices may not be accessible without being in the '
f'"{group}" user group.'
)
logger.warning(msg)
@ -66,7 +66,7 @@ def check_group(group):
if in_group and not group_active:
msg = (
f'You are in the "{group}" group, but your session is not yet '
'using it. Some devices may not be visible. Please log out and '
'using it. Some devices may not be accessible. Please log out and '
'back in or restart'
)
logger.warning(msg)
@ -97,6 +97,7 @@ def can_read_devices():
plugdev_check = check_group('plugdev')
# ubuntu. funnily, individual devices in /dev/input/ have write permitted.
print(is_service_running(), check_injection_rights())
if not is_service_running():
can_write = check_injection_rights()
else:

@ -54,6 +54,7 @@ def gtk_iteration():
CTX_SAVE = 0
CTX_APPLY = 1
CTX_ERROR = 3
CTX_WARNING = 4
def get_selected_row_bg():
@ -74,6 +75,30 @@ def get_selected_row_bg():
return color.to_string()
def with_selected_device(func):
"""Decorate a function to only execute if a device is selected."""
# this should only happen if no device was found at all
def wrapped(window, *args):
if window.selected_device is None:
return True # work with timeout_add
return func(window, *args)
return wrapped
def with_selected_preset(func):
"""Decorate a function to only execute if a preset is selected."""
# this should only happen if no device was found at all
def wrapped(window, *args):
if window.selected_preset is None or window.selected_device is None:
return True # work with timeout_add
return func(window, *args)
return wrapped
class HandlerDisabled:
"""Safely modify a widget without causing handlers to be called.
@ -142,6 +167,11 @@ class Window:
'\n\n'.join(permission_errors)
)
# this is not set to invisible in glade to give the ui a default
# height that doesn't jump when a gamepad is selected
self.get('gamepad_separator').hide()
self.get('gamepad_config').hide()
self.populate_devices()
self.select_newest_preset()
@ -313,6 +343,7 @@ class Window:
return row, focused
@with_selected_device
def consume_newest_keycode(self):
"""To capture events from keyboards, mice and gamepads."""
# the "event" event of Gtk.Window wouldn't trigger on gamepad
@ -332,6 +363,7 @@ class Window:
return True
@with_selected_device
def on_apply_system_layout_clicked(self, _):
"""Load the mapping."""
self.dbus.stop_injecting(self.selected_device)
@ -343,10 +375,14 @@ class Window:
if tooltip is None:
tooltip = message
self.get('error_status_icon').hide()
self.get('warning_status_icon').hide()
if context_id == CTX_ERROR:
self.get('error_status_icon').show()
else:
self.get('error_status_icon').hide()
if context_id == CTX_WARNING:
self.get('warning_status_icon').show()
status_bar = self.get('status_bar')
status_bar.push(context_id, message)
@ -366,6 +402,7 @@ class Window:
msg = f'Syntax error at {position}, hover for info'
self.show_status(CTX_ERROR, msg, error)
@with_selected_preset
def on_save_preset_clicked(self, button):
"""Save changes to a preset to the file system."""
new_name = self.get('preset_name_input').get_text()
@ -389,11 +426,13 @@ class Window:
self.show_status(CTX_ERROR, 'Error: Permission denied!', error)
logger.error(error)
@with_selected_preset
def on_delete_preset_clicked(self, _):
"""Delete a preset from the file system."""
delete_preset(self.selected_device, self.selected_preset)
self.populate_presets()
@with_selected_preset
def on_apply_preset_clicked(self, _):
"""Apply a preset without saving changes."""
preset = self.selected_preset
@ -403,7 +442,7 @@ class Window:
if custom_mapping.changed:
self.show_status(
CTX_APPLY,
CTX_WARNING,
f'Applied outdated preset "{preset}"',
'Click "Save" first for changes to take effect'
)
@ -462,6 +501,7 @@ class Window:
else:
self.get('apply_system_layout').set_opacity(0.4)
@with_selected_device
def on_create_preset_clicked(self, _):
"""Create a new preset and select it."""
if custom_mapping.changed:
@ -520,6 +560,8 @@ class Window:
self.initialize_gamepad_config()
custom_mapping.changed = False
def on_left_joystick_purpose_changed(self, dropdown):
"""Set the purpose of the left joystick."""
purpose = dropdown.get_active_id()
@ -557,9 +599,6 @@ class Window:
def save_preset(self):
"""Write changes to presets to disk."""
if self.selected_device is None or self.selected_preset is None:
return
logger.info(
'Updating configs for "%s", "%s"',
self.selected_device,

@ -54,6 +54,17 @@ class Mapping(ConfigBase):
def __len__(self):
return len(self._mapping)
def set(self, *args):
"""Set a config value. See `ConfigBase.set`."""
print('set', args)
self.changed = True
return super().set(*args)
def remove(self, *args):
"""Remove a config value. See `ConfigBase.remove`."""
self.changed = True
return super().remove(*args)
def change(self, new_key, character, previous_key=None):
"""Replace the mapping of a keycode with a different one.

@ -180,6 +180,8 @@ class TestInjector(unittest.TestCase):
# be able to control the mouse. it probably wants the mouse click.
self.injector = KeycodeInjector('gamepad 2', custom_mapping)
"""gamepad without any existing key capability"""
path = self.new_gamepad
gamepad_template = copy.deepcopy(fixtures['/dev/input/event30'])
fixtures[path] = {
@ -189,19 +191,29 @@ class TestInjector(unittest.TestCase):
'capabilities': gamepad_template['capabilities']
}
del fixtures[path]['capabilities'][EV_KEY]
device, abs_to_rel = self.injector._prepare_device(path)
self.assertNotIn(EV_KEY, device.capabilities())
capabilities = self.injector._modify_capabilities(
{},
device,
abs_to_rel
)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
self.assertNotIn(evdev.ecodes.EV_KEY, device.capabilities())
"""gamepad with existing key capabilities, but not btn_mouse"""
path = '/dev/input/event30'
device, abs_to_rel = self.injector._prepare_device(path)
self.assertIn(EV_KEY, device.capabilities())
self.assertNotIn(evdev.ecodes.BTN_MOUSE, device.capabilities()[EV_KEY])
capabilities = self.injector._modify_capabilities(
{},
device,
abs_to_rel
)
self.assertIn(evdev.ecodes.EV_KEY, capabilities)
self.assertEqual(len(capabilities[evdev.ecodes.EV_KEY]), 1)
self.assertIn(EV_KEY, capabilities)
self.assertIn(evdev.ecodes.BTN_MOUSE, capabilities[EV_KEY])
def test_skip_unused_device(self):
# skips a device because its capabilities are not used in the mapping

@ -567,10 +567,12 @@ class TestIntegration(unittest.TestCase):
# select a device that is not a gamepad
self.window.on_select_device(FakeDropdown('device 1'))
self.assertFalse(self.window.get('gamepad_config').is_visible())
self.assertFalse(custom_mapping.changed)
# select a gamepad
self.window.on_select_device(FakeDropdown('gamepad'))
self.assertTrue(self.window.get('gamepad_config').is_visible())
self.assertFalse(custom_mapping.changed)
# set stuff
self.window.get('left_joystick_purpose').set_active_id(WHEEL)
@ -583,6 +585,7 @@ class TestIntegration(unittest.TestCase):
config.set('gamepad.joystick.left_purpose', MOUSE)
config.set('gamepad.joystick.right_purpose', MOUSE)
config.set('gamepad.joystick.pointer_speed', 50)
self.assertTrue(custom_mapping.changed)
left_purpose = custom_mapping.get('gamepad.joystick.left_purpose')
right_purpose = custom_mapping.get('gamepad.joystick.right_purpose')
pointer_speed = custom_mapping.get('gamepad.joystick.pointer_speed')
@ -593,6 +596,7 @@ class TestIntegration(unittest.TestCase):
# select a device that is not a gamepad again
self.window.on_select_device(FakeDropdown('device 1'))
self.assertFalse(self.window.get('gamepad_config').is_visible())
self.assertFalse(custom_mapping.changed)
def test_start_injecting(self):
keycode_from = 9

@ -115,8 +115,11 @@ class TestMapping(unittest.TestCase):
self.assertEqual(self.mapping.get('a'), None)
self.assertFalse(self.mapping.changed)
self.mapping.set('a', 1)
self.assertEqual(self.mapping.get('a'), 1)
self.assertTrue(self.mapping.changed)
self.mapping.remove('a')
self.mapping.set('a.b', 2)
@ -133,11 +136,15 @@ class TestMapping(unittest.TestCase):
self.mapping.change((EV_KEY, 81, 1), 'a')
self.mapping.set('mapping.a', 2)
self.mapping.save(get_preset_path('foo', 'bar'))
self.assertFalse(self.mapping.changed)
self.mapping.load(get_preset_path('foo', 'bar'))
self.assertEqual(self.mapping.get_character((EV_KEY, 81, 1)), 'a')
self.assertIsNone(self.mapping.get('mapping.a'))
self.assertFalse(self.mapping.changed)
# loading a different preset also removes the configs from memory
self.mapping.remove('a')
self.assertTrue(self.mapping.changed)
self.mapping.set('a.b.c', 6)
self.mapping.load(get_preset_path('foo', 'bar2'))
self.assertIsNone(self.mapping.get('a.b.c'))

@ -102,7 +102,7 @@ class TestPermissions(unittest.TestCase):
def fake_check_output(cmd):
# fake the `groups` output to act like the current session only
# has input and a_unused active
if cmd[0] == 'groups':
if cmd == 'groups' or cmd[0] == 'groups':
return b'foo input a_unused bar'
return original_check_output(cmd)

Loading…
Cancel
Save