A few more web tweaks. Updated version stuff, made a small script to update all version strings.

This commit is contained in:
Storm Dragon
2025-12-27 18:49:01 -05:00
parent 71776ad24c
commit 88a88574ac
7 changed files with 164 additions and 82 deletions
+82 -31
View File
@@ -43,12 +43,13 @@ gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GLib, Gtk
from . import cmdnames
from . import dbus_service
from . import debug
from . import focus_manager
from . import guilabels
from . import input_event
from . import keybindings
from . import messages
from . import cthulhu
from . import cthulhu_state
from . import script_manager
from .ax_object import AXObject
@@ -121,16 +122,21 @@ class ActionList(Gtk.Window):
def populate_actions(self, actions: list[str]) -> None:
"""Populates the list with accessible actions."""
self._actions = actions
if isinstance(actions, dict):
items = list(actions.items())
else:
items = [(action, action) for action in actions]
self._actions = [name for name, _label in items]
# Clear existing items
for child in self._listbox.get_children():
self._listbox.remove(child)
# Add actions to list
for action in actions:
label = Gtk.Label(label=action)
for _action, label_text in items:
label = Gtk.Label(label=label_text)
label.set_xalign(0.0) # Left align
label.set_margin_left(10)
label.set_margin_right(10)
@@ -163,6 +169,11 @@ class ActionPresenter:
self._handlers = self.get_handlers(True)
# _bindings will be initialized lazily in get_bindings()
msg = "ACTION PRESENTER: Registering D-Bus commands."
debug.printMessage(debug.LEVEL_INFO, msg, True)
controller = dbus_service.get_remote_controller()
controller.register_decorated_module("ActionPresenter", self)
def get_handlers(self, refresh: bool = False) -> dict:
"""Returns a dictionary of input event handlers."""
@@ -226,9 +237,28 @@ class ActionPresenter:
script = script_manager.get_manager().get_script(app, self._obj)
script_manager.get_manager().set_active_script(script, reason)
# Update Cthulhu state
cthulhu_state.activeWindow = self._window
cthulhu_state.locusOfFocus = self._obj
manager = focus_manager.get_manager()
manager.clear_state(reason)
manager.set_active_window(self._window)
manager.set_locus_of_focus(None, self._obj)
def _present_message(self, script, full_message, brief_message=None, notify_user=True) -> None:
"""Presents a message using the provided script or the active script."""
if not notify_user:
return
if script is not None:
script.presentMessage(full_message, brief_message)
return
active_script = cthulhu_state.activeScript
if active_script is not None:
active_script.presentMessage(full_message, brief_message)
return
msg = "ACTION PRESENTER: Unable to present message (no script)."
debug.printMessage(debug.LEVEL_INFO, msg, True)
def _clear_gui_and_restore_focus(self) -> None:
"""Clears the GUI reference and then restores focus."""
@@ -252,7 +282,7 @@ class ActionPresenter:
# Use idle_add for asynchronous destruction to allow action to complete
GLib.idle_add(self._gui.destroy)
def present_with_time(self, obj, start_time: float) -> bool:
def present_with_time(self, obj, start_time: float, script=None, notify_user=True) -> bool:
"""Presents accessible actions for the given object with timing."""
try:
@@ -264,28 +294,36 @@ class ActionPresenter:
return False
if obj is None:
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: obj is None, using locusOfFocus", True)
obj = cthulhu_state.locusOfFocus
if obj is None:
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No object found, presenting NO_ACCESSIBLE_ACTIONS", True)
full_message = messages.NO_ACCESSIBLE_ACTIONS
cthulhu.presentMessage(full_message)
return False
msg = "ACTION PRESENTER: No object found, presenting LOCATION_NOT_FOUND"
debug.printMessage(debug.LEVEL_INFO, msg, True)
full_message = messages.LOCATION_NOT_FOUND_FULL
brief_message = messages.LOCATION_NOT_FOUND_BRIEF
self._present_message(script, full_message, brief_message, notify_user)
return True
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Getting actions for object: {obj}", True)
actions = AXObject.get_action_names(obj)
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions) if actions else 0} actions: {actions}", True)
actions = {}
for i in range(AXObject.get_n_actions(obj)):
name = AXObject.get_action_name(obj, i)
if not name:
continue
localized_name = AXObject.get_action_localized_name(obj, i)
description = AXObject.get_action_description(obj, i)
actions[name] = localized_name or description or name
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Found {len(actions)} actions: {actions}", True)
if not actions:
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: No actions found, presenting NO_ACCESSIBLE_ACTIONS", True)
full_message = messages.NO_ACCESSIBLE_ACTIONS
cthulhu.presentMessage(full_message)
return False
msg = "ACTION PRESENTER: No actions found, presenting NO_ACTIONS_FOUND_ON"
debug.printMessage(debug.LEVEL_INFO, msg, True)
name = AXObject.get_name(obj) or AXUtilities.get_localized_role_name(obj)
full_message = messages.NO_ACTIONS_FOUND_ON % name
self._present_message(script, full_message, notify_user=notify_user)
return True
debug.printMessage(debug.LEVEL_INFO, "ACTION PRESENTER: Creating GUI", True)
self._obj = obj
self._window = cthulhu_state.activeWindow
self._window = focus_manager.get_manager().get_active_window()
self._gui = ActionList(self)
self._gui.populate_actions(actions)
@@ -298,7 +336,13 @@ class ActionPresenter:
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Traceback: {traceback.format_exc()}", True)
return False
def show_actions_list(self, script: script.Script = None, input_event: input_event.InputEvent = None) -> bool:
@dbus_service.command
def show_actions_list(
self,
script: script.Script = None,
input_event: input_event.InputEvent = None,
notify_user: bool = True
) -> bool:
"""Shows the accessible actions list."""
try:
@@ -306,12 +350,19 @@ class ActionPresenter:
debug.printMessage(debug.LEVEL_INFO, msg, True)
start_time = time.time()
# Get object from input event if available, otherwise let present_with_time handle it
obj = input_event.get_object() if input_event else None
obj = None
if input_event is not None:
obj = input_event.get_object()
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: Object from input_event: {obj}", True)
result = self.present_with_time(obj, start_time)
if obj is None:
manager = focus_manager.get_manager()
_mode, obj = manager.get_active_mode_and_object_of_interest()
if obj is None:
obj = manager.get_locus_of_focus()
result = self.present_with_time(obj, start_time, script, notify_user)
debug.printMessage(debug.LEVEL_INFO, f"ACTION PRESENTER: show_actions_list returning: {result}", True)
return result
except Exception as e:
@@ -325,4 +376,4 @@ _presenter = ActionPresenter()
def getPresenter() -> ActionPresenter:
"""Returns the Action Presenter singleton."""
return _presenter
return _presenter
+1 -1
View File
@@ -23,5 +23,5 @@
# Forked from Orca screen reader.
# Cthulhu project: https://git.stormux.org/storm/cthulhu
version = "2025.12.09"
version = "2025.12.27"
codeName = "testing"
+21 -47
View File
@@ -31,11 +31,9 @@ __date__ = "$Date$"
__copyright__ = "Copyright (C) 2011-2013 Igalia, S.L."
__license__ = "LGPL"
import gi
gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from . import debug
from .ax_component import AXComponent
from .ax_hypertext import AXHypertext
from .ax_object import AXObject
from .ax_table import AXTable
from .ax_text import AXText
@@ -80,7 +78,9 @@ class LabelInference:
result, objects = self.inferFromTextLeft(obj)
debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Left: '{result}'", True)
if not result or self._preferRight(obj):
result, objects = self.inferFromTextRight(obj) or result
rightResult = self.inferFromTextRight(obj)
if rightResult[0] is not None:
result, objects = rightResult
debug.printMessage(debug.LEVEL_INFO, f"LABEL INFERENCE: Text Right: '{result}'", True)
if not result:
result, objects = self.inferFromTable(obj)
@@ -153,17 +153,12 @@ class LabelInference:
return False
def isMatch(x):
return x is not None \
and not self._script.utilities.isStaticTextLeaf(x) \
and not AXUtilities.is_link(x)
return AXUtilities.is_web_element(x) and not AXUtilities.is_link(x)
children = [child for child in AXObject.iter_children(obj, isMatch)]
children = list(AXObject.iter_children(obj, isMatch))
if len(children) > 1:
return False
if not AXObject.supports_text(obj):
return True
string = AXText.get_all_text(obj).strip()
if string.count(self._script.EMBEDDED_OBJECT_CHARACTER) > 1:
return False
@@ -191,19 +186,7 @@ class LabelInference:
if rv is not None:
return rv
widgetRoles = [Atspi.Role.CHECK_BOX,
Atspi.Role.RADIO_BUTTON,
Atspi.Role.TOGGLE_BUTTON,
Atspi.Role.COMBO_BOX,
Atspi.Role.LIST,
Atspi.Role.LIST_BOX,
Atspi.Role.MENU,
Atspi.Role.MENU_ITEM,
Atspi.Role.ENTRY,
Atspi.Role.PASSWORD_TEXT,
Atspi.Role.PUSH_BUTTON]
isWidget = AXObject.get_role(obj) in widgetRoles
isWidget = AXUtilities.is_widget(obj) or AXUtilities.is_menu_related(obj)
if not isWidget and AXUtilities.is_editable(obj):
isWidget = True
@@ -223,30 +206,15 @@ class LabelInference:
return rv
extents = 0, 0, 0, 0
text = self._script.utilities.queryNonEmptyText(obj)
if text and not AXUtilities.is_text_input(obj):
if AXObject.supports_text(obj) and not AXUtilities.is_text_input(obj):
if endOffset == -1:
endOffset = AXText.get_character_count(obj)
try:
extents = Atspi.Text.get_range_extents(
obj, startOffset, endOffset, Atspi.CoordType.SCREEN)
except Exception:
tokens = ["LABEL INFERENCE: Exception getting character count for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return extents
rect = AXText.get_range_rect(obj, startOffset, endOffset)
extents = rect.x, rect.y, rect.width, rect.height
if not (extents[2] and extents[3]):
if not AXObject.supports_component(obj):
tokens = ["LABEL INFERENCE:", obj, "does not support the component interface"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
else:
try:
ext = Atspi.Component.get_extents(obj, Atspi.CoordType.SCREEN)
extents = ext.x, ext.y, ext.width, ext.height
except Exception:
tokens = ["LABEL INFERENCE: Exception getting extents for", obj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
rect = AXComponent.get_rect(obj)
extents = rect.x, rect.y, rect.width, rect.height
self._extentsCache[(hash(obj), startOffset, endOffset)] = extents
return extents
@@ -278,10 +246,12 @@ class LabelInference:
key = hash(obj)
if self._isWidget(obj):
start, end = self._script.utilities.getHyperlinkRange(obj)
start = AXHypertext.get_link_start_offset(obj)
obj = AXObject.get_parent(obj)
rv = self._script.utilities.getLineContentsAtOffset(obj, start, True, False)
if rv is None:
rv = []
self._lineCache[key] = rv
return rv
@@ -408,6 +378,10 @@ class LabelInference:
if self._cannotLabel(prevObj):
return None, []
if string.endswith("\n"):
string = string[:-1]
end -= 1
if string.strip():
x, y, width, height = self._getExtents(prevObj, start, end)
objX, objY, objWidth, objHeight = self._getExtents(obj)
@@ -528,7 +502,7 @@ class LabelInference:
cellLeft = cellRight = cellAbove = cellBelow = None
gridrow = AXObject.find_ancestor(cell, self._isRow)
rowindex, colindex = self._script.utilities.coordinatesForCell(cell)
rowindex, colindex = AXTable.get_cell_coordinates(cell, prefer_attribute=False)
if colindex > -1:
cellLeft = self._getCellFromTable(grid, rowindex, colindex - 1)
cellRight = self._getCellFromTable(grid, rowindex, colindex + 1)
+5 -1
View File
@@ -3547,7 +3547,11 @@ class Utilities(script_utilities.Utilities):
if rv and not AXObject.get_name(obj) and AXObject.supports_text(obj):
string = AXText.get_all_text(obj)
if not string.strip():
if not string.replace("\ufffc", ""):
tokens = ["WEB:", obj, "is not clickable: its text is just EOCs"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
rv = False
elif not string.strip():
rv = not (AXUtilities.is_static(obj) or AXUtilities.is_link(obj))
self._isClickableElement[hash(obj)] = rv