diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index 645e0f9b..0e50197d 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" __authors__ = ( "Shirley Noël ", "Julien Cochuyt ", @@ -615,7 +615,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 18e22a1d..5f0e3904 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()._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 6dffdac2..e079952e 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -371,13 +371,9 @@ def update(self, nodeManager=None, force=False): results.sort() for result in results: - if not result.properties.mutation: + if not (hasattr(result, "node") and result.properties.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) @@ -542,7 +538,7 @@ def _getIncrementalResult( rule = result.rule if not result.properties.skip or rule.type != ruleTypes.ZONE: continue - zone = Zone(result) + zone = result.zone if not zone.containsTextInfo(caret): skippedZones.append(zone) for result in ( @@ -565,14 +561,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)) @@ -582,13 +577,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.equals(result.zone) ) ) ): @@ -839,12 +830,15 @@ def getCustomFunc(self, webModule=None): class Result(baseObject.ScriptableObject): - def __init__(self, criteria): + def __init__(self, criteria, context, index): super().__init__() self._criteria = weakref.ref(criteria) + self.context = context + self.index = index self.properties = criteria.properties 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): @@ -884,6 +878,12 @@ def _get_value(self): return customValue raise NotImplementedError + def _get_startOffset(self): + raise NotImplementedError + + def _get_endOffset(self): + raise NotImplementedError + def script_moveto(self, gesture): raise NotImplementedError @@ -908,9 +908,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] @@ -924,17 +931,22 @@ def getDisplayString(self): class SingleNodeResult(Result): def __init__(self, criteria, node, context, index): - super().__init__(criteria) self._node = weakref.ref(node) - self.context = context - self.index = index + super().__init__(criteria, context, index) def _get_node(self): return self._node() def _get_value(self): return self.properties.customValue or self.node.getTreeInterceptorText() - + + 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 @@ -1047,10 +1059,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 @@ -1486,111 +1505,124 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): return kwargs -class Zone(textInfos.offsets.Offsets, TrackedObject): +class Zone(baseObject.AutoPropertyObject): def __init__(self, result): + super().__init__() + self.result = result rule = result.rule self._ruleManager = weakref.ref(rule.ruleManager) + self.layer = rule.layer self.name = rule.name self.index = result.index - super().__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 __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 - ) + def _set_result(self, result): + self._result = weakref.ref(result) - def __hash__(self): - return hash((self.startOffset, self.endOffset)) + def __bool__(self): + return bool(self.result) def __repr__(self): + layer = self.layer + 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.startOffset, result.endOffset) def containsTextInfo(self, info): - if not self: - return False - if not isinstance(info, textInfos.offsets.OffsetsTextInfo): - raise ValueError("Not supported {}".format(type(info))) + try: + return self.containsOffsets(info._startOffset, info._endOffset) + except AttributeError: + if not isinstance(info, textInfos.offsets.OffsetsTextInfo): + raise ValueError("Not supported {}".format(type(info))) + raise + + def equals(self, other): + """Check if `obj` represents an instance of the same `Zone`. + + This cannot be achieved by implementing the usual `__eq__` method + because `baseObjects.AutoPropertyObject.__new__` requires it to + operate on identity as it stores the instance as key in a `WeakKeyDictionnary` + in order to later invalidate property cache. + """ return ( - self.startOffset <= info._startOffset - and info._endOffset <= self.endOffset + isinstance(other, type(self)) + and self.name == other.name + and self.index == other.index ) 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._endOffset) + except AttributeError as e: + if not isinstance(info, textInfos.offsets.OffsetsTextInfo): + raise ValueError("Not supported {}".format(type(info))) from e 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: - res = True - info._startOffset = self.startOffset - elif info._startOffset > self.endOffset: + if info._startOffset < result.startOffset: res = True - info._startOffset = self.endOffset - if info._endOffset < self.startOffset: + info._startOffset = result.startOffset + elif info._startOffset > result.endOffset: res = True - info._endOffset = self.startOffset - elif info._endOffset > self.endOffset: - res = True - info._endOffset = self.endOffset + info._startOffset = 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.getResultsByName( + self.name, layer=self.layer + )[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