From a80bca78d1b700877140563055a81b9f19fbb0ad Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Sun, 10 Aug 2025 21:53:49 -0400 Subject: [PATCH] Clickables now present a message when loading. Focus is preserved. Woogoo --- distro-packages/Arch-Linux/PKGBUILD | 2 +- src/cthulhu/scripts/web/script.py | 72 +++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/distro-packages/Arch-Linux/PKGBUILD b/distro-packages/Arch-Linux/PKGBUILD index 8dff3b8..fbfa809 100644 --- a/distro-packages/Arch-Linux/PKGBUILD +++ b/distro-packages/Arch-Linux/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Storm Dragon pkgname=cthulhu -pkgver=2025.08.05 +pkgver=2025.08.06 pkgrel=1 pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca" url="https://git.stormux.org/storm/cthulhu" diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index b995157..e7bb127 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -1359,9 +1359,12 @@ class Script(default.Script): # First try the standard clickable detection if self.utilities.isClickableElement(obj): from cthulhu import ax_event_synthesizer + # Give immediate feedback that activation is starting + self.presentMessage("Activating...") result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) if result: - self.presentMessage("Element activated") + # Schedule success message after a brief delay + self._presentDelayedMessage("Element activated", 50) # Try to restore focus to the clicked element after a brief delay self._restoreFocusAfterClick(original_focus) return True @@ -1370,9 +1373,12 @@ class Script(default.Script): from cthulhu import ax_object if ax_object.AXObject.has_action(obj, "click"): from cthulhu import ax_event_synthesizer + # Give immediate feedback that activation is starting + self.presentMessage("Activating...") result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) if result: - self.presentMessage("Element activated") + # Schedule success message after a brief delay + self._presentDelayedMessage("Element activated", 50) # Try to restore focus to the clicked element after a brief delay self._restoreFocusAfterClick(original_focus) return True @@ -1382,24 +1388,80 @@ class Script(default.Script): def _restoreFocusAfterClick(self, original_focus): """Try to restore focus after a click action that may have caused DOM changes.""" try: - # Schedule focus restoration after a brief delay to allow DOM changes to settle from gi.repository import GObject + from cthulhu import ax_object + + # Store the document and caret position to restore navigation context + document = None + caret_offset = -1 + try: + # Find the document root + obj = original_focus + while obj and not ax_object.AXObject.is_dead(obj): + if self.utilities.isDocument(obj): + document = obj + break + parent = ax_object.AXObject.get_parent(obj) + if parent == obj: # Avoid infinite loops + break + obj = parent + + # Get current caret position if available + if document: + try: + caret_offset = ax_object.AXObject.get_caret_offset(document) + except Exception: + pass + + except Exception: + pass def restore_focus(): try: - from cthulhu import ax_object + # First try direct focus restoration if object still exists if not ax_object.AXObject.is_dead(original_focus): original_focus.grabFocus() + return False + + # If we have document and caret info, try to restore position + if document and not ax_object.AXObject.is_dead(document) and caret_offset >= 0: + try: + # Set caret back to where it was + ax_object.AXObject.set_caret_offset(document, caret_offset) + # Focus the document to make sure screen reader tracks properly + document.grabFocus() + return False + except Exception: + pass + + # Last resort: try to focus the document if we have it + if document and not ax_object.AXObject.is_dead(document): + document.grabFocus() + except Exception: pass # Focus restoration is best effort return False # Don't repeat the timeout # Delay restoration to allow JavaScript and DOM changes to complete - GObject.timeout_add(100, restore_focus) # 100ms delay + GObject.timeout_add(150, restore_focus) # 150ms delay for DOM changes except Exception: pass # Focus restoration is best effort + def _presentDelayedMessage(self, message, delay_ms): + """Present a message after a specified delay in milliseconds.""" + try: + from gi.repository import GObject + + def present_message(): + self.presentMessage(message) + return False # Don't repeat + + GObject.timeout_add(delay_ms, present_message) + except Exception: + # If delay fails, present immediately + self.presentMessage(message) + def activateClickableElement(self, inputEvent): """Activates clickable element at current focus via Return key.""" return self._tryClickableActivation(inputEvent)