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:
|
def _isSteamNotificationEvent(self, event: Atspi.Event) -> bool:
|
||||||
return self._isLiveOrNotificationEvent(event)
|
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:
|
def _isLiveOrNotificationObject(self, obj: Any) -> bool:
|
||||||
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
|
if obj is None or isinstance(obj, (str, bytes, int, float, bool)):
|
||||||
return False
|
return False
|
||||||
@@ -559,18 +671,27 @@ class EventManager:
|
|||||||
if self._inDeluge() and self._ignoreDuringDeluge(event):
|
if self._inDeluge() and self._ignoreDuringDeluge(event):
|
||||||
return _ignore_with_reason("deluge", "event type during deluge")
|
return _ignore_with_reason("deluge", "event type during deluge")
|
||||||
|
|
||||||
|
isSteamOverlayNavigationEvent = self._isSteamOverlayNavigationEvent(event, app)
|
||||||
script = cthulhu_state.activeScript
|
script = cthulhu_state.activeScript
|
||||||
if event.type.startswith('object:children-changed') \
|
if event.type.startswith('object:children-changed') \
|
||||||
or event.type.startswith('object:state-changed:sensitive'):
|
or event.type.startswith('object:state-changed:sensitive'):
|
||||||
if not script:
|
if not script:
|
||||||
|
if isSteamOverlayNavigationEvent:
|
||||||
|
_log_allow("steam-overlay-navigation", "no active script")
|
||||||
|
else:
|
||||||
return _ignore_with_reason("no-active-script", "no active script")
|
return _ignore_with_reason("no-active-script", "no active script")
|
||||||
if script.app != app:
|
elif script.app != app:
|
||||||
# Allow Steam notifications from inactive apps.
|
# Allow Steam notifications from inactive apps.
|
||||||
if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
|
if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
|
||||||
_log_allow("steam-notification", "inactive app notification")
|
_log_allow("steam-notification", "inactive app notification")
|
||||||
|
elif isSteamOverlayNavigationEvent:
|
||||||
|
_log_allow("steam-overlay-navigation", "inactive app overlay navigation")
|
||||||
else:
|
else:
|
||||||
return _ignore_with_reason("inactive-app", "event not from active app")
|
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)
|
relevance = self._classifyRelevance(event, app)
|
||||||
if relevance == self.RELEVANCE_DROP:
|
if relevance == self.RELEVANCE_DROP:
|
||||||
return _ignore_with_reason("event-relevance-drop", "event is background churn")
|
return _ignore_with_reason("event-relevance-drop", "event is background churn")
|
||||||
|
|||||||
@@ -29,15 +29,19 @@ __license__ = "LGPL"
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
|
|
||||||
from cthulhu import debug
|
from cthulhu import debug
|
||||||
|
from cthulhu import input_event_manager
|
||||||
from cthulhu import settings
|
from cthulhu import settings
|
||||||
from cthulhu import cthulhu_state
|
from cthulhu import cthulhu_state
|
||||||
from cthulhu import settings_manager
|
from cthulhu import settings_manager
|
||||||
from cthulhu import structural_navigation
|
from cthulhu import structural_navigation
|
||||||
from cthulhu.ax_object import AXObject
|
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 import AXUtilities
|
||||||
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
||||||
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
||||||
@@ -84,6 +88,10 @@ class Script(Chromium.Script):
|
|||||||
self._lastSteamNotification = ("", 0.0)
|
self._lastSteamNotification = ("", 0.0)
|
||||||
self._steamPendingNotification = None
|
self._steamPendingNotification = None
|
||||||
self._steamNotificationDelayMs = 500
|
self._steamNotificationDelayMs = 500
|
||||||
|
self._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
self._steamActiveQuickAccessTab = ""
|
||||||
|
self._steamFriendsPanelActivationTarget = (None, "", 0.0)
|
||||||
|
self._steamFriendsPanelActivationTargetMaxAge = 10.0
|
||||||
self._steamRelativeTimePattern = re.compile(
|
self._steamRelativeTimePattern = re.compile(
|
||||||
r"^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$",
|
r"^(?:just now|now|\d+\s+(?:second|minute|hour|day|week|month|year)s?\s+ago)$",
|
||||||
re.IGNORECASE
|
re.IGNORECASE
|
||||||
@@ -118,6 +126,9 @@ class Script(Chromium.Script):
|
|||||||
self._presentSteamNotification(event.any_data)
|
self._presentSteamNotification(event.any_data)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._presentSteamFriendsPanelNavigation(event):
|
||||||
|
return True
|
||||||
|
|
||||||
if self._handleSteamVirtualizedListMutation(event):
|
if self._handleSteamVirtualizedListMutation(event):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -156,12 +167,24 @@ class Script(Chromium.Script):
|
|||||||
|
|
||||||
return super().onFocusedChanged(event)
|
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):
|
def onTextInserted(self, event):
|
||||||
"""Callback for object:text-changed:insert accessibility events."""
|
"""Callback for object:text-changed:insert accessibility events."""
|
||||||
|
|
||||||
if self._presentSteamLiveRegionText(event):
|
if self._presentSteamLiveRegionText(event):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._presentSteamFriendsPanelNavigation(event):
|
||||||
|
return True
|
||||||
|
|
||||||
super().onTextInserted(event)
|
super().onTextInserted(event)
|
||||||
|
|
||||||
def onCaretMoved(self, event):
|
def onCaretMoved(self, event):
|
||||||
@@ -178,8 +201,18 @@ class Script(Chromium.Script):
|
|||||||
"""Callback for object:selection-changed accessibility events."""
|
"""Callback for object:selection-changed accessibility events."""
|
||||||
|
|
||||||
self._logSteamNavigationEvent("selection-changed", event)
|
self._logSteamNavigationEvent("selection-changed", event)
|
||||||
|
if self._presentSteamOverlaySelection(event):
|
||||||
|
return True
|
||||||
return super().onSelectionChanged(event)
|
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):
|
def onActiveDescendantChanged(self, event):
|
||||||
"""Callback for object:active-descendant-changed accessibility events."""
|
"""Callback for object:active-descendant-changed accessibility events."""
|
||||||
|
|
||||||
@@ -203,6 +236,445 @@ class Script(Chromium.Script):
|
|||||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||||
return 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:
|
def _isSteamLoadingSpinnerCaretEvent(self, event) -> bool:
|
||||||
if not (event and event.type.startswith("object:text-caret-moved")):
|
if not (event and event.type.startswith("object:text-caret-moved")):
|
||||||
return False
|
return False
|
||||||
@@ -241,8 +713,15 @@ class Script(Chromium.Script):
|
|||||||
if getattr(keyboardEvent, "modifiers", 0):
|
if getattr(keyboardEvent, "modifiers", 0):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
obj = self._getClickableActivationTarget()
|
friendsPanelTarget = self._getSteamFriendsPanelActivationTarget()
|
||||||
if not obj or not self.utilities.inDocumentContent(obj):
|
obj = friendsPanelTarget or self._getClickableActivationTarget()
|
||||||
|
if not obj:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.utilities.inDocumentContent(obj):
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||||
@@ -255,16 +734,33 @@ class Script(Chromium.Script):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
if self._performClickableAction(obj):
|
if self._performClickableAction(obj):
|
||||||
|
self._logSteamFriendsPanelActivation(friendsPanelTarget)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
from cthulhu import ax_event_synthesizer
|
from cthulhu import ax_event_synthesizer
|
||||||
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||||
if result:
|
if result:
|
||||||
|
self._logSteamFriendsPanelActivation(friendsPanelTarget)
|
||||||
self._restoreFocusAfterClick(obj)
|
self._restoreFocusAfterClick(obj)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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):
|
def _isSteamNotification(self, obj):
|
||||||
"""Detect if object is a Steam notification.
|
"""Detect if object is a Steam notification.
|
||||||
|
|
||||||
|
|||||||
@@ -182,6 +182,42 @@ class EventManagerCompositorContextRegressionTests(unittest.TestCase):
|
|||||||
self.assertFalse(self.manager._ignore(firstEvent))
|
self.assertFalse(self.manager._ignore(firstEvent))
|
||||||
self.assertTrue(self.manager._ignore(secondEvent))
|
self.assertTrue(self.manager._ignore(secondEvent))
|
||||||
|
|
||||||
|
def test_steam_quick_access_name_changed_survives_without_active_script(self) -> None:
|
||||||
|
app = object()
|
||||||
|
event = FakeEvent("object:property-change:accessible-name", source="quick-access")
|
||||||
|
cthulhu_state.activeScript = None
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(event_manager.debug, "printMessage"),
|
||||||
|
mock.patch.object(event_manager.debug, "printTokens"),
|
||||||
|
mock.patch.object(event_manager.debug, "print_log"),
|
||||||
|
mock.patch.object(event_manager.AXObject, "get_application", return_value=app),
|
||||||
|
mock.patch.object(event_manager.AXObject, "get_name", return_value="steamwebhelper"),
|
||||||
|
):
|
||||||
|
self.manager._isSteamOverlayNavigationEvent = mock.Mock(return_value=True)
|
||||||
|
|
||||||
|
self.assertFalse(self.manager._ignore(event))
|
||||||
|
|
||||||
|
def test_steam_overlay_navigation_requires_quick_access_context(self) -> None:
|
||||||
|
app = object()
|
||||||
|
event = FakeEvent("object:property-change:accessible-name", source="not-quick-access")
|
||||||
|
|
||||||
|
self.manager._isSteamApp = mock.Mock(return_value=True)
|
||||||
|
self.manager._activeX11WindowIsSteamSelectionContext = mock.Mock(return_value=True)
|
||||||
|
self.manager._eventHasSteamQuickAccessContext = mock.Mock(return_value=False)
|
||||||
|
|
||||||
|
self.assertFalse(self.manager._isSteamOverlayNavigationEvent(event, app))
|
||||||
|
|
||||||
|
def test_steam_overlay_navigation_rejects_children_changed_churn(self) -> None:
|
||||||
|
app = object()
|
||||||
|
event = FakeEvent("object:children-changed:add", source="quick-access", any_data=object())
|
||||||
|
|
||||||
|
self.manager._isSteamApp = mock.Mock(return_value=True)
|
||||||
|
self.manager._activeX11WindowIsSteamSelectionContext = mock.Mock(return_value=True)
|
||||||
|
self.manager._eventHasSteamQuickAccessContext = mock.Mock(return_value=True)
|
||||||
|
|
||||||
|
self.assertFalse(self.manager._isSteamOverlayNavigationEvent(event, app))
|
||||||
|
|
||||||
def test_steam_focus_lost_burst_is_ignored_but_focus_gain_is_preserved(self) -> None:
|
def test_steam_focus_lost_burst_is_ignored_but_focus_gain_is_preserved(self) -> None:
|
||||||
app = object()
|
app = object()
|
||||||
cthulhu_state.activeScript = mock.Mock(app=app)
|
cthulhu_state.activeScript = mock.Mock(app=app)
|
||||||
|
|||||||
@@ -61,6 +61,409 @@ class SteamSelectionChangedTests(unittest.TestCase):
|
|||||||
self.assertEqual(chromiumCalls, [(testScript, event)])
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
|
||||||
|
class SteamOverlaySelectionPresentationTests(unittest.TestCase):
|
||||||
|
def test_selected_changed_presents_quick_access_item_over_steam_game(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = ""
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", return_value="Quick Settings"),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onSelectedChanged(event))
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_called_once_with(
|
||||||
|
"Quick Settings",
|
||||||
|
force=True,
|
||||||
|
interrupt=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(chromiumCalls, [])
|
||||||
|
|
||||||
|
def test_selected_changed_presents_inferred_quick_access_friends_tab_label(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = ""
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", return_value=""),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.AXObject,
|
||||||
|
"get_attribute",
|
||||||
|
side_effect=lambda obj, name: "quickaccess_tab_3" if name == "id" else "",
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onSelectedChanged(event))
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_called_once_with(
|
||||||
|
"Friends",
|
||||||
|
force=True,
|
||||||
|
interrupt=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(chromiumCalls, [])
|
||||||
|
self.assertEqual(testScript._steamActiveQuickAccessTab, "Friends")
|
||||||
|
|
||||||
|
def test_selected_changed_defers_unknown_unnamed_quick_access_tab(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = ""
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return "handled"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", return_value=""),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.AXObject,
|
||||||
|
"get_attribute",
|
||||||
|
side_effect=lambda obj, name: "quickaccess_tab_unknown" if name == "id" else "",
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(testScript.onSelectedChanged(event), "handled")
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
def test_selected_changed_defers_when_foreground_is_not_steam_game(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = ""
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return "handled"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(testScript.onSelectedChanged(event), "handled")
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
def test_selection_changed_uses_selected_child_from_quick_access_container(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
child = object()
|
||||||
|
lastEvent = mock.Mock(event_string=" ", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:selection-changed",
|
||||||
|
source=source,
|
||||||
|
detail1=0,
|
||||||
|
any_data=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = ""
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXSelection, "get_selected_children", return_value=[child]),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", side_effect=lambda obj: "Friends" if obj is child else ""),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", side_effect=lambda obj: obj is child),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onSelectionChanged(event))
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_called_once_with(
|
||||||
|
"Friends",
|
||||||
|
force=True,
|
||||||
|
interrupt=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_selected_changed_presents_steam_big_picture_tab_when_steam_is_foreground(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
document = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Right", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = "ACTIVITY"
|
||||||
|
testScript.utilities.getTopLevelDocumentForObject.return_value = document
|
||||||
|
testScript.utilities.documentFrameURI.return_value = "https://steamloopback.host/index.html"
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=False),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", return_value="ACTIVITY"),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onSelectedChanged(event))
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_called_once_with(
|
||||||
|
"ACTIVITY",
|
||||||
|
force=True,
|
||||||
|
interrupt=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(chromiumCalls, [])
|
||||||
|
|
||||||
|
def test_selected_changed_defers_steam_tab_outside_steam_big_picture_document(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
document = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Right", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:state-changed:selected",
|
||||||
|
source=source,
|
||||||
|
detail1=1,
|
||||||
|
any_data=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript.utilities.displayedText.return_value = "ACTIVITY"
|
||||||
|
testScript.utilities.getTopLevelDocumentForObject.return_value = document
|
||||||
|
testScript.utilities.documentFrameURI.return_value = "https://store.steampowered.com/"
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnSelectedChanged(self, selectedEvent):
|
||||||
|
chromiumCalls.append((self, selectedEvent))
|
||||||
|
return "handled"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=False),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "get_name", return_value="ACTIVITY"),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_menu_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onSelectedChanged",
|
||||||
|
new=chromiumOnSelectedChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(testScript.onSelectedChanged(event), "handled")
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
def test_x11_steam_game_class_detects_steam_app_identifier(self):
|
||||||
|
classGroup = mock.Mock()
|
||||||
|
classGroup.get_name.return_value = "steam_app_1281930"
|
||||||
|
classGroup.get_res_class.return_value = "steam_app_1281930"
|
||||||
|
window = mock.Mock()
|
||||||
|
window.get_class_group_name.return_value = ""
|
||||||
|
window.get_class_instance_name.return_value = ""
|
||||||
|
window.get_class_group.return_value = classGroup
|
||||||
|
|
||||||
|
self.assertTrue(steam_script.Script._x11WindowHasSteamGameClass(window))
|
||||||
|
|
||||||
|
def test_x11_steam_game_class_rejects_plain_chromium_window(self):
|
||||||
|
classGroup = mock.Mock()
|
||||||
|
classGroup.get_name.return_value = "chromium"
|
||||||
|
classGroup.get_res_class.return_value = "Chromium"
|
||||||
|
window = mock.Mock()
|
||||||
|
window.get_class_group_name.return_value = "Chromium"
|
||||||
|
window.get_class_instance_name.return_value = "chromium"
|
||||||
|
window.get_class_group.return_value = classGroup
|
||||||
|
|
||||||
|
self.assertFalse(steam_script.Script._x11WindowHasSteamGameClass(window))
|
||||||
|
|
||||||
|
def test_x11_steam_big_picture_window_is_selection_context(self):
|
||||||
|
classGroup = mock.Mock()
|
||||||
|
classGroup.get_name.return_value = "steam"
|
||||||
|
classGroup.get_res_class.return_value = "steam"
|
||||||
|
window = mock.Mock()
|
||||||
|
window.get_name.return_value = "Steam Big Picture Mode"
|
||||||
|
window.get_class_group_name.return_value = "steam"
|
||||||
|
window.get_class_instance_name.return_value = "steamwebhelper"
|
||||||
|
window.get_class_group.return_value = classGroup
|
||||||
|
|
||||||
|
self.assertTrue(steam_script.Script._x11WindowIsSteamBigPicture(window))
|
||||||
|
|
||||||
|
def test_x11_plain_steam_window_is_not_big_picture_selection_context(self):
|
||||||
|
classGroup = mock.Mock()
|
||||||
|
classGroup.get_name.return_value = "steam"
|
||||||
|
classGroup.get_res_class.return_value = "steam"
|
||||||
|
window = mock.Mock()
|
||||||
|
window.get_name.return_value = "Steam"
|
||||||
|
window.get_class_group_name.return_value = "steam"
|
||||||
|
window.get_class_instance_name.return_value = "steamwebhelper"
|
||||||
|
window.get_class_group.return_value = classGroup
|
||||||
|
|
||||||
|
self.assertFalse(steam_script.Script._x11WindowIsSteamBigPicture(window))
|
||||||
|
|
||||||
|
|
||||||
class SteamReturnActivationTests(unittest.TestCase):
|
class SteamReturnActivationTests(unittest.TestCase):
|
||||||
def test_return_activates_focused_steam_button(self):
|
def test_return_activates_focused_steam_button(self):
|
||||||
testScript = steam_script.Script.__new__(steam_script.Script)
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
@@ -94,8 +497,283 @@ class SteamReturnActivationTests(unittest.TestCase):
|
|||||||
|
|
||||||
performAction.assert_called_once_with(button)
|
performAction.assert_called_once_with(button)
|
||||||
|
|
||||||
|
def test_return_activates_last_spoken_friends_panel_button(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
button = object()
|
||||||
|
keyboardEvent = mock.Mock(event_string="Return", modifiers=0)
|
||||||
|
keyboardEvent.is_pressed_key.return_value = True
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.inDocumentContent.return_value = True
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
testScript._steamFriendsPanelActivationTarget = (
|
||||||
|
button,
|
||||||
|
"Username Playing Terraria",
|
||||||
|
100.0,
|
||||||
|
)
|
||||||
|
testScript._steamFriendsPanelActivationTargetMaxAge = 10.0
|
||||||
|
|
||||||
|
def has_action(obj, action_name):
|
||||||
|
return obj is button and action_name == "press"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script.time, "monotonic", return_value=105.0),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(testScript, "_getClickableActivationTarget", return_value=None),
|
||||||
|
mock.patch.object(steam_script.AXObject, "is_dead", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_table_row", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "has_action", side_effect=has_action),
|
||||||
|
mock.patch.object(steam_script.Script, "_performClickableAction", return_value=True) as performAction,
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.shouldConsumeKeyboardEvent(keyboardEvent, None))
|
||||||
|
|
||||||
|
performAction.assert_called_once_with(button)
|
||||||
|
|
||||||
|
def test_return_defers_stale_friends_panel_activation_target(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
button = object()
|
||||||
|
keyboardEvent = mock.Mock(event_string="Return", modifiers=0)
|
||||||
|
keyboardEvent.is_pressed_key.return_value = True
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
testScript._steamFriendsPanelActivationTarget = (
|
||||||
|
button,
|
||||||
|
"Username Playing Terraria",
|
||||||
|
100.0,
|
||||||
|
)
|
||||||
|
testScript._steamFriendsPanelActivationTargetMaxAge = 10.0
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script.time, "monotonic", return_value=111.0),
|
||||||
|
mock.patch.object(steam_script.debug, "printMessage"),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(testScript, "_getClickableActivationTarget", return_value=None),
|
||||||
|
):
|
||||||
|
self.assertFalse(testScript.shouldConsumeKeyboardEvent(keyboardEvent, None))
|
||||||
|
|
||||||
|
self.assertEqual(testScript._steamFriendsPanelActivationTarget, (None, "", 0.0))
|
||||||
|
|
||||||
|
|
||||||
class SteamVirtualizedListMutationTests(unittest.TestCase):
|
class SteamVirtualizedListMutationTests(unittest.TestCase):
|
||||||
|
def test_name_changed_presents_friends_panel_item_after_friends_tab_activation(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
row = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:property-change:accessible-name",
|
||||||
|
source=row,
|
||||||
|
any_data="Username Playing Terraria",
|
||||||
|
detail1=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.displayedText.side_effect = (
|
||||||
|
lambda obj: "Username Playing Terraria" if obj is row else ""
|
||||||
|
)
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._lastSteamOverlaySelection = ("", 0.0)
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnNameChanged(self, nameEvent):
|
||||||
|
chromiumCalls.append((self, nameEvent))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_table_row", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onNameChanged",
|
||||||
|
new=chromiumOnNameChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertTrue(testScript.onNameChanged(event))
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_called_once_with(
|
||||||
|
"Username Playing Terraria",
|
||||||
|
force=True,
|
||||||
|
interrupt=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
testScript._steamFriendsPanelActivationTarget[:2],
|
||||||
|
(row, "Username Playing Terraria"),
|
||||||
|
)
|
||||||
|
self.assertEqual(chromiumCalls, [])
|
||||||
|
|
||||||
|
def test_children_added_defers_friends_panel_load_artifact(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
source = object()
|
||||||
|
row = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:children-changed:add",
|
||||||
|
source=source,
|
||||||
|
any_data=row,
|
||||||
|
detail1=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.isSteamVirtualizedList.return_value = False
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnChildrenAdded(self, addedEvent):
|
||||||
|
chromiumCalls.append((self, addedEvent))
|
||||||
|
return False
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(steam_script.Script, "_isSteamNotification", return_value=False),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onChildrenAdded",
|
||||||
|
new=chromiumOnChildrenAdded,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
testScript.onChildrenAdded(event)
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
def test_name_changed_ignores_add_friend_panel_button(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
button = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:property-change:accessible-name",
|
||||||
|
source=button,
|
||||||
|
any_data="Add Friend",
|
||||||
|
detail1=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.displayedText.return_value = "Add Friend"
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnNameChanged(self, nameEvent):
|
||||||
|
chromiumCalls.append((self, nameEvent))
|
||||||
|
return "handled"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_table_row", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onNameChanged",
|
||||||
|
new=chromiumOnNameChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(testScript.onNameChanged(event), "handled")
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
|
def test_name_changed_ignores_friends_panel_chat_history_button(self):
|
||||||
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
|
button = object()
|
||||||
|
lastEvent = mock.Mock(event_string="Down", modifiers=0)
|
||||||
|
event = mock.Mock(
|
||||||
|
type="object:property-change:accessible-name",
|
||||||
|
source=button,
|
||||||
|
any_data="LOADING OLDER MESSAGES... Username is typing a message...",
|
||||||
|
detail1=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
testScript.utilities = mock.Mock()
|
||||||
|
testScript.utilities.displayedText.return_value = (
|
||||||
|
"LOADING OLDER MESSAGES... Username is typing a message..."
|
||||||
|
)
|
||||||
|
testScript.presentMessage = mock.Mock()
|
||||||
|
testScript._steamActiveQuickAccessTab = "Friends"
|
||||||
|
|
||||||
|
chromiumCalls = []
|
||||||
|
|
||||||
|
def chromiumOnNameChanged(self, nameEvent):
|
||||||
|
chromiumCalls.append((self, nameEvent))
|
||||||
|
return "handled"
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Script,
|
||||||
|
"_activeX11WindowIsSteamSelectionContext",
|
||||||
|
return_value=True,
|
||||||
|
),
|
||||||
|
mock.patch.object(steam_script.Script, "_hasSteamQuickAccessMarker", return_value=True),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastNonModifierKeyEvent", lastEvent),
|
||||||
|
mock.patch.object(steam_script.cthulhu_state, "lastInputEvent", None),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_page_tab_list", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_list_item", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_button", return_value=True),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_push_button", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXUtilities, "is_table_row", return_value=False),
|
||||||
|
mock.patch.object(steam_script.AXObject, "find_ancestor_inclusive", return_value=None),
|
||||||
|
mock.patch.object(
|
||||||
|
steam_script.Chromium.Script,
|
||||||
|
"onNameChanged",
|
||||||
|
new=chromiumOnNameChanged,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
self.assertEqual(testScript.onNameChanged(event), "handled")
|
||||||
|
|
||||||
|
testScript.presentMessage.assert_not_called()
|
||||||
|
self.assertEqual(chromiumCalls, [(testScript, event)])
|
||||||
|
|
||||||
def test_caret_moved_ignores_transient_loading_spinner(self):
|
def test_caret_moved_ignores_transient_loading_spinner(self):
|
||||||
testScript = steam_script.Script.__new__(steam_script.Script)
|
testScript = steam_script.Script.__new__(steam_script.Script)
|
||||||
source = object()
|
source = object()
|
||||||
|
|||||||
Reference in New Issue
Block a user