More refactor work.
This commit is contained in:
@@ -145,7 +145,7 @@ class AXComponent:
|
||||
return not(rect.width or rect.height)
|
||||
|
||||
@staticmethod
|
||||
def has_no_size_or_invalid_rect(obj: Atspi.Accessible) -> bool:
|
||||
def has_no_size_or_invalid_rect(obj: Atspi.Accessible, clear_cache: bool = True) -> bool:
|
||||
"""Returns True if the rect associated with obj is sizeless or invalid."""
|
||||
|
||||
rect = AXComponent.get_rect(obj)
|
||||
@@ -158,7 +158,8 @@ class AXComponent:
|
||||
if (rect.width < -1 or rect.height < -1):
|
||||
tokens = ["WARNING: ", obj, "has a broken rect:", rect]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
AXObject.clear_cache(obj)
|
||||
if clear_cache:
|
||||
AXObject.clear_cache(obj)
|
||||
rect = AXComponent.get_rect(obj)
|
||||
if (rect.width < -1 or rect.height < -1):
|
||||
msg = "AXComponent: Clearing cache did not fix the rect"
|
||||
|
||||
@@ -109,13 +109,14 @@ class AXUtilities:
|
||||
AXTable.clear_cache_now(reason)
|
||||
|
||||
@staticmethod
|
||||
def can_be_active_window(window: Atspi.Accessible) -> bool:
|
||||
def can_be_active_window(window: Atspi.Accessible, clear_cache: bool = True) -> bool:
|
||||
"""Returns True if window can be the active window based on its state."""
|
||||
|
||||
if window is None:
|
||||
return False
|
||||
|
||||
AXObject.clear_cache(window, False, "Checking if window can be the active window")
|
||||
if clear_cache:
|
||||
AXObject.clear_cache(window, False, "Checking if window can be the active window")
|
||||
app = AXUtilitiesApplication.get_application(window)
|
||||
tokens = ["AXUtilities:", window, "from", app]
|
||||
|
||||
@@ -808,11 +809,13 @@ class AXUtilities:
|
||||
@staticmethod
|
||||
def is_on_screen(
|
||||
obj: Atspi.Accessible,
|
||||
bounding_box: Optional[Atspi.Rect] = None
|
||||
bounding_box: Optional[Atspi.Rect] = None,
|
||||
clear_cache: bool = True
|
||||
) -> bool:
|
||||
"""Returns true if obj should be treated as being on screen."""
|
||||
|
||||
AXObject.clear_cache(obj, False, "Updating to check if object is on screen.")
|
||||
if clear_cache:
|
||||
AXObject.clear_cache(obj, False, "Updating to check if object is on screen.")
|
||||
|
||||
tokens = ["AXUtilities: Checking if", obj, "is showing and visible...."]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
@@ -833,7 +836,7 @@ class AXUtilities:
|
||||
tokens = ["AXUtilities:", obj, "is not hidden. Checking size and rect..."]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
if AXComponent.has_no_size_or_invalid_rect(obj):
|
||||
if AXComponent.has_no_size_or_invalid_rect(obj, clear_cache=clear_cache):
|
||||
tokens = ["AXUtilities: Rect of", obj, "is unhelpful. Treating as on screen."]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return True
|
||||
@@ -972,6 +975,8 @@ class AXUtilities:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
return result
|
||||
|
||||
# Dynamically expose helper methods for compatibility with older callers.
|
||||
# Keep side effects explicit in the underlying helpers (e.g., clear_cache flags).
|
||||
for method_name, method in inspect.getmembers(AXUtilitiesApplication, predicate=inspect.isfunction):
|
||||
setattr(AXUtilities, method_name, method)
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class EventManager:
|
||||
return False
|
||||
|
||||
window = cthulhu_state.activeWindow
|
||||
if not AXUtilities.can_be_active_window(window):
|
||||
if not AXUtilities.can_be_active_window(window, clear_cache=True):
|
||||
window = AXUtilities.find_active_window()
|
||||
if window is not None:
|
||||
tokens = ["EVENT MANAGER: Setting initial active window to", window]
|
||||
@@ -237,40 +237,44 @@ class EventManager:
|
||||
tokens = ["EVENT MANAGER:", event.type, "from", app]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
def _log_ignore(reason, message):
|
||||
msg = f"EVENT MANAGER: Ignoring ({reason}) - {message}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _log_allow(reason, message):
|
||||
msg = f"EVENT MANAGER: Not ignoring ({reason}) - {message}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def _ignore_with_reason(reason, message):
|
||||
_log_ignore(reason, message)
|
||||
return True
|
||||
|
||||
def _allow_with_reason(reason, message):
|
||||
_log_allow(reason, message)
|
||||
return False
|
||||
|
||||
if self._eventsSuspended:
|
||||
tokens = ["EVENT MANAGER: Suspended events:", ', '.join(self._suspendableEvents)]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
if not self._active:
|
||||
msg = 'EVENT MANAGER: Ignoring because event manager is not active'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("inactive", "event manager is not active")
|
||||
|
||||
if list(filter(event.type.startswith, self._ignoredEvents)):
|
||||
msg = 'EVENT MANAGER: Ignoring because event type is ignored'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("type-ignored", "event type is ignored")
|
||||
|
||||
if AXObject.get_name(app) == 'gnome-shell':
|
||||
if event.type.startswith('object:children-changed:remove'):
|
||||
msg = 'EVENT MANAGER: Ignoring event based on type and app'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("gnome-shell", "children-changed:remove")
|
||||
|
||||
if event.type.startswith('window'):
|
||||
msg = 'EVENT MANAGER: Not ignoring because event type is never ignored'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
return _allow_with_reason("window-event", "event type is never ignored")
|
||||
|
||||
if event.type.startswith('mouse:button'):
|
||||
msg = 'EVENT MANAGER: Not ignoring because event type is never ignored'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
return _allow_with_reason("mouse-event", "event type is never ignored")
|
||||
|
||||
if self._isDuplicateEvent(event):
|
||||
msg = 'EVENT MANAGER: Ignoring duplicate event'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("duplicate", "duplicate event")
|
||||
|
||||
# Thunderbird spams us with these when a message list thread is expanded or collapsed.
|
||||
if event.type.endswith('system') \
|
||||
@@ -278,40 +282,29 @@ class EventManager:
|
||||
if AXUtilities.is_table_related(event.source) \
|
||||
or AXUtilities.is_tree_related(event.source) \
|
||||
or AXUtilities.is_section(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring system event based on role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("thunderbird-system", "system event based on role")
|
||||
|
||||
if self._inDeluge() and self._ignoreDuringDeluge(event):
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to deluge'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("deluge", "event type during deluge")
|
||||
|
||||
script = cthulhu_state.activeScript
|
||||
if event.type.startswith('object:children-changed') \
|
||||
or event.type.startswith('object:state-changed:sensitive'):
|
||||
if not script:
|
||||
msg = 'EVENT MANAGER: Ignoring because there is no active script'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("no-active-script", "no active script")
|
||||
if script.app != app:
|
||||
# Allow Steam notifications from inactive apps.
|
||||
if self._isSteamApp(app) and self._isSteamNotificationEvent(event):
|
||||
msg = 'EVENT MANAGER: Allowing Steam notification from inactive app'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
_log_allow("steam-notification", "inactive app notification")
|
||||
else:
|
||||
msg = 'EVENT MANAGER: Ignoring because event is not from active app'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("inactive-app", "event not from active app")
|
||||
|
||||
if event.type.startswith('object:text-changed') \
|
||||
and self.EMBEDDED_OBJECT_CHARACTER in event.any_data \
|
||||
and not event.any_data.replace(self.EMBEDDED_OBJECT_CHARACTER, ""):
|
||||
# We should also get children-changed events telling us the same thing.
|
||||
# Getting a bunch of both can result in a flood that grinds us to a halt.
|
||||
msg = 'EVENT MANAGER: Ignoring because changed text is only embedded objects'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("embedded-only", "changed text only embedded objects")
|
||||
|
||||
# TODO - JD: For now we won't ask for the name. Simply asking for the name should
|
||||
# not break anything, and should be a reliable way to quickly identify defunct
|
||||
@@ -321,14 +314,10 @@ class EventManager:
|
||||
#name = Atspi.Accessible.get_name(event.source)
|
||||
|
||||
if AXUtilities.has_no_state(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring event due to empty state set'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("empty-state", "empty state set")
|
||||
|
||||
if AXUtilities.is_defunct(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring event from defunct source'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("defunct-source", "defunct source")
|
||||
|
||||
role = AXObject.get_role(event.source)
|
||||
if event.type.startswith('object:property-change:accessible-name'):
|
||||
@@ -344,28 +333,20 @@ class EventManager:
|
||||
Atspi.Role.IMAGE, # Thunderbird spam
|
||||
Atspi.Role.MENU,
|
||||
Atspi.Role.MENU_ITEM]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("name-change-role", "role filtered")
|
||||
# TeamTalk5 is notoriously spammy here, and name change events on widgets are
|
||||
# typically only presented if they are focused.
|
||||
if not AXUtilities.is_focused(event.source) \
|
||||
and role in [Atspi.Role.PUSH_BUTTON,
|
||||
Atspi.Role.CHECK_BOX,
|
||||
Atspi.Role.RADIO_BUTTON]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role and state'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("name-change-unfocused", "role and state")
|
||||
elif event.type.startswith('object:property-change:accessible-value'):
|
||||
if role == Atspi.Role.SPLIT_PANE and not AXUtilities.is_focused(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role and state'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("value-change-unfocused", "role and state")
|
||||
elif event.type.startswith('object:text-changed:insert') and event.detail2 > 1000 \
|
||||
and role in [Atspi.Role.TEXT, Atspi.Role.STATIC]:
|
||||
msg = 'EVENT MANAGER: Ignoring because inserted text has more than 1000 chars'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("text-insert-large", "inserted text > 1000 chars")
|
||||
elif event.type.startswith('object:state-changed:sensitive'):
|
||||
if role in [Atspi.Role.MENU_ITEM,
|
||||
Atspi.Role.MENU,
|
||||
@@ -373,14 +354,10 @@ class EventManager:
|
||||
Atspi.Role.PANEL,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_MENU_ITEM]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("sensitive-role", "role filtered")
|
||||
elif event.type.startswith('object:state-changed:selected'):
|
||||
if not event.detail1 and role in [Atspi.Role.PUSH_BUTTON]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role and detail1'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("selected-button-false", "role and detail1")
|
||||
elif event.type.startswith('object:state-changed:showing'):
|
||||
if role not in [Atspi.Role.ALERT,
|
||||
Atspi.Role.ANIMATION,
|
||||
@@ -390,39 +367,27 @@ class EventManager:
|
||||
Atspi.Role.DIALOG,
|
||||
Atspi.Role.STATUS_BAR,
|
||||
Atspi.Role.TOOL_TIP]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("showing-role", "role filtered")
|
||||
|
||||
elif event.type.startswith('object:text-caret-moved'):
|
||||
if role in [Atspi.Role.LABEL] and not AXUtilities.is_focused(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role and state'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("caret-unfocused-label", "role and state")
|
||||
|
||||
elif event.type.startswith('object:selection-changed'):
|
||||
if event.source in self._parentsOfDefunctDescendants:
|
||||
msg = 'EVENT MANAGER: Ignoring event from parent of defunct descendants'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("defunct-descendant-parent", "parent of defunct descendants")
|
||||
|
||||
if AXObject.is_dead(event.source):
|
||||
msg = 'EVENT MANAGER: Ignoring event from dead source'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("dead-source", "dead source")
|
||||
|
||||
if event.type.startswith('object:children-changed') \
|
||||
or event.type.startswith('object:active-descendant-changed'):
|
||||
if role in [Atspi.Role.MENU,
|
||||
Atspi.Role.LAYERED_PANE,
|
||||
Atspi.Role.MENU_ITEM]:
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("children-role", "role filtered")
|
||||
if event.any_data is None:
|
||||
msg = 'EVENT_MANAGER: Ignoring due to lack of event.any_data'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("missing-any-data", "no event.any_data")
|
||||
if event.type.endswith('remove'):
|
||||
if event.any_data == cthulhu_state.locusOfFocus:
|
||||
msg = 'EVENT MANAGER: Locus of focus is being destroyed'
|
||||
@@ -439,8 +404,7 @@ class EventManager:
|
||||
|
||||
defunct = AXObject.is_dead(event.any_data) or AXUtilities.is_defunct(event.any_data)
|
||||
if defunct:
|
||||
msg = 'EVENT MANAGER: Ignoring event for potentially-defunct child/descendant'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
_log_ignore("defunct-child", "potentially defunct child/descendant")
|
||||
if AXUtilities.manages_descendants(event.source) \
|
||||
and event.source not in self._parentsOfDefunctDescendants:
|
||||
self._parentsOfDefunctDescendants.append(event.source)
|
||||
@@ -455,21 +419,15 @@ class EventManager:
|
||||
# reason for ignoring it here rather than quickly processing it is the
|
||||
# potential for event floods like we're seeing from matrix.org.
|
||||
if AXUtilities.is_image(event.any_data):
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("child-image", "role filtered")
|
||||
|
||||
# In normal apps we would have caught this from the parent role.
|
||||
# But gnome-shell has panel parents adding/removing menu items.
|
||||
if event.type.startswith('object:children-changed'):
|
||||
if AXUtilities.is_menu_item(event.any_data):
|
||||
msg = 'EVENT MANAGER: Ignoring event type due to child role'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return True
|
||||
return _ignore_with_reason("child-menu-item", "child role filtered")
|
||||
|
||||
msg = 'EVENT MANAGER: Not ignoring due to lack of cause'
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
return False
|
||||
return _allow_with_reason("no-cause", "no ignore condition met")
|
||||
|
||||
def _addToQueue(self, event, asyncMode):
|
||||
debugging = debug.debugEventQueue
|
||||
|
||||
+95
-86
@@ -111,24 +111,29 @@ class Word:
|
||||
if attr != "chars":
|
||||
return super().__getattribute__(attr)
|
||||
|
||||
chars = []
|
||||
for i, char in enumerate(self.string):
|
||||
start = i + self.startOffset
|
||||
extents = self._getCharExtents(start)
|
||||
chars.append(Char(self, i, start, char, *extents))
|
||||
|
||||
return chars
|
||||
|
||||
def _getCharExtents(self, start):
|
||||
# TODO - JD: For now, don't fake character and word extents.
|
||||
# The main goal is to improve reviewability.
|
||||
extents = self.x, self.y, self.width, self.height
|
||||
|
||||
chars = []
|
||||
for i, char in enumerate(self.string):
|
||||
start = i + self.startOffset
|
||||
if AXObject.supports_text(self.zone.accessible):
|
||||
try:
|
||||
rect = Atspi.Text.get_range_extents(
|
||||
self.zone.accessible, start, start + 1, Atspi.CoordType.SCREEN)
|
||||
extents = rect.x, rect.y, rect.width, rect.height
|
||||
except Exception as error:
|
||||
tokens = ["FLAT REVIEW: Exception in getRangeExtents:", error]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
chars.append(Char(self, i, start, char, *extents))
|
||||
if AXObject.supports_text(self.zone.accessible):
|
||||
try:
|
||||
rect = Atspi.Text.get_range_extents(
|
||||
self.zone.accessible, start, start + 1, Atspi.CoordType.SCREEN)
|
||||
extents = rect.x, rect.y, rect.width, rect.height
|
||||
except Exception as error:
|
||||
tokens = ["FLAT REVIEW: Exception in getRangeExtents:", error]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
return chars
|
||||
return extents
|
||||
|
||||
def getRelativeOffset(self, offset):
|
||||
"""Returns the char offset with respect to this word or -1."""
|
||||
@@ -143,6 +148,16 @@ class Zone:
|
||||
"""Represents text that is a portion of a single horizontal line."""
|
||||
|
||||
WORDS_RE = re.compile(r"(\S+\s*)", re.UNICODE)
|
||||
TEXT_ROLES = (
|
||||
Atspi.Role.LABEL,
|
||||
Atspi.Role.MENU,
|
||||
Atspi.Role.MENU_ITEM,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_MENU_ITEM,
|
||||
Atspi.Role.PAGE_TAB,
|
||||
Atspi.Role.PUSH_BUTTON,
|
||||
Atspi.Role.TABLE_CELL,
|
||||
)
|
||||
|
||||
def __init__(self, accessible, string, x, y, width, height, role=None):
|
||||
"""Creates a new Zone.
|
||||
@@ -180,10 +195,7 @@ class Zone:
|
||||
if not self._shouldFakeText():
|
||||
return self._words
|
||||
|
||||
# TODO - JD: For now, don't fake character and word extents.
|
||||
# The main goal is to improve reviewability.
|
||||
extents = self.x, self.y, self.width, self.height
|
||||
|
||||
extents = self._getFakeTextExtents()
|
||||
words = []
|
||||
for i, word in enumerate(re.finditer(self.WORDS_RE, self._string)):
|
||||
words.append(Word(self, i, word.start(), word.group(), *extents))
|
||||
@@ -194,20 +206,16 @@ class Zone:
|
||||
def _shouldFakeText(self):
|
||||
"""Returns True if we should try to fake the text interface"""
|
||||
|
||||
textRoles = [Atspi.Role.LABEL,
|
||||
Atspi.Role.MENU,
|
||||
Atspi.Role.MENU_ITEM,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_MENU_ITEM,
|
||||
Atspi.Role.PAGE_TAB,
|
||||
Atspi.Role.PUSH_BUTTON,
|
||||
Atspi.Role.TABLE_CELL]
|
||||
|
||||
if self.role in textRoles:
|
||||
if self.role in self.TEXT_ROLES:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _getFakeTextExtents(self):
|
||||
# TODO - JD: For now, don't fake character and word extents.
|
||||
# The main goal is to improve reviewability.
|
||||
return self.x, self.y, self.width, self.height
|
||||
|
||||
def _extentsAreOnSameLine(self, zone, pixelDelta=5):
|
||||
"""Returns True if this Zone is physically on the same line as zone."""
|
||||
|
||||
@@ -291,7 +299,7 @@ class TextZone(Zone):
|
||||
string = AXText.get_substring(self.accessible, self.startOffset, self.endOffset)
|
||||
words = []
|
||||
for i, word in enumerate(re.finditer(self.WORDS_RE, string)):
|
||||
start, end = map(lambda x: x + self.startOffset, word.span())
|
||||
start, end = (pos + self.startOffset for pos in word.span())
|
||||
try:
|
||||
rect = Atspi.Text.get_range_extents(self.accessible, start, end, Atspi.CoordType.SCREEN)
|
||||
extents = rect.x, rect.y, rect.width, rect.height
|
||||
@@ -338,6 +346,9 @@ class StateZone(Zone):
|
||||
else:
|
||||
generator = cthulhu_state.activeScript.brailleGenerator
|
||||
|
||||
return self._getStateString(generator)
|
||||
|
||||
def _getStateString(self, generator):
|
||||
result = generator.getStateIndicator(self.accessible, role=self.role)
|
||||
if result:
|
||||
return result[0]
|
||||
@@ -362,21 +373,33 @@ class ValueZone(Zone):
|
||||
else:
|
||||
generator = cthulhu_state.activeScript.brailleGenerator
|
||||
|
||||
result = ""
|
||||
return self._getValueString(generator)
|
||||
|
||||
def _getValueString(self, generator):
|
||||
# TODO - JD: This cobbling together beats what we had, but the
|
||||
# generators should also be doing the assembly.
|
||||
rolename = generator.getLocalizedRoleName(self.accessible)
|
||||
value = generator.getValue(self.accessible)
|
||||
if rolename and value:
|
||||
result = f"{rolename} {value[0]}"
|
||||
return f"{rolename} {value[0]}"
|
||||
|
||||
return result
|
||||
return ""
|
||||
|
||||
|
||||
class Line:
|
||||
"""A Line is a single line across a window and is composed of Zones."""
|
||||
|
||||
TEXT_BRAILLE_ROLES = (
|
||||
Atspi.Role.TEXT,
|
||||
Atspi.Role.PASSWORD_TEXT,
|
||||
Atspi.Role.TERMINAL,
|
||||
)
|
||||
TEXT_BRAILLE_FALLBACK_ROLES = (
|
||||
Atspi.Role.PARAGRAPH,
|
||||
Atspi.Role.HEADING,
|
||||
Atspi.Role.LINK,
|
||||
)
|
||||
|
||||
def __init__(self,
|
||||
index,
|
||||
zones):
|
||||
@@ -395,16 +418,16 @@ class Line:
|
||||
return " ".join([zone.string for zone in self.zones])
|
||||
|
||||
if attr == "x":
|
||||
return min([zone.x for zone in self.zones])
|
||||
return min(zone.x for zone in self.zones)
|
||||
|
||||
if attr == "y":
|
||||
return min([zone.y for zone in self.zones])
|
||||
return min(zone.y for zone in self.zones)
|
||||
|
||||
if attr == "width":
|
||||
return sum([zone.width for zone in self.zones])
|
||||
return sum(zone.width for zone in self.zones)
|
||||
|
||||
if attr == "height":
|
||||
return max([zone.height for zone in self.zones])
|
||||
return max(zone.height for zone in self.zones)
|
||||
|
||||
return super().__getattribute__(attr)
|
||||
|
||||
@@ -420,19 +443,13 @@ class Line:
|
||||
# The 'isinstance(zone, TextZone)' test is a sanity check
|
||||
# to handle problems with Java text. See Bug 435553.
|
||||
if isinstance(zone, TextZone) and \
|
||||
((AXObject.get_role(zone.accessible) in \
|
||||
(Atspi.Role.TEXT,
|
||||
Atspi.Role.PASSWORD_TEXT,
|
||||
Atspi.Role.TERMINAL)) or \
|
||||
((AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_ROLES) or \
|
||||
# [[[TODO: Eitan - HACK:
|
||||
# This is just to get FF3 cursor key routing support.
|
||||
# We really should not be determining all this stuff here,
|
||||
# it should be in the scripts.
|
||||
# Same applies to roles above.]]]
|
||||
(AXObject.get_role(zone.accessible) in \
|
||||
(Atspi.Role.PARAGRAPH,
|
||||
Atspi.Role.HEADING,
|
||||
Atspi.Role.LINK))):
|
||||
(AXObject.get_role(zone.accessible) in self.TEXT_BRAILLE_FALLBACK_ROLES)):
|
||||
region = braille.ReviewText(zone.accessible,
|
||||
zone.string,
|
||||
zone.startOffset,
|
||||
@@ -485,6 +502,17 @@ class Context:
|
||||
WRAP_TOP_BOTTOM = 1 << 1
|
||||
WRAP_ALL = (WRAP_LINE | WRAP_TOP_BOTTOM)
|
||||
|
||||
CONTAINER_ROLES = (Atspi.Role.MENU,)
|
||||
VALUE_ZONE_ROLES = (Atspi.Role.SCROLL_BAR, Atspi.Role.SLIDER, Atspi.Role.PROGRESS_BAR)
|
||||
REDUNDANT_NAME_ROLES = (Atspi.Role.TABLE_ROW,)
|
||||
USELESS_NAME_ROLES = (Atspi.Role.TABLE_CELL, Atspi.Role.LABEL)
|
||||
STATE_ZONE_ROLES = (
|
||||
Atspi.Role.CHECK_BOX,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_BUTTON,
|
||||
Atspi.Role.RADIO_MENU_ITEM,
|
||||
)
|
||||
|
||||
def __init__(self, script, root=None):
|
||||
"""Create a new Context for script."""
|
||||
|
||||
@@ -518,10 +546,8 @@ class Context:
|
||||
tokens = ["ERROR: Exception getting extents of", self.topLevel]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
containerRoles = [Atspi.Role.MENU]
|
||||
|
||||
def isContainer(x):
|
||||
return AXObject.get_role(x) in containerRoles
|
||||
return AXObject.get_role(x) in self.CONTAINER_ROLES
|
||||
|
||||
container = AXObject.find_ancestor(self.focusObj, isContainer)
|
||||
if not container and isContainer(self.focusObj):
|
||||
@@ -567,8 +593,9 @@ class Context:
|
||||
|
||||
cliprect = self._ensureRect(cliprect)
|
||||
zones = []
|
||||
substrings = [(*m.span(), m.group(0)) for m in re.finditer(r"[^\ufffc]+", string)]
|
||||
substrings = list(map(lambda x: (x[0] + startOffset, x[1] + startOffset, x[2]), substrings))
|
||||
substrings = [(*m.span(), m.group(0)) for m in EMBEDDED_OBJECT_RE.finditer(string)]
|
||||
substrings = [(start + startOffset, end + startOffset, text)
|
||||
for (start, end, text) in substrings]
|
||||
for (start, end, substring) in substrings:
|
||||
try:
|
||||
rect = Atspi.Text.get_range_extents(accessible, start, end, Atspi.CoordType.SCREEN)
|
||||
@@ -581,23 +608,6 @@ class Context:
|
||||
|
||||
return zones
|
||||
|
||||
def _getLines(self, accessible, startOffset, endOffset):
|
||||
# TODO - JD: Move this into the script utilities so we can better handle
|
||||
# app and toolkit quirks and also reuse this (e.g. for SayAll).
|
||||
if not AXObject.supports_text(accessible):
|
||||
return []
|
||||
|
||||
lines = []
|
||||
offset = startOffset
|
||||
maxOffset = min(endOffset, AXText.get_character_count(accessible))
|
||||
while offset < maxOffset:
|
||||
line, start, end = AXText.get_line_at_offset(accessible, offset)
|
||||
if line and (line, start, end) not in lines:
|
||||
lines.append((line, start, end))
|
||||
offset = max(end, offset + 1)
|
||||
|
||||
return lines
|
||||
|
||||
def getZonesFromText(self, accessible, cliprect):
|
||||
"""Gets a list of Zones from an object that implements the
|
||||
AccessibleText specialization.
|
||||
@@ -613,15 +623,9 @@ class Context:
|
||||
if not self.script.utilities.hasPresentableText(accessible):
|
||||
return []
|
||||
|
||||
zones = []
|
||||
|
||||
# TODO - JD: This is here temporarily whilst I sort out the rest
|
||||
# of the text-related mess.
|
||||
if AXObject.supports_editable_text(accessible) \
|
||||
and AXUtilities.is_single_line(accessible):
|
||||
rect = AXComponent.get_rect(accessible)
|
||||
return [TextZone(accessible, 0, AXText.get_substring(accessible, 0, -1),
|
||||
rect.x, rect.y, rect.width, rect.height)]
|
||||
zones = self._getSingleLineEditableZones(accessible)
|
||||
if zones:
|
||||
return zones
|
||||
|
||||
upperMax = lowerMax = AXText.get_character_count(accessible)
|
||||
upperMid = lowerMid = int(upperMax / 2)
|
||||
@@ -657,7 +661,7 @@ class Context:
|
||||
msg = "FLAT REVIEW: Getting lines for %s offsets %i-%i" % (accessible, upperMin, lowerMax)
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
lines = self._getLines(accessible, upperMin, lowerMax)
|
||||
lines = self.script.utilities.getLinesForRange(accessible, upperMin, lowerMax)
|
||||
tokens = ["FLAT REVIEW:", len(lines), "lines found for", accessible]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
|
||||
@@ -666,6 +670,17 @@ class Context:
|
||||
|
||||
return zones
|
||||
|
||||
def _getSingleLineEditableZones(self, accessible):
|
||||
# TODO - JD: This is here temporarily whilst I sort out the rest
|
||||
# of the text-related mess.
|
||||
if not (AXObject.supports_editable_text(accessible)
|
||||
and AXUtilities.is_single_line(accessible)):
|
||||
return []
|
||||
|
||||
rect = AXComponent.get_rect(accessible)
|
||||
return [TextZone(accessible, 0, AXText.get_substring(accessible, 0, -1),
|
||||
rect.x, rect.y, rect.width, rect.height)]
|
||||
|
||||
def _insertStateZone(self, zones, accessible, extents):
|
||||
"""If the accessible presents non-textual state, such as a
|
||||
checkbox or radio button, insert a StateZone representing
|
||||
@@ -687,10 +702,7 @@ class Context:
|
||||
and self.script.utilities.hasMeaningfulToggleAction(accessible):
|
||||
role = Atspi.Role.CHECK_BOX
|
||||
|
||||
if role not in [Atspi.Role.CHECK_BOX,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_BUTTON,
|
||||
Atspi.Role.RADIO_MENU_ITEM]:
|
||||
if role not in self.STATE_ZONE_ROLES:
|
||||
return
|
||||
|
||||
zone = None
|
||||
@@ -733,18 +745,14 @@ class Context:
|
||||
return []
|
||||
|
||||
zones = self.getZonesFromText(accessible, cliprect)
|
||||
if not zones and role in [Atspi.Role.SCROLL_BAR,
|
||||
Atspi.Role.SLIDER,
|
||||
Atspi.Role.PROGRESS_BAR]:
|
||||
if not zones and role in self.VALUE_ZONE_ROLES:
|
||||
zones.append(ValueZone(accessible, *extents))
|
||||
elif not zones:
|
||||
string = ""
|
||||
redundant = [Atspi.Role.TABLE_ROW]
|
||||
if role not in redundant:
|
||||
if role not in self.REDUNDANT_NAME_ROLES:
|
||||
string = self.script.speechGenerator.getName(accessible, inFlatReview=True)
|
||||
|
||||
useless = [Atspi.Role.TABLE_CELL, Atspi.Role.LABEL]
|
||||
if not string and role not in useless:
|
||||
if not string and role not in self.USELESS_NAME_ROLES:
|
||||
string = self.script.speechGenerator.getRoleName(accessible)
|
||||
if string:
|
||||
zones.append(Zone(accessible, string, *extents))
|
||||
@@ -1467,3 +1475,4 @@ class Context:
|
||||
raise Exception("Invalid type: %d" % flatReviewType)
|
||||
|
||||
return moved
|
||||
EMBEDDED_OBJECT_RE = re.compile(r"[^\ufffc]+")
|
||||
|
||||
@@ -332,7 +332,7 @@ class InputEventManager:
|
||||
manager = focus_manager.get_manager()
|
||||
if pressed:
|
||||
window = manager.get_active_window()
|
||||
if not AXUtilities.can_be_active_window(window):
|
||||
if not AXUtilities.can_be_active_window(window, clear_cache=True):
|
||||
new_window = AXUtilities.find_active_window()
|
||||
if new_window is not None:
|
||||
window = new_window
|
||||
|
||||
@@ -101,6 +101,22 @@ class Utilities:
|
||||
SUBSCRIPT_DIGITS = \
|
||||
['\u2080', '\u2081', '\u2082', '\u2083', '\u2084',
|
||||
'\u2085', '\u2086', '\u2087', '\u2088', '\u2089']
|
||||
MENU_ROLES_IN_OPEN_MENU = {
|
||||
Atspi.Role.MENU,
|
||||
Atspi.Role.MENU_ITEM,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_MENU_ITEM,
|
||||
Atspi.Role.SEPARATOR,
|
||||
}
|
||||
ZOMBIE_TOP_LEVEL_ROLES = {
|
||||
Atspi.Role.APPLICATION,
|
||||
Atspi.Role.ALERT,
|
||||
Atspi.Role.DIALOG,
|
||||
Atspi.Role.LABEL, # For Unity Panel Service bug
|
||||
Atspi.Role.PAGE, # For Evince bug
|
||||
Atspi.Role.WINDOW,
|
||||
Atspi.Role.FRAME,
|
||||
}
|
||||
|
||||
flags = re.UNICODE
|
||||
WORDS_RE = re.compile(r"(\W+)", flags)
|
||||
@@ -3687,6 +3703,21 @@ class Utilities:
|
||||
debug.printException(debug.LEVEL_WARNING)
|
||||
return ""
|
||||
|
||||
def getLinesForRange(self, obj, startOffset, endOffset):
|
||||
if not AXObject.supports_text(obj):
|
||||
return []
|
||||
|
||||
lines = []
|
||||
offset = startOffset
|
||||
maxOffset = min(endOffset, AXText.get_character_count(obj))
|
||||
while offset < maxOffset:
|
||||
line, start, end = AXText.get_line_at_offset(obj, offset)
|
||||
if line and (line, start, end) not in lines:
|
||||
lines.append((line, start, end))
|
||||
offset = max(end, offset + 1)
|
||||
|
||||
return lines
|
||||
|
||||
def getLineContentsAtOffset(self, obj, offset, layoutMode=True, useCache=True):
|
||||
return []
|
||||
|
||||
@@ -4573,30 +4604,23 @@ class Utilities:
|
||||
# seems to be present in multiple toolkits, so it's either being
|
||||
# inherited (e.g. from Gtk in Firefox Chrome, LO, Eclipse) or it
|
||||
# may be an AT-SPI2 bug. For now, handling it here.
|
||||
menuRoles = [Atspi.Role.MENU,
|
||||
Atspi.Role.MENU_ITEM,
|
||||
Atspi.Role.CHECK_MENU_ITEM,
|
||||
Atspi.Role.RADIO_MENU_ITEM,
|
||||
Atspi.Role.SEPARATOR]
|
||||
if AXObject.get_role(obj) in menuRoles and self.isInOpenMenuBarMenu(obj):
|
||||
if self._is_open_menu_bar_menu_role(obj):
|
||||
tokens = ["HACK: Treating", obj, "as showing and visible"]
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _is_open_menu_bar_menu_role(self, obj):
|
||||
if AXObject.get_role(obj) not in self.MENU_ROLES_IN_OPEN_MENU:
|
||||
return False
|
||||
return self.isInOpenMenuBarMenu(obj)
|
||||
|
||||
def isZombie(self, obj):
|
||||
index = AXObject.get_index_in_parent(obj)
|
||||
topLevelRoles = [Atspi.Role.APPLICATION,
|
||||
Atspi.Role.ALERT,
|
||||
Atspi.Role.DIALOG,
|
||||
Atspi.Role.LABEL, # For Unity Panel Service bug
|
||||
Atspi.Role.PAGE, # For Evince bug
|
||||
Atspi.Role.WINDOW,
|
||||
Atspi.Role.FRAME]
|
||||
role = AXObject.get_role(obj)
|
||||
tokens = ["SCRIPT UTILITIES: ", obj, "is zombie:"]
|
||||
if index == -1 and role not in topLevelRoles:
|
||||
if index == -1 and role not in self.ZOMBIE_TOP_LEVEL_ROLES:
|
||||
tokens.append("index is -1")
|
||||
debug.printTokens(debug.LEVEL_INFO, tokens, True)
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user