A few more web tweaks. Updated version stuff, made a small script to update all version strings.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user