6 Commits

19 changed files with 534 additions and 51 deletions
-1
View File
@@ -59,7 +59,6 @@ toolkit, OpenOffice/LibreOffice, Gecko, WebKitGtk, and KDE Qt toolkit.
* **pluggy** - Plugin and hook calling mechanisms for Python
* **python-dasbus** - D-Bus remote control interface (optional)
* **libpeas** - Plugin loader library
### Audio and Speech
-1
View File
@@ -19,7 +19,6 @@ This package requires the following packages, all available from SlackBuilds.org
- gst-plugins-good
- gtk3
- liblouis
- libpeas
- libwnck3
- python3-atspi
- python3-cairo
+1 -1
View File
@@ -5,6 +5,6 @@ DOWNLOAD="https://git.stormux.org/storm/cthulhu.git"
MD5SUM="SKIP"
DOWNLOAD_x86_64=""
MD5SUM_x86_64=""
REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libpeas libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher"
REQUIRES="at-spi2-core brltty gobject-introspection gsettings-desktop-schemas gstreamer gst-plugins-base gst-plugins-good gtk3 liblouis libwnck3 python3-atspi python3-cairo python3-gobject python3-setproctitle speech-dispatcher"
MAINTAINER="Storm Dragon"
EMAIL="storm_dragon@stormux.org"
-8
View File
@@ -89,14 +89,6 @@ else
summary += {'sound support': 'no (missing gstreamer)'}
endif
# Check for libpeas
libpeas_dep = dependency('libpeas-1.0', required: false)
if libpeas_dep.found()
summary += {'plugin loading': 'yes (found libpeas)'}
else
summary += {'plugin loading': 'no (missing libpeas)'}
endif
# Integration with session startup
i18n.merge_file(
input: 'cthulhu-autostart.desktop.in',
+22 -3
View File
@@ -1,7 +1,26 @@
# Install sound theme files
# Themes are installed to: {datadir}/cthulhu/sounds/{theme_name}/
install_subdir(
'default',
install_dir: get_option('datadir') / 'cthulhu' / 'sounds'
# Use explicit install_data for better compatibility across Meson versions
default_theme_sounds = files(
'default/browse_mode.wav',
'default/button.wav',
'default/checkbox_checked.wav',
'default/checkbox_mixed.wav',
'default/checkbox_unchecked.wav',
'default/combobox.wav',
'default/focus_mode.wav',
'default/radiobutton_checked.wav',
'default/radiobutton_unchecked.wav',
'default/start.wav',
'default/stop.wav',
'default/switch_off.wav',
'default/switch_on.wav',
'default/togglebutton_checked.wav',
'default/togglebutton_unchecked.wav',
)
install_data(
default_theme_sounds,
install_dir: get_option('datadir') / 'cthulhu' / 'sounds' / 'default'
)
+11 -11
View File
@@ -3776,6 +3776,17 @@
<property name="position">8</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="aiTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">AI Assistant</property>
</object>
<packing>
<property name="position">8</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="ocrGrid">
<property name="visible">True</property>
@@ -3951,17 +3962,6 @@
<property name="position">9</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="aiTabLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">AI Assistant</property>
</object>
<packing>
<property name="position">8</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="ocrTabLabel">
<property name="visible">True</property>
+17
View File
@@ -3088,9 +3088,26 @@ class CthulhuSetupGUI(cthulhu_gtkbuilder.GtkBuilderWrapper):
input_event_manager.get_manager().unmap_all_modifiers()
except Exception:
pass
self._unmapCthulhuModifiersForCapture()
editable.connect('key-press-event', self.kbKeyPressed)
return
def _unmapCthulhuModifiersForCapture(self):
"""Unmap Cthulhu modifier keys so they can be captured as bindings."""
device = cthulhu_state.device
if device is None:
return
for modifierKey in settings.cthulhuModifierKeys:
keycode = keybindings.getKeycode(modifierKey)
if keycode == 0 and modifierKey == "Shift_Lock":
keycode = keybindings.getKeycode("Caps_Lock")
if keycode:
try:
device.unmap_modifier(keycode)
except Exception:
pass
def editingCanceledKey(self, editable):
"""Stops user input of a Key for a selected key binding"""
+46 -3
View File
@@ -60,6 +60,7 @@ class EventManager:
self._active = False
self._enqueueCount = 0
self._dequeueCount = 0
self._cmdlineCache = {}
self._eventQueue = queue.Queue(0)
self._gidleId = 0
self._gidleLock = threading.Lock()
@@ -175,6 +176,43 @@ class EventManager:
return False
def _getAppCmdline(self, app):
pid = AXObject.get_process_id(app)
if pid == -1:
return ""
cmdline = self._cmdlineCache.get(pid)
if cmdline is None:
cmdline = debug.getCmdline(pid)
self._cmdlineCache[pid] = cmdline
return cmdline
def _isSteamApp(self, app):
name = AXObject.get_name(app)
if not name:
nameLower = ""
else:
nameLower = name.lower()
if "steamwebhelper" in nameLower or nameLower in ("steam", "steam web helper"):
return True
cmdline = self._getAppCmdline(app)
return "steamwebhelper" in cmdline
def _isSteamNotificationEvent(self, event):
for obj in (event.any_data, event.source):
if not isinstance(obj, Atspi.Accessible):
continue
if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj):
return True
liveAttr = AXObject.get_attribute(obj, 'live')
containerLive = AXObject.get_attribute(obj, 'container-live')
if liveAttr in ('assertive', 'polite') or containerLive in ('assertive', 'polite'):
return True
return False
def _ignore(self, event):
"""Returns True if this event should be ignored."""
@@ -241,9 +279,14 @@ class EventManager:
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
if script.app != app:
msg = 'EVENT MANAGER: Ignoring because event is not from active app'
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
# Allow Steam notifications from inactive apps.
if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
msg = 'EVENT MANAGER: Allowing Steam notification from inactive app'
debug.printMessage(debug.LEVEL_INFO, msg, True)
else:
msg = 'EVENT MANAGER: Ignoring because event is not from active app'
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
if event.type.startswith('object:text-changed') \
and self.EMBEDDED_OBJECT_CHARACTER in event.any_data \
+12 -12
View File
@@ -479,23 +479,23 @@ class FlatReviewPresenter:
bindings.add(
keybindings.KeyBinding(
"",
"f",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
self._handlers.get("showContentsHandler")))
bindings.add(
keybindings.KeyBinding(
"",
"c",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_CTRL_MODIFIER_MASK,
self._handlers.get("flatReviewCopyHandler")))
bindings.add(
keybindings.KeyBinding(
"",
"c",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
self._handlers.get("flatReviewAppendHandler")))
bindings.add(
@@ -685,23 +685,23 @@ class FlatReviewPresenter:
bindings.add(
keybindings.KeyBinding(
"",
"f",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
self._handlers.get("showContentsHandler")))
bindings.add(
keybindings.KeyBinding(
"",
"c",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_CTRL_MODIFIER_MASK,
self._handlers.get("flatReviewCopyHandler")))
bindings.add(
keybindings.KeyBinding(
"",
"c",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_CTRL_MODIFIER_MASK | keybindings.SHIFT_MODIFIER_MASK,
self._handlers.get("flatReviewAppendHandler")))
bindings.add(
+4 -4
View File
@@ -126,9 +126,9 @@ class NotificationPresenter:
bindings.add(
keybindings.KeyBinding(
"",
"n",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_MODIFIER_MASK,
self._handlers.get("present_last_notification")))
bindings.add(
@@ -147,9 +147,9 @@ class NotificationPresenter:
bindings.add(
keybindings.KeyBinding(
"",
"n",
keybindings.defaultModifierMask,
keybindings.NO_MODIFIER_MASK,
keybindings.CTHULHU_CTRL_MODIFIER_MASK,
self._handlers.get("show_notification_list")))
return bindings
+6
View File
@@ -113,6 +113,12 @@ class ScriptManager:
debug.printMessage(debug.LEVEL_INFO, msg, True)
return None
pid = AXObject.get_process_id(app)
if pid != -1:
cmdline = debug.getCmdline(pid)
if "steamwebhelper" in cmdline:
return "steamwebhelper"
altNames = list(self._appNames.keys())
if name.endswith(".py") or name.endswith(".bin"):
name = name.split('.')[0]
+1
View File
@@ -40,5 +40,6 @@ __all__ = ['Banshee',
'soffice',
'SeaMonkey',
'smuxi-frontend-gnome',
'steamwebhelper',
'Thunderbird',
'xfwm4']
+1
View File
@@ -25,4 +25,5 @@ subdir('notify-osd')
subdir('pidgin')
subdir('soffice')
subdir('smuxi-frontend-gnome')
subdir('steamwebhelper')
subdir('xfwm4')
@@ -0,0 +1 @@
from .script import Script
@@ -0,0 +1,9 @@
steamwebhelper_python_sources = files([
'__init__.py',
'script.py',
])
python3.install_sources(
steamwebhelper_python_sources,
subdir: 'cthulhu/scripts/apps/steamwebhelper'
)
@@ -0,0 +1,305 @@
#!/usr/bin/env python3
#
# Copyright (c) 2024 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA 02110-1301 USA.
#
# Cthulhu project: https://git.stormux.org/storm/cthulhu
"""Custom script for Steam Web Helper (Steam client UI)."""
__id__ = "$Id$"
__version__ = "$Revision$"
__date__ = "$Date$"
__copyright__ = "Copyright (c) 2024 Stormux"
__license__ = "LGPL"
import time
from cthulhu import debug
from cthulhu import settings
from cthulhu import cthulhu_state
from cthulhu import settings_manager
from cthulhu import structural_navigation
from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
from cthulhu.ax_utilities_role import AXUtilitiesRole
from cthulhu.scripts.toolkits import Chromium
settingsManager = settings_manager.getManager()
class SteamStructuralNavigation(structural_navigation.StructuralNavigation):
def _headingGetter(self, document, arg=None):
def isUsefulHeading(candidate):
if not self._script.utilities.inDocumentContent(candidate):
return False
text = self._script.utilities.displayedText(candidate) or AXObject.get_name(candidate)
if not text:
return False
return not text.strip().isdigit()
if arg is None:
return super()._headingGetter(document, arg)
headings = super()._headingGetter(document, None)
headings = [heading for heading in headings if isUsefulHeading(heading)]
return [heading for heading in headings if self._script.utilities.headingLevel(heading) == arg]
class Script(Chromium.Script):
"""Script for Steam Web Helper with background notification support."""
def __init__(self, app):
"""Creates a new script for the Steam Web Helper application.
Arguments:
- app: the application to create a script for.
"""
super().__init__(app)
# CRITICAL: Enable background event processing for notifications
# This allows Steam notifications to be spoken even when Steam
# is not the focused application.
self.presentIfInactive = True
self._lastSteamNotification = ("", 0.0)
def onShowingChanged(self, event):
"""Callback for object:state-changed:showing accessibility events."""
# Check if this is a Steam notification/alert becoming visible
if event.detail1 and self._isSteamNotification(event.source):
self._presentSteamNotification(event.source)
return
# Fall through to Chromium/web handling
super().onShowingChanged(event)
def onChildrenAdded(self, event):
"""Callback for object:children-changed:add accessibility events."""
# Check if a notification element was added
if self._isSteamNotification(event.any_data):
self._presentSteamNotification(event.any_data)
return
super().onChildrenAdded(event)
def getStructuralNavigation(self):
types = self.getEnabledStructuralNavigationTypes()
enable = settingsManager.getSetting('structuralNavigationEnabled')
return SteamStructuralNavigation(self, types, enable)
def onFocusedChanged(self, event):
"""Callback for object:state-changed:focused accessibility events."""
if event.detail1 and AXUtilities.is_document_web(event.source):
prevDocument = self.utilities.getDocumentForObject(cthulhu_state.locusOfFocus)
if prevDocument and AXUtilities.is_embedded(prevDocument) \
and self.utilities.inDocumentContent(cthulhu_state.locusOfFocus):
msg = "STEAM: Ignoring focus event for top-level document while in embedded content"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
if event.detail1 \
and AXUtilitiesRole.is_button(event.source) \
and cthulhu_state.locusOfFocus in AXUtilitiesRelation.get_is_labelled_by(event.source):
msg = "STEAM: Ignoring focus event for labelled button"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
return super().onFocusedChanged(event)
def onTextInserted(self, event):
"""Callback for object:text-changed:insert accessibility events."""
if self._presentSteamLiveRegionText(event):
return
super().onTextInserted(event)
def _isSteamNotification(self, obj):
"""Detect if object is a Steam notification.
Arguments:
- obj: the accessible object to check
Returns True if obj appears to be a notification element.
"""
if obj is None:
return False
# Check for NOTIFICATION or ALERT roles
if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj):
msg = "STEAM: Found notification/alert role"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
# Check for live region with assertive/polite politeness
# (Steam may use ARIA live regions for toasts)
liveAttr = AXObject.get_attribute(obj, 'live')
containerLive = AXObject.get_attribute(obj, 'container-live')
if liveAttr in ['assertive', 'polite'] or containerLive in ['assertive', 'polite']:
politeness = liveAttr or containerLive
msg = f"STEAM: Found live region with politeness: {politeness}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
def isNotificationCandidate(candidate):
if AXUtilities.is_notification(candidate) or AXUtilities.is_alert(candidate):
return True
candidateLive = AXObject.get_attribute(candidate, 'live')
candidateContainerLive = AXObject.get_attribute(candidate, 'container-live')
return candidateLive in ['assertive', 'polite'] or candidateContainerLive in ['assertive', 'polite']
ancestor = AXObject.find_ancestor(obj, isNotificationCandidate)
if ancestor:
msg = "STEAM: Found notification ancestor"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return True
return False
def _presentSteamNotification(self, obj):
"""Speak and save the notification.
Arguments:
- obj: the notification accessible object
"""
text = self._getNotificationText(obj)
if not text:
msg = "STEAM: Notification had no text content"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
self._presentSteamNotificationText(text, obj)
def _presentSteamLiveRegionText(self, event):
if not isinstance(event.any_data, str):
return False
text = event.any_data.strip()
if not text:
return False
if not self._isSteamLiveRegion(event.source):
return False
msg = f"STEAM: Live region text inserted: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
self._presentSteamNotificationText(text, event.source)
return True
def _isSteamLiveRegion(self, obj):
if not obj:
return False
liveAttr = AXObject.get_attribute(obj, 'live')
containerLive = AXObject.get_attribute(obj, 'container-live')
if liveAttr in ['assertive', 'polite'] or containerLive in ['assertive', 'polite']:
return True
def isLiveRegion(candidate):
candidateLive = AXObject.get_attribute(candidate, 'live')
candidateContainerLive = AXObject.get_attribute(candidate, 'container-live')
return candidateLive in ['assertive', 'polite'] \
or candidateContainerLive in ['assertive', 'polite']
return AXObject.find_ancestor(obj, isLiveRegion) is not None
def _presentSteamNotificationText(self, text, obj):
if self._isDuplicateSteamNotification(text):
msg = "STEAM: Suppressing duplicate notification"
debug.printMessage(debug.LEVEL_INFO, msg, True)
return
notificationMsg = f"Steam: {text}"
# Speak the notification
voice = self.speechGenerator.voice(obj=obj, string=notificationMsg)
self.speakMessage(notificationMsg, voice=voice)
# Display on braille
self.displayBrailleMessage(notificationMsg, flashTime=settings.brailleFlashTime)
# Save to notification history
self.notificationPresenter.save_notification(notificationMsg)
msg = f"STEAM: Presented notification: {text}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
def _isDuplicateSteamNotification(self, text):
lastText, lastTime = self._lastSteamNotification
now = time.monotonic()
if text == lastText and (now - lastTime) < 1.0:
return True
self._lastSteamNotification = (text, now)
return False
def _getNotificationText(self, obj):
"""Extract text from notification element.
Arguments:
- obj: the notification accessible object
Returns the text content of the notification, or empty string if none found.
"""
def getText(candidate):
name = AXObject.get_name(candidate)
if name:
return name
return self.utilities.displayedText(candidate)
# Try getting name directly
text = getText(obj)
if text:
return text
# Try getting all text from labels/text elements
texts = []
for label in AXUtilities.find_all_labels(obj):
text = getText(label)
if text:
texts.append(text)
if texts:
return ' '.join(texts)
def isNamedButton(candidate):
if not AXUtilitiesRole.is_button(candidate):
return False
return bool(getText(candidate))
descendantButton = AXObject.find_descendant(obj, isNamedButton)
if descendantButton:
return getText(descendantButton)
ancestorButton = AXObject.find_ancestor(obj, isNamedButton)
if ancestorButton:
return getText(ancestorButton)
def hasReadableText(candidate):
return bool(getText(candidate))
descendantText = AXObject.find_descendant(obj, hasReadableText)
if descendantText:
return getText(descendantText)
# Fall back to displayed text of the object itself
return ""
@@ -330,6 +330,11 @@ class Utilities(script_utilities.Utilities):
if len(documents) == 1:
document = documents[0]
if focusDoc and focusDoc != document and documentHasUri(focusDoc):
tokens = ["WEB: Single showing document differs; using focus-based document:",
focusDoc]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
return focusDoc
if documentHasUri(document):
return document
+73 -3
View File
@@ -99,14 +99,59 @@ class SoundThemeManager:
self._systemSoundsDir = None
self._userSoundsDir = None
def _deriveDatadirFromModulePath(self):
"""Derive the datadir from where this module is actually installed.
If module is at /usr/local/lib/python3.x/site-packages/cthulhu/,
then datadir should be /usr/local/share.
"""
try:
modulePath = os.path.dirname(os.path.abspath(__file__))
# Walk up looking for lib/python* pattern
currentPath = modulePath
for _ in range(10): # Limit iterations to avoid infinite loop
parent = os.path.dirname(currentPath)
if parent == currentPath:
break
dirName = os.path.basename(currentPath)
# Check if we're in a lib directory
if dirName == 'lib' or dirName.startswith('lib64'):
# Parent of lib is the prefix
prefix = parent
datadir = os.path.join(prefix, 'share')
if os.path.isdir(datadir):
return datadir
currentPath = parent
except Exception:
pass
return None
def getSystemSoundsDir(self):
"""Get system sounds directory from platform settings."""
"""Get system sounds directory from platform settings.
First tries cthulhu_platform.datadir, then derives from module location.
"""
if self._systemSoundsDir is None:
datadir = None
# First try the configured datadir from cthulhu_platform
try:
from . import cthulhu_platform
datadir = getattr(cthulhu_platform, 'datadir', '/usr/share')
datadir = getattr(cthulhu_platform, 'datadir', None)
except ImportError:
pass
# If datadir points to user home, it's likely wrong - try to derive it
if datadir and (datadir.startswith(os.path.expanduser('~')) or
'/home/' in datadir):
derivedDatadir = self._deriveDatadirFromModulePath()
if derivedDatadir:
datadir = derivedDatadir
# Fallback to /usr/share if nothing else works
if not datadir:
datadir = '/usr/share'
self._systemSoundsDir = os.path.join(datadir, 'cthulhu', 'sounds')
return self._systemSoundsDir
@@ -123,10 +168,20 @@ class SoundThemeManager:
Returns list of theme names (folder names) from both system and user dirs.
User themes with same name as system themes will override system themes.
Also checks standard fallback paths.
"""
themes = set()
for baseDir in [self.getSystemSoundsDir(), self.getUserSoundsDir()]:
# Check configured paths plus standard fallback locations
# /usr/share is standard, /usr/local/share is non-standard
searchDirs = [
self.getUserSoundsDir(),
self.getSystemSoundsDir(),
'/usr/share/cthulhu/sounds',
'/usr/local/share/cthulhu/sounds',
]
for baseDir in searchDirs:
if os.path.isdir(baseDir):
try:
for entry in os.listdir(baseDir):
@@ -143,15 +198,30 @@ class SoundThemeManager:
"""Get the path to a theme directory.
User themes take precedence over system themes.
Checks multiple standard locations as fallbacks.
"""
# Check user directory first
userPath = os.path.join(self.getUserSoundsDir(), themeName)
if os.path.isdir(userPath):
return userPath
# Check the configured system directory
systemPath = os.path.join(self.getSystemSoundsDir(), themeName)
if os.path.isdir(systemPath):
return systemPath
# Fallback: check standard system paths in case cthulhu_platform.datadir
# doesn't match where sounds are actually installed
# Check /usr/share first (standard), then /usr/local/share (non-standard)
fallbackPaths = [
'/usr/share/cthulhu/sounds',
'/usr/local/share/cthulhu/sounds',
]
for fallbackDir in fallbackPaths:
fallbackPath = os.path.join(fallbackDir, themeName)
if os.path.isdir(fallbackPath):
return fallbackPath
return None
def getSoundPath(self, themeName, soundName):
+20 -4
View File
@@ -37,6 +37,7 @@ gi.require_version("Atspi", "2.0")
from gi.repository import Atspi
from . import cmdnames
from . import cthulhu
from . import dbus_service
from . import debug
from . import guilabels
@@ -1175,23 +1176,38 @@ class StructuralNavigation:
objPath = AXObject.get_path(obj)
objRole = AXObject.get_role(obj)
if objRole == Atspi.Role.INVALID:
return obj, characterOffset
return None, characterOffset
self._script.utilities.setCaretPosition(obj, characterOffset)
AXObject.clear_cache(obj)
if not AXUtilities.is_defunct(obj):
if not AXUtilities.is_defunct(obj) and not self._script.utilities.isZombie(obj):
return obj, characterOffset
tokens = ["STRUCTURAL NAVIGATION:", obj, "became defunct after setting caret position"]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
replicant = self._script.utilities.get_objectFromPath(objPath)
if replicant and AXObject.get_role(replicant) == objRole:
if replicant and AXObject.get_role(replicant) == objRole \
and not self._script.utilities.isZombie(replicant):
tokens = ["STRUCTURAL NAVIGATION: Updating obj to replicant", replicant]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
obj = replicant
return obj, characterOffset
return obj, characterOffset
document = getattr(self._script.utilities, "documentFrame", None)
searchForCaretContext = getattr(self._script.utilities, "searchForCaretContext", None)
if document and searchForCaretContext:
documentFrame = document()
if documentFrame and not self._script.utilities.isZombie(documentFrame):
fallbackObj, fallbackOffset = searchForCaretContext(documentFrame)
if fallbackObj and not self._script.utilities.isZombie(fallbackObj):
tokens = ["STRUCTURAL NAVIGATION: Recovering focus to", fallbackObj]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
cthulhu.setLocusOfFocus(None, fallbackObj, notifyScript=False)
self._script.utilities.setCaretContext(
fallbackObj, fallbackOffset, documentFrame)
return None, characterOffset
def _presentLine(self, obj, offset):
"""Presents the first line of the object to the user.