Port Orca 50 command and input managers
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
# Cthulhu
|
||||||
#
|
#
|
||||||
# Copyright (c) 2026 Stormux
|
# Copyright 2023 Igalia, S.L.
|
||||||
# Copyright (c) 2023 Igalia, S.L.
|
# Copyright 2023 GNOME Foundation Inc.
|
||||||
# Author: Joanmarie Diggs <jdiggs@igalia.com>
|
# Author: Joanmarie Diggs <jdiggs@igalia.com>
|
||||||
#
|
#
|
||||||
# This library is free software; you can redistribute it and/or
|
# This library is free software; you can redistribute it and/or
|
||||||
@@ -18,212 +18,449 @@
|
|||||||
# License along with this library; if not, write to the
|
# License along with this library; if not, write to the
|
||||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
# Boston MA 02110-1301 USA.
|
# Boston MA 02110-1301 USA.
|
||||||
#
|
|
||||||
# Forked from Orca screen reader.
|
|
||||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
|
||||||
|
|
||||||
"""Manages the Cthulhu modifier key(s)."""
|
# pylint: disable=too-many-locals
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
# pylint: disable=too-many-positional-arguments
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
|
||||||
__id__ = "$Id$"
|
"""Manages the Cthulhu modifier key."""
|
||||||
__version__ = "$Revision$"
|
|
||||||
__date__ = "$Date$"
|
from __future__ import annotations
|
||||||
__copyright__ = "Copyright (c) 2026 Stormux"
|
|
||||||
__license__ = "LGPL"
|
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
gi.require_version("Atspi", "2.0")
|
|
||||||
from gi.repository import Atspi
|
|
||||||
from gi.repository import GLib
|
|
||||||
|
|
||||||
import cthulhu.debug as debug
|
gi.require_version("Atspi", "2.0")
|
||||||
import cthulhu.settings as settings
|
gi.require_version("Gdk", "3.0")
|
||||||
|
from gi.repository import (
|
||||||
|
Atspi,
|
||||||
|
Gdk, # pylint: disable=no-name-in-module
|
||||||
|
GLib,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import debug, gsettings_registry, input_event_manager, keybindings
|
||||||
|
|
||||||
|
DESKTOP_MODIFIER_KEYS: list[str] = ["Insert", "KP_Insert"]
|
||||||
|
LAPTOP_MODIFIER_KEYS: list[str] = ["Caps_Lock", "Shift_Lock"]
|
||||||
|
_SCHEMA = "keybindings"
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .input_event import KeyboardEvent
|
||||||
|
|
||||||
|
|
||||||
class CthulhuModifierManager:
|
class CthulhuModifierManager:
|
||||||
"""Manages the Cthulhu modifier."""
|
"""Manages the Cthulhu modifier."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self._originalXmodmap = b""
|
self._modifier_keys_override: list[str] | None = None
|
||||||
self._capsLockCleared = False
|
self._applied_modifier_keys: list[str] = []
|
||||||
|
self._grabbed_modifiers: dict = {}
|
||||||
|
self._is_pressed: bool = False
|
||||||
|
self._modifiers_are_set: bool = False
|
||||||
|
|
||||||
def refreshCthulhuModifiers(self, reason=""):
|
# Related to hacks which will soon die.
|
||||||
"""Refreshes the Cthulhu modifier keys."""
|
self._original_xmodmap: bytes = b""
|
||||||
|
self._caps_lock_cleared: bool = False
|
||||||
|
self._need_to_restore_cthulhu_modifier: bool = False
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Refreshing Cthulhu modifiers"
|
# Event handlers for input devices being plugged in/unplugged.
|
||||||
if reason:
|
display = Gdk.Display.get_default() # pylint: disable=no-value-for-parameter
|
||||||
msg += f": {reason}"
|
if display is not None:
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
device_manager = display.get_device_manager()
|
||||||
|
device_manager.connect("device-added", self._on_device_changed)
|
||||||
display = os.environ.get("DISPLAY")
|
device_manager.connect("device-removed", self._on_device_changed)
|
||||||
if not display:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xkbcomp operations"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.unsetCthulhuModifiers(reason)
|
|
||||||
with subprocess.Popen(["xkbcomp", display, "-"],
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) as process:
|
|
||||||
self._originalXmodmap, _ = process.communicate()
|
|
||||||
self._createCthulhuXmodmap()
|
|
||||||
|
|
||||||
def unsetCthulhuModifiers(self, reason=""):
|
|
||||||
"""Turns the Cthulhu modifiers back into their original purpose."""
|
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Attempting to restore original xmodmap"
|
|
||||||
if reason:
|
|
||||||
msg += f": {reason}"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
if not self._originalXmodmap:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: No stored xmodmap found"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
display = os.environ.get("DISPLAY")
|
|
||||||
if not display:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xmodmap restoration"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._capsLockCleared = False
|
|
||||||
with subprocess.Popen(["xkbcomp", "-w0", "-", display],
|
|
||||||
stdin=subprocess.PIPE, stdout=None, stderr=None) as process:
|
|
||||||
process.communicate(self._originalXmodmap)
|
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Original xmodmap restored"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
def _createCthulhuXmodmap(self):
|
|
||||||
"""Makes a Cthulhu-specific Xmodmap so that the modifier works."""
|
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Creating Cthulhu xmodmap"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
if "Caps_Lock" in settings.cthulhuModifierKeys \
|
|
||||||
or "Shift_Lock" in settings.cthulhuModifierKeys:
|
|
||||||
self.setCapsLockAsCthulhuModifier(True)
|
|
||||||
self._capsLockCleared = True
|
|
||||||
elif self._capsLockCleared:
|
|
||||||
self.setCapsLockAsCthulhuModifier(False)
|
|
||||||
self._capsLockCleared = False
|
|
||||||
|
|
||||||
def setCapsLockAsCthulhuModifier(self, enable):
|
|
||||||
"""Enable or disable use of the caps lock key as a Cthulhu modifier key."""
|
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Setting caps lock as the Cthulhu modifier"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
display = os.environ.get("DISPLAY")
|
|
||||||
if not display:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, cannot modify caps lock"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._originalXmodmap:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: No xmodmap available, cannot modify caps lock"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
return
|
|
||||||
|
|
||||||
interpretCapsLineProg = re.compile(
|
|
||||||
r'^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$', re.I)
|
|
||||||
normalCapsLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$', re.I)
|
|
||||||
interpretShiftLineProg = re.compile(
|
|
||||||
r'^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$', re.I)
|
|
||||||
normalShiftLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$', re.I)
|
|
||||||
disabledModLineProg = re.compile(
|
|
||||||
r'^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$', re.I)
|
|
||||||
normalCapsLine = ' action= LockMods(modifiers=Lock);'
|
|
||||||
normalShiftLine = ' action= LockMods(modifiers=Shift);'
|
|
||||||
disabledModLine = ' action= NoAction();'
|
|
||||||
lines = self._originalXmodmap.decode('UTF-8').split('\n')
|
|
||||||
foundCapsInterpretSection = False
|
|
||||||
foundShiftInterpretSection = False
|
|
||||||
modified = False
|
|
||||||
for i, line in enumerate(lines):
|
|
||||||
if not foundCapsInterpretSection and not foundShiftInterpretSection:
|
|
||||||
if interpretCapsLineProg.match(line):
|
|
||||||
foundCapsInterpretSection = True
|
|
||||||
elif interpretShiftLineProg.match(line):
|
|
||||||
foundShiftInterpretSection = True
|
|
||||||
elif foundCapsInterpretSection:
|
|
||||||
if enable:
|
|
||||||
if normalCapsLineProg.match(line):
|
|
||||||
lines[i] = disabledModLine
|
|
||||||
modified = True
|
|
||||||
else:
|
|
||||||
if disabledModLineProg.match(line):
|
|
||||||
lines[i] = normalCapsLine
|
|
||||||
modified = True
|
|
||||||
if line.find('}'):
|
|
||||||
foundCapsInterpretSection = False
|
|
||||||
elif foundShiftInterpretSection:
|
|
||||||
if enable:
|
|
||||||
if normalShiftLineProg.match(line):
|
|
||||||
lines[i] = disabledModLine
|
|
||||||
modified = True
|
|
||||||
else:
|
|
||||||
if disabledModLineProg.match(line):
|
|
||||||
lines[i] = normalShiftLine
|
|
||||||
modified = True
|
|
||||||
if line.find('}'):
|
|
||||||
foundShiftInterpretSection = False
|
|
||||||
if modified:
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Updating xmodmap"
|
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
|
||||||
|
|
||||||
with subprocess.Popen(["xkbcomp", "-w0", "-", display],
|
|
||||||
stdin=subprocess.PIPE, stdout=None, stderr=None) as process:
|
|
||||||
process.communicate(bytes('\n'.join(lines), 'UTF-8'))
|
|
||||||
else:
|
else:
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Not updating xmodmap"
|
msg = "CTHULHU MODIFIER MANAGER: Cannot listen for input device changes."
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
def toggleModifier(self, keyboardEvent):
|
def _on_device_changed(self, _device_manager, device: Gdk.Device) -> None:
|
||||||
|
"""Handles device-* signals."""
|
||||||
|
|
||||||
|
source = device.get_source()
|
||||||
|
tokens = ["CTHULHU MODIFIER MANAGER: Device changed", source]
|
||||||
|
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||||
|
if source == Gdk.InputSource.KEYBOARD:
|
||||||
|
self.refresh_cthulhu_modifiers("Keyboard change detected.")
|
||||||
|
|
||||||
|
def is_cthulhu_modifier(self, modifier: str) -> bool:
|
||||||
|
"""Returns True if modifier is one of the user's Cthulhu modifier keys."""
|
||||||
|
|
||||||
|
if modifier not in self.get_cthulhu_modifier_keys():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if modifier in ["Insert", "KP_Insert"]:
|
||||||
|
return self.is_modifier_grabbed(modifier)
|
||||||
|
|
||||||
|
return self._modifiers_are_set
|
||||||
|
|
||||||
|
def get_pressed_state(self) -> bool:
|
||||||
|
"""Returns True if the Cthulhu modifier has been pressed but not yet released."""
|
||||||
|
|
||||||
|
return self._is_pressed
|
||||||
|
|
||||||
|
def set_pressed_state(self, is_pressed: bool) -> None:
|
||||||
|
"""Updates the pressed state of the modifier based on event."""
|
||||||
|
|
||||||
|
msg = f"CTHULHU MODIFIER MANAGER: Setting pressed state to {is_pressed}"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._is_pressed = is_pressed
|
||||||
|
|
||||||
|
def is_modifier_grabbed(self, modifier: str) -> bool:
|
||||||
|
"""Returns True if there is an existing grab for modifier."""
|
||||||
|
|
||||||
|
return modifier in self._grabbed_modifiers
|
||||||
|
|
||||||
|
def add_grabs_for_cthulhu_modifiers(self) -> None:
|
||||||
|
"""Adds grabs for all of the user's Cthulhu modifier keys."""
|
||||||
|
|
||||||
|
for modifier in self.get_cthulhu_modifier_keys():
|
||||||
|
if modifier in ["Insert", "KP_Insert"]:
|
||||||
|
self.add_modifier_grab(modifier)
|
||||||
|
|
||||||
|
def remove_grabs_for_cthulhu_modifiers(self) -> None:
|
||||||
|
"""Removes grabs for all of the user's Cthulhu modifier keys."""
|
||||||
|
|
||||||
|
for modifier in self.get_cthulhu_modifier_keys():
|
||||||
|
if modifier in ["Insert", "KP_Insert"]:
|
||||||
|
self.remove_modifier_grab(modifier)
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Setting pressed state to False for grab removal"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
self._is_pressed = False
|
||||||
|
|
||||||
|
def add_modifier_grab(self, modifier: str) -> None:
|
||||||
|
"""Adds a grab for modifier."""
|
||||||
|
|
||||||
|
if modifier in self._grabbed_modifiers:
|
||||||
|
return
|
||||||
|
|
||||||
|
keyval, keycode = keybindings.get_keycodes(modifier)
|
||||||
|
grab_id = input_event_manager.get_manager().add_grab_for_modifier(modifier, keyval, keycode)
|
||||||
|
if grab_id != -1:
|
||||||
|
self._grabbed_modifiers[modifier] = grab_id
|
||||||
|
|
||||||
|
def remove_modifier_grab(self, modifier: str) -> None:
|
||||||
|
"""Removes the grab for modifier."""
|
||||||
|
|
||||||
|
grab_id = self._grabbed_modifiers.get(modifier)
|
||||||
|
if grab_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
input_event_manager.get_manager().remove_grab_for_modifier(modifier, grab_id)
|
||||||
|
del self._grabbed_modifiers[modifier]
|
||||||
|
|
||||||
|
def toggle_modifier(self, keyboard_event: KeyboardEvent) -> None:
|
||||||
"""Toggles the modifier to enable double-clicking causing normal behavior."""
|
"""Toggles the modifier to enable double-clicking causing normal behavior."""
|
||||||
|
|
||||||
if keyboardEvent.keyval_name in ["Caps_Lock", "Shift_Lock"]:
|
if keyboard_event.keyval_name in ["Caps_Lock", "Shift_Lock"]:
|
||||||
self._toggleModifierLock(keyboardEvent)
|
self._toggle_modifier_lock(keyboard_event)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _toggleModifierLock(self, keyboardEvent):
|
self._toggle_modifier_grab(keyboard_event)
|
||||||
|
|
||||||
|
def _toggle_modifier_grab(self, keyboard_event: KeyboardEvent) -> None:
|
||||||
|
"""Toggles the grab for a modifier to enable double-clicking causing normal behavior."""
|
||||||
|
|
||||||
|
# Because we will synthesize another press and release, wait until the real release.
|
||||||
|
if keyboard_event.is_pressed_key():
|
||||||
|
return
|
||||||
|
|
||||||
|
def toggle(hw_code):
|
||||||
|
Atspi.generate_keyboard_event(hw_code, "", Atspi.KeySynthType.PRESSRELEASE)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def restore_grab(modifier):
|
||||||
|
self.add_modifier_grab(modifier)
|
||||||
|
return False
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Removing grab pre-toggle"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
self.remove_modifier_grab(keyboard_event.keyval_name)
|
||||||
|
|
||||||
|
msg = f"CTHULHU MODIFIER MANAGER: Scheduling toggle of {keyboard_event.keyval_name}"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
GLib.timeout_add(1, toggle, keyboard_event.hw_code)
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Scheduling re-adding grab post-toggle"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
GLib.timeout_add(500, restore_grab, keyboard_event.keyval_name)
|
||||||
|
|
||||||
|
def _toggle_modifier_lock(self, keyboard_event: KeyboardEvent) -> None:
|
||||||
"""Toggles the lock for a modifier to enable double-clicking causing normal behavior."""
|
"""Toggles the lock for a modifier to enable double-clicking causing normal behavior."""
|
||||||
|
|
||||||
if not keyboardEvent.is_pressed_key():
|
if not keyboard_event.is_pressed_key():
|
||||||
return
|
return
|
||||||
|
|
||||||
def toggle(modifiers, modifier):
|
def toggle(modifiers, modifier):
|
||||||
if modifiers & modifier:
|
if modifiers & modifier:
|
||||||
lock = Atspi.KeySynthType.UNLOCKMODIFIERS
|
lock = Atspi.KeySynthType.UNLOCKMODIFIERS
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Unlocking CapsLock"
|
msg = "CTHULHU MODIFIER MANAGER: Unlocking CapsLock"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
else:
|
else:
|
||||||
lock = Atspi.KeySynthType.LOCKMODIFIERS
|
lock = Atspi.KeySynthType.LOCKMODIFIERS
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Locking CapsLock"
|
msg = "CTHULHU MODIFIER MANAGER: Locking CapsLock"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
Atspi.generate_keyboard_event(modifier, "", lock)
|
Atspi.generate_keyboard_event(modifier, "", lock)
|
||||||
|
|
||||||
if keyboardEvent.keyval_name == "Caps_Lock":
|
if keyboard_event.keyval_name == "Caps_Lock":
|
||||||
modifier = 1 << Atspi.ModifierType.SHIFTLOCK
|
modifier = 1 << Atspi.ModifierType.SHIFTLOCK
|
||||||
elif keyboardEvent.keyval_name == "Shift_Lock":
|
elif keyboard_event.keyval_name == "Shift_Lock":
|
||||||
modifier = 1 << Atspi.ModifierType.SHIFT
|
modifier = 1 << Atspi.ModifierType.SHIFT
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = "CTHULHU MODIFIER MANAGER: Scheduling lock change"
|
msg = "CTHULHU MODIFIER MANAGER: Scheduling lock change"
|
||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
GLib.timeout_add(1, toggle, keyboardEvent.modifiers, modifier)
|
GLib.timeout_add(1, toggle, keyboard_event.modifiers, modifier)
|
||||||
|
|
||||||
_manager = None
|
def get_cthulhu_modifier_keys(self) -> list[str]:
|
||||||
def getManager():
|
"""Returns the active Cthulhu modifier keys via override or layered lookup."""
|
||||||
"""Returns the Cthulhu Modifier Manager"""
|
|
||||||
|
if self._modifier_keys_override is not None:
|
||||||
|
return self._modifier_keys_override
|
||||||
|
return self._lookup_modifier_keys()
|
||||||
|
|
||||||
|
def _lookup_modifier_keys(self) -> list[str]:
|
||||||
|
"""Returns modifier keys via two-part layered lookup: layout then per-layout keys."""
|
||||||
|
|
||||||
|
registry = gsettings_registry.get_registry()
|
||||||
|
layout = registry.layered_lookup(
|
||||||
|
_SCHEMA,
|
||||||
|
"keyboard-layout",
|
||||||
|
"",
|
||||||
|
genum="org.stormux.Cthulhu.KeyboardLayout",
|
||||||
|
default="desktop",
|
||||||
|
)
|
||||||
|
if layout == "desktop":
|
||||||
|
return registry.layered_lookup(
|
||||||
|
_SCHEMA,
|
||||||
|
"desktop-modifier-keys",
|
||||||
|
"as",
|
||||||
|
default=DESKTOP_MODIFIER_KEYS,
|
||||||
|
)
|
||||||
|
return registry.layered_lookup(
|
||||||
|
_SCHEMA,
|
||||||
|
"laptop-modifier-keys",
|
||||||
|
"as",
|
||||||
|
default=LAPTOP_MODIFIER_KEYS,
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_modifier_keys_override(self, keys: list[str] | None) -> None:
|
||||||
|
"""Sets or clears a temporary override for the modifier keys."""
|
||||||
|
|
||||||
|
self._modifier_keys_override = keys
|
||||||
|
|
||||||
|
def needs_modifier_refresh(self) -> bool:
|
||||||
|
"""Returns True if the current modifier keys differ from what was last applied."""
|
||||||
|
|
||||||
|
return self.get_cthulhu_modifier_keys() != self._applied_modifier_keys
|
||||||
|
|
||||||
|
def set_modifiers_for_layout(self) -> None:
|
||||||
|
"""Unsets and refreshes modifier keys for the current layout."""
|
||||||
|
|
||||||
|
self.unset_cthulhu_modifiers("Keyboard layout changing.")
|
||||||
|
self.refresh_cthulhu_modifiers("Keyboard layout changed.")
|
||||||
|
|
||||||
|
def refresh_cthulhu_modifiers(self, reason: str = "") -> None:
|
||||||
|
"""Refreshes the Cthulhu modifier keys, including grabs and xmodmap."""
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Refreshing Cthulhu modifiers"
|
||||||
|
if reason:
|
||||||
|
msg += f": {reason}"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
for modifier in list(self._grabbed_modifiers.keys()):
|
||||||
|
self.remove_modifier_grab(modifier)
|
||||||
|
self._is_pressed = False
|
||||||
|
self._applied_modifier_keys = list(self.get_cthulhu_modifier_keys())
|
||||||
|
self.add_grabs_for_cthulhu_modifiers()
|
||||||
|
self._modifiers_are_set = True
|
||||||
|
|
||||||
|
display = os.environ.get("DISPLAY")
|
||||||
|
if not display:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xkbcomp operations"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._restore_original_xkbcomp()
|
||||||
|
with subprocess.Popen(
|
||||||
|
["xkbcomp", display, "-"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
) as p:
|
||||||
|
self._original_xmodmap, _ = p.communicate()
|
||||||
|
self._create_cthulhu_xmodmap()
|
||||||
|
|
||||||
|
def _create_cthulhu_xmodmap(self) -> None:
|
||||||
|
"""Makes an Cthulhu-specific Xmodmap so that the Cthulhu modifier works."""
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Creating Cthulhu xmodmap"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
cthulhu_modifiers = self.get_cthulhu_modifier_keys()
|
||||||
|
if "Caps_Lock" in cthulhu_modifiers or "Shift_Lock" in cthulhu_modifiers:
|
||||||
|
self.set_caps_lock_as_cthulhu_modifier(True)
|
||||||
|
self._caps_lock_cleared = True
|
||||||
|
elif self._caps_lock_cleared:
|
||||||
|
self.set_caps_lock_as_cthulhu_modifier(False)
|
||||||
|
self._caps_lock_cleared = False
|
||||||
|
|
||||||
|
def unset_cthulhu_modifiers(self, reason: str = "") -> None:
|
||||||
|
"""Turns the Cthulhu modifiers back into their original purpose."""
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Unsetting Cthulhu modifiers"
|
||||||
|
if reason:
|
||||||
|
msg += f": {reason}"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
self._modifiers_are_set = False
|
||||||
|
self._restore_original_xkbcomp()
|
||||||
|
input_event_manager.get_manager().unmap_all_modifiers()
|
||||||
|
|
||||||
|
def _restore_original_xkbcomp(self) -> None:
|
||||||
|
"""Restores the original xkbcomp keymap."""
|
||||||
|
|
||||||
|
if not self._original_xmodmap:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: No stored xmodmap found"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
display = os.environ.get("DISPLAY")
|
||||||
|
if not display:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, skipping xmodmap restoration"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self._caps_lock_cleared = False
|
||||||
|
with subprocess.Popen(
|
||||||
|
["xkbcomp", "-w0", "-", display],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=None,
|
||||||
|
stderr=None,
|
||||||
|
) as p:
|
||||||
|
p.communicate(self._original_xmodmap)
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Original xmodmap restored"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _update_xkb_line(
|
||||||
|
line: str,
|
||||||
|
enable: bool,
|
||||||
|
normal_pattern: re.Pattern[str],
|
||||||
|
normal_line: str,
|
||||||
|
disabled_pattern: re.Pattern[str],
|
||||||
|
disabled_line: str,
|
||||||
|
) -> tuple[str, bool]:
|
||||||
|
"""Returns the possibly-updated line and whether it was modified."""
|
||||||
|
|
||||||
|
if enable and normal_pattern.match(line):
|
||||||
|
return disabled_line, True
|
||||||
|
if not enable and disabled_pattern.match(line):
|
||||||
|
return normal_line, True
|
||||||
|
return line, False
|
||||||
|
|
||||||
|
def set_caps_lock_as_cthulhu_modifier(self, enable: bool) -> None:
|
||||||
|
"""Enable or disable use of the caps lock key as an Cthulhu modifier key."""
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Setting caps lock as the Cthulhu modifier"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
display = os.environ.get("DISPLAY")
|
||||||
|
if not display:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: DISPLAY not set, cannot modify caps lock"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._original_xmodmap:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: No xmodmap available, cannot modify caps lock"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
interpret_caps_line_prog = re.compile(
|
||||||
|
r"^\s*interpret\s+Caps[_+]Lock[_+]AnyOfOrNone\s*\(all\)\s*{\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
normal_caps_line_prog = re.compile(
|
||||||
|
r"^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Lock\s*\)\s*;\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
interpret_shift_line_prog = re.compile(
|
||||||
|
r"^\s*interpret\s+Shift[_+]Lock[_+]AnyOf\s*\(\s*Shift\s*\+\s*Lock\s*\)\s*{\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
normal_shift_line_prog = re.compile(
|
||||||
|
r"^\s*action\s*=\s*LockMods\s*\(\s*modifiers\s*=\s*Shift\s*\)\s*;\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
disabled_mod_line_prog = re.compile(
|
||||||
|
r"^\s*action\s*=\s*NoAction\s*\(\s*\)\s*;\s*$",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
normal_caps_line = " action= LockMods(modifiers=Lock);"
|
||||||
|
normal_shift_line = " action= LockMods(modifiers=Shift);"
|
||||||
|
disabled_mod_line = " action= NoAction();"
|
||||||
|
lines = self._original_xmodmap.decode("UTF-8").split("\n")
|
||||||
|
found_caps_interpret_section = False
|
||||||
|
found_shift_interpret_section = False
|
||||||
|
modified = False
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if not found_caps_interpret_section and not found_shift_interpret_section:
|
||||||
|
if interpret_caps_line_prog.match(line):
|
||||||
|
found_caps_interpret_section = True
|
||||||
|
elif interpret_shift_line_prog.match(line):
|
||||||
|
found_shift_interpret_section = True
|
||||||
|
elif found_caps_interpret_section:
|
||||||
|
lines[i], changed = self._update_xkb_line(
|
||||||
|
line,
|
||||||
|
enable,
|
||||||
|
normal_caps_line_prog,
|
||||||
|
normal_caps_line,
|
||||||
|
disabled_mod_line_prog,
|
||||||
|
disabled_mod_line,
|
||||||
|
)
|
||||||
|
modified = modified or changed
|
||||||
|
if line.find("}"):
|
||||||
|
found_caps_interpret_section = False
|
||||||
|
elif found_shift_interpret_section:
|
||||||
|
lines[i], changed = self._update_xkb_line(
|
||||||
|
line,
|
||||||
|
enable,
|
||||||
|
normal_shift_line_prog,
|
||||||
|
normal_shift_line,
|
||||||
|
disabled_mod_line_prog,
|
||||||
|
disabled_mod_line,
|
||||||
|
)
|
||||||
|
modified = modified or changed
|
||||||
|
if line.find("}"):
|
||||||
|
found_shift_interpret_section = False
|
||||||
|
|
||||||
|
if not modified:
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Not updating xmodmap"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
return
|
||||||
|
|
||||||
|
msg = "CTHULHU MODIFIER MANAGER: Updating xmodmap"
|
||||||
|
debug.print_message(debug.LEVEL_INFO, msg, True)
|
||||||
|
|
||||||
|
with subprocess.Popen(
|
||||||
|
["xkbcomp", "-w0", "-", display],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=None,
|
||||||
|
stderr=None,
|
||||||
|
) as p:
|
||||||
|
p.communicate(bytes("\n".join(lines), "UTF-8"))
|
||||||
|
|
||||||
|
|
||||||
|
_manager: CthulhuModifierManager = CthulhuModifierManager()
|
||||||
|
|
||||||
|
|
||||||
|
def get_manager() -> CthulhuModifierManager:
|
||||||
|
"""Returns the CthulhuModifierManager singleton."""
|
||||||
|
|
||||||
global _manager
|
|
||||||
if _manager is None:
|
|
||||||
_manager = CthulhuModifierManager()
|
|
||||||
return _manager
|
return _manager
|
||||||
|
|
||||||
|
|||||||
+599
-1010
File diff suppressed because it is too large
Load Diff
+197
-315
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@ cthulhu_python_sources = files([
|
|||||||
'clipboard.py',
|
'clipboard.py',
|
||||||
'cmdnames.py',
|
'cmdnames.py',
|
||||||
'colornames.py',
|
'colornames.py',
|
||||||
|
'command_manager.py',
|
||||||
'compositor_state_adapter.py',
|
'compositor_state_adapter.py',
|
||||||
'compositor_state_types.py',
|
'compositor_state_types.py',
|
||||||
'compositor_state_wayland.py',
|
'compositor_state_wayland.py',
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ from cthulhu_test_fixtures import test_context # noqa: E402,F401
|
|||||||
|
|
||||||
|
|
||||||
def clean_all_cthulhu_modules() -> None:
|
def clean_all_cthulhu_modules() -> None:
|
||||||
|
cthulhu_package = sys.modules.get("cthulhu")
|
||||||
modules_to_remove = [
|
modules_to_remove = [
|
||||||
module_name
|
module_name
|
||||||
for module_name in sys.modules
|
for module_name in sys.modules
|
||||||
@@ -81,6 +82,15 @@ def clean_all_cthulhu_modules() -> None:
|
|||||||
]
|
]
|
||||||
for module_name in modules_to_remove:
|
for module_name in modules_to_remove:
|
||||||
sys.modules.pop(module_name, None)
|
sys.modules.pop(module_name, None)
|
||||||
|
if cthulhu_package is not None:
|
||||||
|
attr_name = module_name.rsplit(".", 1)[-1]
|
||||||
|
if hasattr(cthulhu_package, attr_name):
|
||||||
|
delattr(cthulhu_package, attr_name)
|
||||||
|
if cthulhu_package is not None:
|
||||||
|
for attr_name in list(vars(cthulhu_package)):
|
||||||
|
if attr_name.startswith("_") or attr_name in {"cthulhu_i18n", "cthulhu_platform"}:
|
||||||
|
continue
|
||||||
|
delattr(cthulhu_package, attr_name)
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config: pytest.Config) -> None:
|
def pytest_configure(config: pytest.Config) -> None:
|
||||||
|
|||||||
@@ -83,7 +83,13 @@ class CthulhuTestContext:
|
|||||||
def patch_module(self, module_name: str, mock_module: Any) -> MagicMock:
|
def patch_module(self, module_name: str, mock_module: Any) -> MagicMock:
|
||||||
"""Convenience method for patching sys.modules entries."""
|
"""Convenience method for patching sys.modules entries."""
|
||||||
|
|
||||||
return self.mocker.patch.dict(sys.modules, {module_name: mock_module})
|
patch = self.mocker.patch.dict(sys.modules, {module_name: mock_module})
|
||||||
|
if "." in module_name:
|
||||||
|
package_name, attr_name = module_name.rsplit(".", 1)
|
||||||
|
package = sys.modules.get(package_name)
|
||||||
|
if package is not None:
|
||||||
|
self.monkeypatch.setattr(package, attr_name, mock_module, raising=False)
|
||||||
|
return patch
|
||||||
|
|
||||||
def patch_modules(self, modules: dict[str, Any]) -> MagicMock:
|
def patch_modules(self, modules: dict[str, Any]) -> MagicMock:
|
||||||
"""Convenience method for patching multiple sys.modules entries."""
|
"""Convenience method for patching multiple sys.modules entries."""
|
||||||
@@ -140,6 +146,7 @@ class CthulhuTestContext:
|
|||||||
"cthulhu.focus_manager",
|
"cthulhu.focus_manager",
|
||||||
"cthulhu.braille",
|
"cthulhu.braille",
|
||||||
"cthulhu.cthulhu_platform",
|
"cthulhu.cthulhu_platform",
|
||||||
|
"cthulhu.presentation_manager",
|
||||||
]
|
]
|
||||||
|
|
||||||
if additional_modules:
|
if additional_modules:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,910 @@
|
|||||||
|
# Unit tests for cthulhu_modifier_manager.py methods.
|
||||||
|
#
|
||||||
|
# Copyright 2025 Igalia, S.L.
|
||||||
|
# Author: Joanmarie Diggs <jdiggs@igalia.com>
|
||||||
|
#
|
||||||
|
# This library is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU Lesser General Public
|
||||||
|
# License as published by the Free Software Foundation; either
|
||||||
|
# version 2.1 of the License, or (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This library 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
|
||||||
|
# Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this library; if not, write to the
|
||||||
|
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||||
|
# Boston MA 02110-1301 USA.
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-position
|
||||||
|
# pylint: disable=import-outside-toplevel
|
||||||
|
# pylint: disable=too-many-public-methods
|
||||||
|
# pylint: disable=too-many-statements
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
# pylint: disable=too-many-positional-arguments
|
||||||
|
# pylint: disable=too-many-locals
|
||||||
|
|
||||||
|
"""Unit tests for cthulhu_modifier_manager.py methods."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from cthulhu_test_context import CthulhuTestContext
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestCthulhuModifierManager:
|
||||||
|
"""Test CthulhuModifierManager class methods."""
|
||||||
|
|
||||||
|
def _setup_dependencies(self, test_context: CthulhuTestContext) -> dict[str, MagicMock]:
|
||||||
|
"""Returns dependencies for cthulhu_modifier_manager module testing."""
|
||||||
|
|
||||||
|
additional_modules = ["cthulhu.input_event_manager", "gi.repository"]
|
||||||
|
essential_modules = test_context.setup_shared_dependencies(additional_modules)
|
||||||
|
|
||||||
|
debug_mock = essential_modules["cthulhu.debug"]
|
||||||
|
debug_mock.print_message = test_context.Mock()
|
||||||
|
debug_mock.LEVEL_INFO = 800
|
||||||
|
|
||||||
|
keybindings_mock = essential_modules["cthulhu.keybindings"]
|
||||||
|
keybindings_mock.get_keycodes = test_context.Mock(return_value=(0, 0))
|
||||||
|
|
||||||
|
input_event_manager_mock = essential_modules["cthulhu.input_event_manager"]
|
||||||
|
input_manager_instance = test_context.Mock()
|
||||||
|
input_manager_instance.add_grab_for_modifier = test_context.Mock(return_value=123)
|
||||||
|
input_manager_instance.remove_grab_for_modifier = test_context.Mock()
|
||||||
|
input_event_manager_mock.get_manager = test_context.Mock(
|
||||||
|
return_value=input_manager_instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
gi_repository_mock = essential_modules["gi.repository"]
|
||||||
|
|
||||||
|
gdk_mock = test_context.Mock()
|
||||||
|
display_mock = test_context.Mock()
|
||||||
|
device_manager_mock = test_context.Mock()
|
||||||
|
display_mock.get_device_manager = test_context.Mock(return_value=device_manager_mock)
|
||||||
|
gdk_mock.Display = test_context.Mock()
|
||||||
|
gdk_mock.Display.get_default = test_context.Mock(return_value=display_mock)
|
||||||
|
gdk_mock.InputSource = test_context.Mock()
|
||||||
|
gdk_mock.InputSource.KEYBOARD = 4
|
||||||
|
gi_repository_mock.Gdk = gdk_mock
|
||||||
|
|
||||||
|
atspi_mock = test_context.Mock()
|
||||||
|
atspi_mock.generate_keyboard_event = test_context.Mock()
|
||||||
|
atspi_mock.ModifierType = test_context.Mock()
|
||||||
|
atspi_mock.ModifierType.SHIFTLOCK = 1
|
||||||
|
atspi_mock.ModifierType.SHIFT = 0
|
||||||
|
gi_repository_mock.Atspi = atspi_mock
|
||||||
|
|
||||||
|
glib_mock = test_context.Mock()
|
||||||
|
glib_mock.timeout_add = test_context.Mock()
|
||||||
|
gi_repository_mock.GLib = glib_mock
|
||||||
|
|
||||||
|
essential_modules["input_manager_instance"] = input_manager_instance
|
||||||
|
essential_modules["gdk"] = gdk_mock
|
||||||
|
essential_modules["atspi"] = atspi_mock
|
||||||
|
essential_modules["glib"] = glib_mock
|
||||||
|
|
||||||
|
from cthulhu import gsettings_registry
|
||||||
|
|
||||||
|
gsettings_registry.get_registry().clear_runtime_values()
|
||||||
|
|
||||||
|
return essential_modules
|
||||||
|
|
||||||
|
def test_init(self, test_context: CthulhuTestContext) -> None:
|
||||||
|
"""Test CthulhuModifierManager.__init__."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
mock_gdk = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk", new=mock_gdk)
|
||||||
|
mock_display = test_context.Mock()
|
||||||
|
mock_device_manager = test_context.Mock()
|
||||||
|
mock_display.get_device_manager.return_value = mock_device_manager
|
||||||
|
mock_gdk.Display.get_default.return_value = mock_display
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
assert not manager._grabbed_modifiers
|
||||||
|
assert manager._is_pressed is False
|
||||||
|
assert manager._original_xmodmap == b""
|
||||||
|
assert manager._caps_lock_cleared is False
|
||||||
|
assert manager._need_to_restore_cthulhu_modifier is False
|
||||||
|
|
||||||
|
assert manager is not None
|
||||||
|
|
||||||
|
def test_init_no_display(self, test_context: CthulhuTestContext) -> None:
|
||||||
|
"""Test CthulhuModifierManager.__init__ with no display available."""
|
||||||
|
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
mock_gdk = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk", new=mock_gdk)
|
||||||
|
mock_gdk.Display.get_default.return_value = None
|
||||||
|
essential_modules["cthulhu.debug"].reset_mock()
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
|
||||||
|
assert not manager._grabbed_modifiers
|
||||||
|
assert manager._is_pressed is False
|
||||||
|
assert manager._original_xmodmap == b""
|
||||||
|
assert manager._caps_lock_cleared is False
|
||||||
|
assert manager._need_to_restore_cthulhu_modifier is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"case",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "keyboard_device",
|
||||||
|
"device_source": 4,
|
||||||
|
"should_refresh": True,
|
||||||
|
}, # Gdk.InputSource.KEYBOARD = 4
|
||||||
|
{
|
||||||
|
"id": "mouse_device",
|
||||||
|
"device_source": 0,
|
||||||
|
"should_refresh": False,
|
||||||
|
}, # Gdk.InputSource.MOUSE = 0
|
||||||
|
{
|
||||||
|
"id": "touchscreen_device",
|
||||||
|
"device_source": 5,
|
||||||
|
"should_refresh": False,
|
||||||
|
}, # Gdk.InputSource.TOUCHSCREEN = 5
|
||||||
|
],
|
||||||
|
ids=lambda case: case["id"],
|
||||||
|
)
|
||||||
|
def test_on_device_changed(
|
||||||
|
self,
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
case: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager._on_device_changed."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
|
||||||
|
mock_device = test_context.Mock()
|
||||||
|
mock_device.get_source.return_value = case["device_source"]
|
||||||
|
|
||||||
|
mock_refresh = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "refresh_cthulhu_modifiers", new=mock_refresh)
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.Gdk.InputSource.KEYBOARD", new=4)
|
||||||
|
manager._on_device_changed(None, mock_device)
|
||||||
|
if case["should_refresh"]:
|
||||||
|
mock_refresh.assert_called_once_with("Keyboard change detected.")
|
||||||
|
else:
|
||||||
|
mock_refresh.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"case",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "insert_grabbed",
|
||||||
|
"modifier": "Insert",
|
||||||
|
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
|
||||||
|
"is_grabbed": True,
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "insert_not_grabbed",
|
||||||
|
"modifier": "Insert",
|
||||||
|
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kp_insert_grabbed",
|
||||||
|
"modifier": "KP_Insert",
|
||||||
|
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
|
||||||
|
"is_grabbed": True,
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kp_insert_not_grabbed",
|
||||||
|
"modifier": "KP_Insert",
|
||||||
|
"cthulhu_modifier_keys": ["Insert", "KP_Insert"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caps_lock_modifiers_set",
|
||||||
|
"modifier": "Caps_Lock",
|
||||||
|
"cthulhu_modifier_keys": ["Caps_Lock"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"modifiers_are_set": True,
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "caps_lock_modifiers_not_set",
|
||||||
|
"modifier": "Caps_Lock",
|
||||||
|
"cthulhu_modifier_keys": ["Caps_Lock"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"modifiers_are_set": False,
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_lock_modifiers_set",
|
||||||
|
"modifier": "Shift_Lock",
|
||||||
|
"cthulhu_modifier_keys": ["Shift_Lock"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"modifiers_are_set": True,
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "shift_lock_modifiers_not_set",
|
||||||
|
"modifier": "Shift_Lock",
|
||||||
|
"cthulhu_modifier_keys": ["Shift_Lock"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"modifiers_are_set": False,
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "not_cthulhu_modifier",
|
||||||
|
"modifier": "Control_L",
|
||||||
|
"cthulhu_modifier_keys": ["Insert"],
|
||||||
|
"is_grabbed": False,
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ids=lambda case: case["id"],
|
||||||
|
)
|
||||||
|
def test_is_cthulhu_modifier(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
case: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.is_cthulhu_modifier."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._grabbed_modifiers = {"Insert": 1, "KP_Insert": 2} if case["is_grabbed"] else {}
|
||||||
|
manager._modifiers_are_set = case.get("modifiers_are_set", False)
|
||||||
|
|
||||||
|
manager._modifier_keys_override = case["cthulhu_modifier_keys"]
|
||||||
|
result = manager.is_cthulhu_modifier(case["modifier"])
|
||||||
|
assert result == case["expected_result"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"case",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "get_initial_false",
|
||||||
|
"test_type": "get",
|
||||||
|
"initial_state": False,
|
||||||
|
"new_state": None,
|
||||||
|
"expected_get": False,
|
||||||
|
"expected_set": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "get_set_true",
|
||||||
|
"test_type": "get",
|
||||||
|
"initial_state": True,
|
||||||
|
"new_state": None,
|
||||||
|
"expected_get": True,
|
||||||
|
"expected_set": None,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set_to_true",
|
||||||
|
"test_type": "set",
|
||||||
|
"initial_state": None,
|
||||||
|
"new_state": True,
|
||||||
|
"expected_get": None,
|
||||||
|
"expected_set": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "set_to_false",
|
||||||
|
"test_type": "set",
|
||||||
|
"initial_state": None,
|
||||||
|
"new_state": False,
|
||||||
|
"expected_get": None,
|
||||||
|
"expected_set": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ids=lambda case: case["id"],
|
||||||
|
)
|
||||||
|
def test_pressed_state_operations(self, test_context, case: dict) -> None:
|
||||||
|
"""Test CthulhuModifierManager get_pressed_state and set_pressed_state."""
|
||||||
|
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
essential_modules["cthulhu.debug"].reset_mock()
|
||||||
|
|
||||||
|
if case["test_type"] == "get":
|
||||||
|
if case["initial_state"] is not None:
|
||||||
|
manager._is_pressed = case["initial_state"]
|
||||||
|
result = manager.get_pressed_state()
|
||||||
|
assert result == case["expected_get"]
|
||||||
|
else:
|
||||||
|
manager.set_pressed_state(case["new_state"])
|
||||||
|
assert manager._is_pressed == case["expected_set"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"case",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "modifier_grabbed",
|
||||||
|
"grabbed_modifiers": {"Insert": 1, "KP_Insert": 2},
|
||||||
|
"modifier": "Insert",
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "kp_modifier_grabbed",
|
||||||
|
"grabbed_modifiers": {"Insert": 1, "KP_Insert": 2},
|
||||||
|
"modifier": "KP_Insert",
|
||||||
|
"expected_result": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "modifier_not_grabbed",
|
||||||
|
"grabbed_modifiers": {"Insert": 1},
|
||||||
|
"modifier": "KP_Insert",
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "no_grabs",
|
||||||
|
"grabbed_modifiers": {},
|
||||||
|
"modifier": "Insert",
|
||||||
|
"expected_result": False,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
ids=lambda case: case["id"],
|
||||||
|
)
|
||||||
|
def test_is_modifier_grabbed(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
case: dict,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.is_modifier_grabbed."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._grabbed_modifiers = case["grabbed_modifiers"]
|
||||||
|
result = manager.is_modifier_grabbed(case["modifier"])
|
||||||
|
assert result == case["expected_result"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cthulhu_modifier_keys, expected_calls",
|
||||||
|
[
|
||||||
|
pytest.param(["Insert", "KP_Insert"], ["Insert", "KP_Insert"], id="insert_keys"),
|
||||||
|
pytest.param(["Caps_Lock"], [], id="caps_lock_no_grab"),
|
||||||
|
pytest.param(
|
||||||
|
["Insert", "Caps_Lock", "KP_Insert"],
|
||||||
|
["Insert", "KP_Insert"],
|
||||||
|
id="mixed_keys",
|
||||||
|
),
|
||||||
|
pytest.param([], [], id="no_keys"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_add_grabs_for_cthulhu_modifiers(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
cthulhu_modifier_keys,
|
||||||
|
expected_calls,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.add_grabs_for_cthulhu_modifiers."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._modifier_keys_override = cthulhu_modifier_keys
|
||||||
|
|
||||||
|
mock_add_grab = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "add_modifier_grab", new=mock_add_grab)
|
||||||
|
manager.add_grabs_for_cthulhu_modifiers()
|
||||||
|
calls = [call(modifier) for modifier in expected_calls]
|
||||||
|
mock_add_grab.assert_has_calls(calls, any_order=True)
|
||||||
|
assert mock_add_grab.call_count == len(expected_calls)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cthulhu_modifier_keys, expected_calls",
|
||||||
|
[
|
||||||
|
pytest.param(["Insert", "KP_Insert"], ["Insert", "KP_Insert"], id="insert_keys"),
|
||||||
|
pytest.param(["Caps_Lock"], [], id="caps_lock_no_ungrab"),
|
||||||
|
pytest.param(
|
||||||
|
["Insert", "Caps_Lock", "KP_Insert"],
|
||||||
|
["Insert", "KP_Insert"],
|
||||||
|
id="mixed_keys",
|
||||||
|
),
|
||||||
|
pytest.param([], [], id="no_keys"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_remove_grabs_for_cthulhu_modifiers(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
cthulhu_modifier_keys,
|
||||||
|
expected_calls,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.remove_grabs_for_cthulhu_modifiers."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._modifier_keys_override = cthulhu_modifier_keys
|
||||||
|
|
||||||
|
mock_remove_grab = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "remove_modifier_grab", new=mock_remove_grab)
|
||||||
|
manager.remove_grabs_for_cthulhu_modifiers()
|
||||||
|
calls = [call(modifier) for modifier in expected_calls]
|
||||||
|
mock_remove_grab.assert_has_calls(calls, any_order=True)
|
||||||
|
assert mock_remove_grab.call_count == len(expected_calls)
|
||||||
|
assert manager._is_pressed is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"scenario,has_existing,grab_result,expects_keycodes_call,expects_grab_call,expects_in_dict",
|
||||||
|
[
|
||||||
|
pytest.param("new", False, 123, True, True, True, id="new_modifier"),
|
||||||
|
pytest.param("existing", True, None, False, False, True, id="existing_modifier"),
|
||||||
|
pytest.param("failed", False, -1, True, True, False, id="failed_grab"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_add_modifier_grab(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
scenario: str,
|
||||||
|
has_existing: bool,
|
||||||
|
grab_result: int | None,
|
||||||
|
expects_keycodes_call: bool,
|
||||||
|
expects_grab_call: bool,
|
||||||
|
expects_in_dict: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.add_modifier_grab with various scenarios."""
|
||||||
|
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
|
||||||
|
if has_existing:
|
||||||
|
manager._grabbed_modifiers["Insert"] = 123
|
||||||
|
|
||||||
|
if scenario != "existing":
|
||||||
|
mock_get_keycodes = test_context.Mock()
|
||||||
|
test_context.patch(
|
||||||
|
"cthulhu.cthulhu_modifier_manager.keybindings.get_keycodes",
|
||||||
|
new=mock_get_keycodes,
|
||||||
|
)
|
||||||
|
mock_iem = test_context.Mock()
|
||||||
|
test_context.patch(
|
||||||
|
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
|
||||||
|
new=mock_iem,
|
||||||
|
)
|
||||||
|
mock_get_keycodes.return_value = (65379, 110)
|
||||||
|
mock_input_manager = test_context.Mock()
|
||||||
|
mock_input_manager.add_grab_for_modifier.return_value = grab_result
|
||||||
|
mock_iem.return_value = mock_input_manager
|
||||||
|
|
||||||
|
manager.add_modifier_grab("Insert")
|
||||||
|
|
||||||
|
if expects_keycodes_call:
|
||||||
|
mock_get_keycodes.assert_called_once_with("Insert")
|
||||||
|
elif scenario == "existing":
|
||||||
|
essential_modules["cthulhu.keybindings"].get_keycodes.assert_not_called()
|
||||||
|
|
||||||
|
if expects_grab_call:
|
||||||
|
mock_input_manager.add_grab_for_modifier.assert_called_once_with("Insert", 65379, 110)
|
||||||
|
elif scenario == "existing":
|
||||||
|
essential_modules["input_manager_instance"].add_grab_for_modifier.assert_not_called()
|
||||||
|
|
||||||
|
if expects_in_dict:
|
||||||
|
if scenario in ("new", "existing"):
|
||||||
|
assert manager._grabbed_modifiers["Insert"] == 123
|
||||||
|
else:
|
||||||
|
assert "Insert" not in manager._grabbed_modifiers
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"has_grabbed,expects_call",
|
||||||
|
[
|
||||||
|
pytest.param(True, True, id="grabbed_modifier"),
|
||||||
|
pytest.param(False, False, id="not_grabbed_modifier"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_remove_modifier_grab(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
has_grabbed: bool,
|
||||||
|
expects_call: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.remove_modifier_grab with grabbed and non-grabbed modifiers."""
|
||||||
|
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
|
||||||
|
if has_grabbed:
|
||||||
|
manager._grabbed_modifiers["Insert"] = 123
|
||||||
|
mock_iem = test_context.Mock()
|
||||||
|
test_context.patch(
|
||||||
|
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
|
||||||
|
new=mock_iem,
|
||||||
|
)
|
||||||
|
mock_input_manager = test_context.Mock()
|
||||||
|
mock_iem.return_value = mock_input_manager
|
||||||
|
|
||||||
|
manager.remove_modifier_grab("Insert")
|
||||||
|
|
||||||
|
if expects_call:
|
||||||
|
mock_input_manager.remove_grab_for_modifier.assert_called_once_with("Insert", 123)
|
||||||
|
assert "Insert" not in manager._grabbed_modifiers
|
||||||
|
else:
|
||||||
|
essential_modules["input_manager_instance"].remove_grab_for_modifier.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"keyval_name, expected_method",
|
||||||
|
[
|
||||||
|
pytest.param("Caps_Lock", "_toggle_modifier_lock", id="caps_lock"),
|
||||||
|
pytest.param("Shift_Lock", "_toggle_modifier_lock", id="shift_lock"),
|
||||||
|
pytest.param("Insert", "_toggle_modifier_grab", id="insert"),
|
||||||
|
pytest.param("KP_Insert", "_toggle_modifier_grab", id="kp_insert"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_toggle_modifier(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
keyval_name,
|
||||||
|
expected_method,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.toggle_modifier."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
mock_keyboard_event = test_context.Mock()
|
||||||
|
mock_keyboard_event.keyval_name = keyval_name
|
||||||
|
mock_keyboard_event.hw_code = 110
|
||||||
|
mock_keyboard_event.modifiers = 0
|
||||||
|
mock_keyboard_event.is_pressed_key.return_value = False
|
||||||
|
mock_method = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, expected_method, new=mock_method)
|
||||||
|
manager.toggle_modifier(mock_keyboard_event)
|
||||||
|
mock_method.assert_called_once_with(mock_keyboard_event)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"is_pressed_key, expects_remove_call, expects_timeout_calls",
|
||||||
|
[
|
||||||
|
pytest.param(True, False, 0, id="pressed_key_no_action"),
|
||||||
|
pytest.param(False, True, 2, id="released_key_full_action"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_toggle_modifier_grab(
|
||||||
|
self,
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
is_pressed_key,
|
||||||
|
expects_remove_call,
|
||||||
|
expects_timeout_calls,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager._toggle_modifier_grab with pressed and released keys."""
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
mock_keyboard_event = test_context.Mock()
|
||||||
|
mock_keyboard_event.keyval_name = "Insert"
|
||||||
|
mock_keyboard_event.hw_code = 110
|
||||||
|
mock_keyboard_event.modifiers = 0
|
||||||
|
mock_keyboard_event.is_pressed_key.return_value = is_pressed_key
|
||||||
|
mock_remove = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "remove_modifier_grab", new=mock_remove)
|
||||||
|
mock_add_grab = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "add_modifier_grab", new=mock_add_grab)
|
||||||
|
mock_timeout = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.GLib.timeout_add", new=mock_timeout)
|
||||||
|
test_context.patch(
|
||||||
|
"cthulhu.cthulhu_modifier_manager.Atspi.generate_keyboard_event",
|
||||||
|
side_effect=lambda *args, **kwargs: None,
|
||||||
|
)
|
||||||
|
|
||||||
|
manager._toggle_modifier_grab(mock_keyboard_event)
|
||||||
|
|
||||||
|
if expects_remove_call:
|
||||||
|
mock_remove.assert_called_once_with("Insert")
|
||||||
|
assert mock_timeout.call_count == expects_timeout_calls
|
||||||
|
if expects_timeout_calls > 0:
|
||||||
|
timeout_calls = mock_timeout.call_args_list
|
||||||
|
assert timeout_calls[0][0][0] == 1
|
||||||
|
assert timeout_calls[0][0][2] == 110 # hw_code
|
||||||
|
if expects_timeout_calls > 1:
|
||||||
|
assert timeout_calls[1][0][0] == 500
|
||||||
|
assert timeout_calls[1][0][2] == "Insert" # modifier name
|
||||||
|
else:
|
||||||
|
mock_remove.assert_not_called()
|
||||||
|
assert mock_timeout.call_count == expects_timeout_calls
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"keyval_name, is_pressed, expected_modifier",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
"Caps_Lock",
|
||||||
|
True,
|
||||||
|
1 << 1,
|
||||||
|
id="caps_lock_pressed", # 1 << Atspi.ModifierType.SHIFTLOCK
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
"Shift_Lock",
|
||||||
|
True,
|
||||||
|
1 << 0,
|
||||||
|
id="shift_lock_pressed", # 1 << Atspi.ModifierType.SHIFT
|
||||||
|
),
|
||||||
|
pytest.param("Caps_Lock", False, None, id="caps_lock_released"),
|
||||||
|
pytest.param("Other_Key", True, None, id="other_key"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_toggle_modifier_lock(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
keyval_name,
|
||||||
|
is_pressed,
|
||||||
|
expected_modifier,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager._toggle_modifier_lock."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.Atspi.ModifierType.SHIFTLOCK", new=1)
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.Atspi.ModifierType.SHIFT", new=0)
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
mock_keyboard_event = test_context.Mock()
|
||||||
|
mock_keyboard_event.keyval_name = keyval_name
|
||||||
|
mock_keyboard_event.hw_code = 110
|
||||||
|
mock_keyboard_event.modifiers = 0
|
||||||
|
mock_keyboard_event.is_pressed_key.return_value = is_pressed
|
||||||
|
mock_timeout = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.GLib.timeout_add", new=mock_timeout)
|
||||||
|
manager._toggle_modifier_lock(mock_keyboard_event)
|
||||||
|
if expected_modifier is not None:
|
||||||
|
mock_timeout.assert_called_once()
|
||||||
|
timeout_call = mock_timeout.call_args_list[0]
|
||||||
|
assert timeout_call[0][0] == 1 # 1ms delay
|
||||||
|
assert timeout_call[0][2] == 0 # modifiers
|
||||||
|
assert timeout_call[0][3] == expected_modifier # modifier value
|
||||||
|
else:
|
||||||
|
mock_timeout.assert_not_called()
|
||||||
|
|
||||||
|
def test_refresh_cthulhu_modifiers(self, test_context: CthulhuTestContext) -> None:
|
||||||
|
"""Test CthulhuModifierManager.refresh_cthulhu_modifiers."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
test_context.patch("os.environ", new={"DISPLAY": ":0"})
|
||||||
|
mock_popen = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
|
||||||
|
mock_process = test_context.Mock()
|
||||||
|
mock_process.communicate.return_value = (b"xmodmap_content", b"")
|
||||||
|
mock_context_manager = test_context.Mock()
|
||||||
|
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
|
||||||
|
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
|
||||||
|
mock_popen.return_value = mock_context_manager
|
||||||
|
mock_restore = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "_restore_original_xkbcomp", new=mock_restore)
|
||||||
|
mock_create = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "_create_cthulhu_xmodmap", new=mock_create)
|
||||||
|
manager.refresh_cthulhu_modifiers("test reason")
|
||||||
|
mock_restore.assert_called_once()
|
||||||
|
mock_popen.assert_called_once_with(
|
||||||
|
["xkbcomp", ":0", "-"],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
assert manager._original_xmodmap == b"xmodmap_content"
|
||||||
|
assert manager._modifiers_are_set is True
|
||||||
|
mock_create.assert_called_once()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"caps_lock_cthulhu, shift_lock_cthulhu, caps_cleared, expected_calls",
|
||||||
|
[
|
||||||
|
pytest.param(True, False, False, [("set_caps", True)], id="caps_lock_enable"),
|
||||||
|
pytest.param(False, True, False, [("set_caps", True)], id="shift_lock_enable"),
|
||||||
|
pytest.param(True, True, False, [("set_caps", True)], id="both_enable"),
|
||||||
|
pytest.param(
|
||||||
|
False,
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
[("set_caps", False)],
|
||||||
|
id="disable_previously_cleared",
|
||||||
|
),
|
||||||
|
pytest.param(False, False, False, [], id="no_changes"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_create_cthulhu_xmodmap(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
caps_lock_cthulhu,
|
||||||
|
shift_lock_cthulhu,
|
||||||
|
caps_cleared,
|
||||||
|
expected_calls,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager._create_cthulhu_xmodmap."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._caps_lock_cleared = caps_cleared
|
||||||
|
|
||||||
|
cthulhu_modifier_keys: list[str] = []
|
||||||
|
if caps_lock_cthulhu:
|
||||||
|
cthulhu_modifier_keys.append("Caps_Lock")
|
||||||
|
if shift_lock_cthulhu:
|
||||||
|
cthulhu_modifier_keys.append("Shift_Lock")
|
||||||
|
manager._modifier_keys_override = cthulhu_modifier_keys
|
||||||
|
mock_set_caps = test_context.Mock()
|
||||||
|
test_context.patch_object(manager, "set_caps_lock_as_cthulhu_modifier", new=mock_set_caps)
|
||||||
|
manager._create_cthulhu_xmodmap()
|
||||||
|
for call_type, enable in expected_calls:
|
||||||
|
if call_type == "set_caps":
|
||||||
|
mock_set_caps.assert_called_with(enable)
|
||||||
|
|
||||||
|
if caps_lock_cthulhu or shift_lock_cthulhu:
|
||||||
|
assert manager._caps_lock_cleared is True
|
||||||
|
elif caps_cleared and not caps_lock_cthulhu and not shift_lock_cthulhu:
|
||||||
|
assert manager._caps_lock_cleared is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"has_xmodmap, expects_popen_call",
|
||||||
|
[
|
||||||
|
pytest.param(True, True, id="with_xmodmap"),
|
||||||
|
pytest.param(False, False, id="no_xmodmap"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_unset_cthulhu_modifiers(
|
||||||
|
self,
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
has_xmodmap,
|
||||||
|
expects_popen_call,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.unset_cthulhu_modifiers with and without xmodmap."""
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
manager._modifiers_are_set = True
|
||||||
|
if has_xmodmap:
|
||||||
|
test_context.patch("os.environ", new={"DISPLAY": ":0"})
|
||||||
|
manager._original_xmodmap = b"original_xmodmap_content"
|
||||||
|
else:
|
||||||
|
manager._original_xmodmap = b""
|
||||||
|
|
||||||
|
essential_modules["cthulhu.debug"].reset_mock()
|
||||||
|
mock_popen = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
|
||||||
|
|
||||||
|
mock_unmap = test_context.Mock()
|
||||||
|
mock_iem = test_context.Mock()
|
||||||
|
mock_iem.unmap_all_modifiers = mock_unmap
|
||||||
|
test_context.patch(
|
||||||
|
"cthulhu.cthulhu_modifier_manager.input_event_manager.get_manager",
|
||||||
|
return_value=mock_iem,
|
||||||
|
)
|
||||||
|
|
||||||
|
if has_xmodmap:
|
||||||
|
mock_process = test_context.Mock()
|
||||||
|
mock_context_manager = test_context.Mock()
|
||||||
|
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
|
||||||
|
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
|
||||||
|
mock_popen.return_value = mock_context_manager
|
||||||
|
|
||||||
|
manager.unset_cthulhu_modifiers("test reason" if has_xmodmap else "")
|
||||||
|
|
||||||
|
assert manager._modifiers_are_set is False
|
||||||
|
mock_unmap.assert_called_once()
|
||||||
|
if expects_popen_call:
|
||||||
|
mock_popen.assert_called_once_with(
|
||||||
|
["xkbcomp", "-w0", "-", ":0"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=None,
|
||||||
|
stderr=None,
|
||||||
|
)
|
||||||
|
mock_process.communicate.assert_called_once_with(b"original_xmodmap_content")
|
||||||
|
assert manager._caps_lock_cleared is False
|
||||||
|
else:
|
||||||
|
mock_popen.assert_not_called()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"enable, xmodmap_content, expects_popen_call, expected_content",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
True,
|
||||||
|
"""interpret Caps_Lock+AnyOfOrNone(all) {
|
||||||
|
action= LockMods(modifiers=Lock);
|
||||||
|
};""",
|
||||||
|
True,
|
||||||
|
b"NoAction()",
|
||||||
|
id="enable_caps_lock",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
False,
|
||||||
|
"""interpret Caps_Lock+AnyOfOrNone(all) {
|
||||||
|
action= NoAction();
|
||||||
|
};""",
|
||||||
|
True,
|
||||||
|
b"LockMods(modifiers=Lock)",
|
||||||
|
id="disable_caps_lock",
|
||||||
|
),
|
||||||
|
pytest.param(
|
||||||
|
True,
|
||||||
|
"some other xmodmap content",
|
||||||
|
False,
|
||||||
|
None,
|
||||||
|
id="no_changes_needed",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_set_caps_lock_as_cthulhu_modifier(
|
||||||
|
self,
|
||||||
|
test_context: CthulhuTestContext,
|
||||||
|
enable,
|
||||||
|
xmodmap_content,
|
||||||
|
expects_popen_call,
|
||||||
|
expected_content,
|
||||||
|
) -> None:
|
||||||
|
"""Test CthulhuModifierManager.set_caps_lock_as_cthulhu_modifier with various scenarios."""
|
||||||
|
essential_modules: dict[str, MagicMock] = self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager = cthulhu_modifier_manager.CthulhuModifierManager()
|
||||||
|
if expects_popen_call:
|
||||||
|
test_context.patch("os.environ", new={"DISPLAY": ":0"})
|
||||||
|
|
||||||
|
manager._original_xmodmap = xmodmap_content.encode("UTF-8")
|
||||||
|
essential_modules["cthulhu.debug"].reset_mock()
|
||||||
|
mock_popen = test_context.Mock()
|
||||||
|
test_context.patch("cthulhu.cthulhu_modifier_manager.subprocess.Popen", new=mock_popen)
|
||||||
|
|
||||||
|
if expects_popen_call:
|
||||||
|
mock_process = test_context.Mock()
|
||||||
|
mock_context_manager = test_context.Mock()
|
||||||
|
mock_context_manager.__enter__ = test_context.Mock(return_value=mock_process)
|
||||||
|
mock_context_manager.__exit__ = test_context.Mock(return_value=None)
|
||||||
|
mock_popen.return_value = mock_context_manager
|
||||||
|
|
||||||
|
manager.set_caps_lock_as_cthulhu_modifier(enable)
|
||||||
|
|
||||||
|
if expects_popen_call:
|
||||||
|
mock_popen.assert_called_once_with(
|
||||||
|
["xkbcomp", "-w0", "-", ":0"],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=None,
|
||||||
|
stderr=None,
|
||||||
|
)
|
||||||
|
called_data = mock_process.communicate.call_args[0][0]
|
||||||
|
assert expected_content in called_data
|
||||||
|
else:
|
||||||
|
mock_popen.assert_not_called()
|
||||||
|
|
||||||
|
def test_get_manager(
|
||||||
|
self,
|
||||||
|
test_context,
|
||||||
|
) -> None:
|
||||||
|
"""Test cthulhu_modifier_manager.get_manager."""
|
||||||
|
|
||||||
|
self._setup_dependencies(test_context)
|
||||||
|
from cthulhu import cthulhu_modifier_manager
|
||||||
|
|
||||||
|
manager1 = cthulhu_modifier_manager.get_manager()
|
||||||
|
assert manager1 is not None
|
||||||
|
assert isinstance(manager1, cthulhu_modifier_manager.CthulhuModifierManager)
|
||||||
|
|
||||||
|
manager2 = cthulhu_modifier_manager.get_manager()
|
||||||
|
assert manager2 is manager1
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user