Tightened up Steam notifications. Fixed a system notifications regression.?

This commit is contained in:
Storm Dragon
2026-04-04 19:04:45 -04:00
parent 518d2b3bb6
commit f4af54228a
4 changed files with 204 additions and 8 deletions

View File

@@ -34,6 +34,7 @@ __license__ = "LGPL"
import cthulhu.messages as messages
import cthulhu.scripts.default as default
import cthulhu.settings as settings
from cthulhu.ax_object import AXObject
from cthulhu.ax_utilities import AXUtilities
@@ -49,8 +50,15 @@ class Script(default.Script):
"""Callback for window:create accessibility events."""
allLabels = AXUtilities.find_all_labels(event.source)
texts = [self.utilities.displayedText(acc) for acc in allLabels]
text = f"{messages.NOTIFICATION} {' '.join(texts)}"
texts = []
for acc in allLabels:
text = self.utilities.displayedText(acc) or AXObject.get_name(acc)
if text:
texts.append(text)
text = messages.NOTIFICATION
if texts:
text = f"{text} {' '.join(texts)}"
voice = self.speechGenerator.voice(obj=event.source, string=text)
self.speakMessage(text, voice=voice)

View File

@@ -203,19 +203,24 @@ class Script(Chromium.Script):
if AXUtilities.is_notification(obj) or AXUtilities.is_alert(obj):
return obj
def isNotificationRole(candidate):
return AXUtilities.is_notification(candidate) or AXUtilities.is_alert(candidate)
ancestorNotification = AXObject.find_ancestor(obj, isNotificationRole)
if ancestorNotification:
return ancestorNotification
liveAttr = AXObject.get_attribute(obj, 'live')
containerLive = AXObject.get_attribute(obj, 'container-live')
if liveAttr in ['assertive', 'polite'] or containerLive in ['assertive', 'polite']:
return obj
def isNotificationCandidate(candidate):
if AXUtilities.is_notification(candidate) or AXUtilities.is_alert(candidate):
return True
def isLiveRegionCandidate(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, isNotificationCandidate)
return AXObject.find_ancestor(obj, isLiveRegionCandidate)
def _presentSteamNotification(self, obj):
"""Speak and save the notification.
@@ -363,6 +368,27 @@ class Script(Chromium.Script):
return f"{baseText} {timestampText}"
return f"{baseText}. {timestampText}"
def _getSteamNotificationIdentity(self, obj):
if obj is None:
return None
try:
path = AXObject.get_path(obj)
except Exception:
path = None
if path is not None:
return tuple(path)
try:
return hash(obj)
except TypeError:
return id(obj)
def _combineSteamNotificationFragments(self, firstText, secondText):
combined = f"{firstText} {secondText}"
return self._normalizeSteamNotificationText(combined)
def _steamTextContains(self, text, other):
textNorm = self._normalizeSteamNotificationText(text).lower()
otherNorm = self._normalizeSteamNotificationText(other).lower()
@@ -376,6 +402,7 @@ class Script(Chromium.Script):
text = self._normalizeSteamNotificationText(text)
if not text:
return
sourceKey = self._getSteamNotificationIdentity(obj)
pending = self._steamPendingNotification
if self._isSteamRelativeTimestamp(text):
@@ -386,6 +413,7 @@ class Script(Chromium.Script):
pending["text"] = self._appendSteamTimestamp(pending["text"], text)
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
self._resetSteamPendingTimer()
return
@@ -393,7 +421,8 @@ class Script(Chromium.Script):
"text": text,
"obj": obj,
"timerId": None,
"timestampOnly": True
"timestampOnly": True,
"sourceKey": sourceKey
}
self._resetSteamPendingTimer()
return
@@ -405,24 +434,39 @@ class Script(Chromium.Script):
pending["timestampOnly"] = False
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
self._resetSteamPendingTimer()
return
if text == pendingText:
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
return
if self._steamTextContains(text, pendingText):
pending["text"] = text
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
self._resetSteamPendingTimer()
return
if self._steamTextContains(pendingText, text):
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
return
# Steam often emits multi-line toasts as separate live-region fragments.
# Keep fragments from the same toast together instead of speaking the first
# line immediately and the complete toast later.
if sourceKey is not None and sourceKey == pending.get("sourceKey"):
pending["text"] = self._combineSteamNotificationFragments(pendingText, text)
if obj:
pending["obj"] = obj
pending["sourceKey"] = sourceKey
self._resetSteamPendingTimer()
return
self._flushSteamPendingNotification(fromTimer=False)
@@ -431,7 +475,8 @@ class Script(Chromium.Script):
"text": text,
"obj": obj,
"timerId": None,
"timestampOnly": False
"timestampOnly": False,
"sourceKey": sourceKey
}
self._resetSteamPendingTimer()