Initial work on making Steam overlay accessible.
This commit is contained in:
@@ -328,6 +328,118 @@ class EventManager:
|
||||
def _isSteamNotificationEvent(self, event: Atspi.Event) -> bool:
|
||||
return self._isLiveOrNotificationEvent(event)
|
||||
|
||||
def _isSteamOverlayNavigationEvent(self, event: Atspi.Event, app: Atspi.Accessible) -> bool:
|
||||
if not self._isSteamApp(app):
|
||||
return False
|
||||
|
||||
if not event.type.startswith((
|
||||
"object:property-change:accessible-name",
|
||||
"object:active-descendant-changed",
|
||||
)):
|
||||
return False
|
||||
|
||||
if not self._activeX11WindowIsSteamSelectionContext():
|
||||
return False
|
||||
|
||||
return self._eventHasSteamQuickAccessContext(event)
|
||||
|
||||
def _eventHasSteamQuickAccessContext(self, event: Atspi.Event) -> bool:
|
||||
for obj in (event.source, getattr(event, "any_data", None)):
|
||||
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
|
||||
continue
|
||||
try:
|
||||
if self._hasSteamQuickAccessMarker(obj):
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _hasSteamQuickAccessMarker(obj: Any) -> bool:
|
||||
def hasMarker(candidate: Any) -> bool:
|
||||
if candidate is None:
|
||||
return False
|
||||
|
||||
name = AXObject.get_name(candidate) or ""
|
||||
if name.startswith("QuickAccess_uid"):
|
||||
return True
|
||||
|
||||
accessibleId = AXObject.get_attribute(candidate, "id") or ""
|
||||
if accessibleId.startswith("quickaccess_"):
|
||||
return True
|
||||
|
||||
className = AXObject.get_attribute(candidate, "class") or ""
|
||||
return "quickaccess" in className.casefold()
|
||||
|
||||
return AXObject.find_ancestor_inclusive(obj, hasMarker) is not None
|
||||
|
||||
def _activeX11WindowIsSteamSelectionContext(self) -> bool:
|
||||
window = input_event_manager.get_manager()._get_active_x11_window()
|
||||
return self._x11WindowHasSteamGameClass(window) or self._x11WindowIsSteamBigPicture(window)
|
||||
|
||||
@classmethod
|
||||
def _x11WindowHasSteamGameClass(cls, window: Any) -> bool:
|
||||
if window is None:
|
||||
return False
|
||||
|
||||
identifiers = [
|
||||
cls._safeSteamWindowCall(window, "get_class_group_name"),
|
||||
cls._safeSteamWindowCall(window, "get_class_instance_name"),
|
||||
]
|
||||
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
|
||||
if classGroup is not None:
|
||||
identifiers.extend([
|
||||
cls._safeSteamWindowCall(classGroup, "get_name"),
|
||||
cls._safeSteamWindowCall(classGroup, "get_res_class"),
|
||||
])
|
||||
|
||||
return any(cls._isSteamGameWindowIdentifier(identifier) for identifier in identifiers)
|
||||
|
||||
@classmethod
|
||||
def _x11WindowIsSteamBigPicture(cls, window: Any) -> bool:
|
||||
if window is None:
|
||||
return False
|
||||
|
||||
name = cls._safeSteamWindowCall(window, "get_name")
|
||||
if not isinstance(name, str) or name.strip() != "Steam Big Picture Mode":
|
||||
return False
|
||||
|
||||
identifiers = [
|
||||
cls._safeSteamWindowCall(window, "get_class_group_name"),
|
||||
cls._safeSteamWindowCall(window, "get_class_instance_name"),
|
||||
]
|
||||
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
|
||||
if classGroup is not None:
|
||||
identifiers.extend([
|
||||
cls._safeSteamWindowCall(classGroup, "get_name"),
|
||||
cls._safeSteamWindowCall(classGroup, "get_res_class"),
|
||||
])
|
||||
|
||||
normalized = {
|
||||
identifier.strip().casefold()
|
||||
for identifier in identifiers
|
||||
if isinstance(identifier, str)
|
||||
}
|
||||
return bool(normalized.intersection({"steam", "steamwebhelper"}))
|
||||
|
||||
@staticmethod
|
||||
def _safeSteamWindowCall(window: Any, attrName: str):
|
||||
attr = getattr(window, attrName, None)
|
||||
if not callable(attr):
|
||||
return None
|
||||
|
||||
try:
|
||||
return attr()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _isSteamGameWindowIdentifier(identifier: Any) -> bool:
|
||||
if not isinstance(identifier, str):
|
||||
return False
|
||||
return identifier.strip().casefold().startswith("steam_app_")
|
||||
|
||||
def _isLiveOrNotificationObject(self, obj: Any) -> bool:
|
||||
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
|
||||
return False
|
||||
@@ -559,18 +671,27 @@ class EventManager:
|
||||
if self._inDeluge() and self._ignoreDuringDeluge(event):
|
||||
return _ignore_with_reason("deluge", "event type during deluge")
|
||||
|
||||
isSteamOverlayNavigationEvent = self._isSteamOverlayNavigationEvent(event, app)
|
||||
script = cthulhu_state.activeScript
|
||||
if event.type.startswith('object:children-changed') \
|
||||
or event.type.startswith('object:state-changed:sensitive'):
|
||||
if not script:
|
||||
return _ignore_with_reason("no-active-script", "no active script")
|
||||
if script.app != app:
|
||||
if isSteamOverlayNavigationEvent:
|
||||
_log_allow("steam-overlay-navigation", "no active script")
|
||||
else:
|
||||
return _ignore_with_reason("no-active-script", "no active script")
|
||||
elif script.app != app:
|
||||
# Allow Steam notifications from inactive apps.
|
||||
if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
|
||||
_log_allow("steam-notification", "inactive app notification")
|
||||
elif isSteamOverlayNavigationEvent:
|
||||
_log_allow("steam-overlay-navigation", "inactive app overlay navigation")
|
||||
else:
|
||||
return _ignore_with_reason("inactive-app", "event not from active app")
|
||||
|
||||
if isSteamOverlayNavigationEvent:
|
||||
return _allow_with_reason("steam-overlay-navigation", "Steam overlay navigation event")
|
||||
|
||||
relevance = self._classifyRelevance(event, app)
|
||||
if relevance == self.RELEVANCE_DROP:
|
||||
return _ignore_with_reason("event-relevance-drop", "event is background churn")
|
||||
|
||||
@@ -29,15 +29,19 @@ __license__ = "LGPL"
|
||||
|
||||
import time
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from cthulhu import debug
|
||||
from cthulhu import input_event_manager
|
||||
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_selection import AXSelection
|
||||
from cthulhu.ax_text import AXText
|
||||
from cthulhu.ax_utilities import AXUtilities
|
||||
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
||||
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
||||
@@ -84,6 +88,10 @@ class Script(Chromium.Script):
|
||||
self._lastSteamNotification = ("", 0.0)
|
||||
self._steamPendingNotification = None
|
||||
self._steamNotificationDelayMs = 500
|
||||
self._lastSteamOverlaySelection = ("", 0.0)
|
||||
self._steamActiveQuickAccessTab = ""
|
||||
self._steamFriendsPanelActivationTarget = (None, "", 0.0)
|
||||
self._steamFriendsPanelActivationTargetMaxAge = 10.0
|
||||
self._steamRelativeTimePattern = re.compile(
|
||||
r"^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$",
|
||||
re.IGNORECASE
|
||||
@@ -118,6 +126,9 @@ class Script(Chromium.Script):
|
||||
self._presentSteamNotification(event.any_data)
|
||||
return
|
||||
|
||||
if self._presentSteamFriendsPanelNavigation(event):
|
||||
return True
|
||||
|
||||
if self._handleSteamVirtualizedListMutation(event):
|
||||
return True
|
||||
|
||||
@@ -156,12 +167,24 @@ class Script(Chromium.Script):
|
||||
|
||||
return super().onFocusedChanged(event)
|
||||
|
||||
def onNameChanged(self, event):
|
||||
"""Callback for object:property-change:accessible-name events."""
|
||||
|
||||
self._logSteamNavigationEvent("name-changed", event)
|
||||
if self._presentSteamFriendsPanelNavigation(event):
|
||||
return True
|
||||
|
||||
return super().onNameChanged(event)
|
||||
|
||||
def onTextInserted(self, event):
|
||||
"""Callback for object:text-changed:insert accessibility events."""
|
||||
|
||||
if self._presentSteamLiveRegionText(event):
|
||||
return
|
||||
|
||||
if self._presentSteamFriendsPanelNavigation(event):
|
||||
return True
|
||||
|
||||
super().onTextInserted(event)
|
||||
|
||||
def onCaretMoved(self, event):
|
||||
@@ -178,8 +201,18 @@ class Script(Chromium.Script):
|
||||
"""Callback for object:selection-changed accessibility events."""
|
||||
|
||||
self._logSteamNavigationEvent("selection-changed", event)
|
||||
if self._presentSteamOverlaySelection(event):
|
||||
return True
|
||||
return super().onSelectionChanged(event)
|
||||
|
||||
def onSelectedChanged(self, event):
|
||||
"""Callback for object:state-changed:selected accessibility events."""
|
||||
|
||||
self._logSteamNavigationEvent("selected-changed", event)
|
||||
if self._presentSteamOverlaySelection(event):
|
||||
return True
|
||||
return super().onSelectedChanged(event)
|
||||
|
||||
def onActiveDescendantChanged(self, event):
|
||||
"""Callback for object:active-descendant-changed accessibility events."""
|
||||
|
||||
@@ -203,6 +236,445 @@ class Script(Chromium.Script):
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
def _presentSteamOverlaySelection(self, event) -> bool:
|
||||
if not event:
|
||||
return False
|
||||
|
||||
eventType = getattr(event, "type", "")
|
||||
if not isinstance(eventType, str) or not (
|
||||
eventType.startswith("object:selection-changed")
|
||||
or eventType.startswith("object:state-changed:selected")
|
||||
):
|
||||
return False
|
||||
|
||||
if not self._activeX11WindowIsSteamSelectionContext():
|
||||
return False
|
||||
|
||||
if not self._steamOverlaySelectionWasUserInitiated():
|
||||
return False
|
||||
|
||||
obj = self._getSteamOverlaySelectionObject(event)
|
||||
if not obj or not self._isSteamOverlaySelectionObject(obj):
|
||||
return False
|
||||
|
||||
text = self._getSteamOverlaySelectionText(obj)
|
||||
if not text:
|
||||
return False
|
||||
|
||||
if AXUtilities.is_page_tab(obj):
|
||||
self._steamActiveQuickAccessTab = text
|
||||
self._clearSteamFriendsPanelActivationTarget()
|
||||
|
||||
if self._isDuplicateSteamOverlaySelection(text):
|
||||
msg = f"STEAM: Suppressing duplicate overlay selection: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
self.presentMessage(text, force=True, interrupt=True)
|
||||
msg = f"STEAM: Presented overlay selection: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
def _getSteamOverlaySelectionObject(self, event):
|
||||
if event.type.startswith("object:state-changed:selected"):
|
||||
if not event.detail1:
|
||||
return None
|
||||
return event.source
|
||||
|
||||
if not event.type.startswith("object:selection-changed"):
|
||||
return None
|
||||
|
||||
for child in AXSelection.get_selected_children(event.source):
|
||||
if child:
|
||||
return child
|
||||
|
||||
anyData = event.any_data
|
||||
if anyData is not None and not isinstance(anyData, (bool, int, float, str)):
|
||||
return anyData
|
||||
|
||||
return event.source
|
||||
|
||||
def _isSteamOverlaySelectionObject(self, obj) -> bool:
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
try:
|
||||
if not self.utilities.inDocumentContent(obj):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not self._hasSteamOverlaySelectionContext(obj):
|
||||
return False
|
||||
|
||||
if not (
|
||||
AXUtilities.is_page_tab(obj)
|
||||
or AXUtilities.is_list_item(obj)
|
||||
or AXUtilities.is_menu_item(obj)
|
||||
or AXUtilities.is_button(obj)
|
||||
or AXUtilities.is_push_button(obj)
|
||||
):
|
||||
return False
|
||||
|
||||
return bool(self._getSteamOverlaySelectionText(obj))
|
||||
|
||||
def _getSteamOverlaySelectionText(self, obj) -> str:
|
||||
text = self.utilities.displayedText(obj) or AXObject.get_name(obj) or ""
|
||||
text = self._normalizeSteamNotificationText(text)
|
||||
if text:
|
||||
return text
|
||||
|
||||
return self._getSteamQuickAccessTabLabel(obj)
|
||||
|
||||
def _getSteamQuickAccessTabLabel(self, obj) -> str:
|
||||
if not AXUtilities.is_page_tab(obj):
|
||||
return ""
|
||||
|
||||
if not self._hasSteamQuickAccessMarker(obj):
|
||||
return ""
|
||||
|
||||
tabId = AXObject.get_attribute(obj, "id") or ""
|
||||
quickAccessTabLabels = {
|
||||
"quickaccess_tab_3": "Friends",
|
||||
}
|
||||
label = quickAccessTabLabels.get(tabId, "")
|
||||
if label:
|
||||
msg = f"STEAM: Inferred QuickAccess tab label: {label}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
return label
|
||||
|
||||
def _presentSteamFriendsPanelNavigation(self, event) -> bool:
|
||||
if not self._steamFriendsPanelNavigationIsExpected(event):
|
||||
return False
|
||||
|
||||
obj = self._getSteamFriendsPanelNavigationObject(event)
|
||||
if not obj or not self._isSteamFriendsPanelNavigationObject(obj):
|
||||
return False
|
||||
|
||||
text = self._getSteamFriendsPanelNavigationText(obj)
|
||||
if not text:
|
||||
return False
|
||||
|
||||
self._rememberSteamFriendsPanelActivationTarget(obj, text)
|
||||
if self._isDuplicateSteamOverlaySelection(text):
|
||||
msg = f"STEAM: Suppressing duplicate Friends panel item: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
self.presentMessage(text, force=True, interrupt=True)
|
||||
msg = f"STEAM: Presented Friends panel item: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
|
||||
def _steamFriendsPanelNavigationIsExpected(self, event) -> bool:
|
||||
if not event:
|
||||
return False
|
||||
|
||||
eventType = getattr(event, "type", "")
|
||||
if not isinstance(eventType, str) or not eventType.startswith((
|
||||
"object:property-change:accessible-name",
|
||||
"object:active-descendant-changed",
|
||||
)):
|
||||
return False
|
||||
|
||||
if getattr(self, "_steamActiveQuickAccessTab", "") != "Friends":
|
||||
return False
|
||||
|
||||
if not self._activeX11WindowIsSteamSelectionContext():
|
||||
return False
|
||||
|
||||
return self._steamOverlaySelectionWasUserInitiated()
|
||||
|
||||
@staticmethod
|
||||
def _getSteamFriendsPanelNavigationObject(event):
|
||||
anyData = getattr(event, "any_data", None)
|
||||
if anyData is not None and not isinstance(anyData, (bool, int, float, str, bytes)):
|
||||
return anyData
|
||||
|
||||
return getattr(event, "source", None)
|
||||
|
||||
def _isSteamFriendsPanelNavigationObject(self, obj) -> bool:
|
||||
try:
|
||||
if not self._hasSteamQuickAccessMarker(obj):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if AXUtilities.is_page_tab(obj) or AXUtilities.is_page_tab_list(obj):
|
||||
return False
|
||||
|
||||
if self._isIgnoredSteamFriendsPanelObject(obj):
|
||||
return False
|
||||
|
||||
return (
|
||||
AXUtilities.is_list_item(obj)
|
||||
or AXUtilities.is_button(obj)
|
||||
or AXUtilities.is_push_button(obj)
|
||||
or AXUtilities.is_table_row(obj)
|
||||
)
|
||||
|
||||
def _isIgnoredSteamFriendsPanelObject(self, obj) -> bool:
|
||||
def isIgnored(candidate) -> bool:
|
||||
if candidate is None:
|
||||
return False
|
||||
|
||||
className = AXObject.get_attribute(candidate, "class") or ""
|
||||
accessibleId = AXObject.get_attribute(candidate, "id") or ""
|
||||
searchable = f"{className} {accessibleId}".casefold()
|
||||
ignoredMarkers = (
|
||||
"addfriendbutton",
|
||||
"chathistorycontainer",
|
||||
"chatdialogs",
|
||||
"chatwindow",
|
||||
"chatentry",
|
||||
"chatmessage",
|
||||
"messagelist",
|
||||
"messageinput",
|
||||
"textarea",
|
||||
)
|
||||
return any(marker in searchable for marker in ignoredMarkers)
|
||||
|
||||
try:
|
||||
return AXObject.find_ancestor_inclusive(obj, isIgnored) is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _getSteamFriendsPanelNavigationText(self, obj) -> str:
|
||||
text = self._getSteamFriendsPanelReadableText(obj)
|
||||
if self._isUsefulSteamFriendsPanelText(text):
|
||||
return text
|
||||
|
||||
return ""
|
||||
|
||||
def _rememberSteamFriendsPanelActivationTarget(self, obj: Any, text: str) -> None:
|
||||
self._steamFriendsPanelActivationTarget = (obj, text, time.monotonic())
|
||||
|
||||
def _clearSteamFriendsPanelActivationTarget(self) -> None:
|
||||
self._steamFriendsPanelActivationTarget = (None, "", 0.0)
|
||||
|
||||
def _getSteamFriendsPanelActivationTarget(self) -> Any:
|
||||
if getattr(self, "_steamActiveQuickAccessTab", "") != "Friends":
|
||||
return None
|
||||
|
||||
if not self._activeX11WindowIsSteamSelectionContext():
|
||||
return None
|
||||
|
||||
obj, text, timestamp = getattr(
|
||||
self,
|
||||
"_steamFriendsPanelActivationTarget",
|
||||
(None, "", 0.0),
|
||||
)
|
||||
if not obj or not text:
|
||||
return None
|
||||
|
||||
maxAge = getattr(self, "_steamFriendsPanelActivationTargetMaxAge", 10.0)
|
||||
if time.monotonic() - timestamp > maxAge:
|
||||
msg = f"STEAM: Clearing stale Friends panel activation target: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
self._clearSteamFriendsPanelActivationTarget()
|
||||
return None
|
||||
|
||||
try:
|
||||
if AXObject.is_dead(obj):
|
||||
self._clearSteamFriendsPanelActivationTarget()
|
||||
return None
|
||||
except Exception:
|
||||
self._clearSteamFriendsPanelActivationTarget()
|
||||
return None
|
||||
|
||||
if not self._isSteamFriendsPanelNavigationObject(obj):
|
||||
self._clearSteamFriendsPanelActivationTarget()
|
||||
return None
|
||||
|
||||
return obj
|
||||
|
||||
def _getSteamFriendsPanelReadableText(self, obj) -> str:
|
||||
text = ""
|
||||
try:
|
||||
text = self.utilities.displayedText(obj) or AXObject.get_name(obj) or ""
|
||||
except Exception:
|
||||
try:
|
||||
text = AXObject.get_name(obj) or ""
|
||||
except Exception:
|
||||
text = ""
|
||||
|
||||
text = self._normalizeSteamFriendsPanelText(text)
|
||||
if text:
|
||||
return text
|
||||
|
||||
try:
|
||||
if AXObject.supports_text(obj):
|
||||
text = AXText.get_all_text(obj) or ""
|
||||
except Exception:
|
||||
text = ""
|
||||
|
||||
return self._normalizeSteamFriendsPanelText(text)
|
||||
|
||||
def _normalizeSteamFriendsPanelText(self, text: str) -> str:
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
text = text.replace(self.EMBEDDED_OBJECT_CHARACTER, " ")
|
||||
text = text.replace("[OBJ]", " ")
|
||||
return " ".join(text.split())
|
||||
|
||||
@staticmethod
|
||||
def _isUsefulSteamFriendsPanelText(text: str) -> bool:
|
||||
if not text:
|
||||
return False
|
||||
|
||||
textLower = text.casefold()
|
||||
if textLower in {"friends", "add friend", "unlabeled image"}:
|
||||
return False
|
||||
|
||||
ignoredFragments = (
|
||||
"loading older messages",
|
||||
"hold to send a quick message",
|
||||
"is typing a message",
|
||||
)
|
||||
if any(fragment in textLower for fragment in ignoredFragments):
|
||||
return False
|
||||
|
||||
return not text.isdigit()
|
||||
|
||||
def _hasSteamQuickAccessMarker(self, obj) -> bool:
|
||||
def hasMarker(candidate) -> bool:
|
||||
if candidate is None:
|
||||
return False
|
||||
|
||||
name = AXObject.get_name(candidate) or ""
|
||||
if name.startswith("QuickAccess_uid"):
|
||||
return True
|
||||
|
||||
accessibleId = AXObject.get_attribute(candidate, "id") or ""
|
||||
if accessibleId.startswith("quickaccess_"):
|
||||
return True
|
||||
|
||||
className = AXObject.get_attribute(candidate, "class") or ""
|
||||
return "quickaccess" in className.casefold()
|
||||
|
||||
return AXObject.find_ancestor_inclusive(obj, hasMarker) is not None
|
||||
|
||||
def _hasSteamOverlaySelectionContext(self, obj) -> bool:
|
||||
if self._hasSteamQuickAccessMarker(obj):
|
||||
return True
|
||||
|
||||
return self._isSteamBigPictureDocumentObject(obj)
|
||||
|
||||
def _isSteamBigPictureDocumentObject(self, obj) -> bool:
|
||||
try:
|
||||
document = self.utilities.getTopLevelDocumentForObject(obj)
|
||||
if not document:
|
||||
document = self.utilities.getDocumentForObject(obj)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not document:
|
||||
return False
|
||||
|
||||
try:
|
||||
uri = self.utilities.documentFrameURI(document) or ""
|
||||
except Exception:
|
||||
uri = ""
|
||||
|
||||
if "steamloopback.host" in uri.casefold():
|
||||
return True
|
||||
|
||||
name = AXObject.get_name(document) or ""
|
||||
return name.strip() == "Steam Big Picture Mode"
|
||||
|
||||
def _steamOverlaySelectionWasUserInitiated(self) -> bool:
|
||||
event = cthulhu_state.lastNonModifierKeyEvent or cthulhu_state.lastInputEvent
|
||||
if event is None:
|
||||
return False
|
||||
|
||||
modifiers = getattr(event, "modifiers", 0) or 0
|
||||
if modifiers:
|
||||
return False
|
||||
|
||||
key = getattr(event, "event_string", None) or getattr(event, "keyval_name", "")
|
||||
return key in {"Left", "Right", "Up", "Down", "Return", "KP_Enter", "space", " "}
|
||||
|
||||
def _activeX11WindowIsSteamGame(self) -> bool:
|
||||
window = input_event_manager.get_manager()._get_active_x11_window()
|
||||
return self._x11WindowHasSteamGameClass(window)
|
||||
|
||||
def _activeX11WindowIsSteamSelectionContext(self) -> bool:
|
||||
window = input_event_manager.get_manager()._get_active_x11_window()
|
||||
return self._x11WindowHasSteamGameClass(window) or self._x11WindowIsSteamBigPicture(window)
|
||||
|
||||
@classmethod
|
||||
def _x11WindowHasSteamGameClass(cls, window) -> bool:
|
||||
if window is None:
|
||||
return False
|
||||
|
||||
identifiers = [
|
||||
cls._safeSteamWindowCall(window, "get_class_group_name"),
|
||||
cls._safeSteamWindowCall(window, "get_class_instance_name"),
|
||||
]
|
||||
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
|
||||
if classGroup is not None:
|
||||
identifiers.extend([
|
||||
cls._safeSteamWindowCall(classGroup, "get_name"),
|
||||
cls._safeSteamWindowCall(classGroup, "get_res_class"),
|
||||
])
|
||||
|
||||
return any(cls._isSteamGameWindowIdentifier(identifier) for identifier in identifiers)
|
||||
|
||||
@classmethod
|
||||
def _x11WindowIsSteamBigPicture(cls, window) -> bool:
|
||||
if window is None:
|
||||
return False
|
||||
|
||||
name = cls._safeSteamWindowCall(window, "get_name")
|
||||
if not isinstance(name, str) or name.strip() != "Steam Big Picture Mode":
|
||||
return False
|
||||
|
||||
identifiers = [
|
||||
cls._safeSteamWindowCall(window, "get_class_group_name"),
|
||||
cls._safeSteamWindowCall(window, "get_class_instance_name"),
|
||||
]
|
||||
classGroup = cls._safeSteamWindowCall(window, "get_class_group")
|
||||
if classGroup is not None:
|
||||
identifiers.extend([
|
||||
cls._safeSteamWindowCall(classGroup, "get_name"),
|
||||
cls._safeSteamWindowCall(classGroup, "get_res_class"),
|
||||
])
|
||||
|
||||
normalized = {
|
||||
identifier.strip().casefold()
|
||||
for identifier in identifiers
|
||||
if isinstance(identifier, str)
|
||||
}
|
||||
return bool(normalized.intersection({"steam", "steamwebhelper"}))
|
||||
|
||||
@staticmethod
|
||||
def _safeSteamWindowCall(window: Any, attrName: str):
|
||||
attr = getattr(window, attrName, None)
|
||||
if not callable(attr):
|
||||
return None
|
||||
|
||||
try:
|
||||
return attr()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _isSteamGameWindowIdentifier(identifier) -> bool:
|
||||
if not isinstance(identifier, str):
|
||||
return False
|
||||
return identifier.strip().casefold().startswith("steam_app_")
|
||||
|
||||
def _isDuplicateSteamOverlaySelection(self, text: str) -> bool:
|
||||
lastText, lastTime = getattr(self, "_lastSteamOverlaySelection", ("", 0.0))
|
||||
now = time.monotonic()
|
||||
if text == lastText and now - lastTime < 0.75:
|
||||
return True
|
||||
|
||||
self._lastSteamOverlaySelection = (text, now)
|
||||
return False
|
||||
|
||||
def _isSteamLoadingSpinnerCaretEvent(self, event) -> bool:
|
||||
if not (event and event.type.startswith("object:text-caret-moved")):
|
||||
return False
|
||||
@@ -241,8 +713,15 @@ class Script(Chromium.Script):
|
||||
if getattr(keyboardEvent, "modifiers", 0):
|
||||
return False
|
||||
|
||||
obj = self._getClickableActivationTarget()
|
||||
if not obj or not self.utilities.inDocumentContent(obj):
|
||||
friendsPanelTarget = self._getSteamFriendsPanelActivationTarget()
|
||||
obj = friendsPanelTarget or self._getClickableActivationTarget()
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
try:
|
||||
if not self.utilities.inDocumentContent(obj):
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||
@@ -255,16 +734,33 @@ class Script(Chromium.Script):
|
||||
return False
|
||||
|
||||
if self._performClickableAction(obj):
|
||||
self._logSteamFriendsPanelActivation(friendsPanelTarget)
|
||||
return True
|
||||
|
||||
from cthulhu import ax_event_synthesizer
|
||||
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||
if result:
|
||||
self._logSteamFriendsPanelActivation(friendsPanelTarget)
|
||||
self._restoreFocusAfterClick(obj)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _logSteamFriendsPanelActivation(self, obj: Any) -> None:
|
||||
if obj is None:
|
||||
return
|
||||
|
||||
target, text, _timestamp = getattr(
|
||||
self,
|
||||
"_steamFriendsPanelActivationTarget",
|
||||
(None, "", 0.0),
|
||||
)
|
||||
if obj is not target or not text:
|
||||
return
|
||||
|
||||
msg = f"STEAM: Activated Friends panel item: {text}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _isSteamNotification(self, obj):
|
||||
"""Detect if object is a Steam notification.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user