Improve modifier key detection for speech interruption.
This commit is contained in:
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
version = "2026.05.21"
|
version = "2026.05.22"
|
||||||
code_name = "testing"
|
code_name = "testing"
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ class driver(inputDriver):
|
|||||||
self.fenrir_keys = set()
|
self.fenrir_keys = set()
|
||||||
self.failed_grabs = 0
|
self.failed_grabs = 0
|
||||||
self.modifier_state = 0
|
self.modifier_state = 0
|
||||||
|
self.modifier_interrupt_state = 0
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
@@ -194,6 +195,7 @@ class driver(inputDriver):
|
|||||||
)
|
)
|
||||||
self.num_lock_mask = self.find_num_lock_mask()
|
self.num_lock_mask = self.find_num_lock_mask()
|
||||||
self.refresh_modifier_state()
|
self.refresh_modifier_state()
|
||||||
|
self.modifier_interrupt_state = self.modifier_state
|
||||||
self.refresh_interesting_keys()
|
self.refresh_interesting_keys()
|
||||||
self.refresh_grabs(force=True)
|
self.refresh_grabs(force=True)
|
||||||
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
self.env["runtime"]["ProcessManager"].add_custom_event_thread(
|
||||||
@@ -274,6 +276,7 @@ class driver(inputDriver):
|
|||||||
while active.value:
|
while active.value:
|
||||||
try:
|
try:
|
||||||
self.refresh_grabs()
|
self.refresh_grabs()
|
||||||
|
self.poll_modifier_interrupt_keys(event_queue)
|
||||||
if not self.display.pending_events():
|
if not self.display.pending_events():
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
continue
|
continue
|
||||||
@@ -371,6 +374,67 @@ class driver(inputDriver):
|
|||||||
"event_x_time": getattr(event, "time", X.CurrentTime),
|
"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):
|
def refresh_modifier_state(self):
|
||||||
try:
|
try:
|
||||||
pointer = self.root.query_pointer()
|
pointer = self.root.query_pointer()
|
||||||
@@ -612,6 +676,9 @@ class driver(inputDriver):
|
|||||||
value: key for key, value in self.key_name_overrides.items()
|
value: key for key, value in self.key_name_overrides.items()
|
||||||
}
|
}
|
||||||
aliases = {
|
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_META": ["Super_L", "Super_R", "Meta_L", "Meta_R"],
|
||||||
"KEY_LEFTMETA": ["Super_L", "Meta_L"],
|
"KEY_LEFTMETA": ["Super_L", "Meta_L"],
|
||||||
"KEY_RIGHTMETA": ["Super_R", "Meta_R"],
|
"KEY_RIGHTMETA": ["Super_R", "Meta_R"],
|
||||||
@@ -659,6 +726,8 @@ class driver(inputDriver):
|
|||||||
def replay_key_event(self, event):
|
def replay_key_event(self, event):
|
||||||
if not isinstance(event, dict):
|
if not isinstance(event, dict):
|
||||||
return
|
return
|
||||||
|
if event.get("event_synthetic"):
|
||||||
|
return
|
||||||
if event.get("event_type") != X.KeyPress:
|
if event.get("event_type") != X.KeyPress:
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -165,6 +165,15 @@ def test_x11_keycode_mapping_detects_keypad_insert():
|
|||||||
def test_x11_key_name_to_keysym_names_includes_numlock_aliases():
|
def test_x11_key_name_to_keysym_names_includes_numlock_aliases():
|
||||||
x11 = X11Driver()
|
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 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 "KP_Delete" in x11.key_name_to_keysym_names("KEY_KPDOT")
|
||||||
assert "Caps_Lock" in x11.key_name_to_keysym_names("KEY_CAPSLOCK")
|
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
|
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
|
@pytest.mark.unit
|
||||||
def test_x11_optional_modifier_masks_can_exclude_numlock():
|
def test_x11_optional_modifier_masks_can_exclude_numlock():
|
||||||
x11 = X11Driver()
|
x11 = X11Driver()
|
||||||
@@ -331,6 +378,30 @@ def test_x11_write_event_buffer_does_not_replay_key_release():
|
|||||||
assert x11.env["input"]["event_buffer"] == []
|
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
|
@pytest.mark.unit
|
||||||
def test_x11_map_event_keeps_x_event_time_for_replay():
|
def test_x11_map_event_keeps_x_event_time_for_replay():
|
||||||
x11 = X11Driver()
|
x11 = X11Driver()
|
||||||
|
|||||||
Reference in New Issue
Block a user