Attempt to speed up Steam even more. Sound for link added.

This commit is contained in:
Storm Dragon
2026-04-08 06:41:24 -04:00
parent ee4292564a
commit 6e049e35e2
10 changed files with 349 additions and 11 deletions
+33 -2
View File
@@ -914,6 +914,37 @@ class PluginSystemManager:
inactive_plugins = [p.get_module_name() for p in self.plugins if not p.loaded]
logger.info(f"Inactive plugins after sync: {inactive_plugins}")
@staticmethod
def _lifecycle_accepts_plugin_instance(method: Any) -> bool:
"""Return True if the bound lifecycle method expects a plugin argument."""
try:
parameters = list(inspect.signature(method).parameters.values())
except (TypeError, ValueError):
return True
if any(parameter.kind == inspect.Parameter.VAR_POSITIONAL for parameter in parameters):
return True
positional_parameters = [
parameter for parameter in parameters
if parameter.kind in (
inspect.Parameter.POSITIONAL_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD,
)
]
return bool(positional_parameters)
def _callPluginLifecycle(self, pluginInstance: Any, methodName: str) -> None:
"""Call a plugin lifecycle method without replaying it through pluggy."""
method = getattr(pluginInstance, methodName)
if self._lifecycle_accepts_plugin_instance(method):
method(pluginInstance)
return
method()
def loadPlugin(self, pluginInfo):
"""Load a plugin."""
module_name = pluginInfo.get_module_name()
@@ -998,7 +1029,7 @@ class PluginSystemManager:
logger.info(f"Activating plugin: {module_name}")
# Lifecycle is per-plugin. Broadcasting through pluggy replays
# activate() on every previously-registered plugin.
plugin_instance.activate(plugin_instance)
self._callPluginLifecycle(plugin_instance, "activate")
except Exception as e:
logger.error(f"Error activating plugin {module_name}: {e}")
import traceback
@@ -1039,7 +1070,7 @@ class PluginSystemManager:
try:
# Mirror targeted activation and only deactivate the plugin
# instance being unloaded.
plugin_instance.deactivate(plugin_instance)
self._callPluginLifecycle(plugin_instance, "deactivate")
except Exception as e:
logger.error(f"Error deactivating plugin {module_name}: {e}")
+55 -3
View File
@@ -41,7 +41,6 @@ import gi
import locale
import math
import re
import subprocess
import time
from difflib import SequenceMatcher
from typing import Any, Callable, Generator, Optional, TYPE_CHECKING
@@ -200,7 +199,8 @@ class Utilities:
return ""
try:
cmdline = subprocess.getoutput(f"cat /proc/{pid}/cmdline")
with open(f"/proc/{pid}/cmdline", "rb") as cmdline_file:
cmdline = cmdline_file.read().decode("utf-8", errors="replace")
except Exception:
return ""
@@ -211,7 +211,9 @@ class Utilities:
return False
app = AXObject.get_application(window)
tokens = ["SCRIPT UTILITIES: Looking at", window, "from", app, self._getAppCommandLine(app)]
tokens = ["SCRIPT UTILITIES: Looking at", window, "from", app]
if debug.debugLevel <= debug.LEVEL_INFO:
tokens.append(self._getAppCommandLine(app))
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if clearCache:
@@ -3082,6 +3084,56 @@ class Utilities:
return "".join(adjustedLine)
def getLinkIndicatorPresentation(self, obj: Atspi.Accessible, line: Any, startOffset: Any):
"""Return the spoken text and any link icons for a line fragment."""
spoken = self.adjustForLinks(obj, line, startOffset)
roleSoundPresentation = cthulhu.cthulhuApp.settingsManager.getSetting('roleSoundPresentation')
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY:
return spoken, []
if not cthulhu.cthulhuApp.settingsManager.getSetting('enableSound'):
return spoken, []
if not AXObject.supports_hypertext(obj):
return spoken, []
from . import sound_theme_manager
manager = sound_theme_manager.getManager()
if manager is None:
return spoken, []
endOffset = startOffset + len(line)
icons = []
missingIcon = False
for link in AXHypertext.get_all_links(obj):
start_index = AXHypertext.get_link_start_offset(link)
end_index = AXHypertext.get_link_end_offset(link)
if start_index < 0 or end_index < 0:
continue
if startOffset < end_index <= endOffset:
pass
elif startOffset <= start_index < endOffset:
pass
else:
continue
icon = manager.getLinkSoundIcon(visited=AXUtilities.is_visited(link))
if icon:
icons.append(icon)
else:
missingIcon = True
if not icons:
return spoken, []
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY and not missingIcon:
return line, icons
return spoken, icons
def _processMultiCaseString(self, string: Any) -> Any:
return re.sub(r'(?<=[a-z])(?=[A-Z])', ' ', string)
+2 -1
View File
@@ -2402,7 +2402,7 @@ class Script(script.Script):
voice = self.speechGenerator.voice(
obj=obj, string=string, language=language, dialect=dialect)
string = self.utilities.adjustForLinks(obj, string, start)
string, linkIcons = self.utilities.getLinkIndicatorPresentation(obj, string, start)
string = self.utilities.adjustForRepeats(string)
if self.utilities.shouldVerbalizeAllPunctuation(obj):
string = self.utilities.verbalizeAllPunctuation(string)
@@ -2414,6 +2414,7 @@ class Script(script.Script):
result = [string]
result.extend(voice)
result.extend(linkIcons)
utterance.append(result)
speech.speak(utterance)
else:
+1 -1
View File
@@ -570,7 +570,7 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
roleSoundIcon = None
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
and soundEnabled:
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
roleSoundIcon = self._getRoleSoundIcon(obj, role)
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
and soundEnabled:
# Stateful controls present their role via state sounds; suppress
+18
View File
@@ -303,6 +303,24 @@ class SoundThemeManager:
return None
def getLinkSoundIcon(self, visited=False, themeName=None):
"""Return an Icon for a plain or visited link sound from the current theme."""
themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default'
if themeName == THEME_NONE:
return None
candidates = ["link"]
if visited:
candidates.insert(0, "link_visited")
for candidate in candidates:
soundPath = self.getSoundPath(themeName, candidate)
if soundPath:
return Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
return None
def getRoleStateSoundIcon(self, role, stateKey, themeName=None):
"""Return an Icon for the role/state sound from the current theme, if any."""
themeName = themeName or self.app.getSettingsManager().getSetting('soundTheme') or 'default'
+13 -2
View File
@@ -118,6 +118,16 @@ class SpeechGenerator(generator.Generator):
def __init__(self, script):
generator.Generator.__init__(self, script, "speech")
def _getRoleSoundIcon(self, obj, role):
"""Return the themed sound icon for obj's role, if any."""
if role == Atspi.Role.LINK or AXUtilities.is_link(obj):
return sound_theme_manager.getManager().getLinkSoundIcon(
visited=AXUtilities.is_visited(obj)
)
return sound_theme_manager.getManager().getRoleSoundIcon(role)
def _addGlobals(self, globalsDict):
"""Other things to make available from the formatting string.
"""
@@ -676,7 +686,7 @@ class SpeechGenerator(generator.Generator):
roleSoundIcon = None
if roleSoundPresentation != settings.ROLE_SOUND_PRESENTATION_SPEECH_ONLY \
and soundEnabled:
roleSoundIcon = sound_theme_manager.getManager().getRoleSoundIcon(role)
roleSoundIcon = self._getRoleSoundIcon(obj, role)
if roleSoundPresentation == settings.ROLE_SOUND_PRESENTATION_SOUND_ONLY \
and soundEnabled:
# Stateful controls present their role via state sounds; suppress
@@ -1442,9 +1452,10 @@ class SpeechGenerator(generator.Generator):
args.pop("string")
voice = self.voice(string=string, obj=obj, **args)
string = self._script.utilities.adjustForLinks(obj, string, start)
string, linkIcons = self._script.utilities.getLinkIndicatorPresentation(obj, string, start)
rv = [self._script.utilities.adjustForRepeats(string)]
rv.extend(voice)
rv.extend(linkIcons)
# TODO - JD: speech.speak() has a bug which causes a list of utterances to
# be presented before a string+voice pair that comes first. Until we can