Improve modifier key detection for speech interruption.
This commit is contained in:
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.05.21"
|
||||
version = "2026.05.22"
|
||||
code_name = "testing"
|
||||
|
||||
@@ -162,6 +162,7 @@ class driver(inputDriver):
|
||||
self.fenrir_keys = set()
|
||||
self.failed_grabs = 0
|
||||
self.modifier_state = 0
|
||||
self.modifier_interrupt_state = 0
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
@@ -194,6 +195,7 @@ class driver(inputDriver):
|
||||
)
|
||||
self.num_lock_mask = self.find_num_lock_mask()
|
||||
self.refresh_modifier_state()
|
||||
self.modifier_interrupt_state = self.modifier_state
|
||||
self.refresh_interesting_keys()
|
||||
self.refresh_grabs(force=True)
|
||||
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
||||
@@ -274,6 +276,7 @@ class driver(inputDriver):
|
||||
while active.value:
|
||||
try:
|
||||
self.refresh_grabs()
|
||||
self.poll_modifier_interrupt_keys(event_queue)
|
||||
if not self.display.pending_events():
|
||||
time.sleep(0.01)
|
||||
continue
|
||||
@@ -371,6 +374,67 @@ class driver(inputDriver):
|
||||
"event_x_time": getattr(event, "time", X.CurrentTime),
|
||||
}
|
||||
|
||||
def poll_modifier_interrupt_keys(self, event_queue):
|
||||
if not self.active or not self.should_poll_modifier_interrupt_keys():
|
||||
return
|
||||
try:
|
||||
pointer = self.root.query_pointer()
|
||||
current_state = getattr(pointer, "mask", 0)
|
||||
except Exception:
|
||||
return
|
||||
previous_state = self.modifier_interrupt_state
|
||||
self.modifier_interrupt_state = current_state
|
||||
self.modifier_state = current_state
|
||||
for key_name, modifier_mask in self.interrupt_modifier_masks():
|
||||
if current_state & modifier_mask and not previous_state & modifier_mask:
|
||||
self.queue_modifier_interrupt_event(event_queue, key_name, 1)
|
||||
elif previous_state & modifier_mask and not current_state & modifier_mask:
|
||||
self.queue_modifier_interrupt_event(event_queue, key_name, 0)
|
||||
|
||||
def should_poll_modifier_interrupt_keys(self):
|
||||
try:
|
||||
settings_manager = self.env["runtime"]["SettingsManager"]
|
||||
except Exception:
|
||||
return False
|
||||
if not settings_manager.get_setting_as_bool(
|
||||
"keyboard", "interrupt_on_key_press"
|
||||
):
|
||||
return False
|
||||
return (
|
||||
settings_manager.get_setting(
|
||||
"keyboard", "interrupt_on_key_press_filter"
|
||||
).strip()
|
||||
== ""
|
||||
)
|
||||
|
||||
def interrupt_modifier_masks(self):
|
||||
return [
|
||||
("KEY_CTRL", X.ControlMask),
|
||||
("KEY_SHIFT", X.ShiftMask),
|
||||
("KEY_ALT", X.Mod1Mask),
|
||||
]
|
||||
|
||||
def queue_modifier_interrupt_event(self, event_queue, key_name, event_state):
|
||||
event_time = time.time()
|
||||
input_event = {
|
||||
"event_name": key_name,
|
||||
"event_value": 0,
|
||||
"event_sec": int(event_time),
|
||||
"event_usec": int((event_time % 1) * 1000000),
|
||||
"event_state": event_state,
|
||||
"event_type": X.KeyPress if event_state == 1 else X.KeyRelease,
|
||||
"event_raw_state": self.modifier_state,
|
||||
"event_x_time": X.CurrentTime,
|
||||
"event_synthetic": True,
|
||||
}
|
||||
self.env["input"]["event_buffer"].append(input_event.copy())
|
||||
event_queue.put(
|
||||
{
|
||||
"Type": FenrirEventType.keyboard_input,
|
||||
"data": input_event,
|
||||
}
|
||||
)
|
||||
|
||||
def refresh_modifier_state(self):
|
||||
try:
|
||||
pointer = self.root.query_pointer()
|
||||
@@ -612,6 +676,9 @@ class driver(inputDriver):
|
||||
value: key for key, value in self.key_name_overrides.items()
|
||||
}
|
||||
aliases = {
|
||||
"KEY_CTRL": ["Control_L", "Control_R"],
|
||||
"KEY_SHIFT": ["Shift_L", "Shift_R"],
|
||||
"KEY_ALT": ["Alt_L", "Alt_R"],
|
||||
"KEY_META": ["Super_L", "Super_R", "Meta_L", "Meta_R"],
|
||||
"KEY_LEFTMETA": ["Super_L", "Meta_L"],
|
||||
"KEY_RIGHTMETA": ["Super_R", "Meta_R"],
|
||||
@@ -659,6 +726,8 @@ class driver(inputDriver):
|
||||
def replay_key_event(self, event):
|
||||
if not isinstance(event, dict):
|
||||
return
|
||||
if event.get("event_synthetic"):
|
||||
return
|
||||
if event.get("event_type") != X.KeyPress:
|
||||
return
|
||||
try:
|
||||
|
||||
@@ -165,6 +165,15 @@ def test_x11_keycode_mapping_detects_keypad_insert():
|
||||
def test_x11_key_name_to_keysym_names_includes_numlock_aliases():
|
||||
x11 = X11Driver()
|
||||
|
||||
assert x11.key_name_to_keysym_names("KEY_CTRL") == [
|
||||
"Control_L",
|
||||
"Control_R",
|
||||
]
|
||||
assert x11.key_name_to_keysym_names("KEY_SHIFT") == [
|
||||
"Shift_L",
|
||||
"Shift_R",
|
||||
]
|
||||
assert x11.key_name_to_keysym_names("KEY_ALT") == ["Alt_L", "Alt_R"]
|
||||
assert x11.key_name_to_keysym_names("KEY_KP0") == ["KP_0", "KP_Insert"]
|
||||
assert "KP_Delete" in x11.key_name_to_keysym_names("KEY_KPDOT")
|
||||
assert "Caps_Lock" in x11.key_name_to_keysym_names("KEY_CAPSLOCK")
|
||||
@@ -217,6 +226,44 @@ def test_x11_build_passive_grabs_for_fenrir_keys_and_shortcuts():
|
||||
assert ("KEY_BACKSPACE", X.Mod4Mask, True) in grabs
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_poll_modifier_interrupt_keys_emits_press_and_release():
|
||||
x11 = X11Driver()
|
||||
x11.active = True
|
||||
x11.modifier_interrupt_state = 0
|
||||
x11.modifier_state = 0
|
||||
x11.root = Mock()
|
||||
x11.root.query_pointer.return_value = Mock(mask=X.ControlMask)
|
||||
settings_manager = Mock()
|
||||
settings_manager.get_setting_as_bool.return_value = True
|
||||
settings_manager.get_setting.return_value = ""
|
||||
x11.env = {
|
||||
"input": {"event_buffer": []},
|
||||
"runtime": {"SettingsManager": settings_manager},
|
||||
}
|
||||
event_queue = Mock()
|
||||
|
||||
x11.poll_modifier_interrupt_keys(event_queue)
|
||||
|
||||
event_queue.put.assert_called_once()
|
||||
event = event_queue.put.call_args.args[0]
|
||||
assert event["data"]["event_name"] == "KEY_CTRL"
|
||||
assert event["data"]["event_state"] == 1
|
||||
assert x11.env["input"]["event_buffer"][0]["event_synthetic"] is True
|
||||
|
||||
event_queue.reset_mock()
|
||||
x11.env["input"]["event_buffer"] = []
|
||||
x11.root.query_pointer.return_value = Mock(mask=0)
|
||||
|
||||
x11.poll_modifier_interrupt_keys(event_queue)
|
||||
|
||||
event_queue.put.assert_called_once()
|
||||
event = event_queue.put.call_args.args[0]
|
||||
assert event["data"]["event_name"] == "KEY_CTRL"
|
||||
assert event["data"]["event_state"] == 0
|
||||
assert x11.env["input"]["event_buffer"][0]["event_synthetic"] is True
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_optional_modifier_masks_can_exclude_numlock():
|
||||
x11 = X11Driver()
|
||||
@@ -331,6 +378,30 @@ def test_x11_write_event_buffer_does_not_replay_key_release():
|
||||
assert x11.env["input"]["event_buffer"] == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_write_event_buffer_does_not_replay_synthetic_modifier_event():
|
||||
x11 = X11Driver()
|
||||
x11._initialized = True
|
||||
x11.display = Mock()
|
||||
x11.env = {
|
||||
"input": {
|
||||
"event_buffer": [
|
||||
{
|
||||
"event_name": "KEY_CTRL",
|
||||
"event_state": 1,
|
||||
"event_type": X.KeyPress,
|
||||
"event_synthetic": True,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
x11.write_event_buffer()
|
||||
|
||||
x11.display.allow_events.assert_not_called()
|
||||
assert x11.env["input"]["event_buffer"] == []
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_x11_map_event_keeps_x_event_time_for_replay():
|
||||
x11 = X11Driver()
|
||||
|
||||
Reference in New Issue
Block a user