Merge branch 'testing'
This commit is contained in:
@@ -870,6 +870,8 @@ class KeyboardEvent(InputEvent):
|
||||
return True, 'Cthulhu modifier'
|
||||
|
||||
if not self._handler:
|
||||
if scriptConsumes:
|
||||
return True, 'Script consumed without handler'
|
||||
return False, 'No handler'
|
||||
|
||||
return scriptConsumes, 'Script indication'
|
||||
@@ -1044,7 +1046,7 @@ class KeyboardEvent(InputEvent):
|
||||
return False, 'Should not consume'
|
||||
|
||||
if not (self._consumer or self._handler):
|
||||
return False, 'No consumer or handler'
|
||||
return True, 'Consumed during shouldConsume'
|
||||
|
||||
if self._consumer or self._handler.function:
|
||||
GLib.timeout_add(1, self._consume)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
steamwebhelper_python_sources = files([
|
||||
'__init__.py',
|
||||
'script.py',
|
||||
'script_utilities.py',
|
||||
])
|
||||
|
||||
python3.install_sources(
|
||||
|
||||
@@ -42,6 +42,7 @@ from cthulhu.ax_utilities import AXUtilities
|
||||
from cthulhu.ax_utilities_relation import AXUtilitiesRelation
|
||||
from cthulhu.ax_utilities_role import AXUtilitiesRole
|
||||
from cthulhu.scripts.toolkits import Chromium
|
||||
from .script_utilities import Utilities
|
||||
|
||||
settingsManager = settings_manager.getManager()
|
||||
|
||||
@@ -88,6 +89,13 @@ class Script(Chromium.Script):
|
||||
re.IGNORECASE
|
||||
)
|
||||
|
||||
def shouldConsumeKeyboardEvent(self, keyboardEvent, handler) -> bool:
|
||||
consumes = super().shouldConsumeKeyboardEvent(keyboardEvent, handler)
|
||||
if consumes:
|
||||
return True
|
||||
|
||||
return self._trySteamButtonActivation(keyboardEvent)
|
||||
|
||||
def onShowingChanged(self, event):
|
||||
"""Callback for object:state-changed:showing accessibility events."""
|
||||
|
||||
@@ -99,6 +107,9 @@ class Script(Chromium.Script):
|
||||
# Fall through to Chromium/web handling
|
||||
super().onShowingChanged(event)
|
||||
|
||||
def getUtilities(self):
|
||||
return Utilities(self)
|
||||
|
||||
def onChildrenAdded(self, event):
|
||||
"""Callback for object:children-changed:add accessibility events."""
|
||||
|
||||
@@ -154,6 +165,40 @@ class Script(Chromium.Script):
|
||||
self._logSteamNavigationEvent("active-descendant-changed", event)
|
||||
return super().onActiveDescendantChanged(event)
|
||||
|
||||
def _trySteamButtonActivation(self, keyboardEvent) -> bool:
|
||||
if keyboardEvent.event_string not in ["Return", "KP_Enter"]:
|
||||
return False
|
||||
|
||||
if not keyboardEvent.is_pressed_key():
|
||||
return False
|
||||
|
||||
if getattr(keyboardEvent, "modifiers", 0):
|
||||
return False
|
||||
|
||||
obj = self._getClickableActivationTarget()
|
||||
if not obj or not self.utilities.inDocumentContent(obj):
|
||||
return False
|
||||
|
||||
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||
return False
|
||||
|
||||
if not any(
|
||||
AXObject.has_action(obj, actionName)
|
||||
for actionName in ["press", "click", "click-ancestor", "activate", "open", "jump"]
|
||||
):
|
||||
return False
|
||||
|
||||
if self._performClickableAction(obj):
|
||||
return True
|
||||
|
||||
from cthulhu import ax_event_synthesizer
|
||||
result = ax_event_synthesizer.AXEventSynthesizer.click_object(obj)
|
||||
if result:
|
||||
self._restoreFocusAfterClick(obj)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _isSteamNotification(self, obj):
|
||||
"""Detect if object is a Steam notification.
|
||||
|
||||
@@ -253,29 +298,37 @@ class Script(Chromium.Script):
|
||||
return f"string('{text}')"
|
||||
if anyData is None:
|
||||
return "None"
|
||||
if isinstance(anyData, (bool, int, float)):
|
||||
return repr(anyData)
|
||||
return self._describeSteamObject(anyData)
|
||||
|
||||
def _describeSteamObject(self, obj):
|
||||
if obj is None:
|
||||
return "None"
|
||||
|
||||
name = AXObject.get_name(obj) or ""
|
||||
description = AXObject.get_description(obj) or ""
|
||||
roleName = AXObject.get_role_name(obj) or ""
|
||||
text = self.utilities.displayedText(obj) or ""
|
||||
path = AXObject.get_path(obj)
|
||||
try:
|
||||
name = AXObject.get_name(obj) or ""
|
||||
description = AXObject.get_description(obj) or ""
|
||||
roleName = AXObject.get_role_name(obj) or ""
|
||||
text = self.utilities.displayedText(obj) or ""
|
||||
path = AXObject.get_path(obj)
|
||||
|
||||
name = self._normalizeSteamNotificationText(name)
|
||||
description = self._normalizeSteamNotificationText(description)
|
||||
text = self._normalizeSteamNotificationText(text)
|
||||
name = self._normalizeSteamNotificationText(name)
|
||||
description = self._normalizeSteamNotificationText(description)
|
||||
text = self._normalizeSteamNotificationText(text)
|
||||
|
||||
return (
|
||||
f"role='{roleName}' "
|
||||
f"name='{name}' "
|
||||
f"description='{description}' "
|
||||
f"text='{text}' "
|
||||
f"path={path}"
|
||||
)
|
||||
return (
|
||||
f"role='{roleName}' "
|
||||
f"name='{name}' "
|
||||
f"description='{description}' "
|
||||
f"text='{text}' "
|
||||
f"path={path}"
|
||||
)
|
||||
except Exception as error:
|
||||
return (
|
||||
f"uninspectable(type={type(obj).__name__}, "
|
||||
f"value={obj!r}, error={error})"
|
||||
)
|
||||
|
||||
def _presentSteamLiveRegionText(self, event):
|
||||
if not isinstance(event.any_data, str):
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 Stormux
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public
|
||||
# License along with this library; if not, write to the
|
||||
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
|
||||
# Boston MA 02110-1301 USA.
|
||||
#
|
||||
# Cthulhu project: https://git.stormux.org/storm/cthulhu
|
||||
|
||||
"""Steam-specific utility helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from cthulhu import debug
|
||||
from cthulhu.ax_object import AXObject
|
||||
from cthulhu.ax_text import AXText
|
||||
from cthulhu.ax_utilities import AXUtilities
|
||||
from cthulhu.scripts.toolkits.Chromium.script_utilities import Utilities as ChromiumUtilities
|
||||
|
||||
|
||||
class Utilities(ChromiumUtilities):
|
||||
def __init__(self, script) -> None:
|
||||
super().__init__(script)
|
||||
self._steamInferredButtonLabels: dict[int, str] = {}
|
||||
|
||||
def clearCachedObjects(self) -> None:
|
||||
super().clearCachedObjects()
|
||||
self._steamInferredButtonLabels = {}
|
||||
|
||||
def displayedLabel(self, obj):
|
||||
label = super().displayedLabel(obj)
|
||||
if label or not self._shouldInferSteamButtonLabel(obj):
|
||||
return label
|
||||
|
||||
inferredLabel = self._getSteamInferredButtonLabel(obj)
|
||||
if inferredLabel:
|
||||
self._displayedLabelText[hash(obj)] = inferredLabel
|
||||
return inferredLabel
|
||||
|
||||
def displayedText(self, obj):
|
||||
text = super().displayedText(obj)
|
||||
if text or not self._shouldInferSteamButtonLabel(obj):
|
||||
return text
|
||||
|
||||
inferredLabel = self._getSteamInferredButtonLabel(obj)
|
||||
if inferredLabel:
|
||||
cache = self._script.generatorCache.setdefault(self.DISPLAYED_TEXT, {})
|
||||
cache[obj] = inferredLabel
|
||||
return inferredLabel
|
||||
|
||||
def _shouldInferSteamButtonLabel(self, obj) -> bool:
|
||||
if not (obj and self.inDocumentContent(obj)):
|
||||
return False
|
||||
|
||||
if AXObject.get_name(obj):
|
||||
return False
|
||||
|
||||
if not (AXUtilities.is_button(obj) or AXUtilities.is_push_button(obj)):
|
||||
return False
|
||||
|
||||
className = AXObject.get_attribute(obj, "class") or ""
|
||||
return "FriendsListTab" in className or "AddFriendButton" in className
|
||||
|
||||
def _getSteamInferredButtonLabel(self, obj) -> str:
|
||||
cached = self._steamInferredButtonLabels.get(hash(obj))
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
className = AXObject.get_attribute(obj, "class") or ""
|
||||
inferredLabel = self._getSteamButtonLabelFromClass(className)
|
||||
if not inferredLabel:
|
||||
inferredLabel = self._getSteamNearbyButtonLabel(obj)
|
||||
|
||||
inferredLabel = inferredLabel or ""
|
||||
self._steamInferredButtonLabels[hash(obj)] = inferredLabel
|
||||
if inferredLabel:
|
||||
tokens = ["STEAM: Inferred label for", obj, ":", inferredLabel]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
return inferredLabel
|
||||
|
||||
@staticmethod
|
||||
def _getSteamButtonLabelFromClass(className: str) -> str:
|
||||
if "AddFriendButton" in className:
|
||||
return "Add Friend"
|
||||
return ""
|
||||
|
||||
def _getSteamNearbyButtonLabel(self, obj) -> str:
|
||||
parent = AXObject.get_parent(obj)
|
||||
if parent is None:
|
||||
return ""
|
||||
|
||||
siblingLabel = self._getSteamLabelFromChildren(parent, ignore=obj)
|
||||
if siblingLabel:
|
||||
return siblingLabel
|
||||
|
||||
parentLabel = self._getSteamReadableText(parent)
|
||||
if self._isUsefulSteamLabel(parentLabel):
|
||||
return parentLabel
|
||||
|
||||
grandParent = AXObject.get_parent(parent)
|
||||
if grandParent is None:
|
||||
return ""
|
||||
|
||||
return self._getSteamLabelFromChildren(grandParent, ignore=parent)
|
||||
|
||||
def _getSteamLabelFromChildren(self, obj, ignore=None) -> str:
|
||||
for child in AXObject.iter_children(obj):
|
||||
if child == ignore:
|
||||
continue
|
||||
if AXUtilities.is_button(child) or AXUtilities.is_push_button(child):
|
||||
continue
|
||||
|
||||
label = self._getSteamReadableText(child)
|
||||
if self._isUsefulSteamLabel(label):
|
||||
return label
|
||||
|
||||
return ""
|
||||
|
||||
def _getSteamReadableText(self, obj) -> str:
|
||||
if obj is None:
|
||||
return ""
|
||||
|
||||
name = self._normalizeSteamLabelText(AXObject.get_name(obj) or "")
|
||||
if self._isUsefulSteamLabel(name):
|
||||
return name
|
||||
|
||||
if not AXObject.supports_text(obj):
|
||||
return ""
|
||||
|
||||
text = AXText.get_all_text(obj) or ""
|
||||
text = text.replace(self.EMBEDDED_OBJECT_CHARACTER, " ")
|
||||
text = self._normalizeSteamLabelText(text)
|
||||
if self._isUsefulSteamLabel(text):
|
||||
return text
|
||||
|
||||
return ""
|
||||
|
||||
@staticmethod
|
||||
def _normalizeSteamLabelText(text: str) -> str:
|
||||
return " ".join(text.split())
|
||||
|
||||
@staticmethod
|
||||
def _isUsefulSteamLabel(text: Optional[str]) -> bool:
|
||||
if not text:
|
||||
return False
|
||||
|
||||
return not text.isdigit() and text.casefold() != "unlabeled image"
|
||||
@@ -5093,7 +5093,9 @@ class Utilities(script_utilities.Utilities):
|
||||
|
||||
obj, offset = None, -1
|
||||
notify = True
|
||||
lastWasUp = input_event_manager.get_manager().last_event_was_up()
|
||||
manager = input_event_manager.get_manager()
|
||||
lastWasUp = manager.last_event_was_up()
|
||||
lastWasDown = manager.last_event_was_down()
|
||||
childCount = AXObject.get_child_count(event.source)
|
||||
if lastWasUp:
|
||||
if event.detail1 >= childCount:
|
||||
@@ -5112,7 +5114,7 @@ class Utilities(script_utilities.Utilities):
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
obj, offset = self.previousContext(prevObj, -1)
|
||||
|
||||
elif keyString == "Down":
|
||||
elif lastWasDown:
|
||||
if event.detail1 == 0:
|
||||
msg = "WEB: First child removed. Getting new location from start of parent."
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
Reference in New Issue
Block a user