From 000a68023d1371afaaecddc3708d2653cffc32bc Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 25 Jul 2024 11:07:53 +0200 Subject: [PATCH] Zone: Step towards zones not bound to a single node (#43) Remaining stumble points: - AutoAction: Based on controlID - Context: Based on nodes --- .../webAccess/gui/criteriaEditor.py | 4 +- addon/globalPlugins/webAccess/overlay.py | 12 +- .../webAccess/ruleHandler/__init__.py | 209 +++++++++++------- 3 files changed, 131 insertions(+), 94 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index fe753a17..f5154660 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/criteriaEditor.py @@ -21,7 +21,7 @@ -__version__ = "2024.07.19" +__version__ = "2024.07.25" __author__ = "Shirley Noël " @@ -608,7 +608,7 @@ def initData(self, context): rule = result.rule if ( rule.type in (ruleTypes.PARENT, ruleTypes.ZONE) - and node in result.node + and result.containsNode(node) ): parents.insert(0, rule.name) self.contextParentCombo.Set(parents) diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index 80cc5782..26c3deff 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. -# Copyright (C) 2015-2021 Accessolutions (http://accessolutions.fr) +# Copyright (C) 2015-2024 Accessolutions (http://accessolutions.fr) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -23,7 +23,7 @@ WebAccess overlay classes """ -__version__ = "2021.03.12" +__version__ = "2024.07.24" __author__ = "Julien Cochuyt " @@ -505,10 +505,10 @@ def _caretMovementScriptHelper( msg += _("Press escape to cancel zone restriction.") ui.message(msg) if posConstant == textInfos.POSITION_FIRST: - pos = zone.startOffset + pos = zone.result.startOffset posConstant = textInfos.offsets.Offsets(pos, pos) elif posConstant == textInfos.POSITION_LAST: - pos = max(zone.endOffset - 1, zone.startOffset) + pos = max(zone.result.endOffset - 1, zone.result.startOffset) posConstant = textInfos.offsets.Offsets(pos, pos) super(WebAccessBmdti, self)._caretMovementScriptHelper( gesture, @@ -557,12 +557,12 @@ def _iterNodesByType(self, itemType, direction="next", pos=None): direction ): if zone: - if item.textInfo._startOffset < zone.startOffset: + if item.textInfo._startOffset < zone.result.startOffset: if direction == "next": continue else: return - elif item.textInfo._startOffset >= zone.endOffset: + elif item.textInfo._startOffset >= zone.result.endOffset: if direction == "previous": continue else: diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 1c48b550..a29dfed2 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -363,13 +363,9 @@ def update(self, nodeManager=None, force=False): results.sort() for result in results: - if not result.get_property("mutation"): + if not (hasattr(result, "node") and result.get_property("mutation")): continue - try: - controlId = int(result.node.controlIdentifier) - except Exception: - log.exception("rule: {}, node: {}".format(result.name, result.node)) - raise + controlId = int(result.node.controlIdentifier) entry = self._mutatedControlsById.get(controlId) if entry is None: entry = MutatedControl(result) @@ -534,7 +530,7 @@ def _getIncrementalResult( rule = result.rule if not result.get_property("skip") or rule.type != ruleTypes.ZONE: continue - zone = Zone(result) + zone = result.zone if not zone.containsTextInfo(caret): skippedZones.append(zone) for result in ( @@ -557,14 +553,13 @@ def _getIncrementalResult( ): continue if ( - hasattr(result, "node") - and ( + ( not relative or ( not previous - and caret._startOffset < result.node.offset + and caret._startOffset < result.startOffset ) - or (previous and caret._startOffset > result.node.offset) + or (previous and caret._startOffset > result.startOffset) ) and ( not (respectZone or (previous and relative)) @@ -574,13 +569,9 @@ def _getIncrementalResult( not respectZone or self.zone.containsResult(result) ) - and not ( - # If respecting zone restriction or iterating - # backwards relative to the caret position, - # avoid returning the current zone itself. - self.zone.name == result.rule.name - and self.zone.containsResult(result) - ) + # If respecting zone restriction or iterating backwards relative to the + # caret position, avoid returning the current zone itself. + and not self.zone == result.zone ) ) ): @@ -928,11 +919,14 @@ def getCustomFunc(self, webModule=None): class Result(baseObject.ScriptableObject): - def __init__(self, criteria): + def __init__(self, criteria, context, index): super(Result, self).__init__() self._criteria = weakref.ref(criteria) + self.context = context + self.index = index rule = criteria.rule self._rule = weakref.ref(rule) + self.zone = Zone(self) if rule.type == ruleTypes.ZONE else None webModule = rule.ruleManager.webModule prefix = "action_" for key in dir(webModule): @@ -970,6 +964,12 @@ def _get_value(self): customValue = self.get_property("customValue") return customValue or self.node.getTreeInterceptorText() + def _get_startOffset(self): + raise NotImplementedError + + def _get_endOffset(self): + raise NotImplementedError + def get_property(self, name): return getattr( self.criteria.properties, @@ -1001,9 +1001,16 @@ def script_speak(self, gesture): def script_mouseMove(self, gesture): raise NotImplementedError + def __bool__(self): + raise NotImplementedError + def __lt__(self, other): raise NotImplementedError + def containsNode(self, node): + offset = node.offset + return self.startOffset <= offset and self.endOffset >= offset + node.size + def getDisplayString(self): return " ".join( [self.name] @@ -1017,14 +1024,19 @@ def getDisplayString(self): class SingleNodeResult(Result): def __init__(self, criteria, node, context, index): - super(SingleNodeResult, self).__init__(criteria) self._node = weakref.ref(node) - self.context = context - self.index = index + super(SingleNodeResult, self).__init__(criteria, context, index) def _get_node(self): return self._node() + def _get_startOffset(self): + return self.node.offset + + def _get_endOffset(self): + node = self.node + return node.offset + node.size + def script_moveto(self, gesture, fromQuickNav=False, fromSpeak=False): if self.node is None or self.node.nodeManager is None: return @@ -1137,10 +1149,17 @@ def script_mouseMove(self, gesture): def getTextInfo(self): return self.node.getTextInfo() + def __bool__(self): + return bool(self.node) + def __lt__(self, other): - if hasattr(other, "node") is None: - return other >= self - return self.node.offset < other.node.offset + try: + return self.startOffset < other.startOffset + except AttributeError as e: + raise TypeError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'") from e + + def containsNode(self, node): + return node in self.node def getTitle(self): return self.label + " - " + self.node.innerText @@ -1593,111 +1612,129 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): return kwargs -class Zone(textInfos.offsets.Offsets, TrackedObject): +class Zone(baseObject.AutoPropertyObject): def __init__(self, result): - rule = result.rule + super(Zone, self).__init__() self._ruleManager = weakref.ref(rule.ruleManager) + self.result = result + rule = result.rule self.name = rule.name self.index = result.index - super(Zone, self).__init__(startOffset=None, endOffset=None) - self._update(result) - @property - def ruleManager(self): + def _get_ruleManager(self): return self._ruleManager() - def __bool__(self): # Python 3 - return self.startOffset is not None and self.endOffset is not None + def _get_result(self): + return self._result and self._result() + + def _set_result(self): + self._result = weakref.ref(result) + + def __bool__(self): + return bool(self.result) def __eq__(self, other): - return ( - isinstance(other, Zone) - and other.ruleManager == self.ruleManager - and other.name == self.name - and other.startOffset == self.startOffset - and other.endOffset == self.endOffset - ) + try: + return self.name == other.name and self.index == other.index + except AttributeError: + raise False def __hash__(self): - return hash((self.startOffset, self.endOffset)) + result = self.result + return hash((result.startOffset, result.endOffset)) + + def __lt__(self): + try: + return self.startOffset < other.startOffset + except AttributeError: + raise TypeError(f"'<' not supported between instances of '{type(self)}' and '{type(other)}'") def __repr__(self): + name = self.name if not self: - return "".format(repr(self.name)) - return "".format( - repr(self.name), self.startOffset, self.endOffset - ) + return f"" + result = self.result + startOffset = result.startOffset + endOffset = result.endOffset + return f"" def containsNode(self, node): - if not self: - return False - return self.startOffset <= node.offset < self.endOffset + offset = node.offset + return self.containsOffsets(offset, offset + node.size) + + def containsOffsets(self, startOffset, endOffset): + result = self.result + return ( + result + and result.startOffset <= startOffset + and result.endOffset >= endOffset + ) def containsResult(self, result): - if not self: - return False - if hasattr(result, "node"): - return self.containsNode(result.node) - return False + return self.containsOffsets(result.startOffsets, result.endOffsets) def containsTextInfo(self, info): - if not self: - return False - if not isinstance(info, textInfos.offsets.OffsetsTextInfo): - raise ValueError("Not supported {}".format(type(info))) - return ( - self.startOffset <= info._startOffset - and info._endOffset <= self.endOffset - ) + try: + return self.containsOffsets(info._startOffset, info._endOffsets) + except AttributeError: + if not isinstance(info, textInfos.offsets.OffsetsTextInfo): + raise ValueError("Not supported {}".format(type(info))) + raise def getRule(self): return self.ruleManager.getRule(self.name) + def isOffsetAtStart(self, offset): + result = self.result + return result and result.startOffset == offset + + def isOffsetAtEnd(self, offset): + result = self.result + return result and result.endOffset == offset + def isTextInfoAtStart(self, info): - if not isinstance(info, textInfos.offsets.OffsetsTextInfo): - raise ValueError("Not supported {}".format(type(info))) - return self and info._startOffset == self.startOffset + try: + return self.isOffsetAtStart(info._startOffset) + except AttributeError: + if not isinstance(info, textInfos.offsets.OffsetsTextInfo): + raise ValueError("Not supported {}".format(type(info))) + raise def isTextInfoAtEnd(self, info): - if not isinstance(info, textInfos.offsets.OffsetsTextInfo): - raise ValueError("Not supported {}".format(type(info))) - return self and info._endOffset == self.endOffset + try: + return self.isOffsetAtEnd(info._startOffset) + except AttributeError: + if not isinstance(info, textInfos.offsets.OffsetsTextInfo): + raise ValueError("Not supported {}".format(type(info))) + raise def restrictTextInfo(self, info): if not isinstance(info, textInfos.offsets.OffsetsTextInfo): raise ValueError("Not supported {}".format(type(info))) - if not self: + result = self.result + if not result: return False res = False - if info._startOffset < self.startOffset: + if info._startOffset < result.startOffset: res = True - info._startOffset = self.startOffset - elif info._startOffset > self.endOffset: + info._startOffset = result.startOffset + elif info._startOffset > result.endOffset: res = True - info._startOffset = self.endOffset + info._startOffset = result.endOffset if info._endOffset < self.startOffset: res = True - info._endOffset = self.startOffset - elif info._endOffset > self.endOffset: + info._endOffset = result.startOffset + elif info._endOffset > result.endOffset: res = True - info._endOffset = self.endOffset + info._endOffset = result.endOffset return res def update(self): try: # Result index is 1-based - result = self.ruleManager.iterResultsByName(self.name)[self.index - 1] + self.result = self.ruleManager.iterResultsByName(self.name)[self.index - 1] except IndexError: - self.startOffset = self.endOffset = None - return False - return self._update(result) - - def _update(self, result): - node = result.node - if not node: - self.startOffset = self.endOffset = None + self._result = None return False - self.startOffset = node.offset - self.endOffset = node.offset + node.size return True