diff --git a/src/cthulhu/cthulhuVersion.py b/src/cthulhu/cthulhuVersion.py index 9ae1fe8..6045de0 100644 --- a/src/cthulhu/cthulhuVersion.py +++ b/src/cthulhu/cthulhuVersion.py @@ -23,5 +23,5 @@ # Fork of Orca Screen Reader (GNOME) # Original source: https://gitlab.gnome.org/GNOME/orca -version = "2025.08.06" +version = "2025.08.10" codeName = "testing" diff --git a/src/cthulhu/scripts/web/script.py b/src/cthulhu/scripts/web/script.py index 6749618..b995157 100644 --- a/src/cthulhu/scripts/web/script.py +++ b/src/cthulhu/scripts/web/script.py @@ -250,6 +250,12 @@ class Script(default.Script): Script.toggleLayoutMode, cmdnames.TOGGLE_LAYOUT_MODE) + self.inputEventHandlers["activateClickableHandler"] = \ + input_event.InputEventHandler( + Script.activateClickableElement, + "Activate clickable element") + + def getBookmarks(self): """Returns the "bookmarks" class for this script.""" @@ -566,7 +572,14 @@ class Script(default.Script): self._lastCommandWasStructNav = False self._lastCommandWasMouseButton = False - return super().consumesKeyboardEvent(keyboardEvent) + # Check parent first + consumes = super().consumesKeyboardEvent(keyboardEvent) + + # If parent doesn't consume Return key, try our clickable fallback + if not consumes and keyboardEvent.event_string == "Return": + return self._tryClickableActivation(keyboardEvent) + + return consumes def getEnabledKeyBindings(self): all = super().getEnabledKeyBindings() @@ -1321,6 +1334,76 @@ class Script(default.Script): self._browseModeIsSticky = False self.refreshKeyGrabs() + def _tryClickableActivation(self, keyboardEvent): + """Try to activate clickable element - returns True if we should consume the event.""" + + obj = cthulhu_state.locusOfFocus + if not obj or not self.utilities.inDocumentContent(obj): + return False + + # Skip form controls where Return should have normal behavior + from cthulhu import ax_utilities + if (ax_utilities.AXUtilities.is_entry(obj) or + ax_utilities.AXUtilities.is_text(obj) or + ax_utilities.AXUtilities.is_password_text(obj) or + ax_utilities.AXUtilities.is_combo_box(obj) or + ax_utilities.AXUtilities.is_button(obj) or + ax_utilities.AXUtilities.is_push_button(obj) or + ax_utilities.AXUtilities.is_link(obj)): + return False + + # Try clickable activation for non-standard clickable elements + # Store focus information before clicking for potential restoration + original_focus = obj + + # First try the standard clickable detection + if self.utilities.isClickableElement(obj): + from cthulhu import ax_event_synthesizer + result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) + if result: + self.presentMessage("Element activated") + # Try to restore focus to the clicked element after a brief delay + self._restoreFocusAfterClick(original_focus) + return True + + # If that didn't work, try a more permissive approach for any element with click action + from cthulhu import ax_object + if ax_object.AXObject.has_action(obj, "click"): + from cthulhu import ax_event_synthesizer + result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj) + if result: + self.presentMessage("Element activated") + # Try to restore focus to the clicked element after a brief delay + self._restoreFocusAfterClick(original_focus) + return True + + return False + + 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 + + def restore_focus(): + try: + from cthulhu import ax_object + if not ax_object.AXObject.is_dead(original_focus): + original_focus.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 + + except Exception: + pass # Focus restoration is best effort + + def activateClickableElement(self, inputEvent): + """Activates clickable element at current focus via Return key.""" + return self._tryClickableActivation(inputEvent) + def locusOfFocusChanged(self, event, oldFocus, newFocus): """Handles changes of focus of interest to the script."""