Clickables now present a message when loading. Focus is preserved. Woogoo
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
# Maintainer: Storm Dragon <storm_dragon@stormux.org>
|
||||||
|
|
||||||
pkgname=cthulhu
|
pkgname=cthulhu
|
||||||
pkgver=2025.08.05
|
pkgver=2025.08.06
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
pkgdesc="Desktop-agnostic screen reader with plugin system, forked from Orca"
|
||||||
url="https://git.stormux.org/storm/cthulhu"
|
url="https://git.stormux.org/storm/cthulhu"
|
||||||
|
|||||||
@@ -1359,9 +1359,12 @@ class Script(default.Script):
|
|||||||
# First try the standard clickable detection
|
# First try the standard clickable detection
|
||||||
if self.utilities.isClickableElement(obj):
|
if self.utilities.isClickableElement(obj):
|
||||||
from cthulhu import ax_event_synthesizer
|
from cthulhu import ax_event_synthesizer
|
||||||
|
# Give immediate feedback that activation is starting
|
||||||
|
self.presentMessage("Activating...")
|
||||||
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||||
if result:
|
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
|
# Try to restore focus to the clicked element after a brief delay
|
||||||
self._restoreFocusAfterClick(original_focus)
|
self._restoreFocusAfterClick(original_focus)
|
||||||
return True
|
return True
|
||||||
@@ -1370,9 +1373,12 @@ class Script(default.Script):
|
|||||||
from cthulhu import ax_object
|
from cthulhu import ax_object
|
||||||
if ax_object.AXObject.has_action(obj, "click"):
|
if ax_object.AXObject.has_action(obj, "click"):
|
||||||
from cthulhu import ax_event_synthesizer
|
from cthulhu import ax_event_synthesizer
|
||||||
|
# Give immediate feedback that activation is starting
|
||||||
|
self.presentMessage("Activating...")
|
||||||
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||||
if result:
|
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
|
# Try to restore focus to the clicked element after a brief delay
|
||||||
self._restoreFocusAfterClick(original_focus)
|
self._restoreFocusAfterClick(original_focus)
|
||||||
return True
|
return True
|
||||||
@@ -1382,24 +1388,80 @@ class Script(default.Script):
|
|||||||
def _restoreFocusAfterClick(self, original_focus):
|
def _restoreFocusAfterClick(self, original_focus):
|
||||||
"""Try to restore focus after a click action that may have caused DOM changes."""
|
"""Try to restore focus after a click action that may have caused DOM changes."""
|
||||||
try:
|
try:
|
||||||
# Schedule focus restoration after a brief delay to allow DOM changes to settle
|
|
||||||
from gi.repository import GObject
|
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():
|
def restore_focus():
|
||||||
try:
|
try:
|
||||||
from cthulhu import ax_object
|
# First try direct focus restoration if object still exists
|
||||||
if not ax_object.AXObject.is_dead(original_focus):
|
if not ax_object.AXObject.is_dead(original_focus):
|
||||||
original_focus.grabFocus()
|
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:
|
except Exception:
|
||||||
pass # Focus restoration is best effort
|
pass # Focus restoration is best effort
|
||||||
return False # Don't repeat the timeout
|
return False # Don't repeat the timeout
|
||||||
|
|
||||||
# Delay restoration to allow JavaScript and DOM changes to complete
|
# 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:
|
except Exception:
|
||||||
pass # Focus restoration is best effort
|
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):
|
def activateClickableElement(self, inputEvent):
|
||||||
"""Activates clickable element at current focus via Return key."""
|
"""Activates clickable element at current focus via Return key."""
|
||||||
return self._tryClickableActivation(inputEvent)
|
return self._tryClickableActivation(inputEvent)
|
||||||
|
|||||||
Reference in New Issue
Block a user