From 255a3e09313514a60bca8cc906482b00f816ae27 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 19 Jul 2024 17:03:32 +0200 Subject: [PATCH 01/54] Bump addon_lastTestedNVDAVersion --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index 332041f4..caef9c23 100644 --- a/buildVars.py +++ b/buildVars.py @@ -44,7 +44,7 @@ def _(arg): # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) "addon_minimumNVDAVersion": "2021.1", # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) - "addon_lastTestedNVDAVersion": "2024.1", + "addon_lastTestedNVDAVersion": "2024.2", # Add-on update channel (default is None, denoting stable releases, # and for development releases, use "dev".) # Do not change unless you know what you are doing! From c5b85f599128171ff014b639d9032f7482622eeb Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 19 Jul 2024 17:00:09 +0200 Subject: [PATCH 02/54] SubModules: Set addon feature version --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index caef9c23..1f6381ef 100644 --- a/buildVars.py +++ b/buildVars.py @@ -26,7 +26,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Web application modules support for modern or complex web sites."""), # version - "addon_version" : "2024.08.31", + "addon_version" : "2024.08.31-dev+subModules", # Author(s) "addon_author" : ( "Accessolutions (https://accessolutions.fr), " From 2711cbf5ce85fb2f100bec60ad750c4bb963e481 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 21 Jul 2024 18:51:13 +0200 Subject: [PATCH 03/54] ElementDescription: Add document URL --- addon/globalPlugins/webAccess/gui/elementDescription.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/elementDescription.py b/addon/globalPlugins/webAccess/gui/elementDescription.py index 462c8b8e..8f748e4d 100644 --- a/addon/globalPlugins/webAccess/gui/elementDescription.py +++ b/addon/globalPlugins/webAccess/gui/elementDescription.py @@ -108,7 +108,6 @@ def getNodeDescription(): results = ruleManager.getResults() if ruleManager else [] node = focus.webAccess.nodeManager.getCaretNode() node = node.parent - obj = node.getNVDAObject() branch = [] while node is not None: parts = [] @@ -131,10 +130,15 @@ def getNodeDescription(): ))))) if node.src: parts.append(" src %s" % node.src) + if node.role == controlTypes.ROLE_DOCUMENT: + obj = node.getNVDAObject() + while obj.role != node.role: + obj = obj.parent + url = obj.IAccessibleObject.accValue(obj.IAccessibleChildID) + parts.append(" url %s" % url) parts.append(" text %s" % truncText(node)) branch.append("\n".join(parts)) node = node.parent - obj = obj.parent return "\n\n".join(branch) From 8443a8c2c2cc64b13cb12f135440c2e39ea6ec18 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 21 Jul 2024 19:42:53 +0200 Subject: [PATCH 04/54] SubModules: GUI/JSON: Add a new `subModule` property to zones Bump format version to 0.10-dev --- addon/globalPlugins/webAccess/gui/properties.py | 4 ++-- .../globalPlugins/webAccess/ruleHandler/properties.py | 10 ++++++++++ .../webAccess/webModuleHandler/webModule.py | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/properties.py b/addon/globalPlugins/webAccess/gui/properties.py index ed87fee6..79b9ebfd 100644 --- a/addon/globalPlugins/webAccess/gui/properties.py +++ b/addon/globalPlugins/webAccess/gui/properties.py @@ -165,7 +165,7 @@ def reset(self) -> None: class Properties(PropertiesBase): - + def __init__(self, context: Mapping[str, Any], *maps: Mapping[str, PropertyValue], iterOnlyFirstMap=False): """iterOnlyFirstMap: True: When iterating, include only the properties defined in the first map. @@ -538,4 +538,4 @@ def onListCtrl_itemSelected(self, evt): # called by TreeMultiCategorySettingsDialog.onKeyDown def spaceIsPressedOnTreeNode(self, withShift=False): # No shift special handling on category panels tree node - self.listCtrl.SetFocus() \ No newline at end of file + self.listCtrl.SetFocus() diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index 13e62688..48f9e031 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -155,6 +155,16 @@ class PropertySpec(Enum): displayValueIfUndefined=pgettext("webAccess.ruleProperty.mutation", "None"), isRestrictedChoice=True ) + subModule = PropertySpecValue( + ruleTypes=("zone",), + valueType=str, + default="", + # Translators: The display name for a rule property + displayName=pgettext("webAccess.ruleProperty", "Load sub-module"), + # Translators: The displayed text if there is no value for the "Load sub-module" property + displayValueIfUndefined=pgettext("webAccess.ruleProperty.subModule", "No"), + isRestrictedChoice=False + ) def __getattr__(self, name: str): """Convenience method for easier reading of client code diff --git a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py index dbb7f96c..9177cc47 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py @@ -97,7 +97,7 @@ class WebModule(baseObject.ScriptableObject): API_VERSION = version.parse("0.5") - FORMAT_VERSION_STR = "0.9-dev" + FORMAT_VERSION_STR = "0.10-dev" FORMAT_VERSION = version.parse(FORMAT_VERSION_STR) def __init__(self): From a97d2df51b89e94f626c5572d349ff7180ef0330 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 21 Jul 2024 20:45:29 +0200 Subject: [PATCH 05/54] SubModules: GUI/JSON: Add new `url` criterion --- .../webAccess/gui/criteriaEditor.py | 36 ++++++++++++++++++- .../webAccess/ruleHandler/__init__.py | 2 ++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index 99f60388..746d2cd2 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/criteriaEditor.py @@ -21,7 +21,14 @@ -__author__ = "Shirley Noël " +__version__ = "2024.07.19" +__authors__ = ( + "Shirley Noël ", + "Julien Cochuyt ", + "André-Abush Clause ", + "Sendhil Randon ", + "Gatien Bouyssou ", +) from collections import OrderedDict @@ -396,6 +403,8 @@ class CriteriaPanel(CriteriaEditorPanel): # Translator: The label for a Rule Criteria field ("src", pgettext("webAccess.ruleCriteria", "Ima&ge source:")), # Translator: The label for a Rule Criteria field + ("url", pgettext("webAccess.ruleCriteria", "Document &URL:")), + # Translator: The label for a Rule Criteria field ("relativePath", pgettext("webAccess.ruleCriteria", "R&elative path:")), # Translator: The label for a Rule Criteria field ("index", pgettext("webAccess.ruleCriteria", "Inde&x:")), @@ -560,6 +569,16 @@ def makeSettings(self, settingsSizer): row += 1 gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), pos=(row, 0)) + row += 1 + item = wx.StaticText(self, label=self.FIELDS["url"]) + gbSizer.Add(item, pos=(row, 0)) + gbSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), pos=(row, 1)) + item = self.urlCombo = SizeFrugalComboBox(self) + gbSizer.Add(item, pos=(row, 2), flag=wx.EXPAND) + + row += 1 + gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), pos=(row, 0)) + row += 1 item = wx.StaticText(self, label=self.FIELDS["relativePath"]) gbSizer.Add(item, pos=(row, 0)) @@ -623,6 +642,7 @@ def initData(self, context): classChoices = [] statesChoices = [] srcChoices = [] + urlChoices = [] # todo: actually there are empty choices created while node is not None: roleChoices.append(controlTypes.roleLabels.get(node.role, "") or "") @@ -631,6 +651,17 @@ def initData(self, context): classChoices.append(node.className or "") statesChoices.append(getStatesLblExprForSet(node.states) or "") srcChoices.append(node.src or "") + url = "" + if node.role == controlTypes.ROLE_DOCUMENT: + obj = node.getNVDAObject() + while obj.role != node.role: + try: + obj = obj.parent + except Exception: + break + if obj.role == node.role: + url = obj.IAccessibleObject.accValue(obj.IAccessibleChildID) + urlChoices.append(url) node = node.parent self.textCombo.Set(textChoices) @@ -640,6 +671,7 @@ def initData(self, context): self.classNameCombo.Set(classChoices) self.statesCombo.Set(statesChoices) self.srcCombo.Set(srcChoices) + self.urlCombo.Set(urlChoices) self.refreshContextMacroChoices(initial=True) self.onContextMacroChoice(None) @@ -662,6 +694,7 @@ def initData(self, context): else: self.statesCombo.Value = translateStatesIdToLbl(value) self.srcCombo.Value = data.get("src", "") + self.urlCombo.Value = data.get("url", "") self.relativePathCombo.Value = str(data.get("relativePath", "")) value = data.get("index", "") if isinstance(value, InvalidValue): @@ -689,6 +722,7 @@ def updateData(self): except ValidationError: data["states"] = InvalidValue(value) updateOrDrop(data, "src", self.srcCombo.Value) + updateOrDrop(data, "url", self.urlCombo.Value) updateOrDrop(data, "relativePath", self.relativePathCombo.Value) value = self.indexText.Value try: diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 480559b1..54fe2136 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1091,6 +1091,7 @@ def load(self, data): self.className = data.pop("className", None) self.states = data.pop("states", None) self.src = data.pop("src", None) + self.url = data.pop("url", None) self.relativePath = data.pop("relativePath", None) self.index = data.pop("index", None) self.gestures = data.pop("gestures", {}) @@ -1130,6 +1131,7 @@ def setIfNotNoneOrEmptyString(key, value): setIfNotNoneOrEmptyString("className", self.className) setIfNotNoneOrEmptyString("states", self.states) setIfNotNoneOrEmptyString("src", self.src) + setIfNotNoneOrEmptyString("url", self.url) setIfNotNoneOrEmptyString("relativePath", self.relativePath) setIfNotDefault("index", self.index) setIfNotDefault("gestures", self.gestures, {}) From ad14036ec677316e64910e804287ad742765bd4f Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 22 Jul 2024 00:52:24 +0200 Subject: [PATCH 06/54] Node search: Slight optimization of role and states search Convert the searched values back to ints only once, instead of once per node. --- addon/globalPlugins/webAccess/nodeHandler.py | 22 ++++++++++--------- .../webAccess/ruleHandler/__init__.py | 5 +++++ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index d3eae958..6efc6860 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -22,6 +22,7 @@ __authors__ = ( "Frédéric Brugnot ", "Julien Cochuyt ", + "Yannick Plassiard ", "André-Abush Clause ", ) @@ -639,6 +640,9 @@ def searchNode( Properties `text` and `prevText` are mutually exclusive, are only valid for the `in` test and do not support multiple values. + Properties `role` and `states`, being integers, are only valid for + the `eq` and `notEq` tests. + Returns a list of the matching nodes. """ # noqa global _count @@ -662,16 +666,14 @@ def searchNode( candidateValues = (candidateValue,) if prop == "className": if candidateValue is not None: - candidateValues = candidateValue.split(" ") - elif prop in ("role", "states"): - try: - allowedValues = [int(value) for value in allowedValues] - except ValueError: - log.error(( - "Invalid search criterion: {key}={allowedValues!r}" - ).format(**locals())) - if prop == "states": - candidateValues = candidateValue + candidateValue = candidateValue.strip() + if candidateValue: + candidateValues = candidateValue.split(" ") + elif prop == "states": + # states is a set + candidateValues = candidateValue + else: + candidateValues = (candidateValue,) for candidateValue in candidateValues: if test == "eq": if self.search_eq(allowedValues, candidateValue): diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 54fe2136..d4e2c013 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1460,6 +1460,11 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): ): if not values: continue + if prop in ("role", "states"): + try: + values = [int(value) for value in values] + except ValueError: + log.error(f"Invalid search criterion: {prop} {test} {values}") key = "{test}_{prop}#{index}".format( test=test, prop=prop, From 96177c2c37273e7024837527243461fb8122ad56 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 22 Jul 2024 01:40:29 +0200 Subject: [PATCH 07/54] Node search: Slight optimization and rationalize `in` test --- addon/globalPlugins/webAccess/nodeHandler.py | 37 ++++++------------- .../webAccess/ruleHandler/__init__.py | 11 +++--- 2 files changed, 17 insertions(+), 31 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 6efc6860..41b780f8 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -590,24 +590,6 @@ def searchString(self, text, exclude=None, limit=None): return result return [] - def search_eq(self, itemList, value): - if not isinstance(itemList, list): - itemList = [itemList] - for item in itemList: - if item == value: - return True - return False - - def search_in(self, itemList, value): - if value is None or value == "": - return False - if not isinstance(itemList, list): - itemList = [itemList] - for item in itemList: - if item.replace("*", "") in value: - return True - return False - def searchNode( self, exclude=None, @@ -632,9 +614,8 @@ def searchNode( Additional keyword arguments names are of the form: `test_property[#index]` - All of the criteria must be matched (logical `and`). - Values can be lists, in which case any value in the list can match - (logical `or`). + All of the keywords must be matched (logical `and`). + Values are collections, any element can match (logical `or`). Supported tests are: `eq`, `notEq`, `in` and `notIn`. Properties `text` and `prevText` are mutually exclusive, are only valid @@ -650,7 +631,7 @@ def searchNode( _count += 1 found = True # Copy kwargs dict to get ready for Python 3: - for key, allowedValues in list(kwargs.copy().items()): + for key, searchedValues in list(kwargs.copy().items()): if "_" not in key: log.warning("Unexpected argument: {arg}".format(arg=key)) continue @@ -676,18 +657,22 @@ def searchNode( candidateValues = (candidateValue,) for candidateValue in candidateValues: if test == "eq": - if self.search_eq(allowedValues, candidateValue): + if candidateValue in searchedValues: del kwargs[key] break elif test == "in": - if self.search_in(allowedValues, candidateValue): + if candidateValue and any( + True for searchedValue in searchedValues if searchedValue in candidateValue + ): del kwargs[key] break elif test == "notEq": - if self.search_eq(allowedValues, candidateValue): + if candidateValue in searchedValues: return [] elif test == "notIn": - if self.search_in(allowedValues, candidateValue): + if candidateValue and any( + True for searchedValue in searchedValues if searchedValue in candidateValue + ): return [] else: # no break if test in ("eq", "in"): diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index d4e2c013..e0561a9a 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1443,13 +1443,14 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): if not expr: continue if expr[0] == "!": - if "*" in expr: - notIn.append(expr[1:].strip()) + expr = expr[1:].strip() + if "*" in (expr[0], expr[-1]): + notIn.append(expr.strip("*").strip()) else: - notEq.append(expr[1:].strip()) + notEq.append(expr) else: - if "*" in expr: - in_.append(expr) + if "*" in (expr[0], expr[-1]): + in_.append(expr.strip("*").strip()) else: eq.append(expr) for test, values in ( From 49d621af2f63f241a2ed9dfb886a05663a0dd44a Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 22 Jul 2024 01:59:53 +0200 Subject: [PATCH 08/54] SubModules: Node search: Implement `url` criterion The URL is lazily fetched and cached as this can be an expensive lookup. Bump API version to 0.5 --- .../webAccess/gui/criteriaEditor.py | 12 +------- .../webAccess/gui/elementDescription.py | 8 ++--- addon/globalPlugins/webAccess/nodeHandler.py | 20 ++++++++++++- .../webAccess/ruleHandler/__init__.py | 29 ++++++++++++++----- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index 746d2cd2..a163fda4 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/criteriaEditor.py @@ -651,17 +651,7 @@ def initData(self, context): classChoices.append(node.className or "") statesChoices.append(getStatesLblExprForSet(node.states) or "") srcChoices.append(node.src or "") - url = "" - if node.role == controlTypes.ROLE_DOCUMENT: - obj = node.getNVDAObject() - while obj.role != node.role: - try: - obj = obj.parent - except Exception: - break - if obj.role == node.role: - url = obj.IAccessibleObject.accValue(obj.IAccessibleChildID) - urlChoices.append(url) + urlChoices.append(node.url or "") node = node.parent self.textCombo.Set(textChoices) diff --git a/addon/globalPlugins/webAccess/gui/elementDescription.py b/addon/globalPlugins/webAccess/gui/elementDescription.py index 8f748e4d..084f55d3 100644 --- a/addon/globalPlugins/webAccess/gui/elementDescription.py +++ b/addon/globalPlugins/webAccess/gui/elementDescription.py @@ -130,12 +130,8 @@ def getNodeDescription(): ))))) if node.src: parts.append(" src %s" % node.src) - if node.role == controlTypes.ROLE_DOCUMENT: - obj = node.getNVDAObject() - while obj.role != node.role: - obj = obj.parent - url = obj.IAccessibleObject.accValue(obj.IAccessibleChildID) - parts.append(" url %s" % url) + if node.url: + parts.append(" url %s" % node.url) parts.append(" text %s" % truncText(node)) branch.append("\n".join(parts)) node = node.parent diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 41b780f8..6dcacef8 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -525,7 +525,25 @@ def parent(self): @property def previousTextNode(self): return self._previousTextNode and self._previousTextNode() - + + @property + def url(self): + if hasattr(self, "_url"): + return self._url + if self.role != controlTypes.ROLE_DOCUMENT: + return None + url = None + obj = self.getNVDAObject() + while obj.role != self.role: + try: + obj = obj.parent + except Exception: + break + else: + url = obj.IAccessibleObject.accValue(obj.IAccessibleChildID) + self._url = url + return url + def isReady(self): return self.nodeManager and self.nodeManager.isReady diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index e0561a9a..7121ea1e 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1411,6 +1411,7 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): "states", "tag", "text", + "url", ]: if raiseOnUnsupported: raise ValueError( @@ -1431,8 +1432,10 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): kwargs["in_text"] = expr[1:] continue if prop == "className": + # For "className", both space and ampersand are treated as "and" operator expr = expr.replace(" ", "&") - for andIndex, expr in enumerate(expr.split("&")): + # For "url", only space is treated as "and" operator + for andIndex, expr in enumerate(expr.split("&" if prop != "url" else " ")): expr = expr.strip() eq = [] notEq = [] @@ -1444,15 +1447,27 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): continue if expr[0] == "!": expr = expr[1:].strip() - if "*" in (expr[0], expr[-1]): - notIn.append(expr.strip("*").strip()) + if prop == "url": + if expr[0] == "=": + notEq.append(expr[1:].strip()) + else: + notIn.append(expr) else: - notEq.append(expr) + if "*" in (expr[0], expr[-1]): + notIn.append(expr.strip("*").strip()) + else: + notEq.append(expr) else: - if "*" in (expr[0], expr[-1]): - in_.append(expr.strip("*").strip()) + if prop == "url": + if expr[0] == "=": + eq.append(expr[1:].strip()) + else: + in_.append(expr) else: - eq.append(expr) + if "*" in (expr[0], expr[-1]): + in_.append(expr.strip("*").strip()) + else: + eq.append(expr) for test, values in ( ("eq", eq), ("notEq", notEq), From 3c45d7f48c7aa4d8d9f3fc93a5ae42e5254f3121 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 23 Jul 2024 11:35:10 +0200 Subject: [PATCH 09/54] Zones: Support rules matching multiple results Also fixes a rare issue where an async doc update moves the zone away from the caret without canceling zone restriction. --- .../webAccess/ruleHandler/__init__.py | 14 +++++++++----- .../webAccess/ruleHandler/properties.py | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 7121ea1e..fa40a78d 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -395,7 +395,9 @@ def update(self, nodeManager=None, force=False): self._ready = True self.nodeManagerIdentifier = self.nodeManager.identifier if self.zone is not None: - if not self.zone.update(): + if not self.zone.update() or not self.zone.containsTextInfo( + self.nodeManager.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) + ): self.zone = None #logTime("update marker", t) if self.isReady: @@ -584,14 +586,14 @@ def _getIncrementalResult( or ( ( not respectZone - or self.zone.containsNode(result.node) + 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.startOffset == result.node.offset + and self.zone.containsResult(result) ) ) ) @@ -1496,6 +1498,7 @@ def __init__(self, result): rule = result.rule self._ruleManager = weakref.ref(rule.ruleManager) self.name = rule.name + self.index = result.index super().__init__(startOffset=None, endOffset=None) self._update(result) @@ -1582,8 +1585,9 @@ def restrictTextInfo(self, info): def update(self): try: - result = next(self.ruleManager.iterResultsByName(self.name)) - except StopIteration: + # Result index is 1-based + result = self.ruleManager.iterResultsByName(self.name)[self.index - 1] + except IndexError: self.startOffset = self.endOffset = None return False return self._update(result) diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index 48f9e031..ba3e48ed 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -84,7 +84,7 @@ class PropertySpec(Enum): isRestrictedChoice=True ) multiple = PropertySpecValue( - ruleTypes=("marker",), + ruleTypes=("marker", "zone"), valueType=bool, default=False, # Translators: The display name for a rule property From 295d7327a0ccba6d2db9108fce4f60dc97a879fd Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 25 Jul 2024 10:57:53 +0200 Subject: [PATCH 10/54] SubModules: Switch WebModules package to `webModulesSM` for easier testing --- addon/globalPlugins/webAccess/store/webModule.py | 2 +- addon/globalPlugins/webAccess/webModuleHandler/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/webAccess/store/webModule.py b/addon/globalPlugins/webAccess/store/webModule.py index f44f4bdd..1e2500b7 100644 --- a/addon/globalPlugins/webAccess/store/webModule.py +++ b/addon/globalPlugins/webAccess/store/webModule.py @@ -61,7 +61,7 @@ class WebModuleJsonFileDataStore(Store): - def __init__(self, name, basePath, dirName="webModulesMC"): + def __init__(self, name, basePath, dirName="webModulesSM"): super().__init__(name=name) self.basePath = basePath self.path = os.path.join(basePath, dirName) diff --git a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py index 8204e3a1..4728a02f 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py @@ -46,7 +46,7 @@ from ..store import MalformedRefError -PACKAGE_NAME = "webModulesMC" +PACKAGE_NAME = "webModulesSM" store = None _catalog = None From ef8b77ae1a78ff2dab5944f746b72e43a42c29b7 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 25 Jul 2024 11:07:53 +0200 Subject: [PATCH 11/54] 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 | 8 +- .../webAccess/ruleHandler/__init__.py | 219 ++++++++++-------- 3 files changed, 131 insertions(+), 100 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index a163fda4..807c996a 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 ", @@ -622,7 +622,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 d9bb9f0f..87607c7d 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -504,10 +504,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, @@ -556,12 +556,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 fa40a78d..bf694e6e 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -377,13 +377,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) @@ -548,7 +544,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 ( @@ -571,14 +567,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)) @@ -588,13 +583,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) ) ) ): @@ -845,12 +836,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: textInfos.offsets.Offsets = 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): @@ -890,6 +884,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 @@ -914,9 +914,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] @@ -930,17 +937,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 @@ -955,7 +967,7 @@ def script_moveto(self, gesture, fromQuickNav=False, fromSpeak=False): ) elif self.properties.sayName: speech.speakMessage(self.label) - treeInterceptor = self.node.nodeManager.treeInterceptor + treeInterceptor = self.rule.ruleManager.nodeManager.treeInterceptor if not treeInterceptor or not treeInterceptor.isReady: return treeInterceptor.passThrough = self.properties.formMode @@ -975,9 +987,8 @@ def script_moveto(self, gesture, fromQuickNav=False, fromSpeak=False): break else: rule.ruleManager.zone = None - info = treeInterceptor.makeTextInfo( - textInfos.offsets.Offsets(self.node.offset, self.node.offset) - ) + offset = self.startOffset + info = treeInterceptor.makeTextInfo(textInfos.offsets.Offsets(offset, offset)) treeInterceptor.selection = info # Refetch the position in case some dynamic content has shrunk as we left it. info = treeInterceptor.selection.copy() @@ -1053,10 +1064,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 @@ -1492,111 +1510,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) + return self.ruleManager.getRule(self.name, layer=self.layer) + + 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 From e83a67fb55c6275d502efa74a2fb415d8e36b94a Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 21 Aug 2024 11:17:17 +0200 Subject: [PATCH 12/54] Remove dead code --- addon/globalPlugins/webAccess/__init__.py | 112 ------------------ .../webAccess/webAppScheduler.py | 30 +---- .../webAccess/webModuleHandler/webModule.py | 4 - 3 files changed, 6 insertions(+), 140 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index af3c262b..2efebe2f 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -268,118 +268,6 @@ def script_showWebAccessSettings(self, gesture): # @UnusedVariable category=SCRIPT_CATEGORY, gesture="kb:nvda+control+shift+w" ) - def script_debugWebModule(self, gesture): # @UnusedVariable - global activeWebApp - focus = api.getFocusObject() - if \ - activeWebApp is None \ - and not hasattr(focus, "_webApp") \ - and not hasattr(focus, "treeInterceptor") \ - and not hasattr(focus.treeInterceptor, "_webApp") \ - and not hasattr(focus.treeInterceptor, "nodeManager"): - ui.message("Pas de WebModule actif") - return - - diverged = False - focusModule = None - treeModule = None - msg = "Divergence :" - msg += os.linesep - msg += "activeWebApp = {webModule}".format( - webModule=activeWebApp.storeRef - if hasattr(activeWebApp, "storeRef") - else activeWebApp - ) - if activeWebApp is not None: - msg += " ({id})".format(id=id(activeWebApp)) - if not hasattr(focus, "_webApp"): - msg += os.linesep - msg += "focus._webApp absent" - else: - focusModule = focus._webApp - if activeWebApp is not focusModule: - diverged = True - msg += os.linesep - msg += "focus._webApp = {webModule}".format( - webModule= - focusModule.storeRef - if hasattr(focusModule, "storeRef") - else focusModule - ) - if focusModule is not None: - msg += " ({id})".format(id=id(focusModule)) - if not hasattr(focus, "treeInterceptor"): - diverged = True - msg += os.linesep - msg += "focus.treeInterceptor absent" - else: - if focus.treeInterceptor is None: - diverged = True - msg += os.linesep - msg += "focus.treeInterceptor None" -# if not hasattr(focusModule, "treeInterceptor"): -# diverged = True -# msg += os.linesep -# msg += u"focus._webApp.treeInterceptor absent" -# elif focusModule.treeInterceptor is None: -# diverged = True -# msg += os.linesep -# msg += u"focus._webApp.treeInterceptor None" -# elif focus.treeInterceptor is not focusModule.treeInterceptor: -# diverged = True -# msg += os.linesep -# msg += u"TreeInterceptors différents" - if hasattr(focus.treeInterceptor, "_webApp"): - treeModule = focus.treeInterceptor._webApp - if \ - treeModule is not focusModule \ - or treeModule is not activeWebApp: - diverged = True - msg += os.linesep - msg += "treeInterceptor._webApp = {webModule}".format( - webModule= - treeModule.storeRef - if hasattr(treeModule, "storeRef") - else treeModule - ) - if treeModule is not None: - msg += " ({id})".format(id=id(treeModule)) - if hasattr(focus.treeInterceptor, "nodeManager"): - if focusModule is None: - diverged = True - msg += "treeInterceptor.nodeManager " - if focus.treeInterceptor.nodeManager is None: - msg += "est None" - else: - msg += "n'est pas None" - elif \ - focusModule.ruleManager.nodeManager is not \ - focus.treeInterceptor.nodeManager: - diverged = True - msg += os.linesep - msg += "NodeManagers différents" - elif focusModule.ruleManager.nodeManager is None: - msg += os.linesep - msg += "NodeManagers None" - - - allMsg = "" - - if not diverged: - try: - from six import text_type - except ImportError: - # NVDA version < 2018.3 - text_type = str - msg = text_type(focusModule.storeRef) - speech.speakMessage(msg) - allMsg += msg + os.linesep - - treeInterceptor = html.getTreeInterceptor() - msg = "nodeManager %d caractères, %s, %s" % (treeInterceptor.nodeManager.treeInterceptorSize, treeInterceptor.nodeManager.isReady, treeInterceptor.nodeManager.mainNode is not None) - speech.speakMessage(msg) - allMsg += msg + os.linesep - api.copyToClip(allMsg) @script( # Translators: Input help mode message for show Web Access menu command. diff --git a/addon/globalPlugins/webAccess/webAppScheduler.py b/addon/globalPlugins/webAccess/webAppScheduler.py index a3039ee8..269d3c1e 100644 --- a/addon/globalPlugins/webAccess/webAppScheduler.py +++ b/addon/globalPlugins/webAccess/webAppScheduler.py @@ -22,7 +22,12 @@ # Get ready for Python 3 -__author__ = "Frédéric Brugnot " +__authors__ = ( + "Frédéric Brugnot ", + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) import threading @@ -130,29 +135,6 @@ def event_treeInterceptor_gainFocus(self, treeInterceptor, firstGainFocus): #browseMode.reportPassThrough(treeInterceptor) self.send(eventName="updateNodeManager", treeInterceptor=treeInterceptor) - def event_checkWebAppManager(self): - # TODO: Should not be triggered anymore - log.error("event_checkWebAppManager") - focus = api.getFocusObject() - webApp = focus.webAccess.webModule if isinstance(focus, WebAccessObject) else None - TRACE("event_checkWebAppManager: webApp={webApp}".format( - webApp=id(webApp) if webApp is not None else None - )) - if webApp: - treeInterceptor = focus.treeInterceptor - if treeInterceptor: - #webApp.treeInterceptor = treeInterceptor - nodeManager = getattr(treeInterceptor, "nodeManager", None) - TRACE( - "event_checkWebAppManager: " - "nodeManager={nodeManager}".format( - nodeManager=id(nodeManager) - if nodeManager is not None else None - ) - ) - if nodeManager: - webApp.markerManager.update(nodeManager) - def event_updateNodeManager(self, treeInterceptor): if not ( isinstance(treeInterceptor, WebAccessBmdti) diff --git a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py index 9177cc47..21de1f58 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py @@ -280,10 +280,6 @@ def _setLayeredProperty(self, name, value): layer.dirty = True data["overrides"][name] = overridden - def event_webApp_init(self, obj, nextHandler): - self.loadUserFile() - nextHandler() - def event_webApp_pageChanged(self, pageTitle, nextHandler): speech.cancelSpeech() playWebAppSound("pageChanged") From 49e835c17e9efe8e3497c45d8c5bcfa98b315d6c Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 21 Aug 2024 13:06:58 +0200 Subject: [PATCH 13/54] Rename more legacy "WebApp" and "MarkerManager" leftovers Notably: - in `webAccess`: `supportWebApp` -> `canHaveWebAccessSupport` - in `webAppLib`: `playWebAppSound` -> `playWebAccessSound` - in `webAppScheduler`: `event_webApp` -> `event_webModule` - in `webAppScheduler`: `event_markerManagerUpdated` -> `event_ruleManagerUpdated` - in `WebModule`: `event_webApp_gainFocus` -> `event_webModule_gainFocus` - in `WebModule`: `event_webApp_loseFocus` -> `event_webModule_loseFocus` The latter two are currently dead code but will likely be revived for issue #17. --- addon/globalPlugins/webAccess/__init__.py | 73 ++++++++----------- .../webAccess/gui/webModulesManager.py | 16 ++-- addon/globalPlugins/webAccess/nodeHandler.py | 4 +- addon/globalPlugins/webAccess/overlay.py | 10 ++- .../webAccess/ruleHandler/__init__.py | 25 ++++--- .../webAccess/webAppLib/__init__.py | 12 ++- .../webAccess/webAppScheduler.py | 19 +++-- .../webAccess/webModuleHandler/webModule.py | 14 ++-- 8 files changed, 92 insertions(+), 81 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 2efebe2f..ebd84334 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -56,58 +56,49 @@ import os import re -import core import wx from NVDAObjects.IAccessible import IAccessible from NVDAObjects.IAccessible.MSHTML import MSHTML from NVDAObjects.IAccessible.ia2Web import Ia2Web from NVDAObjects.IAccessible.mozilla import Mozilla -from scriptHandler import script import addonHandler import api import baseObject from buildVersion import version_detailed as NVDA_VERSION import controlTypes +import core import eventHandler import globalPluginHandler import gui from logHandler import log -import scriptHandler -import speech +from scriptHandler import script import ui import virtualBuffers -from . import nodeHandler from . import overlay -from . import webAppLib -from .webAppLib import * -from .webAppScheduler import WebAppScheduler from . import webModuleHandler +from .webAppLib import playWebAccessSound, sleep +from .webAppScheduler import WebAppScheduler addonHandler.initTranslation() -TRACE = lambda *args, **kwargs: None # @UnusedVariable -#TRACE = log.info - SCRIPT_CATEGORY = "WebAccess" - -# -# defines sound directory -# - SOUND_DIRECTORY = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "..", "sounds") +SUPPORTED_HOSTS = ['brave', 'firefox', 'chrome', 'java', 'iexplore', 'microsoftedgecp', 'msedge'] +TRACE = lambda *args, **kwargs: None # @UnusedVariable +#TRACE = log.info +# Currently dead code, but will likely be revived for issue #17. +activeWebModule = None -supportedWebAppHosts = ['brave', 'firefox', 'chrome', 'java', 'iexplore', 'microsoftedgecp', 'msedge'] - -activeWebApp = None webAccessEnabled = True scheduler = None + class DefaultBrowserScripts(baseObject.ScriptableObject): def __init__(self, warningMessage): @@ -118,7 +109,7 @@ def __init__(self, warningMessage): self.__class__.__gestures["kb:control+shift+%s" % character] = "notAssigned" def script_notAssigned(self, gesture): # @UnusedVariable - playWebAppSound("keyError") + playWebAccessSound("keyError") sleep(0.2) ui.message(self.warningMessage) @@ -224,7 +215,7 @@ def showWebAccessGui(self): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("The current object does not support Web Access.")) return - if not supportWebApp(obj): + if not canHaveWebAccessSupport(obj): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("You must be in a web browser to use Web Access.")) return @@ -305,23 +296,23 @@ def script_toggleWebAccessSupport(self, gesture): # @UnusedVariable ui.message(_("Web Access support enabled.")) # FR: u"Support Web Access activé." -def getActiveWebApp(): - global activeWebApp - return activeWebApp +def getActiveWebModule(): + global activeWebModule + return activeWebModule -def webAppLoseFocus(obj): - global activeWebApp - if activeWebApp is not None: - sendWebAppEvent('webApp_loseFocus', obj, activeWebApp) - activeWebApp = None - #log.info("Losing webApp focus for object:\n%s\n" % ("\n".join(obj.devInfo))) +def webModuleLoseFocus(obj): + global activeWebModule + if activeWebModule is not None: + sendWebModuleEvent('webModule_loseFocus', obj, activeWebModule) + activeWebModule = None + #log.info("Losing webModule focus for object:\n%s\n" % ("\n".join(obj.devInfo))) -def supportWebApp(obj): +def canHaveWebAccessSupport(obj): if obj is None or obj.appModule is None: - return None - return obj.appModule.appName in supportedWebAppHosts + return False + return obj.appModule.appName in SUPPORTED_HOSTS def VirtualBuffer_changeNotify(cls, rootDocHandle, rootID): @@ -336,10 +327,10 @@ def virtualBuffer_loadBufferDone(self, success=True): virtualBuffer_loadBufferDone.super.__get__(self)(success=success) -def sendWebAppEvent(eventName, obj, webApp=None): - if webApp is None: +def sendWebModuleEvent(eventName, obj, webModule=None): + if webModule is None: return - scheduler.send(eventName="webApp", name=eventName, obj=obj, webApp=webApp) + scheduler.send(eventName="webModule", name=eventName, obj=obj, webModule=webModule) def eventExecuter_gen(self, eventName, obj): @@ -354,18 +345,18 @@ def eventExecuter_gen(self, eventName, obj): yield func, (obj, self.next) # webApp level - if not supportWebApp(obj) and eventName in ["gainFocus"] and activeWebApp is not None: + if not canHaveWebAccessSupport(obj) and eventName in ["gainFocus"] and activeWebModule is not None: # log.info("Received event %s on a non-hosted object" % eventName) webAppLoseFocus(obj) else: - webApp = obj.webAccess.webModule if isinstance(obj, overlay.WebAccessObject) else None - if webApp is None: - if activeWebApp is not None and obj.hasFocus: + webModule = obj.webAccess.webModule if isinstance(obj, overlay.WebAccessObject) else None + if webModule is None: + if activeWebModule is not None and obj.hasFocus: #log.info("Disabling active webApp event %s" % eventName) webAppLoseFocus(obj) else: # log.info("Getting method %s -> %s" %(webApp.name, funcName)) - func = getattr(webApp, funcName, None) + func = getattr(webModule, funcName, None) if func: yield func,(obj, self.next) diff --git a/addon/globalPlugins/webAccess/gui/webModulesManager.py b/addon/globalPlugins/webAccess/gui/webModulesManager.py index 6b815b0d..0dcf84e1 100644 --- a/addon/globalPlugins/webAccess/gui/webModulesManager.py +++ b/addon/globalPlugins/webAccess/gui/webModulesManager.py @@ -20,7 +20,11 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Julien Cochuyt " +__authors__ = ( + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) import os @@ -228,9 +232,11 @@ def onModulesListItemSelected(self, evt): self.moduleEditButton.Enable(item is not None) self.rulesManagerButton.Enable( item is not None - and hasattr(item, "markerManager") - and item.markerManager.isReady - ) + # FIXME: This test never succeeds as a live WebModule is not + # taken from the context. + # TODO: Remove this restriction for issue #42 + and item.ruleManager.isReady + ) self.moduleDeleteButton.Enable(item is not None) def onRulesManager(self, evt=None): @@ -289,4 +295,4 @@ def Show(self, context): self.Fit() self.modulesList.SetFocus() self.CentreOnScreen() - return super().Show() \ No newline at end of file + return super().Show() diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 6dcacef8..2f3682ef 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -273,7 +273,7 @@ def update(self): self.identifier = time.time() # logTime ("Update node manager %d nodes" % len(fields), t) self.updating = False - # playWebAppSound ("tick") + # playWebAccessSound("tick") self._curNode = self.caretNode = self.getCaretNode() try: info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_LAST) @@ -549,7 +549,7 @@ def isReady(self): def checkNodeManager(self): if self.nodeManager is None or not self.nodeManager.isReady: - playWebAppSound("keyError") + playWebAccessSound("keyError") return False else: return True diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index 87607c7d..8b0da048 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -23,7 +23,11 @@ WebAccess overlay classes """ -__author__ = "Julien Cochuyt " +__authors__ = ( + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) import weakref @@ -192,7 +196,7 @@ def treeInterceptor(self): @property def webModule(self): - from . import supportWebApp, webAccessEnabled + from . import canHaveWebAccessSupport, webAccessEnabled if not webAccessEnabled: return None ti = self.treeInterceptor @@ -202,7 +206,7 @@ def webModule(self): webModule = self._webModule if not webModule: obj = ti.rootNVDAObject - if not supportWebApp(obj): + if not canHaveWebAccessSupport(obj): return None from . import webModuleHandler try: diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index bf694e6e..4f6b2061 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -59,7 +59,7 @@ from ..webAppLib import ( html, logTimeStart, - playWebAppSound, + playWebAccessSound, ) from .. import webAppScheduler from . import ruleTypes @@ -107,7 +107,7 @@ def showManager(context): webModule = context["webModule"] mgr = webModule.ruleManager if not mgr.isReady: - playWebAppSound("keyError") + playWebAccessSound("keyError") time.sleep(0.2) speech.cancelSpeech() ui.message(_("Not ready")) @@ -129,7 +129,7 @@ def __init__(self, warningMessage): self.__class__.__gestures["kb:control+shift+%s" % character] = "notAssigned" def script_notAssigned(self, gesture): - playWebAppSound("keyError") + playWebAccessSound("keyError") callLater(200, ui.message, self.warningMessage) __gestures = {} @@ -397,7 +397,7 @@ def update(self, nodeManager=None, force=False): self.zone = None #logTime("update marker", t) if self.isReady: - webAppScheduler.scheduler.send(eventName="markerManagerUpdated", markerManager=self) + webAppScheduler.scheduler.send(eventName="ruleManagerUpdated", ruleManager=self) self.timerCheckAutoAction = threading.Timer( 1, # Accepts floating point number for sub-second precision self.checkAutoAction @@ -416,7 +416,12 @@ def checkPageTitle(self): webModule = self.webModule if title != webModule.activePageTitle: webModule.activePageTitle = title - webAppScheduler.scheduler.send(eventName="webApp", name="webApp_pageChanged", obj=title, webApp=webModule) + webAppScheduler.scheduler.send( + eventName="webModule", + name="webModule_pageChanged", + obj=title, + webModule=webModule + ) return True return False @@ -441,7 +446,7 @@ def checkAutoAction(self): if (lastText is None or text != lastText): self.triggeredIdentifiers[controlIdentifier] = text if autoActionName == "speak": - playWebAppSound("errorMessage") + playWebAccessSound("errorMessage") elif autoActionName == "moveto": if lastText is None: # only if it's a new identifier @@ -648,7 +653,7 @@ def quickNav( quiet=False, ): if not self.isReady: - playWebAppSound("keyError") + playWebAccessSound("keyError") ui.message(_("Not ready")) return None @@ -657,7 +662,7 @@ def quickNav( position = html.getCaretInfo() if position is None: - playWebAppSound("keyError") + playWebAccessSound("keyError") ui.message(_("Not ready")) return None @@ -675,11 +680,11 @@ def quickNav( ) if result: if not relative: - playWebAppSound("loop") + playWebAccessSound("loop") time.sleep(0.2) break else: - playWebAppSound("keyError") + playWebAccessSound("keyError") time.sleep(0.2) if quiet: return False diff --git a/addon/globalPlugins/webAccess/webAppLib/__init__.py b/addon/globalPlugins/webAccess/webAppLib/__init__.py index 6aadbc49..c8257fc1 100644 --- a/addon/globalPlugins/webAccess/webAppLib/__init__.py +++ b/addon/globalPlugins/webAccess/webAppLib/__init__.py @@ -22,7 +22,11 @@ # Get ready for Python 3 -__author__ = "Frédéric Brugnot " +__authors__ = ( + "Frédéric Brugnot ", + "Julien Cochuyt ", + "André-Abush Clause ", +) import os @@ -62,12 +66,12 @@ def speechOn(delay=0): api.processPendingEvents () speech.setSpeechMode(speech.SpeechMode.talk) -def playWebAppSound (name): +def playWebAccessSound(name): from ... import webAccess try: playSound(os.path.join(webAccess.SOUND_DIRECTORY, "%s.wav" % name)) - except: - pass + except Exception: + log.exception() def playSound(sound): sound = os.path.abspath(os.path.join(os.path.dirname(__file__), sound)) diff --git a/addon/globalPlugins/webAccess/webAppScheduler.py b/addon/globalPlugins/webAccess/webAppScheduler.py index 269d3c1e..a12e940a 100644 --- a/addon/globalPlugins/webAccess/webAppScheduler.py +++ b/addon/globalPlugins/webAccess/webAppScheduler.py @@ -85,7 +85,7 @@ def run(self): else: log.info("event %s is not found" % eventName) - log.info ("webAppScheduler stopped !") + log.info("webAppScheduler stopped!") def send(self, **kwargs): self.queue.put(kwargs) @@ -108,10 +108,10 @@ def event_timeout(self): def fakeNext(self = None): return True - def event_webApp(self, name=None, obj=None, webApp=None): + def event_webModule(self, name=None, obj=None, webModule=None): funcName = 'event_%s' % name - #log.info("webApp %s will handle the event %s" % (webApp.name, name)) - func = getattr(webApp, funcName, None) + #log.info("webModule %s will handle the event %s" % (webModule.name, name)) + func = getattr(webModule, funcName, None) if func: func(obj, self.fakeNext) @@ -153,10 +153,9 @@ def event_nodeManagerUpdated(self, nodeManager): return nodeManager.treeInterceptor.webAccess.ruleManager.update(nodeManager) - def event_markerManagerUpdated(self, markerManager): + def event_ruleManagerUpdated(self, ruleManager): # Doesn't work outside of the main thread for Google Chrome 83 - wx.CallAfter(markerManager.checkPageTitle) - # markerManager.checkAutoAction() + wx.CallAfter(ruleManager.checkPageTitle) def event_gainFocus(self, obj): pass @@ -169,7 +168,7 @@ def onNodeMoveto(self, node, reason): if webModule is not None: scheduler.send( - eventName="webApp", + eventName="webModule", name='node_gainFocus', - obj=node, webApp=webModule - ) + obj=node, webModule=webModule + ) diff --git a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py index 21de1f58..db6e051a 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py @@ -48,7 +48,7 @@ import ui from ..lib.markdown2 import markdown from ..lib.packaging import version -from ..webAppLib import playWebAppSound +from ..webAppLib import playWebAccessSound from .. import ruleHandler class InvalidApiVersion(version.InvalidVersion): @@ -280,15 +280,17 @@ def _setLayeredProperty(self, name, value): layer.dirty = True data["overrides"][name] = overridden - def event_webApp_pageChanged(self, pageTitle, nextHandler): + def event_webModule_pageChanged(self, pageTitle, nextHandler): speech.cancelSpeech() - playWebAppSound("pageChanged") + playWebAccessSound("pageChanged") speech.speakMessage(pageTitle) - def event_webApp_gainFocus(self, obj, nextHandler): + # Currently dead code, but will likely be revived for issue #17 + def event_webModule_gainFocus(self, obj, nextHandler): if obj.role not in [controlTypes.ROLE_DOCUMENT, controlTypes.ROLE_FRAME, controlTypes.ROLE_INTERNALFRAME]: nextHandler() + # Currently dead code, but will likely be revived for issue #17 def event_focusEntered(self, obj, nextHandler): if obj.role != controlTypes.ROLE_DOCUMENT: nextHandler() @@ -296,8 +298,8 @@ def event_focusEntered(self, obj, nextHandler): def event_gainFocus(self, obj, nextHandler): nextHandler() - def event_webApp_loseFocus(self, obj, nextHandler): - playWebAppSound("webAppLoseFocus") + def event_webModule_loseFocus(self, obj, nextHandler): + playWebAccessSound("webAppLoseFocus") nextHandler() def script_contextualHelp(self, gesture): From 20c013a9c012ea9b15fa9121c13ea28d82c731c9 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 25 Aug 2024 15:17:13 +0200 Subject: [PATCH 14/54] GUI: Follow-up of NVDA #15121 - `gui.mainFrame._popupSettingsDialog` is deprecated as of NVDA 2023.2 - `gui.mainFrame.popupSettingsDialog` is now part of the public API --- addon/globalPlugins/webAccess/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index ebd84334..982987ac 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -76,8 +76,7 @@ import ui import virtualBuffers -from . import overlay -from . import webModuleHandler +from . import overlay, webModuleHandler from .webAppLib import playWebAccessSound, sleep from .webAppScheduler import WebAppScheduler From 5761d4cd03e6d5f638bdf09d5969705b62013f6f Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 26 Aug 2024 02:51:10 +0200 Subject: [PATCH 15/54] Rules offline edition (#42) - RulesManager: Disable group by position when results aren't available - GUI: RulesManager: Add current group by option to dialog title - GUI: RulesManager: `ctrl(+shift)+tab` cycles through group by options - GUI: RulesManager: `F6` switches from the list to the summary or comments back and forth - GUI: RulesManager: `enter` on the "Group By" radios sets focus to the tree - GUI: RulesManager: `*` and `/` on the numpad while on the tree expands/collapses it all - GUI: RulesManager: Fix layout - GUI: RuleEditor: Fix checking for conflicting name when renaming a rule - GUI: RuleEditor: Fix layout - Remove `showManager`, `showCreator` and `showEditor` from `webModuleHandler` and `ruleHandler` - webModuleHandler: Remove `getEditableScratchpadWebModule` and `getEditableUserConfigWebModule` from public API - WebModule: Add `getWritableLayer` to public API - WebModuleDataLayer: Remove obsolete `rulesOnly` attribute - Bump API version to 0.6 --- addon/globalPlugins/webAccess/__init__.py | 14 +- addon/globalPlugins/webAccess/gui/__init__.py | 38 +- addon/globalPlugins/webAccess/gui/actions.py | 18 +- .../webAccess/gui/criteriaEditor.py | 17 +- .../webAccess/gui/elementDescription.py | 19 +- .../webAccess/gui/gestureBinding.py | 11 +- addon/globalPlugins/webAccess/gui/menu.py | 35 +- addon/globalPlugins/webAccess/gui/rule/abc.py | 2 +- .../webAccess/gui/rule/editor.py | 179 +++++---- .../webAccess/gui/rule/manager.py | 355 +++++++++++------- .../webAccess/gui/webModuleEditor.py | 92 ++--- .../webAccess/gui/webModulesManager.py | 250 ++++++------ .../webAccess/ruleHandler/__init__.py | 38 +- .../webAccess/store/webModule.py | 3 +- .../webAccess/webModuleHandler/__init__.py | 118 ++---- .../webAccess/webModuleHandler/webModule.py | 72 ++-- 16 files changed, 630 insertions(+), 631 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 982987ac..3571784d 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -210,10 +210,6 @@ def script_showWebAccessGui(self, gesture): # @UnusedVariable def showWebAccessGui(self): obj = api.getFocusObject() - if obj is None or obj.appModule is None: - # Translators: Error message when attempting to show the Web Access GUI. - ui.message(_("The current object does not support Web Access.")) - return if not canHaveWebAccessSupport(obj): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("You must be in a web browser to use Web Access.")) @@ -222,15 +218,17 @@ def showWebAccessGui(self): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("You must be on the web page to use Web Access.")) return - from .gui import menu - context = {} - context["webAccess"] = self - context["focusObject"] = obj + context = { + "webAccess": self, + "focusObject": obj, + } webModule = obj.webAccess.webModule if webModule is not None: context["webModule"] = webModule context["pageTitle"] = webModule.pageTitle + mgr = webModule.ruleManager + context["result"] = mgr.getResultAtCaret(focus=obj) menu.show(context) @script( diff --git a/addon/globalPlugins/webAccess/gui/__init__.py b/addon/globalPlugins/webAccess/gui/__init__.py index 7934c0e0..e8ea9c4d 100644 --- a/addon/globalPlugins/webAccess/gui/__init__.py +++ b/addon/globalPlugins/webAccess/gui/__init__.py @@ -38,7 +38,8 @@ import wx import wx.lib.mixins.listctrl as listmix -from gui import guiHelper, nvdaControls, _isDebug +import gui +from gui import guiHelper, nvdaControls from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit from gui.settingsDialogs import ( MultiCategorySettingsDialog, @@ -217,6 +218,7 @@ def __str__(self): class ScalingMixin(DpiScalingHelperMixinWithoutInit): + def scale(self, *args): sizes = tuple(( self.scaleSize(arg) if arg > 0 else arg @@ -246,6 +248,13 @@ def _buildGui(self): self.SetSizer(self.mainSizer) +# TODO: Consider migrating to NVDA's SettingsDialog once we hit 2023.2 as minimum version +class ContextualDialog(ScalingMixin, wx.Dialog): + + def initData(self, context): + self.context = context + + class ContextualSettingsPanel(FillableSettingsPanel, metaclass=guiHelper.SIPABCMeta): """ABC for the different editor panels. @@ -298,7 +307,9 @@ class FillableMultiCategorySettingsDialog(MultiCategorySettingsDialog, ScalingMi See `FillableSettingsPanel` """ - + + onCategoryChange = guarded(MultiCategorySettingsDialog.onCategoryChange) + def _getCategoryPanel(self, catId): # Changes to the original implementation: # - Add `proportion=1` @@ -331,7 +342,7 @@ def _getCategoryPanel(self, catId): panel.SetAccessible(SettingsPanelAccessible(panel)) return panel - + @guarded def _enterActivatesOk_ctrlSActivatesApply(self, evt): if evt.KeyCode in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER): @@ -403,6 +414,7 @@ def focusContainerControl(self, index: int): class ContextualMultiCategorySettingsDialog( KbNavMultiCategorySettingsDialog, configuredSettingsDialogType(hasApplyButton=False), + ContextualDialog, ): def __new__(cls, *args, **kwargs): @@ -456,20 +468,17 @@ def _getCategoryPanel(self, catId): # Changed from NVDA's MultiCategorySettingsDialog: Use ValidationError instead of ValueError, # in order to not misinterpret a real unintentional ValueError. - # Hence, ContextualSettingsPanel.isValid can either return False or willingly raise a ValidationError - # with the same outcome of cancelling the save operation and the destruction of the dialog. # Additionnaly, this implementation selects the category for the invalid panel. def _validateAllPanels(self): """Check if all panels are valid, and can be saved @note: raises ValidationError if a panel is not valid. See c{SettingsPanel.isValid} """ for panel in self.catIdToInstanceMap.values(): - if panel.isValid() is False: + if not panel.isValid(): self.selectPanel(panel) raise ValidationError("Validation for %s blocked saving settings" % panel.__class__.__name__) - class TreeContextualPanel(ContextualSettingsPanel): CATEGORY_PARAMS_CONTEXT_KEY = "TreeContextualPanel.categoryParams" @@ -783,12 +792,23 @@ def updateTreeParams(self, tree, treeNode, treeParent=None): prm.treeParent = treeParent -def showContextualDialog(cls, context, parent, *args, **kwargs): +def showContextualDialog( + cls: type(ContextualDialog), + context: Mapping[str, Any], + parent: wx.Window, + *args, + **kwargs +): + """ + Show a `ContextualDialog` + + If a `parent` is specified, the dialog is shown modal and this function + returns its return code. + """ if parent is not None: with cls(parent, *args, **kwargs) as dlg: dlg.initData(context) return dlg.ShowModal() - import gui gui.mainFrame.prePopup() try: dlg = cls(gui.mainFrame, *args, **kwargs) diff --git a/addon/globalPlugins/webAccess/gui/actions.py b/addon/globalPlugins/webAccess/gui/actions.py index a2cd5fed..ad4727fc 100644 --- a/addon/globalPlugins/webAccess/gui/actions.py +++ b/addon/globalPlugins/webAccess/gui/actions.py @@ -23,6 +23,9 @@ __authors__ = ( "Shirley Noel ", "Julien Cochuyt ", + "André-Abush Clause ", + "Sendhil Randon ", + "Gatien Bouyssou ", ) @@ -192,13 +195,12 @@ def onAutoActionChoice(self, evt): @guarded def onAddGesture(self, evt): - context = self.context + context = self.context.copy() context["data"]["gestures"] = self.gesturesMap - if gestureBinding.show(context=context, parent=self) == wx.ID_OK: - id = context["data"].pop("gestureBinding")["gestureIdentifier"] + if gestureBinding.show(context, self): + id = context["data"]["gestureBinding"]["gestureIdentifier"] self.onGestureChange(Change.CREATION, id) - del context["data"]["gestures"] - + @guarded def onDeleteGesture(self, evt): index = self.gesturesListBox.Selection @@ -208,15 +210,13 @@ def onDeleteGesture(self, evt): @guarded def onEditGesture(self, evt): - context = self.context + context = self.context.copy() gestures = context["data"]["gestures"] = self.gesturesMap id = self.getSelectedGesture() context["data"]["gestureBinding"] = {"gestureIdentifier": id, "action": gestures[id]} - if gestureBinding.show(context=context, parent=self) == wx.ID_OK: + if gestureBinding.show(context=context, parent=self): id = context["data"]["gestureBinding"]["gestureIdentifier"] self.onGestureChange(Change.UPDATE, id) - del context["data"]["gestureBinding"] - del context["data"]["gestures"] def onGestureChange(self, change: Change, id: str): if change is Change.DELETION: diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index 807c996a..e94b7095 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/criteriaEditor.py @@ -236,14 +236,14 @@ def getSummary(context, data, indent="", condensed=False) -> str: def testCriteria(context): ruleData = deepcopy(context["data"]["rule"]) ruleData["name"] = "__tmp__" - ruleData.pop("new", None) + # Other rule types might not support the "multiple" property we are forcing for the test ruleData["type"] = ruleTypes.MARKER critData = context["data"]["criteria"].copy() critData.pop("new", None) critData.pop("criteriaIndex", None) ruleData["criteria"] = [critData] ruleData.setdefault("properties", {})['multiple'] = True - critData.setdefault("properties", {}).pop("multiple", True) + critData.setdefault("properties", {}).pop("multiple", None) mgr = context["webModule"].ruleManager from ..ruleHandler import Rule rule = Rule(mgr, ruleData) @@ -331,17 +331,18 @@ def makeSettings(self, settingsSizer): def initData(self, context): super().initData(context) - data = self.getData() - new = data.get("new", False) self.sequenceOrderChoice.Clear() - nbCriteria = len(context["data"]["rule"]["criteria"]) + (1 if new else 0) - if nbCriteria == 1: + nbAlternatives = len(context["data"]["rule"]["criteria"]) + if context.get("new"): + nbAlternatives += 1 + data = self.getData() + if nbAlternatives == 1: for item in self.hideable: item.Show(False) else: - for index in range(nbCriteria): + for index in range(nbAlternatives): self.sequenceOrderChoice.Append(str(index + 1)) - index = data.get("criteriaIndex", nbCriteria + 1) + index = data.get("criteriaIndex", nbAlternatives + 1) self.sequenceOrderChoice.SetSelection(index) self.criteriaName.Value = data.get("name", "") self.commentText.Value = data.get("comment", "") diff --git a/addon/globalPlugins/webAccess/gui/elementDescription.py b/addon/globalPlugins/webAccess/gui/elementDescription.py index 084f55d3..919e6366 100644 --- a/addon/globalPlugins/webAccess/gui/elementDescription.py +++ b/addon/globalPlugins/webAccess/gui/elementDescription.py @@ -23,7 +23,12 @@ # Get ready for Python 3 -__author__ = "Frédéric Brugnot " +__authors__ = ( + "Frédéric Brugnot ", + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) __license__ = "GPL" @@ -98,11 +103,13 @@ def formatAttributes(dic): def getNodeDescription(): import api from ..overlay import WebAccessObject - focus = api.getFocusObject() - if not ( - isinstance(focus, WebAccessObject) - and focus.webAccess.nodeManager - ): + for focus in (api.getFocusObject(), gui.mainFrame.prevFocus): + if ( + isinstance(focus, WebAccessObject) + and focus.webAccess.nodeManager + ): + break + else: return _("No NodeManager") ruleManager = focus.webAccess.ruleManager results = ruleManager.getResults() if ruleManager else [] diff --git a/addon/globalPlugins/webAccess/gui/gestureBinding.py b/addon/globalPlugins/webAccess/gui/gestureBinding.py index dce0da70..d994c593 100644 --- a/addon/globalPlugins/webAccess/gui/gestureBinding.py +++ b/addon/globalPlugins/webAccess/gui/gestureBinding.py @@ -20,7 +20,14 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Shirley Noel " +__authors__ = ( + "Shirley Noel ", + "Frédéric Brugnot ", + "Julien Cochuyt ", + "André-Abush Clause ", + "Sendhil Randon ", + "Gatien Bouyssou ", +) import sys @@ -216,4 +223,4 @@ def _captureFunc(self, gesture): def show(context: Mapping[str, Any], parent: wx.Window): - return showContextualDialog(GestureBindingDialog, context, parent) + return showContextualDialog(GestureBindingDialog, context, parent) == wx.ID_OK diff --git a/addon/globalPlugins/webAccess/gui/menu.py b/addon/globalPlugins/webAccess/gui/menu.py index db56c3ab..3e2b9759 100644 --- a/addon/globalPlugins/webAccess/gui/menu.py +++ b/addon/globalPlugins/webAccess/gui/menu.py @@ -22,7 +22,11 @@ """Web Access GUI.""" -__author__ = "Julien Cochuyt " +__authors__ = ( + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) import wx @@ -32,7 +36,6 @@ from ... import webAccess from .. import ruleHandler -from .. import webModuleHandler from ..utils import guarded from . import webModulesManager @@ -52,17 +55,15 @@ def __init__(self, context): self.context = context if webAccess.webAccessEnabled: - webModule = context["webModule"] if "webModule" in context else None + webModule = context.get("webModule") - if webModule is not None: + if webModule: item = self.Append( wx.ID_ANY, # Translators: Web Access menu item label. _("&New rule...") ) self.Bind(wx.EVT_MENU, self.onRuleCreate, item) - - if webModule is not None: item = self.Append( wx.ID_ANY, # Translators: Web Access menu item label. @@ -71,13 +72,14 @@ def __init__(self, context): self.Bind(wx.EVT_MENU, self.onRulesManager, item) self.AppendSeparator() - if webModule is None: + if not webModule: item = self.Append( wx.ID_ANY, # Translators: Web Access menu item label. _("&New web module...")) self.Bind(wx.EVT_MENU, self.onWebModuleCreate, item) - else: + + if webModule: item = self.Append( wx.ID_ANY, # Translators: Web Access menu item label. @@ -109,23 +111,30 @@ def show(self): @guarded def onRuleCreate(self, evt): - ruleHandler.showCreator(self.context) + self.context["new"] = True + from .rule.editor import show + show(self.context, gui.mainFrame) @guarded def onRulesManager(self, evt): - ruleHandler.showManager(self.context) + from .rule.manager import show + show(self.context, gui.mainFrame) @guarded def onWebModuleCreate(self, evt): - webModuleHandler.showCreator(self.context) + self.context["new"] = True + from .webModuleEditor import show + show(self.context, gui.mainFrame) @guarded def onWebModuleEdit(self, evt): - webModuleHandler.showEditor(self.context) + from .webModuleEditor import show + show(self.context) @guarded def onWebModulesManager(self, evt): - webModuleHandler.showManager(self.context) + from .webModulesManager import show + show(self.context) @guarded def onWebAccessToggle(self, evt): diff --git a/addon/globalPlugins/webAccess/gui/rule/abc.py b/addon/globalPlugins/webAccess/gui/rule/abc.py index 5e598079..cf1968d3 100644 --- a/addon/globalPlugins/webAccess/gui/rule/abc.py +++ b/addon/globalPlugins/webAccess/gui/rule/abc.py @@ -31,7 +31,7 @@ class RuleAwarePanelBase(ContextualSettingsPanel, metaclass=guiHelper.SIPABCMeta): def getRuleData(self): - return self.context["data"].setdefault("rule", {}) + return self.context.setdefault("data", {}).setdefault("rule", {}) def getRuleManager(self): return self.context["webModule"].ruleManager diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index 7ca2b67a..d90d0f12 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -61,6 +61,7 @@ TreeContextualPanel, TreeMultiCategorySettingsDialog, TreeNodeInfo, + ValidationError, criteriaEditor, gestureBinding, showContextualDialog, @@ -328,36 +329,34 @@ def isValid(self): ) self.ruleName.SetFocus() return False - - mgr = self.getRuleManager() - layerName = self.context["rule"].layer if "rule" in self.context else None - webModule = webModuleHandler.getEditableWebModule(mgr.webModule, layerName=layerName) - if not webModule: - return False - if layerName == "addon": - if not webModule.getLayer("addon") and webModule.getLayer("scratchpad"): - layerName = "scratchpad" - elif layerName is None: - layerName = webModule._getWritableLayer().name - if layerName is None: - layerName = False - try: - rule = mgr.getRule(self.ruleName.Value, layer=layerName) - except LookupError: - rule = None - if rule is not None: - moduleRules = self.getRuleManager().getRules() - isExists = [True if i.name is rule.name else False for i in moduleRules] - if "new" in self.context and self.context["new"]: - if isExists: - gui.messageBox( - # Translators: Error message when another rule with the same name already exists - message=_("There already is another rule with the same name."), - caption=_("Error"), - style=wx.ICON_ERROR | wx.OK, - parent=self - ) - return False + newName = data["name"] + context = self.context + if context.get("new"): + prevName = None + webModule = webModuleHandler.getEditableWebModule(context["webModule"]) + if not webModule: + # Raising rather than returning False does not focus the panel + raise ValidationError("The WebModule is not editable") + layer = webModule.getWritableLayer() + else: + rule = context["rule"] + prevName = rule.name + layer = rule.layer + if newName != prevName: + mgr = self.getRuleManager() + try: + mgr.getRule(newName, layer) + except LookupError: + pass + else: + gui.messageBox( + # Translators: Error message when another rule with the same name already exists + message=_("There already is another rule with the same name."), + caption=_("Error"), + style=wx.ICON_ERROR | wx.OK, + parent=self + ) + return False return True @@ -480,28 +479,30 @@ def onCriteriaChange(self, change: Change, index: int): @guarded def onNewCriteria(self, evt): - context = self.context prm = self.categoryParams - context["data"]["criteria"] = OrderedDict({ - "new": True, - "criteriaIndex": len(context["data"]["rule"]["criteria"]) + listData = self.getData() + context = self.context.copy() + context["new"] = True + itemData = context["data"]["criteria"] = OrderedDict({ + "criteriaIndex": len(self.getData()) }) - if criteriaEditor.show(context, parent=self) == wx.ID_OK: - context["data"]["criteria"].pop("new", None) - index = context["data"]["criteria"].pop("criteriaIndex") - context["data"]["rule"]["criteria"].insert(index, context["data"].pop("criteria")) + if criteriaEditor.show(context, parent=self): + index = itemData.pop("criteriaIndex") + listData.insert(index, itemData) self.onCriteriaChange(Change.CREATION, index) @guarded def onEditCriteria(self, evt): - context = self.context + context = self.context.copy() + context["new"] = False + listData = self.getData() index = self.getIndex() - context["data"]["criteria"] = context["data"]["rule"]["criteria"][index].copy() - context["data"]["criteria"]["criteriaIndex"] = index - if criteriaEditor.show(context, self) == wx.ID_OK: - del context["data"]["rule"]["criteria"][index] - index = context["data"]["criteria"].pop("criteriaIndex") - context["data"]["rule"]["criteria"].insert(index, context["data"].pop("criteria")) + itemData = context["data"]["criteria"] = listData[index].copy() + itemData["criteriaIndex"] = index + if criteriaEditor.show(context, self): + del listData[index] + index = itemData.pop("criteriaIndex") + listData.insert(index, itemData) self.onCriteriaChange(Change.UPDATE, index) @guarded @@ -780,13 +781,11 @@ def delete(self): @guarded def onAddGesture(self, evt): - context = self.context + context = self.context.copy() context["data"]["gestures"] = self.getData() - if gestureBinding.show(context=context, parent=self) == wx.ID_OK: + if gestureBinding.show(context, parent): index = context["data"]["gestureBinding"]["index"] self.updateTreeAndSelectItemAtIndex(index) - del context["data"]["gestureBinding"] - del context["data"]["gestures"] @guarded def onDeleteGesture(self, evt): @@ -804,17 +803,15 @@ def onDeleteGesture(self, evt): @guarded def onEditGesture(self, evt): prm = self.categoryParams - context = self.context + context = self.context.copy() gestures = context["data"]["gestures"] = self.getData() context["data"]["gestureBinding"] = { "gestureIdentifier": prm.gestureIdentifier, "action": gestures[prm.gestureIdentifier], } - if gestureBinding.show(context=context, parent=self) == wx.ID_OK: + if gestureBinding.show(context, self): index = context["data"]["gestureBinding"]["index"] self.updateTreeAndSelectItemAtIndex(index) - del context["data"]["gestureBinding"] - del context["data"]["gestures"] def updateTreeAndSelectItemAtIndex(self, index): prm = self.categoryParams @@ -876,8 +873,7 @@ def __init__(self, *args, **kwargs): def getGeneralChildren(self): cls = RuleEditorSingleFieldChildPanel - data = self.context["data"]["rule"] - data.setdefault("type", ruleTypes.MARKER) + data = self.getData() return tuple( TreeNodeInfo( partial(cls, editorType=editorType), @@ -901,27 +897,22 @@ def getGeneralChildren(self): ) def getAlternativeChildren(self): - ruleData = self.context['data']['rule'] - criteriaPanels = [] - for criterion in ruleData.get('criteria', []): - title = ChildAlternativePanel.getTreeNodeLabel(criterion) - criteriaPanels.append( - TreeNodeInfo( - ChildAlternativePanel, - title=title, - categoryParams=ChildAlternativePanel.CategoryParams() - ) + return tuple( + TreeNodeInfo( + ChildAlternativePanel, + title=ChildAlternativePanel.getTreeNodeLabel(data), + categoryParams=ChildAlternativePanel.CategoryParams() ) - return criteriaPanels + for data in self.getData().get("criteria", []) + ) def getActionsChildren(self): - ruleData = self.context['data']['rule'] - type = ruleData.get('type', '') - if type not in [ruleTypes.ZONE, ruleTypes.MARKER]: + data = self.getData() + if data["type"] not in [ruleTypes.ZONE, ruleTypes.MARKER]: return [] mgr = self.context["webModule"].ruleManager actionsPanel = [] - for key, value in ruleData.get('gestures', {}).items(): + for key, value in data.get('gestures', {}).items(): title = ChildActionPanel.getTreeNodeLabel(mgr, key, value) prm = ChildActionPanel.CategoryParams(title=title, gestureIdentifier=key) actionsPanel.append(TreeNodeInfo(ChildActionPanel, title=title, categoryParams=prm)) @@ -929,7 +920,7 @@ def getActionsChildren(self): def getPropertiesChildren(self) -> Sequence[TreeNodeInfo]: context = self.context - data = context.setdefault("data", {}).setdefault("rule", {}).setdefault("properties", {}) + data = self.getData().setdefault("properties", {}) props = Properties(context, data) cls = ChildPropertyPanel return tuple( @@ -940,16 +931,19 @@ def getPropertiesChildren(self) -> Sequence[TreeNodeInfo]: ) for prop in props ) - + + def getData(self): + return self.context["data"]["rule"] + def initData(self, context: Mapping[str, Any]) -> None: - rule = context.get("rule") - data = context.setdefault("data", {}).setdefault( - "rule", - rule.dump() if rule else {} - ) - mgr = context["webModule"].ruleManager if "webModule" in context else None - if not rule and mgr and mgr.nodeManager: - node = mgr.nodeManager.getCaretNode() + context.setdefault("data", {}) + if context.get("new"): + data = context["data"]["rule"] = {"type": ruleTypes.MARKER} + else: + data = context["data"]["rule"] = context["rule"].dump() + mgr = context["webModule"].ruleManager.nodeManager + if mgr: + node = mgr.getCaretNode() while node is not None: if node.role in formModeRoles: data.setdefault("properties", {})["formMode"] = True @@ -960,27 +954,24 @@ def initData(self, context: Mapping[str, Any]) -> None: def _doSave(self): super()._doSave() context = self.context + data = self.getData() mgr = context["webModule"].ruleManager - data = context["data"]["rule"] - rule = context.get("rule") - layerName = rule.layer if rule is not None else None + if context.get("new"): + layerName = None + else: + rule = context["rule"] + layerName = rule.layer webModule = webModuleHandler.getEditableWebModule(mgr.webModule, layerName=layerName) if not webModule: return - if rule is not None: - # modification mode, remove old rule + if context.get("new"): + layerName = webModule.getWritableLayer().name + else: mgr.removeRule(rule) - if layerName == "addon": - if not webModule.getLayer("addon") and webModule.getLayer("scratchpad"): - layerName = "scratchpad" - elif layerName is None: - layerName = webModule._getWritableLayer().name - - rule = webModule.createRule(data) - mgr.loadRule(layerName, rule.name, data) + context["rule"] = mgr.loadRule(layerName, data["name"], data) webModule.getLayer(layerName, raiseIfMissing=True).dirty = True webModuleHandler.save(webModule, layerName=layerName) def show(context, parent=None): - return showContextualDialog(RuleEditorDialog, context, parent) + return showContextualDialog(RuleEditorDialog, context, parent) == wx.ID_OK diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index d05c1bcb..cb5a6e9c 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -20,7 +20,13 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Shirley Noël " +__authors__ = ( + "Julien Cochuyt ", + "Shirley Noël ", + "Frédéric Brugnot ", + "André-Abush Clause ", + "Gatien Bouyssou ", +) from collections import namedtuple @@ -32,6 +38,7 @@ from gui import guiHelper import inputCore import queueHandler +import ui from ...ruleHandler import ( Rule, @@ -39,13 +46,11 @@ Zone, builtinRuleActions, ruleTypes, - showCreator, - showEditor, ) from ...utils import guarded from ...webModuleHandler import getEditableWebModule, save -from .. import ScalingMixin -from ..rule.editor import getSummary +from .. import ContextualDialog, showContextualDialog, stripAccel +from .editor import getSummary try: from six import iteritems @@ -61,10 +66,8 @@ lastActiveOnly = False -def show(context): - gui.mainFrame.prePopup() - Dialog(gui.mainFrame).ShowModal(context) - gui.mainFrame.postPopup() +def show(context, parent): + showContextualDialog(Dialog, context, parent) TreeItemData = namedtuple("TreeItemData", ("label", "obj", "children")) @@ -92,7 +95,7 @@ def getRuleLabel(rule): def getRules(ruleManager): webModule = ruleManager.webModule if not webModule.isReadOnly(): - layer = webModule._getWritableLayer().name + layer = webModule.getWritableLayer().name elif config.conf["webAccess"]["devMode"]: layer = None else: @@ -110,7 +113,7 @@ def rule_getResults_safe(rule): def getRulesByGesture(ruleManager, filter=None, active=False): gestures = {} noGesture = [] - + for rule in getRules(ruleManager): if filter and filter not in rule.name: continue @@ -177,7 +180,7 @@ def getRulesByPosition(ruleManager, filter=None, active=True): As position depends on result, the `active` criteria is ignored. """ Parent = namedtuple("Parent", ("parent", "tid", "zone")) - + def filterChildlessParent(parent): if ( not filter @@ -188,19 +191,19 @@ def filterChildlessParent(parent): if parent.parent: parent.parent.tid.children.remove(parent) return True - + webModule = ruleManager.webModule if not webModule.isReadOnly(): - layer = webModule._getWritableLayer() + layer = webModule.getWritableLayer().name elif config.conf["webAccess"]["devMode"]: layer = None else: return - + parent = None for result in ruleManager.getResults(): rule = result.rule - if layer is not None and rule.layer != layer.name: + if layer and rule.layer != layer: continue tid = TreeItemData( label=getRuleLabel(rule), @@ -287,19 +290,19 @@ def getRulesByType(ruleManager, filter=None, active=False): ) -class Dialog(wx.Dialog, ScalingMixin): - +class Dialog(ContextualDialog): + def __init__(self, parent): - scale = self.scale super().__init__( - parent=gui.mainFrame, - id=wx.ID_ANY, + parent, style=wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX ) - + + scale = self.scale + self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) mainSizer = wx.BoxSizer(wx.VERTICAL) contentsSizer = wx.BoxSizer(wx.VERTICAL) - + item = self.groupByRadio = wx.RadioBox( self, # Translator: A label on the RulesManager dialog. @@ -310,9 +313,9 @@ def __init__(self, parent): item.Bind(wx.EVT_RADIOBOX, self.onGroupByRadio) contentsSizer.Add(item, flag=wx.EXPAND) contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) - + filtersSizer = wx.GridSizer(1, 2, 10, 10) - + labeledCtrlHelper = guiHelper.LabeledControlHelper( self, # Translator: A label on the RulesManager dialog. @@ -323,7 +326,7 @@ def __init__(self, parent): item.Bind(wx.EVT_TEXT, lambda evt: self.refreshRuleList()) item.Bind(wx.EVT_TEXT_ENTER, lambda evt: self.tree.SetFocus()) filtersSizer.Add(labeledCtrlHelper.sizer, flag=wx.EXPAND) - + self.activeOnlyCheckBox = wx.CheckBox( self, # Translator: A label on the RulesManager dialog. @@ -331,10 +334,10 @@ def __init__(self, parent): ) self.activeOnlyCheckBox.Bind(wx.EVT_CHECKBOX, self.onActiveOnlyCheckBox) filtersSizer.Add(self.activeOnlyCheckBox) - + contentsSizer.Add(filtersSizer, flag=wx.EXPAND) contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) - + item = self.tree = wx.TreeCtrl( self, size=scale(700, 300), @@ -346,12 +349,12 @@ def __init__(self, parent): self.treeRoot = item.AddRoot("root") contentsSizer.Add(item, flag=wx.EXPAND, proportion=2) contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) - + descSizer = wx.GridBagSizer() descSizer.EmptyCellSize = (0, 0) contentsSizer.Add(descSizer, flag=wx.EXPAND, proportion=1) #contentsSizer.Add(descSizer, flag=wx.EXPAND) - + # Translator: The label for a field on the Rules manager item = wx.StaticText(self, label=_("Summary")) descSizer.Add(item, pos=(0, 0), flag=wx.EXPAND) @@ -360,22 +363,22 @@ def __init__(self, parent): self, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP | wx.TE_RICH ) descSizer.Add(item, pos=(2, 0), flag=wx.EXPAND) - + descSizer.Add(scale(guiHelper.SPACE_BETWEEN_BUTTONS_HORIZONTAL, 0), pos=(0, 1)) - + # Translator: The label for a field on the Rules manager item = wx.StaticText(self, label=_("Technical notes")) descSizer.Add(item, pos=(0, 2), flag=wx.EXPAND) descSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL), pos=(1, 2)) item = self.ruleComment = wx.TextCtrl(self, style=wx.TE_MULTILINE | wx.TE_READONLY) descSizer.Add(item, pos=(2, 2), flag=wx.EXPAND) - + descSizer.AddGrowableCol(0) descSizer.AddGrowableCol(2) descSizer.AddGrowableRow(2) - + contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) - + btnHelper = guiHelper.ButtonHelper(wx.HORIZONTAL) item = self.resultMoveToButton = btnHelper.addButton( self, @@ -385,14 +388,14 @@ def __init__(self, parent): item.Bind(wx.EVT_BUTTON, self.onResultMoveTo) self.AffirmativeId = item.Id item.SetDefault() - + item = btnHelper.addButton( self, # Translator: The label for a button on the RulesManager dialog. label=_("&New rule...") ) item.Bind(wx.EVT_BUTTON, self.onRuleNew) - + item = self.ruleEditButton = btnHelper.addButton( self, # Translator: The label for a button on the RulesManager dialog. @@ -400,7 +403,7 @@ def __init__(self, parent): ) item.Bind(wx.EVT_BUTTON, self.onRuleEdit) item.Enabled = False - + item = self.ruleDeleteButton = btnHelper.addButton( self, # Translator: The label for a button on the RulesManager dialog. @@ -408,7 +411,7 @@ def __init__(self, parent): ) item.Bind(wx.EVT_BUTTON, self.onRuleDelete) item.Enabled = False - + contentsSizer.Add(btnHelper.sizer, flag=wx.ALIGN_RIGHT) mainSizer.Add( contentsSizer, @@ -424,31 +427,23 @@ def __init__(self, parent): mainSizer.Fit(self) self.Sizer = mainSizer self.CentreOnScreen() - + self.tree.SetFocus() + def initData(self, context): global lastGroupBy, lastActiveOnly - self.context = context - ruleManager = self.ruleManager = context["webModule"].ruleManager - webModule = ruleManager.webModule - title = "Web Module - {}".format(webModule.name) - if config.conf["webAccess"]["devMode"]: - title += " ({})".format("/".join((layer.name for layer in webModule.layers))) - self.Title = title + super().initData(context) + context["initialSelectedResult"] = context.get("result") self.activeOnlyCheckBox.Value = lastActiveOnly - self.groupByRadio.Selection = next(( - index - for index, groupBy in enumerate(GROUP_BY) - if groupBy.id == lastGroupBy - )) - self.onGroupByRadio(None, refresh=True) - self.refreshRuleList(selectObj=context.get("result")) - + mgr = context["webModule"].ruleManager + # disableGroupByPosition returns True if it triggered refresh + not mgr.isReady and self.disableGroupByPosition() or self.refreshRuleList() + def getSelectedObject(self): selection = self.tree.Selection if not selection.IsOk(): return None return self.tree.GetItemData(self.tree.Selection).obj - + def getSelectedRule(self): obj = self.getSelectedObject() if not obj: @@ -458,64 +453,104 @@ def getSelectedRule(self): elif isinstance(obj, Result): return obj.rule return None - - def refreshRuleList(self, selectName=None, selectObj=None): + + def cycleGroupBy(self, previous: bool = False, report: bool = True): + radioBox = self.groupByRadio + index = radioBox.Selection + for safeGuard in range(radioBox.Count): + index = (index + (-1 if previous else 1)) % radioBox.Count + if radioBox.IsItemEnabled(index): + break + safeGuard += 1 + radioBox.SetSelection(index) + if report: + # Translators: Reported when cycling through rules grouping on the Rules Manager dialog + ui.message(_("Group by: {}").format( + stripAccel(GROUP_BY[self.groupByRadio.GetSelection()].label).lower()) + ) + self.onGroupByRadio(None) + + def disableGroupByPosition(self) -> bool: + """Returns `True` if the tree was refreshed as of this call. + """ + radioBox = self.groupByRadio + index = next(i for i, g in enumerate(GROUP_BY) if g.id == "position") + if radioBox.IsItemEnabled(index): + radioBox.EnableItem(index, False) + if radioBox.Selection == index: + self.cycleGroupBy(previous=True, report=False) # Selects groupBy name + return True + return False + + def refreshGroupByRadio(self): + radioBox = self.groupByRadio + index = next(i for i, g in enumerate(GROUP_BY) if g.id == lastGroupBy) + if not radioBox.IsItemEnabled(index): + index = next(i for i in range(radioBox.Count) if radioBox.IsItemEnabled(i)) + if radioBox.Selection != index: + radioBox.SetSelection(index) + self.onGroupByRadio(None) + + def refreshRuleList(self): + context = self.context + result = context.pop("initialSelectedResult", None) groupBy = GROUP_BY[self.groupByRadio.GetSelection()] + if groupBy.id == "position": + selectObj = result + else: + # Pop the just created or edited rule in order to avoid keeping it selected + # when later cycling through groupBy + selectObj = context.pop("rule", result.rule if result else None) filter = self.filterEdit.GetValue() active = self.activeOnlyCheckBox.Value - self.tree.DeleteChildren(self.treeRoot) - + tree = self.tree + root = self.treeRoot + tree.DeleteChildren(root) + tids = groupBy.func( - self.ruleManager, + self.context["webModule"].ruleManager, filter, active ) if groupBy.func else [] - - # Would be replaced by use of nonlocal in Python 3 - class SharedScope(object): - __slots__ = ("selectTreeItem",) - - shared = SharedScope() - shared.selectTreeItem = None - selectRule = None - if selectObj and isinstance(selectObj, Result): - selectRule = selectObj.rule - + + selectTreeItem = None + def addToTree(parent, tids): + nonlocal selectTreeItem for tid in tids: - tii = self.tree.AppendItem(parent, tid.label) - self.tree.SetItemData(tii, tid) - if shared.selectTreeItem is None: - if selectName: - if tid.label == selectName: - shared.selectTreeItem = tii - elif selectObj is not None: - if tid.obj is selectObj: - shared.selectTreeItem = tii - elif selectRule is not None and tid.obj is selectRule: - shared.selectTreeItem = tii + tii = tree.AppendItem(parent, tid.label) + tree.SetItemData(tii, tid) + if selectTreeItem is None: + if tid.obj is selectObj: + selectTreeItem = tii if tid.children: addToTree(tii, tid.children) - - addToTree(self.treeRoot, tids) - + + addToTree(root, tids) + if filter or groupBy.id == "position": - self.tree.ExpandAllChildren(self.treeRoot) - - if shared.selectTreeItem is not None: - # Async call ensures the selection won't get lost. - wx.CallAfter(self.tree.SelectItem, shared.selectTreeItem) - # Sync call ensures NVDA won't announce the first item of - # the tree before reporting the selection. - #self.tree.SelectItem(shared.selectTreeItem) - return - - def unselect(): - self.tree.Unselect() - self.onTreeSelChanged(None) - - wx.CallAfter(unselect) - + tree.ExpandAllChildren(root) + + if selectTreeItem is None and groupBy.id != "position": + firstChild, cookie = tree.GetFirstChild(root) + if firstChild.IsOk(): + selectTreeItem = firstChild + + if selectTreeItem: + tree.SelectItem(selectTreeItem) + tree.EnsureVisible(selectTreeItem) + + def refreshTitle(self): + webModule = self.context["webModule"] + # Translators: The title of the Rules Manager dialog + title = "Web Module {} - Rules by {}".format( + webModule.name, + stripAccel(GROUP_BY[self.groupByRadio.GetSelection()].label).lower(), + ) + if config.conf["webAccess"]["devMode"]: + title += " ({})".format("/".join((layer.name for layer in webModule.layers))) + self.Title = title + @guarded def onActiveOnlyCheckBox(self, evt): global lastActiveOnly @@ -523,10 +558,52 @@ def onActiveOnlyCheckBox(self, evt): return lastActiveOnly = self.activeOnlyCheckBox.Value self.refreshRuleList() - + + @guarded + def onCharHook(self, evt: wx.KeyEvent): + keycode = evt.KeyCode + if keycode == wx.WXK_ESCAPE: + # Try to limit the difficulty of closing the dialog using the keyboard + # in the event of an error later in this function + evt.Skip() + return + elif keycode == wx.WXK_F6 and not evt.GetModifiers(): + if self.tree.HasFocus(): + getattr(self, "_lastDetails", self.ruleSummary).SetFocus() + return + else: + for ctrl in (self.ruleSummary, self.ruleComment): + if ctrl.HasFocus(): + self._lastDetails = ctrl + self.tree.SetFocus() + return + elif keycode == wx.WXK_RETURN and not evt.GetModifiers(): + # filterEdit is handled separately (TE_PROCESS_ENTER) + for ctrl in (self.groupByRadio, self.activeOnlyCheckBox): + if ctrl.HasFocus(): + self.tree.SetFocus() + return + elif keycode == wx.WXK_TAB and evt.ControlDown(): + self.cycleGroupBy(previous=evt.ShiftDown()) + return + elif self.tree.HasFocus(): + # Collapse/Expand all instead of current node as there are only two levels. + # To also handle "*" and "/" from alphanum section of the keyboard with respect to the + # currently active keyboard layout would require calling GetKeyboardLayout and ToUnicodeEx + # (passing 0 as vkState) from user32.dll. An example can be found in NVDA's keyboardHandler. + # Probably overkill, though. + if keycode == wx.WXK_NUMPAD_MULTIPLY: + self.tree.ExpandAll() + return + elif keycode == wx.WXK_NUMPAD_DIVIDE: + self.tree.CollapseAll() + return + evt.Skip() + @guarded - def onGroupByRadio(self, evt, refresh=True): + def onGroupByRadio(self, evt, report=False): global lastGroupBy, lastActiveOnly + self.refreshTitle() groupBy = GROUP_BY[self.groupByRadio.GetSelection()] lastGroupBy = groupBy.id if groupBy.id == "position": @@ -536,9 +613,8 @@ def onGroupByRadio(self, evt, refresh=True): else: self.activeOnlyCheckBox.Value = lastActiveOnly self.activeOnlyCheckBox.Enabled = True - if refresh: - self.refreshRuleList() - + self.refreshRuleList() + @guarded def onResultMoveTo(self, evt): obj = self.getSelectedObject() @@ -559,7 +635,7 @@ def onResultMoveTo(self, evt): None ) self.Close() - + @guarded def onRuleDelete(self, evt): rule = self.getSelectedRule() @@ -576,9 +652,9 @@ def onRuleDelete(self, evt): # Translator: The title for a confirmation prompt on the # RulesManager dialog. _("Confirm Deletion"), - wx.YES | wx.NO | wx.CANCEL | wx.ICON_QUESTION, self + wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, self ) == wx.YES: - webModule = getEditableWebModule(self.ruleManager.webModule, layerName=rule.layer) + webModule = getEditableWebModule(self.context["webModule"], layerName=rule.layer) if not webModule: return self.ruleManager.removeRule(rule) @@ -588,48 +664,50 @@ def onRuleDelete(self, evt): ) self.refreshRuleList() wx.CallAfter(self.tree.SetFocus) - + @guarded def onRuleEdit(self, evt): rule = self.getSelectedRule() if not rule: wx.Bell() return - context = self.context.copy() # Shallow copy + context = self.context.copy() + context["new"] = False context["rule"] = rule - if showEditor(context, parent=self): - # Pass the eventually changed rule name - self.refreshRuleList(context["data"]["rule"]["name"]) + from .editor import show + if show(context, parent=self): + rule = self.context["rule"] = context["rule"] + # As the rule changed, all results are to be considered obsolete + if not self.disableGroupByPosition(): + self.refreshRuleList() wx.CallAfter(self.tree.SetFocus) - + @guarded def onRuleNew(self, evt): - context = self.context.copy() # Shallow copy - if showCreator(context, parent=self): - self.Close() - return -# self.groupByRadio.SetSelection(next(iter(( -# index -# for index, groupBy in enumerate(GROUP_BY) -# if groupBy.id == "name" -# )))) -# self.refreshRuleList(context["data"]["rule"]["name"]) + context = self.context.copy() + context["new"] = True + from .editor import show + if show(context, self.Parent): + rule = self.context["rule"] = context["rule"] + # As a new rule was created, all results are to be considered obsolete + if not self.disableGroupByPosition(): + self.refreshRuleList() wx.CallAfter(self.tree.SetFocus) - + @guarded def onTreeItemActivated(self, evt): self.onResultMoveTo(evt) - + @guarded def onTreeKeyDown(self, evt): - if evt.KeyCode == wx.WXK_F2: - self.onRuleEdit(evt) - elif evt.KeyCode == wx.WXK_DELETE: - self.onRuleDelete(evt) + keycode = evt.KeyCode + if keycode == wx.WXK_DELETE: + self.onRuleDelete(None) + elif keycode == wx.WXK_F2: + self.onRuleEdit(None) else: - return - evt.Skip() - + evt.Skip() + @guarded def onTreeSelChanged(self, evt): if ( @@ -653,10 +731,3 @@ def onTreeSelChanged(self, evt): context["rule"] = rule self.ruleSummary.Value = getSummary(context, rule.dump()) self.ruleComment.Value = rule.comment or "" - - def ShowModal(self, context): - self.initData(context) - self.Fit() - self.CentreOnScreen() - self.tree.SetFocus() - return super().ShowModal() diff --git a/addon/globalPlugins/webAccess/gui/webModuleEditor.py b/addon/globalPlugins/webAccess/gui/webModuleEditor.py index eb55aca1..e834be42 100644 --- a/addon/globalPlugins/webAccess/gui/webModuleEditor.py +++ b/addon/globalPlugins/webAccess/gui/webModuleEditor.py @@ -42,7 +42,7 @@ import ui from ..webModuleHandler import WebModule, getEditableWebModule, getUrl, getWindowTitle, save -from . import ScalingMixin +from . import ContextualDialog, showContextualDialog addonHandler.initTranslation() @@ -69,26 +69,14 @@ def show(context): return result == wx.ID_OK -class Dialog(wx.Dialog, ScalingMixin): - - # Singleton - _instance = None - def __new__(cls, *args, **kwargs): - if Dialog._instance is None: - return super().__new__(cls, *args, **kwargs) - return Dialog._instance +class Dialog(ContextualDialog): def __init__(self, parent): - scale = self.scale - if Dialog._instance is not None: - return - Dialog._instance = self - super().__init__( parent, style=wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX | wx.RESIZE_BORDER, ) - + scale = self.scale mainSizer = wx.BoxSizer(wx.VERTICAL) gbSizer = wx.GridBagSizer() mainSizer.Add( @@ -145,26 +133,25 @@ def __init__(self, parent): mainSizer.Add( self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL), flag=wx.EXPAND | wx.TOP | wx.DOWN, - border=4 + border=scale(guiHelper.BORDER_FOR_DIALOGS), ) self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - self.Bind(wx.EVT_BUTTON, self.onCancel, id=wx.ID_CANCEL) - #self.Sizer = mainSizer - self.SetSizerAndFit(mainSizer) + self.SetSize(scale(790, 400)) + self.SetSizer(mainSizer) + self.CentreOnScreen() + self.webModuleName.SetFocus() def initData(self, context): - self.context = context - webModule = context.get("webModule") - if webModule is None: - new = True + super().initData(context) + data = context.setdefault("data", {})["webModule"] = {} + if not context.get("new"): + webModule = context.get("webModule") + data.update(webModule.dump(webModule.layers[-1].name).data["WebModule"]) + # Translators: Web module edition dialog title + title = _("Edit Web Module") + if config.conf["webAccess"]["devMode"]: + title += " ({})".format("/".join((layer.name for layer in webModule.layers))) else: - if any(layer.dirty and layer.storeRef is None for layer in webModule.layers): - new = True - elif any(layer.storeRef is not None for layer in webModule.layers): - new = False - else: - new = True - if new: # Translators: Web module creation dialog title title = _("New Web Module") if config.conf["webAccess"]["devMode"]: @@ -180,20 +167,15 @@ def initData(self, context): ) except Exception: log.exception() - else: - # Translators: Web module edition dialog title - title = _("Edit Web Module") - if config.conf["webAccess"]["devMode"]: - title += " ({})".format("/".join((layer.name for layer in webModule.layers))) self.Title = title - self.webModuleName.Value = (webModule.name or "") if webModule is not None else "" + self.webModuleName.Value = data.get("name", "") urls = [] selectedUrl = None - if webModule is not None and webModule.url: - url = selectedUrl = ", ".join(webModule.url) - for candidate in itertools.chain([url], webModule.url): + if data.get("url"): + url = selectedUrl = ", ".join(data["url"]) + for candidate in itertools.chain([url], data["url"]): if candidate not in urls: urls.append(candidate) if "focusObject" in context: @@ -201,7 +183,7 @@ def initData(self, context): if focus and focus.treeInterceptor and focus.treeInterceptor.rootNVDAObject: urlFromObject = getUrl(focus.treeInterceptor.rootNVDAObject) if not urlFromObject: - if not webModule: + if context.get("new"): ui.message(_("URL not found")) elif urlFromObject not in urls: urls.append(urlFromObject) @@ -242,9 +224,9 @@ def initData(self, context): windowTitleChoices = [] windowTitleIsFilled = False - if webModule is not None and webModule.windowTitle: + if data.get("windowTitle"): windowTitleIsFilled = True - windowTitleChoices.append(webModule.windowTitle) + windowTitleChoices.append(data["windowTitle"]) if "focusObject" in context: obj = context["focusObject"] windowTitle = getWindowTitle(obj) @@ -257,7 +239,7 @@ def initData(self, context): else: item.Value = "" - self.help.Value = webModule.help if webModule and webModule.help else "" + self.help.Value = data.get("help", "") def onOk(self, evt): name = self.webModuleName.Value.strip() @@ -285,9 +267,10 @@ def onOk(self, evt): return context = self.context - webModule = context.get("webModule") - if webModule is None: - webModule = context["webModule"] = WebModule() + if context.get("new"): + webModule = WebModule() + else: + webModule = context["webModule"] if webModule.isReadOnly(): webModule = getEditableWebModule(webModule) if not webModule: @@ -298,19 +281,12 @@ def onOk(self, evt): webModule.windowTitle = windowTitle webModule.help = help - if not save(webModule): + if not save(webModule, prompt=self.Title): return - assert self.IsModal() - self.EndModal(wx.ID_OK) - - def onCancel(self, evt): - self.EndModal(wx.ID_CANCEL) + self.DestroyLater() + self.SetReturnCode(wx.ID_OK) - def ShowModal(self, context): - self.initData(context) - self.Fit() - self.CentreOnScreen() - self.webModuleName.SetFocus() - return super().ShowModal() +def show(context, parent=None): + return showContextualDialog(Dialog, context, parent or gui.mainFrame) == wx.ID_OK diff --git a/addon/globalPlugins/webAccess/gui/webModulesManager.py b/addon/globalPlugins/webAccess/gui/webModulesManager.py index 0dcf84e1..a3da4d4d 100644 --- a/addon/globalPlugins/webAccess/gui/webModulesManager.py +++ b/addon/globalPlugins/webAccess/gui/webModulesManager.py @@ -36,11 +36,12 @@ import core import globalVars import gui -from gui.nvdaControls import AutoWidthColumnListCtrl +from gui import guiHelper import languageHandler from logHandler import log -from . import ScalingMixin +from ..utils import guarded +from . import ContextualDialog, ListCtrlAutoWidth, showContextualDialog def promptDelete(webModule): @@ -86,77 +87,86 @@ def promptMask(webModule): ) == wx.YES -def show(context): - gui.mainFrame.prePopup() - Dialog(gui.mainFrame).Show(context) - gui.mainFrame.postPopup() - - -class Dialog(wx.Dialog, ScalingMixin): - # Singleton - _instance = None - def __new__(cls, *args, **kwargs): - if Dialog._instance is None: - return super().__new__(cls, *args, **kwargs) - return Dialog._instance - +class Dialog(ContextualDialog): + def __init__(self, parent): - if Dialog._instance is not None: - return - Dialog._instance = self - super().__init__( parent, # Translators: The title of the Web Modules Manager dialog title=_("Web Modules Manager"), style=wx.DEFAULT_DIALOG_STYLE|wx.MAXIMIZE_BOX|wx.RESIZE_BORDER, - #size=(600,400) - ) - - modulesListLabel = wx.StaticText( + ) + scale = self.scale + mainSizer = wx.BoxSizer(wx.VERTICAL) + gbSizer = wx.GridBagSizer() + mainSizer.Add( + gbSizer, + border=scale(guiHelper.BORDER_FOR_DIALOGS), + flag=wx.ALL | wx.EXPAND, + proportion=1 + ) + row = 0 + col = 0 + item = modulesListLabel = wx.StaticText( self, # Translators: The label for the modules list in the # Web Modules dialog. label=_("Available Web Modules:"), - ) - - item = self.modulesList = AutoWidthColumnListCtrl( - self, - style=wx.LC_REPORT|wx.LC_SINGLE_SEL, - #size=(550,350), - ) + ) + gbSizer.Add(item, (row, col), flag=wx.EXPAND) + + row += 1 + gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL), (row, col)) + + row += 1 + item = self.modulesList = ListCtrlAutoWidth(self, style=wx.LC_REPORT) # Translators: The label for a column of the web modules list item.InsertColumn(0, _("Name"), width=150) # Translators: The label for a column of the web modules list item.InsertColumn(1, _("Trigger")) - ## Translators: The label for a column of the web modules list - ##item.InsertColumn(1, _("URL"), width=50) - #item.InsertColumn(1, _("URL")) - ## Translators: The label for a column of the web modules list - ##item.InsertColumn(2, _("Title"), width=50) - #item.InsertColumn(2, _("Title")) - item.resizeLastColumn(50) item.Bind( wx.EVT_LIST_ITEM_FOCUSED, self.onModulesListItemSelected) - + gbSizer.Add(item, (row, col), span=(8, 1), flag=wx.EXPAND) + gbSizer.AddGrowableRow(row+7) + gbSizer.AddGrowableCol(col) + + col += 1 + gbSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), (row, col)) + + col += 1 item = self.moduleCreateButton = wx.Button( self, # Translators: The label for a button in the Web Modules Manager label=_("&New web module..."), - ) + ) item.Bind(wx.EVT_BUTTON, self.onModuleCreate) - + gbSizer.Add(item, (row, col), flag=wx.EXPAND) + + row += 1 + gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), (row, col)) + + row += 1 # Translators: The label for a button in the Web Modules Manager dialog item = self.moduleEditButton = wx.Button(self, label=_("&Edit...")) item.Disable() item.Bind(wx.EVT_BUTTON, self.onModuleEdit) + gbSizer.Add(item, (row, col), flag=wx.EXPAND) + row += 1 + gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), (row, col)) + + row += 1 # Translators: The label for a button in the Web Modules Manager dialog item = self.rulesManagerButton = wx.Button(self, label=_("Manage &rules...")) item.Disable() item.Bind(wx.EVT_BUTTON, self.onRulesManager) + gbSizer.Add(item, (row, col), flag=wx.EXPAND) + row += 1 + gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), (row, col)) + + row += 1 item = self.moduleDeleteButton = wx.Button( self, # Translators: The label for a button in the @@ -164,135 +174,117 @@ def __init__(self, parent): label=_("&Delete")) item.Disable() item.Bind(wx.EVT_BUTTON, self.onModuleDelete) + gbSizer.Add(item, (row, col), flag=wx.EXPAND) - vSizer = wx.BoxSizer(wx.VERTICAL) - vSizer.Add(self.moduleCreateButton, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - vSizer.Add(self.moduleEditButton, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - vSizer.Add(self.rulesManagerButton, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - vSizer.Add(self.moduleDeleteButton, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - - hSizer = wx.BoxSizer(wx.HORIZONTAL) - hSizer.Add(self.modulesList, proportion=1, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - hSizer.Add(vSizer) - - vSizer = wx.BoxSizer(wx.VERTICAL) - vSizer.Add(modulesListLabel, flag=wx.EXPAND|wx.LEFT|wx.RIGHT, border=4) - vSizer.Add(hSizer, proportion=1, flag=wx.EXPAND|wx.DOWN, border=4) - vSizer.Add( + mainSizer.Add( self.CreateSeparatedButtonSizer(wx.CLOSE), - flag=wx.EXPAND|wx.ALIGN_LEFT|wx.TOP|wx.DOWN, - border=4 - ) - - hSizer = wx.BoxSizer(wx.HORIZONTAL) - hSizer.Add(vSizer, proportion=1, flag=wx.EXPAND|wx.ALL, border=4) - - self.Sizer = hSizer - - + flag=wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, + border=scale(guiHelper.BORDER_FOR_DIALOGS), + ) + self.SetSize(scale(790, 400)) + self.SetSizer(mainSizer) + self.CentreOnScreen() + self.modulesList.SetFocus() + def __del__(self): Dialog._instance = None - + def initData(self, context): - self.context = context + super().initData(context) module = context["webModule"] if "webModule" in context else None self.refreshModulesList(selectItem=module) - + + @guarded def onModuleCreate(self, evt=None): - from .. import webModuleHandler - context = dict(self.context) # Shallow copy - webModuleHandler.showCreator(context) - if "webModule" in context: - module = context["webModule"] - self.refreshModulesList(selectItem=module) - + context = self.context.copy() + context["new"] = True + from .webModuleEditor import show + if show(context, self): + self.refreshModulesList(selectItem=context["webModule"]) + + @guarded def onModuleDelete(self, evt=None): index = self.modulesList.GetFirstSelected() if index < 0: return - pass webModule = self.modules[index] from .. import webModuleHandler if webModuleHandler.delete(webModule=webModule): self.refreshModulesList() - + + @guarded def onModuleEdit(self, evt=None): index = self.modulesList.GetFirstSelected() if index < 0: + wx.Bell() return - context = dict(self.context) # Shallow copy + context = self.context + context.pop("new", None) context["webModule"] = self.modules[index] - from .. import webModuleHandler - webModuleHandler.showEditor(context) - self.refreshModulesList(selectIndex=index) - + from .webModuleEditor import show + if show(context, self): + self.refreshModulesList(selectIndex=index) + + @guarded def onModulesListItemSelected(self, evt): - index = evt.GetIndex() - item = self.modules[index] if index >= 0 else None - self.moduleEditButton.Enable(item is not None) - self.rulesManagerButton.Enable( - item is not None - # FIXME: This test never succeeds as a live WebModule is not - # taken from the context. - # TODO: Remove this restriction for issue #42 - and item.ruleManager.isReady - ) - self.moduleDeleteButton.Enable(item is not None) - + self.refreshButtons() + + @guarded def onRulesManager(self, evt=None): index = self.modulesList.GetFirstSelected() if index < 0: + wx.Bell() return webModule = self.modules[index] - context = self.context.copy() # Shallow copy - context["webModule"] = self.modules[index] - from .. import ruleHandler - ruleHandler.showManager(context) - - def refreshModulesList(self, selectIndex=0, selectItem=None): - self.modulesList.DeleteAllItems() + context = self.context + if not webModule.equals(context.get("webModule")): + context["webModule"] = webModule + context.pop("result", None) + from .rule.manager import show + show(context, self) + + def refreshButtons(self): + index = self.modulesList.GetFirstSelected() + hasSelection = index >= 0 + self.moduleEditButton.Enable(hasSelection) + self.rulesManagerButton.Enable(hasSelection) + self.moduleDeleteButton.Enable(hasSelection) + + def refreshModulesList(self, selectIndex: int = None, selectItem: "WebModule" = None): + ctrl = self.modulesList + ctrl.DeleteAllItems() + contextModule = self.context.get("webModule") modules = self.modules = [] - modulesList = self.modulesList - from .. import webModuleHandler for index, module in enumerate(webModuleHandler.getWebModules()): - if module is selectItem: + if selectIndex is None and module.equals(selectItem): selectIndex = index + if module.equals(contextModule): + module = contextModule trigger = (" %s " % _("and")).join( ([ "url=%s" % url for url in (module.url if module.url else []) - ]) - + ( + ]) + ( ["title=%s" % module.windowTitle] if module.windowTitle else [] - ) ) - modulesList.Append(( + ) + ctrl.Append(( module.name, trigger, - #module.url, - #module.windowTitle, - )) + )) modules.append(module) - # Select the item at given index, or the first item if unspecified - len_ = len(modules) - if len_ > 0: - if selectIndex == -1: - selectIndex = len_ - 1 - elif selectIndex<0 or selectIndex>=len_: - selectIndex = 0 - modulesList.Select(selectIndex, on=1) - modulesList.Focus(selectIndex) + if selectIndex is None: + selectIndex = min(0, len(modules) - 1) else: - self.moduleEditButton.Disable() - self.rulesManagerButton.Disable() - self.moduleDeleteButton.Disable() + selectIndex %= len(modules) + if selectIndex >= 0: + ctrl.Select(selectIndex, on=1) + ctrl.Focus(selectIndex) + self.refreshButtons() - def Show(self, context): - self.initData(context) - self.Fit() - self.modulesList.SetFocus() - self.CentreOnScreen() - return super().Show() + +def show(context): + showContextualDialog(Dialog, context, gui.mainFrame) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 4f6b2061..150a9470 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -34,6 +34,7 @@ import threading import time import sys +from typing import Any import weakref import wx @@ -43,7 +44,6 @@ import baseObject import browseMode import controlTypes -import gui import inputCore from logHandler import log import queueHandler @@ -90,35 +90,6 @@ builtinRuleActions["mouseMove"] = pgettext("webAccess.action", "Mouse move") -def showCreator(context, parent=None): - context.pop("rule", None) - context["new"] = True - return showEditor(context, parent=parent) - - -def showEditor(context, parent=None): - context.get("data", {}).pop("rule", None) - from ..gui.rule import editor - return editor.show(context, parent=parent) - - -def showManager(context): - api.processPendingEvents() - webModule = context["webModule"] - mgr = webModule.ruleManager - if not mgr.isReady: - playWebAccessSound("keyError") - time.sleep(0.2) - speech.cancelSpeech() - ui.message(_("Not ready")) - time.sleep(0.5) - return - focus = context["focusObject"] - context["result"] = mgr.getResultAtCaret(focus=focus) - from ..gui.rule import manager as dlg - dlg.show(context) - - class DefaultScripts(baseObject.ScriptableObject): def __init__(self, warningMessage): @@ -182,16 +153,17 @@ def _initLayer(self, layer, index): ((layerName, layerIndex) for layerIndex, layerName in enumerate(self._layers.keys())) ) - def loadRule(self, layer, name, data): + def loadRule(self, layer: str, name: str, data: Mapping[str, Any]) -> "Rule": if layer not in self._layers: self._initLayer(layer, None) - self._loadRule(layer, name, data) + return self._loadRule(layer, name, data) - def _loadRule(self, layer, name, data): + def _loadRule(self, layer: str, name: str, data: Mapping[str, Any]) -> "Rule": rule = self.webModule.createRule(data) rule.layer = layer self._layers[layer][name] = rule self._rules.setdefault(name, {})[layer] = rule + return rule def unload(self, layer): for index in range(len(self._results)): diff --git a/addon/globalPlugins/webAccess/store/webModule.py b/addon/globalPlugins/webAccess/store/webModule.py index 1e2500b7..65e92d1e 100644 --- a/addon/globalPlugins/webAccess/store/webModule.py +++ b/addon/globalPlugins/webAccess/store/webModule.py @@ -82,6 +82,7 @@ def catalog(self, errors=None): data = self.get(ref).data meta = {} for key in ("windowTitle", "url"): + # "WebApp" corresponds to legacy format version (pre 0.1) value = data.get("WebModule", data.get("WebApp", {})).get(key) if value: meta[key] = value @@ -214,7 +215,7 @@ def write(self, path, data): class WebModuleStore(DispatchStore): def __init__(self, *args, **kwargs): - # The order of this list is meaningful. See `catalog` + # The order of this list is meaningful. See `catalog` stores = kwargs["stores"] = [] store = self.userStore = WebModuleJsonFileDataStore( name="userConfig", basePath=globalVars.appArgs.configPath diff --git a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py index 4728a02f..7dfb2c99 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py @@ -44,6 +44,7 @@ from ..overlay import WebAccessBmdti from ..store import DuplicateRefError from ..store import MalformedRefError +from ..utils import notifyError PACKAGE_NAME = "webModulesSM" @@ -232,23 +233,18 @@ def save(webModule, layerName=None, prompt=True, force=False, fromRuleEditor=Fal + os.linesep + "\t" + "\\ / : * ? \" | " ), - caption=webModuleEditor.Dialog._instance.Title, + caption=prompt, style=wx.OK | wx.ICON_EXCLAMATION ) return False except Exception: - log.exception("save(webModule={!r}, layerName=={!r}, prompt=={!r}, force=={!r}".format( + msg = "save(webModule={!r}, layerName=={!r}, prompt=={!r}, force=={!r}".format( webModule, layerName, prompt, force - )) + ) if prompt: - gui.messageBox( - # Translators: The text of a generic error message dialog - message=_("An error occured.\n\nPlease consult NVDA's log."), - # Translators: The title of an error message dialog - caption=_("Web Access for NVDA"), - style=wx.OK | wx.ICON_EXCLAMATION - ) - getWebModules(refresh=True) + notifyError(msg) + else: + log.exception(msg) return False if not fromRuleEditor: # only if webModule creation or modification @@ -258,97 +254,29 @@ def save(webModule, layerName=None, prompt=True, force=False, fromRuleEditor=Fal return True -def showCreator(context): - showEditor(context, new=True) - - -def showEditor(context, new=False): - from ..gui import webModuleEditor - from .webModule import WebModule - - if "data" in context: - del context["data"] - if new: - if "webModule" in context: - del context["webModule"] - webModuleEditor.show(context) - return - keepShowing = True - force = False - while keepShowing: - if webModuleEditor.show(context): - keepTrying = True - while keepTrying: - try: - if new: - webModule = context["webModule"] = WebModule() - webModule.load(None, data=context["data"]) - create( - webModule=webModule, - focus=context.get("focusObject"), - force=force - ) - # Translators: Confirmation message after web module creation. - ui.message( - _( - "Your new web module {name} has been created." - ).format(name=webModule.name) - ) - else: - webModule = context["webModule"] - for name, value in list(context["data"]["WebModule"].items()): - setattr(webModule, name, value) - update( - webModule=webModule, - focus=context.get("focusObject"), - force=force - ) - keepShowing = keepTrying = False - except DuplicateRefError as e: - if webModuleEditor.promptOverwrite(): - force = True - else: - keepTrying = force = False - except MalformedRefError: - keepTrying = force = False - gui.messageBox( - message=( - _("The web module name should be a valid file name.") - + " " + os.linesep - + _("It should not contain any of the following:") - + os.linesep - + "\t" + "\\ / : * ? \" | " - ), - caption=webModuleEditor.Dialog._instance.Title, - style=wx.OK | wx.ICON_EXCLAMATION - ) - finally: - if not new: - getWebModules(refresh=True) - else: - keepShowing = False - if new: - # Translator: Canceling web module creation. - ui.message(_("Cancel")) - - -def showManager(context): - from ..gui import webModulesManager - webModulesManager.show(context) - - def getEditableWebModule(webModule, layerName=None, prompt=True): + """Ensure a WebModule is suitable for edition, eventually initializing a writable layer + + `layerName` + The name of the layer from which a Rule is to be edited. Should be `None` for + updating WebModule properties such as triggers and help content. + + See `WebModuleHandler.webModule.WebModuleDataLayer._get_readonly` for details regarding + what configuration allows editing WebModules from which layer. + + Returns `None` if the current configuration does not allow editing the specified WebModule. + """ try: if layerName is not None: if not webModule.getLayer(layerName).readOnly: return webModule - webModule = getEditableScratchpadWebModule(webModule, layerName=layerName, prompt=prompt) + webModule = _getEditableScratchpadWebModule(webModule, layerName=layerName, prompt=prompt) else: if not webModule.isReadOnly(): return webModule webModule = ( - getEditableUserConfigWebModule(webModule) - or getEditableScratchpadWebModule(webModule, prompt=prompt) + _getEditableUserConfigWebModule(webModule) + or _getEditableScratchpadWebModule(webModule, prompt=prompt) ) except Exception: log.exception("webModule={!r}, layerName={!r}".format(webModule, layerName)) @@ -392,7 +320,7 @@ def getEditableWebModule(webModule, layerName=None, prompt=True): ) -def getEditableScratchpadWebModule(webModule, layerName=None, prompt=True): +def _getEditableScratchpadWebModule(webModule, layerName=None, prompt=True): if not ( config.conf["development"]["enableScratchpadDir"] and config.conf["webAccess"]["devMode"] @@ -422,7 +350,7 @@ def getEditableScratchpadWebModule(webModule, layerName=None, prompt=True): return mask -def getEditableUserConfigWebModule(webModule): +def _getEditableUserConfigWebModule(webModule): if config.conf["webAccess"]["disableUserConfig"]: return None layer = webModule.getLayer("user") diff --git a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py index db6e051a..6e66ac9d 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py @@ -33,6 +33,7 @@ import datetime import json import os +import sys import addonHandler addonHandler.initTranslation() @@ -51,25 +52,28 @@ from ..webAppLib import playWebAccessSound from .. import ruleHandler +if sys.version_info[1] < 9: + from typing import Sequence +else: + from collections.abc import Sequence + + class InvalidApiVersion(version.InvalidVersion): pass class WebModuleDataLayer(baseObject.AutoPropertyObject): - def __init__(self, name, data, storeRef, rulesOnly=False, readOnly=None): + def __init__(self, name, data, storeRef, readOnly=None): self.name = name self.data = data self.storeRef = storeRef - self.rulesOnly = rulesOnly if readOnly is not None: self.readOnly = readOnly self.dirty = False def __repr__(self): - return "".format(self.name, self.storeRef) def _get_readOnly(self): storeRef = self.storeRef @@ -95,14 +99,14 @@ def _get_readOnly(self): class WebModule(baseObject.ScriptableObject): - API_VERSION = version.parse("0.5") + API_VERSION = version.parse("0.6") FORMAT_VERSION_STR = "0.10-dev" FORMAT_VERSION = version.parse(FORMAT_VERSION_STR) def __init__(self): super().__init__() - self.layers = [] # List of `WebModuleDataLayer` instances + self.layers: Sequence[WebModuleDataLayer] = [] self.activePageTitle = None self.activePageIdentifier = None self.ruleManager = ruleHandler.RuleManager(self) @@ -183,13 +187,34 @@ def dump(self, layerName): data["Rules"] = self.ruleManager.dump(layerName) return layer + def equals(self, obj): + """Check if `obj` represents an instance of the same `WebModule`. + + 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. + """ + if type(self) is not type(obj): + return False + if self.name != obj.name: + return False + if len(self.layers) != len(obj.layers): + return False + for i in range(len(self.layers)): + l1 = self.layers[i] + l2 = obj.layers[i] + if l1.name != l2.name or l1.storeRef != l2.storeRef: + return False + return True + def isReadOnly(self): try: - return not bool(self._getWritableLayer()) + return not bool(self.getWritableLayer()) except LookupError: return True - def load(self, layerName, index=None, data=None, storeRef=None, rulesOnly=False, readOnly=None): + def load(self, layerName, index=None, data=None, storeRef=None, readOnly=None): for candidateIndex, layer in enumerate(self.layers): if layer.name == layerName: self.unload(layerName) @@ -198,12 +223,11 @@ def load(self, layerName, index=None, data=None, storeRef=None, rulesOnly=False, if data is not None: from .dataRecovery import recover recover(data) - layer = WebModuleDataLayer(layerName, data, storeRef, rulesOnly=rulesOnly) + layer = WebModuleDataLayer(layerName, data, storeRef) elif storeRef is not None: from . import store layer = store.getData(storeRef) layer.name = layerName - layer.rulesOnly = rulesOnly data = layer.data from .dataRecovery import recover recover(data) @@ -211,11 +235,11 @@ def load(self, layerName, index=None, data=None, storeRef=None, rulesOnly=False, data = OrderedDict({"WebModule": OrderedDict()}) data["WebModule"] = OrderedDict() data["WebModule"]["name"] = self.name - for attr in ("url", "windowTitle"): + for attr in ("url", "windowTitle"): # FIXME: Why not "help" as whell? value = getattr(self, attr) if value: data["WebModule"][attr] = value - layer = WebModuleDataLayer(layerName, data, storeRef, rulesOnly=rulesOnly) + layer = WebModuleDataLayer(layerName, data, storeRef) if index is not None: self.layers.insert(index, layer) else: @@ -230,6 +254,17 @@ def getLayer(self, layerName, raiseIfMissing=False): raise LookupError(repr(layerName)) return None + def getWritableLayer(self) -> WebModuleDataLayer: + """Retreive the lowest writable layer of this WebModule + + See also: `webModuleHandler.getEditableWebModule` + """ + for layer in reversed(self.layers): + if not layer.readOnly: + return layer + break + raise LookupError("No suitable data layer") + def unload(self, layerName): for index, layer in enumerate(self.layers): if layer.name == layerName: @@ -244,8 +279,6 @@ def terminate(self): def _getLayeredProperty(self, name, startLayerIndex=-1, raiseIfMissing=False): for index, layer in list(enumerate(self.layers))[startLayerIndex::-1]: - if layer.rulesOnly: - continue data = layer.data["WebModule"] if name not in data: continue @@ -257,15 +290,8 @@ def _getLayeredProperty(self, name, startLayerIndex=-1, raiseIfMissing=False): if raiseIfMissing: raise LookupError("name={!r}, startLayerIndex={!r}".format(name, startLayerIndex)) - def _getWritableLayer(self): - for layer in reversed(self.layers): - if not layer.readOnly and not layer.rulesOnly: - return layer - break - raise LookupError("No suitable data layer") - def _setLayeredProperty(self, name, value): - layer = self._getWritableLayer() + layer = self.getWritableLayer() data = layer.data["WebModule"] if data.get(name) != value: layer.dirty = True From e33f9cefb3055bfa275d4fe091f0674c9b890ede Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 26 Aug 2024 03:04:04 +0200 Subject: [PATCH 16/54] Support the "multiple" property for rules of type "parent" Step towards #46 --- .../webAccess/ruleHandler/properties.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index ba3e48ed..f9eefd1b 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -32,6 +32,8 @@ from typing import Any, TypeAlias import weakref +from . import ruleTypes + import addonHandler @@ -74,7 +76,7 @@ def getDisplayName(self, ruleType) -> str: class PropertySpec(Enum): autoAction = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default=None, # Translators: The display name for a rule property @@ -84,7 +86,7 @@ class PropertySpec(Enum): isRestrictedChoice=True ) multiple = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.PARENT, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -93,7 +95,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) formMode = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -102,7 +104,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) skip = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -111,7 +113,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) sayName = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -120,7 +122,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) customName = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default="", # Translators: The display name for a rule property @@ -130,11 +132,16 @@ class PropertySpec(Enum): isRestrictedChoice=False ) customValue = PropertySpecValue( - ruleTypes=("marker", "pageTitle1", "pageTitle2", "zone"), + ruleTypes=( + ruleTypes.MARKER, + ruleTypes.PAGE_TITLE_1, + ruleTypes.PAGE_TITLE_2, + ruleTypes.ZONE + ), valueType=str, default="", displayName={ - ("marker", "zone"): + (ruleTypes.MARKER, ruleTypes.ZONE): # Translators: The display name for a rule property pgettext("webAccess.ruleProperty", "Custom message"), ("pageTitle1", "pageTitle2"): @@ -146,7 +153,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) mutation = PropertySpecValue( - ruleTypes=("marker", "zone"), + ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default=None, # Translators: The display name for a rule property @@ -156,7 +163,7 @@ class PropertySpec(Enum): isRestrictedChoice=True ) subModule = PropertySpecValue( - ruleTypes=("zone",), + ruleTypes=(ruleTypes.ZONE,), valueType=str, default="", # Translators: The display name for a rule property From b55ffe9fcfa14499608218cc40cab101cb03fde3 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 26 Aug 2024 13:48:01 +0200 Subject: [PATCH 17/54] SubModules WIP - Very early alpha - Load child WebModule named via the new subModule Zone property - For the child, restrict Node search to its Zone - For the parent, exclude child's Node from search - For the child, don't respond to Marker or Zone gestures unless caret is in Zone - Add new Rule Type "globalMarker" which can be triggered from anywhere, with priority given to children - Menu actions target the WebModule at caret position - Menu "Edit WebModule" presents when suitable a list of options from caret to root --- addon/globalPlugins/webAccess/__init__.py | 10 +- addon/globalPlugins/webAccess/gui/actions.py | 4 +- addon/globalPlugins/webAccess/gui/menu.py | 41 +++- .../webAccess/gui/rule/editor.py | 17 +- .../webAccess/gui/webModulesManager.py | 7 +- addon/globalPlugins/webAccess/overlay.py | 25 +-- .../webAccess/ruleHandler/__init__.py | 193 +++++++++++++----- .../webAccess/ruleHandler/properties.py | 17 +- .../webAccess/ruleHandler/ruleTypes.py | 4 + .../webAccess/store/webModule.py | 2 +- .../webAccess/webModuleHandler/__init__.py | 7 + .../webAccess/webModuleHandler/webModule.py | 11 +- 12 files changed, 237 insertions(+), 101 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 3571784d..ab19307c 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -223,12 +223,18 @@ def showWebAccessGui(self): "webAccess": self, "focusObject": obj, } - webModule = obj.webAccess.webModule - if webModule is not None: + if obj.webAccess.webModule: + webModule = obj.treeInterceptor.webAccess.webModuleAtCaret context["webModule"] = webModule context["pageTitle"] = webModule.pageTitle mgr = webModule.ruleManager context["result"] = mgr.getResultAtCaret(focus=obj) + stack = [] + while webModule: + stack.append(webModule) + webModule = webModule.ruleManager.parentModule + if len(stack) > 1: + context["webModuleStackAtCaret"] = stack menu.show(context) @script( diff --git a/addon/globalPlugins/webAccess/gui/actions.py b/addon/globalPlugins/webAccess/gui/actions.py index ad4727fc..5955340b 100644 --- a/addon/globalPlugins/webAccess/gui/actions.py +++ b/addon/globalPlugins/webAccess/gui/actions.py @@ -265,7 +265,7 @@ def updateGesturesListBox(self, selectId: str = None, focus: bool = False): @guarded def onPanelActivated(self): super().onPanelActivated() - supported = self.getRuleType() in (ruleTypes.ZONE, ruleTypes.MARKER) + supported = self.getRuleType() in ruleTypes.ACTION_TYPES self.panelDescription = "" if supported else self.descriptionIfNoneSupported self.Freeze() for item in self.hideable["IfSupported"]: @@ -278,7 +278,7 @@ def onPanelActivated(self): def onSave(self): super().onSave() data = self.getData() - if self.getRuleType() not in (ruleTypes.ZONE, ruleTypes.MARKER): + if self.getRuleType() not in ruleTypes.ACTION_TYPES: data.pop("gestures", None) data.get("properties", {}).pop("autoAction", None) elif not data.get("gestures"): diff --git a/addon/globalPlugins/webAccess/gui/menu.py b/addon/globalPlugins/webAccess/gui/menu.py index 3e2b9759..7bd23ae8 100644 --- a/addon/globalPlugins/webAccess/gui/menu.py +++ b/addon/globalPlugins/webAccess/gui/menu.py @@ -32,6 +32,7 @@ import wx import addonHandler +import config import gui from ... import webAccess @@ -79,7 +80,20 @@ def __init__(self, context): _("&New web module...")) self.Bind(wx.EVT_MENU, self.onWebModuleCreate, item) - if webModule: + stack = context.get("webModuleStackAtCaret", []).copy() + if stack: + subMenu = wx.Menu() + while stack: + mod = stack.pop(0) + handler = lambda evt, webModule=mod: self.onWebModuleEdit(evt, webModule=webModule) + item = subMenu.Append(wx.ID_ANY, mod.name) + subMenu.Bind(wx.EVT_MENU, handler, item) + self.AppendSubMenu( + subMenu, + # Translators: Web Access menu item label. + _("Edit &web module") + ) + elif webModule: item = self.Append( wx.ID_ANY, # Translators: Web Access menu item label. @@ -96,6 +110,16 @@ def __init__(self, context): self.AppendSeparator() + if config.conf["webAccess"]["devMode"]: + item = self.Append( + wx.ID_ANY, + # Translators: Web Access menu item label. + _("&Element description...") + ) + self.Bind(wx.EVT_MENU, self.onElementDescription, item) + + self.AppendSeparator() + item = self.AppendCheckItem( wx.ID_ANY, # Translators: Web Access menu item label. @@ -109,6 +133,11 @@ def show(self): gui.mainFrame.PopupMenu(self) gui.mainFrame.postPopup() + @guarded + def onElementDescription(self, evt): + from .elementDescription import showElementDescriptionDialog + showElementDescriptionDialog() + @guarded def onRuleCreate(self, evt): self.context["new"] = True @@ -121,13 +150,15 @@ def onRulesManager(self, evt): show(self.context, gui.mainFrame) @guarded - def onWebModuleCreate(self, evt): - self.context["new"] = True + def onWebModuleEdit(self, evt, webModule=None): + if webModule is not None: + self.context["webModule"] = webModule from .webModuleEditor import show - show(self.context, gui.mainFrame) + show(self.context) @guarded - def onWebModuleEdit(self, evt): + def onWebModuleCreate(self, evt, webModule=None): + self.context["new"] = True from .webModuleEditor import show show(self.context) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index d90d0f12..a9bd0433 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -158,7 +158,7 @@ def getData(self): def onRuleType_change(self): prm = self.categoryParams categoryClasses = tuple(nodeInfo.categoryClass for nodeInfo in self.Parent.Parent.categoryClasses) - for index in (categoryClasses.index(cls) for cls in (ActionsPanel, PropertiesPanel)): + for index in (categoryClasses.index(cls) for cls in (GeneralPanel, ActionsPanel, PropertiesPanel)): category = prm.tree.getXChild(prm.tree.GetRootItem(), index) self.refreshParent(category) @@ -231,23 +231,12 @@ def makeSettings(self, settingsSizer): def initData(self, context: Mapping[str, Any]) -> None: super().initData(context) data = self.getData() - if 'type' in data: - self.ruleType.SetSelection(list(ruleTypes.ruleTypeLabels.keys()).index(data['type'])) - else: - self.ruleType.SetSelection(0) - + self.ruleType.SetSelection(tuple(ruleTypes.ruleTypeLabels.keys()).index(data["type"])) # Does not emit EVT_TEXT self.ruleName.ChangeValue(data.get("name", "")) self.commentText.ChangeValue(data.get("comment", "")) self.refreshSummary() - @staticmethod - def initRuleTypeChoice(data, ruleTypeChoice): - for index, key in enumerate(ruleTypes.ruleTypeLabels.keys()): - if key == data["type"]: - ruleTypeChoice.Selection = index - break - def updateData(self): data = self.getData() # The type and name are already stored by their respective event handlers and should @@ -908,7 +897,7 @@ def getAlternativeChildren(self): def getActionsChildren(self): data = self.getData() - if data["type"] not in [ruleTypes.ZONE, ruleTypes.MARKER]: + if data["type"] not in [ruleTypes.ACTION_TYPES]: return [] mgr = self.context["webModule"].ruleManager actionsPanel = [] diff --git a/addon/globalPlugins/webAccess/gui/webModulesManager.py b/addon/globalPlugins/webAccess/gui/webModulesManager.py index a3da4d4d..011fc130 100644 --- a/addon/globalPlugins/webAccess/gui/webModulesManager.py +++ b/addon/globalPlugins/webAccess/gui/webModulesManager.py @@ -254,13 +254,16 @@ def refreshModulesList(self, selectIndex: int = None, selectItem: "WebModule" = ctrl = self.modulesList ctrl.DeleteAllItems() contextModule = self.context.get("webModule") + contextModules = { + (module.name, module.layers[0].storeRef): module + for module in list(reversed(contextModule.ruleManager.subModules.all())) + [contextModule] + } if contextModule else {} modules = self.modules = [] from .. import webModuleHandler for index, module in enumerate(webModuleHandler.getWebModules()): if selectIndex is None and module.equals(selectItem): selectIndex = index - if module.equals(contextModule): - module = contextModule + module = contextModules.get((module.name, module.layers[0].storeRef), module) trigger = (" %s " % _("and")).join( ([ "url=%s" % url diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index 8b0da048..123695a6 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -214,7 +214,15 @@ def webModule(self): except Exception: log.exception() return webModule - + + @property + def webModuleAtCaret(self): + rootModule = self.webModule + if not rootModule: + return None + info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) + return self.ruleManager.subModules.atPosition(info._startOffset) or rootModule + @property def zone(self): ruleManager = self.ruleManager @@ -930,19 +938,12 @@ class Break(Exception): return super().getAlternativeScript(gesture, script) def getScript(self, gesture): - webModule = self.webAccess.webModule + webModule = self.webAccess.webModuleAtCaret if webModule: - func = webModule.getScript(gesture) - if func: - return ScriptWrapper( - func, ignoreTreeInterceptorPassThrough=True - ) - mgr = self.webAccess.ruleManager - if mgr: - func = mgr.getScript(gesture) - if func: + script = webModule.getScript(gesture) + if script: return ScriptWrapper( - func, ignoreTreeInterceptorPassThrough=True + script, ignoreTreeInterceptorPassThrough=True ) return super().getScript(gesture) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 150a9470..a6c8621a 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -29,6 +29,7 @@ ) +from functools import partial from itertools import chain from pprint import pformat import threading @@ -41,7 +42,7 @@ import addonHandler import api -import baseObject +from baseObject import AutoPropertyObject, ScriptableObject import browseMode import controlTypes import inputCore @@ -56,6 +57,7 @@ from garbageHandler import TrackedObject from .. import nodeHandler +from ..utils import logException from ..webAppLib import ( html, logTimeStart, @@ -68,9 +70,9 @@ if sys.version_info[1] < 9: - from typing import Mapping + from typing import Mapping, Sequence else: - from collections.abc import Mapping + from collections.abc import Mapping, Sequence addonHandler.initTranslation() @@ -90,7 +92,7 @@ builtinRuleActions["mouseMove"] = pgettext("webAccess.action", "Mouse move") -class DefaultScripts(baseObject.ScriptableObject): +class DefaultScripts(ScriptableObject): def __init__(self, warningMessage): super().__init__() @@ -106,7 +108,7 @@ def script_notAssigned(self, gesture): __gestures = {} -class RuleManager(baseObject.ScriptableObject): +class RuleManager(ScriptableObject): def __init__(self, webModule): super().__init__() @@ -126,7 +128,11 @@ def __init__(self, webModule): self.lastAutoMovetoTime = 0 self.defaultScripts = DefaultScripts("Aucun marqueur associé à cette touche") self.timerCheckAutoAction = None - self.zone = None + self.zone: "Zone" = None + self.parentModule: "WebModule" = None + self.parentNode: "NodeField" = None + self.subModules: SubModules = SubModules(self) + self._subModuleResults: Sequence["SingleNodeResult"] = [] def _get_webModule(self): return self._webModule() @@ -184,7 +190,7 @@ def getRules(self, layer=None): return tuple([ rule for ruleLayers in list(self._rules.values()) - for rule in list(ruleLayers.values()) + for rule in reversed(list(ruleLayers.values())) ]) def getRule(self, name, layer=None): @@ -196,7 +202,7 @@ def getRule(self, name, layer=None): if layer not in (None, False): return ruleLayers[layer] try: - return next(iter(list(ruleLayers.values()))) + return next(iter(reversed(ruleLayers.values()))) except StopIteration: raise LookupError({"name": name, "layer": layer}) @@ -221,7 +227,7 @@ def iterResultsByName(self, name, layer=None): continue elif ( layer not in (None, False) - or self._layersIndex[layer] >= self._layersIndex[rule.layer] + or tuple(self._layers.keys()).index(layer) >= tuple(self._layers.keys()).index(rule.layer) ): yield result @@ -266,26 +272,52 @@ def getActions(self) -> Mapping[str, str]: actions.setdefault(actionId, actionLabel) return actions + def getGlobalScript(self, gesture, caret=None, fromParent=False): + if caret is None: + webModuleAtCaret = self.nodeManager.treeInterceptor.webAccess.webModuleAtCaret + return webModuleAtCaret.ruleManager.getGlobalScript(gesture, caret=webModuleAtCaret) + + def gen(): + nonlocal caret + for subMod in self.subModules.all(): + if subMod is not caret: + yield partial(subMod.ruleManager.getGlobalScript, caret=caret, fromParent=True) + for result in self.getResults(): + if result.rule.type == ruleTypes.GLOBAL_MARKER: + yield result.getScript + if not fromParent and self.parentZone: + parent = self.parentRuleManager + if parent.webModule is not caret: + yield partial(parent.getGlobalScript, caret=caret) + + for func in gen(): + script = func(gesture) + if script: + return script + def getScript(self, gesture): - func = super().getScript(gesture) - if func is not None: - return func + script = super().getScript(gesture) + if script: + return script + script = self.getGlobalScript(gesture) + if script: + return script for layer in reversed(list(self._layers.keys())): for result in self.getResults(): - if result.rule.layer != layer: + if result.rule.type is ruleTypes.GLOBAL_MARKER or result.rule.layer != layer: continue - func = result.getScript(gesture) - if func is not None: - return func - for rules in reversed(list(self._layers.values())): - for rule in list(rules.values()): - for criterion in rule.criteria: - func = rule.getScript(gesture) - if func is not None: - return func - func = rule.getScript(gesture) - if func is not None: - return func + script = result.getScript(gesture) + if script: + return script +# for rules in reversed(list(self._layers.values())): +# for rule in list(rules.values()): +# for criterion in rule.criteria: +# func = rule.getScript(gesture) +# if func is not None: +# return func +# func = rule.getScript(gesture) +# if func is not None: +# return func return self.defaultScripts.getScript(gesture) def _get_isReady(self): @@ -294,21 +326,24 @@ def _get_isReady(self): return True def terminate(self): - self._webModule = None self._ready = False + self._webModule = None + self.subModules.terminate() try: self.timerCheckAutoAction.cancel() except Exception: pass self.timerCheckAutoAction = None self._nodeManager = None - del self._results[:] + self._results.clear() + self._subModuleResults.clear() self._mutatedControlsById.clear() - self._mutatedControlsByOffset[:] = [] + self._mutatedControlsByOffset.clear() for ruleLayers in list(self._rules.values()): for rule in list(ruleLayers.values()): rule.resetResults() + @logException def update(self, nodeManager=None, force=False): if self.webModule is None: # This instance has been terminated @@ -329,26 +364,29 @@ def update(self, nodeManager=None, force=False): self._ready = True return False t = logTimeStart() - self._results[:] = [] + self._results.clear() + self._subModuleResults.clear() self._mutatedControlsById.clear() self._mutatedControlsByOffset.clear() - for ruleLayers in list(self._rules.values()): - for rule in list(ruleLayers.values()): - rule.resetResults() - - results = self._results + for rule in self.getRules(): + rule.resetResults() + + for rule in (rule for rule in self.getRules() if rule.properties.subModule): + results = rule.getResults() + self._results.extend(results) + self._subModuleResults.extend(results) for rule in sorted( - [rule for ruleLayers in list(self._rules.values()) for rule in list(ruleLayers.values())], + (rule for rule in self.getRules() if not rule.properties.subModule), key=lambda rule: ( 0 if rule.type in ( ruleTypes.PAGE_TITLE_1, ruleTypes.PAGE_TITLE_2 - ) else 1 + ) else 2 ) ): - results.extend(rule.getResults()) - results.sort() - - for result in results: + self._results.extend(rule.getResults()) + self._results.sort() + + for result in self._results: if not (hasattr(result, "node") and result.properties.mutation): continue controlId = int(result.node.controlIdentifier) @@ -359,9 +397,9 @@ def update(self, nodeManager=None, force=False): self._mutatedControlsByOffset.append(entry) else: entry.apply(result) - self._ready = True self.nodeManagerIdentifier = self.nodeManager.identifier + self.subModules.update() if self.zone is not None: if not self.zone.update() or not self.zone.containsTextInfo( self.nodeManager.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) @@ -626,6 +664,7 @@ def quickNav( ): if not self.isReady: playWebAccessSound("keyError") + # Translators: Reported when attempting an action while WebAccess is not ready ui.message(_("Not ready")) return None @@ -635,6 +674,7 @@ def quickNav( if position is None: playWebAccessSound("keyError") + # Translators: Reported when attempting an action while WebAccess is not ready ui.message(_("Not ready")) return None @@ -700,14 +740,14 @@ def quickNavToPreviousLevel1(self): self.quickNav(previous=True, types=(ruleTypes.ZONE,), honourSkip=False) def quickNavToNextLevel2(self): - self.quickNav(types=(ruleTypes.ZONE, ruleTypes.MARKER)) + self.quickNav(types=ruleTypes.ACTION_TYPES) def quickNavToPreviousLevel2(self): - self.quickNav(previous=True, types=(ruleTypes.ZONE, ruleTypes.MARKER)) + self.quickNav(previous=True, types=ruleTypes.ACTION_TYPES) def quickNavToNextLevel3(self): self.quickNav( - types=(ruleTypes.ZONE, ruleTypes.MARKER), + types=ruleTypes.ACTION_TYPES, respectZone=True, honourSkip=False, cycle=False @@ -716,13 +756,58 @@ def quickNavToNextLevel3(self): def quickNavToPreviousLevel3(self): self.quickNav( previous=True, - types=(ruleTypes.ZONE, ruleTypes.MARKER), + types=ruleTypes.ACTION_TYPES, respectZone=True, honourSkip=False, cycle=False ) +class SubModules(AutoPropertyObject): + + def __init__(self, parent: RuleManager): + self._parent = weakref.ref(parent) + self.modulesByNameAndIndex: Mapping[tuple(str, int), "WebModule"] = {} + self.modulesByPosition: Mapping[tuple(int, int), "WebModule"] = {} + + def _get_parent(self): + return self._parent and self._parent() + + def all(self) -> Sequence["WebModule"]: + return tuple(self.modulesByNameAndIndex.values()) + + @logException + def atPosition(self, offset) -> "WebModule": + for (start, end), module in self.modulesByPosition.items(): + if start <= offset <= end: + return module.ruleManager.subModules.atPosition(offset) or module + + @logException + def update(self): + from ..webModuleHandler import getWebModule + previous = self.modulesByNameAndIndex.copy() + self.modulesByNameAndIndex.clear() + for result in self.parent.getResults(): + if result.properties.subModule: + key = (result.rule.name, result.index) + webModule = previous.get(key) + if not webModule: + webModule = getWebModule(result.properties.subModule) + if not webModule: + log.error(f"WebModule not found: {result.properties.subModule!r}") + continue + self.modulesByNameAndIndex[key] = webModule + self.modulesByPosition[(result.startOffset, result.endOffset)] = webModule + webModule.ruleManager.parentModule = self.parent.webModule + webModule.ruleManager.parentNode = result.node + webModule.ruleManager.update(self.parent.nodeManager) + + def terminate(self): + self.modulesByNameAndIndex.clear() + self.modulesByPosition.clear() + self._parent = None + + class CustomActionDispatcher(object): """ Execute a custom action, eventually overriding a standard action. @@ -811,7 +896,7 @@ def getCustomFunc(self, webModule=None): ) -class Result(baseObject.ScriptableObject): +class Result(ScriptableObject): def __init__(self, criteria, context, index): super().__init__() @@ -1057,7 +1142,7 @@ def getTitle(self): return self.label + " - " + self.node.innerText -class Criteria(baseObject.ScriptableObject): +class Criteria(ScriptableObject): def __init__(self, rule, data): super().__init__() @@ -1095,7 +1180,7 @@ def load(self, data): gesturesMap = {} for gestureIdentifier in list(self.gestures.keys()): gesturesMap[gestureIdentifier] = "notFound" - self.bindGestures(gesturesMap) + # self.bindGestures(gesturesMap) self.properties.load(data.pop("properties", {})) if data: raise ValueError( @@ -1215,7 +1300,8 @@ def iterResults(self): return if not self.checkContextPageType(): return - + # Restrict to current SubModule + parentNode = mgr.parentNode or mgr.nodeManager.mainNode # Handle contextParent rootNodes = set() # Set of possible parent nodes excludedNodes = set() # Set of excluded parent nodes @@ -1248,7 +1334,7 @@ def iterResults(self): else: multipleContext = False if results: - nodes = [result.node for result in results] + nodes = [result.node for result in results if result.node in parentNode] if exclude: excludedNodes.update(nodes) else: @@ -1270,6 +1356,9 @@ def iterResults(self): return rootNodes = newRootNodes kwargs = getSimpleSearchKwargs(self) + excludedNodes.update({ + result.node for result in self.rule.ruleManager._subModuleResults + }) if excludedNodes: kwargs["exclude"] = excludedNodes limit = None @@ -1304,7 +1393,7 @@ def script_notFound(self, gesture): ) -class Rule(baseObject.ScriptableObject): +class Rule(ScriptableObject): def __init__(self, ruleManager, data): super().__init__() @@ -1487,7 +1576,7 @@ def getSimpleSearchKwargs(criteria, raiseOnUnsupported=False): return kwargs -class Zone(baseObject.AutoPropertyObject): +class Zone(AutoPropertyObject): def __init__(self, result): super().__init__() diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index f9eefd1b..f362e2f9 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -76,7 +76,7 @@ def getDisplayName(self, ruleType) -> str: class PropertySpec(Enum): autoAction = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default=None, # Translators: The display name for a rule property @@ -86,7 +86,7 @@ class PropertySpec(Enum): isRestrictedChoice=True ) multiple = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.PARENT, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.PARENT, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -95,7 +95,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) formMode = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -104,7 +104,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) skip = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -113,7 +113,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) sayName = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=bool, default=False, # Translators: The display name for a rule property @@ -122,7 +122,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) customName = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default="", # Translators: The display name for a rule property @@ -133,6 +133,7 @@ class PropertySpec(Enum): ) customValue = PropertySpecValue( ruleTypes=( + ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.PAGE_TITLE_1, ruleTypes.PAGE_TITLE_2, @@ -141,7 +142,7 @@ class PropertySpec(Enum): valueType=str, default="", displayName={ - (ruleTypes.MARKER, ruleTypes.ZONE): + (ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE): # Translators: The display name for a rule property pgettext("webAccess.ruleProperty", "Custom message"), ("pageTitle1", "pageTitle2"): @@ -153,7 +154,7 @@ class PropertySpec(Enum): isRestrictedChoice=False ) mutation = PropertySpecValue( - ruleTypes=(ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), valueType=str, default=None, # Translators: The display name for a rule property diff --git a/addon/globalPlugins/webAccess/ruleHandler/ruleTypes.py b/addon/globalPlugins/webAccess/ruleHandler/ruleTypes.py index a5a930ba..19fddfcc 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/ruleTypes.py +++ b/addon/globalPlugins/webAccess/ruleHandler/ruleTypes.py @@ -30,6 +30,7 @@ addonHandler.initTranslation() +GLOBAL_MARKER = "globalMarker" MARKER = "marker" ZONE = "zone" PAGE_TYPE = "pageType" @@ -37,8 +38,11 @@ PAGE_TITLE_1 = "pageTitle1" PAGE_TITLE_2 = "pageTitle2" +ACTION_TYPES = (GLOBAL_MARKER, MARKER, ZONE) ruleTypeLabels = { + # Translators: The label for a rule type. + GLOBAL_MARKER: _("Global Marker"), # Translators: The label for a rule type. MARKER: _("Marker"), # Translators: The label for a rule type. diff --git a/addon/globalPlugins/webAccess/store/webModule.py b/addon/globalPlugins/webAccess/store/webModule.py index 65e92d1e..582f7e37 100644 --- a/addon/globalPlugins/webAccess/store/webModule.py +++ b/addon/globalPlugins/webAccess/store/webModule.py @@ -81,7 +81,7 @@ def catalog(self, errors=None): try: data = self.get(ref).data meta = {} - for key in ("windowTitle", "url"): + for key in ("name", "url", "windowTitle"): # "WebApp" corresponds to legacy format version (pre 0.1) value = data.get("WebModule", data.get("WebApp", {})).get(key) if value: diff --git a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py index 7dfb2c99..ce709d6d 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py @@ -78,6 +78,13 @@ def getCatalog(refresh=False, errors=None): return _catalog +def getWebModule(name: str) -> WebModule: + for ref, meta in getCatalog(): + candidate = meta.get("name") + if candidate == name: + return store.get(ref) + + def getWebModuleForTreeInterceptor(treeInterceptor): obj = treeInterceptor.rootNVDAObject windowTitle = getWindowTitle(obj) diff --git a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py index 6e66ac9d..819dbff1 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/webModule.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/webModule.py @@ -50,7 +50,7 @@ from ..lib.markdown2 import markdown from ..lib.packaging import version from ..webAppLib import playWebAccessSound -from .. import ruleHandler + if sys.version_info[1] < 9: from typing import Sequence @@ -109,7 +109,8 @@ def __init__(self): self.layers: Sequence[WebModuleDataLayer] = [] self.activePageTitle = None self.activePageIdentifier = None - self.ruleManager = ruleHandler.RuleManager(self) + from ..ruleHandler import RuleManager + self.ruleManager = RuleManager(self) def __repr__(self): return "WebModule {name}".format( @@ -177,8 +178,12 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): """ return False + def getScript(self,gesture): + return super().getScript(gesture) or self.ruleManager.getScript(gesture) + def createRule(self, data): - return ruleHandler.Rule(self.ruleManager, data) + from ..ruleHandler import Rule + return Rule(self.ruleManager, data) def dump(self, layerName): layer = self.getLayer(layerName, raiseIfMissing=True) From e5167767e9531c39e34228b7a402768304d2b0cf Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 30 Aug 2024 00:31:07 +0200 Subject: [PATCH 18/54] SubModules: GUI: Editor: Offer a combo for SubModule selection rather than a bare free text field - PropertySpecValue has a new attribute `hasSuggestions` - GUI: SingleFieldEditor* classes handle the new EditorType.COMBO - GUI: CriteriaEditor: Fix selecting a newly overridden property when refreshing the list --- addon/globalPlugins/webAccess/gui/__init__.py | 46 ++++++++++--- .../webAccess/gui/criteriaEditor.py | 6 +- .../globalPlugins/webAccess/gui/properties.py | 66 +++++++++++++++---- .../webAccess/ruleHandler/properties.py | 38 +++++++---- 4 files changed, 121 insertions(+), 35 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/__init__.py b/addon/globalPlugins/webAccess/gui/__init__.py index e8ea9c4d..55f4c853 100644 --- a/addon/globalPlugins/webAccess/gui/__init__.py +++ b/addon/globalPlugins/webAccess/gui/__init__.py @@ -58,9 +58,9 @@ if sys.version_info[1] < 9: - from typing import Mapping, Sequence, Set + from typing import Iterable, Mapping, Sequence, Set else: - from collections.abc import Mapping, Sequence, Set + from collections.abc import Iterable, Mapping, Sequence, Set addonHandler.initTranslation() @@ -902,6 +902,12 @@ def __refresh(self, default=None): self.setSelectedChoiceKey(default) +class Change(Enum): + CREATION = auto() + UPDATE = auto() + DELETION = auto() + + @dataclass class EditorTypeValue: editorClass: type(wx.Control) = None @@ -910,15 +916,10 @@ class EditorTypeValue: isLabeled: bool = None -class Change(Enum): - CREATION = auto() - UPDATE = auto() - DELETION = auto() - - class EditorType(Enum): CHECKBOX = EditorTypeValue(wx.CheckBox, wx.EVT_CHECKBOX, "onEditor_checkBox", True) CHOICE = EditorTypeValue(wx.Choice, wx.EVT_CHOICE, "onEditor_choice", False) + COMBO = EditorTypeValue(wx.ComboBox, wx.EVT_TEXT, "onEditor_combo", False) TEXT = EditorTypeValue(wx.TextCtrl, wx.EVT_TEXT, "onEditor_text", False) @@ -965,6 +966,11 @@ def editorChoices(self) -> Mapping[Any, str]: def editorLabel(self) -> wx.Control: raise NotImplementedError + @property + @abstractmethod + def editorSuggestions(self) -> Iterable[str]: + raise NotImplementedError + @property @abstractmethod def editorType(self) -> EditorType: @@ -998,6 +1004,11 @@ def onEditor_choice(self, evt): self.setFieldValue(tuple(self.editorChoices.keys())[index]) self.onEditor_change() + @guarded + def onEditor_combo(self, evt): + self.setFieldValue(evt.EventObject.Value) + self.onEditor_change() + @guarded def onEditor_text(self, evt): self.setFieldValue(evt.EventObject.Value) @@ -1017,7 +1028,7 @@ def toggleFieldValue(self, previous: bool = False) -> None: notifyError(f"value: {value!r}, choices: {choices!r}") return value = keys[index] - elif editorType is EditorType.TEXT: + elif editorType in (EditorType.COMBO, EditorType.TEXT): self.editor.SetFocus() return else: @@ -1043,7 +1054,7 @@ def updateEditor(self) -> None: elif editorType is EditorType.CHOICE: # Does not emit wx.EVT_CHOICE editor.Selection = tuple(self.editorChoices.keys()).index(value) - elif editorType is EditorType.TEXT: + elif editorType in (EditorType.COMBO, EditorType.TEXT): # Does not emit wx.EVT_TEXT editor.ChangeValue(value if value is not None else "") @@ -1052,6 +1063,14 @@ def updateEditorChoices(self): editor.Clear() editor.AppendItems(tuple(self.editorChoices.values())) + def updateEditorSuggestions(self): + editor = self.editor + value = editor.Value + editor.SetEvtHandlerEnabled(False) + editor.Set(tuple(self.editorSuggestions)) + editor.SetEvtHandlerEnabled(True) + editor.ChangeValue(value) + def updateEditorLabel(self): # Translators: A field label. French typically adds a space before the colon. self.editorLabel.Label = _("{field}:").format(field=self.fieldDisplayName) @@ -1075,6 +1094,7 @@ class SingleFieldEditorPanelBase(SingleFieldEditorMixin, TreeContextualPanel): @dataclass class CategoryParams(TreeContextualPanel.CategoryParams): editorChoices: Mapping[Any, str] = None + editorSuggestions: Sequence[str] = None fieldDisplayName: str = None fieldName: str = None # Type hint "Self" was added only in Python 3.11 (NVDA >= 2024.1) @@ -1101,6 +1121,10 @@ def __init__(self, *args, editorType: EditorType = None, **kwargs): def editorChoices(self) -> str: return self.categoryParams.editorChoices + @property + def editorSuggestions(self) -> str: + return self.categoryParams.editorSuggestions + @property def fieldDisplayName(self) -> str: return self.categoryParams.fieldDisplayName @@ -1133,6 +1157,8 @@ def initData(self, context): self.editorLabel = self.editor if editorType is EditorType.CHOICE: self.updateEditorChoices() + elif editorType is EditorType.COMBO: + self.updateEditorSuggestions() self.updateEditor() self.updateEditorLabel() diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/criteriaEditor.py index e94b7095..81242c90 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/criteriaEditor.py @@ -21,7 +21,7 @@ -__version__ = "2024.07.25" +__version__ = "2024.08.29" __authors__ = ( "Shirley Noël ", "Julien Cochuyt ", @@ -990,9 +990,9 @@ def onAddPropBtn(self, evt): if not prop: return prop.value = prop.default # Setting any value actually adds to the ChainMap based container - self.listCtrl_update_all() self.prop = prop - if prop.editorType is EditorType.TEXT: + self.listCtrl_update_all() + if prop.editorType in (EditorType.COMBO, EditorType.TEXT): self.editor.SetFocus() else: self.listCtrl.SetFocus() diff --git a/addon/globalPlugins/webAccess/gui/properties.py b/addon/globalPlugins/webAccess/gui/properties.py index 79b9ebfd..2075c979 100644 --- a/addon/globalPlugins/webAccess/gui/properties.py +++ b/addon/globalPlugins/webAccess/gui/properties.py @@ -19,8 +19,10 @@ # # See the file COPYING.txt at the root of this distribution for more details. + __author__ = "Sendhil Randon " + from collections import ChainMap from abc import abstractmethod from enum import Enum @@ -128,6 +130,8 @@ def displayValueIfUndefined(self) -> str: def editorType(self) -> EditorType: if self.isRestrictedChoice: return EditorType.CHOICE + elif self.hasSuggestions: + return EditorType.COMBO elif issubclass(self.valueType, bool): return EditorType.CHECKBOX elif issubclass(self.valueType, str): @@ -135,6 +139,25 @@ def editorType(self) -> EditorType: else: raise Exception(f"Unable to determine EditorType for property {self.name!r}") + @property + @logException + def suggestions(self): + if self.editorType is not EditorType.COMBO: + return None + container = self._container + context = container._context + cache = context.setdefault("propertySuggestionsCache", {}) + name = self.name + if name in cache: + return cache[name] + if name == "subModule": + from ..webModuleHandler import getCatalog + suggestions = tuple(sorted({meta["name"] for ref, meta in getCatalog()})) + else: + raise ValueError(f"prop.name: {name!r}") + cache[name] = suggestions + return suggestions + @property @logException def value(self) -> PropertyValue: @@ -228,6 +251,10 @@ class SinglePropertyEditorPanelBase(SingleFieldEditorMixin, ContextualSettingsPa def editorChoices(self): return self.prop.choices + @property + def editorSuggestions(self): + return self.prop.suggestions + @property def editorType(self): return self.prop.editorType @@ -307,6 +334,7 @@ def editor(self) -> wx.Control: return { EditorType.CHECKBOX: self.editor_checkBox, EditorType.CHOICE: self.editor_choice_ctrl, + EditorType.COMBO: self.editor_combo_ctrl, EditorType.TEXT: self.editor_text_ctrl, }[self.prop.editorType] @@ -316,6 +344,7 @@ def editorLabel(self) -> wx.Control: return { EditorType.CHECKBOX: self.editor_checkBox, EditorType.CHOICE: self.editor_choice_label, + EditorType.COMBO: self.editor_combo_label, EditorType.TEXT: self.editor_text_label, }[self.prop.editorType] @@ -331,6 +360,8 @@ def prop(self, prop: Property) -> None: editor = self.editor if prop.editorType is EditorType.CHOICE: self.updateEditorChoices() + elif prop.editorType is EditorType.COMBO: + self.updateEditorSuggestions() self.updateEditor() self.updateEditorLabel() self.Freeze() @@ -350,13 +381,13 @@ def makeSettings(self, settingsSizer): gbSizer = wx.GridBagSizer() gbSizer.EmptyCellSize = (0, 0) settingsSizer.Add(gbSizer, flag=wx.EXPAND, proportion=1) - + row = 0 items = hideable["hideIfSupported"] = [] item = wx.StaticText(self, label=self.descriptionIfNoneSupported) items.append(item) gbSizer.Add(item, pos=(row, 0), span=(1, 3), flag=wx.EXPAND) - + row += 1 items = hideable["hideIfNoneSupported"] = [] # Translators: The label for a list on the Rule Editor dialog @@ -364,11 +395,11 @@ def makeSettings(self, settingsSizer): item.Hide() items.append(item) gbSizer.Add(item, pos=(row, 0), span=(1, 3), flag=wx.EXPAND) - + row += 1 item = gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL), pos=(row, 0)) items.append(item) - + row += 1 item = self.listCtrl = ListCtrlAutoWidth(self, style=wx.LC_REPORT | wx.BORDER_SUNKEN) # Translators: A column header on the Rule Editor dialog @@ -380,11 +411,11 @@ def makeSettings(self, settingsSizer): items.append(item) gbSizer.Add(item, pos=(row, 0), span=(4, 3), flag=wx.EXPAND) gbSizer.AddGrowableRow(row + 3) - + row += 4 item = gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), pos=(row, 0)) items.append(item) - + row += 1 items = hideable["hideIfNotCHECKBOX"] = [] item = self.editor_checkBox = wx.CheckBox(self, label="") @@ -392,7 +423,7 @@ def makeSettings(self, settingsSizer): item.Bind(wx.EVT_CHAR_HOOK, self.onEditor_charHook) items.append(item) gbSizer.Add(item, pos=(row, 0), span=(1, 3), flag=wx.EXPAND) - + row += 1 items = hideable["hideIfNotCHOICE"] = [] item = self.editor_choice_label = wx.StaticText(self, label="") @@ -405,7 +436,20 @@ def makeSettings(self, settingsSizer): item.Bind(wx.EVT_CHAR_HOOK, self.onEditor_charHook) items.append(item) gbSizer.Add(item, pos=(row, 2), flag=wx.EXPAND) - + + row += 1 + items = hideable["hideIfNotCOMBO"] = [] + item = self.editor_combo_label = wx.StaticText(self, label="") + items.append(item) + gbSizer.Add(item, pos=(row, 0)) + item = gbSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), pos=(row, 1)) + items.append(item) + item = self.editor_combo_ctrl = wx.ComboBox(self) + item.Bind(wx.EVT_TEXT, self.onEditor_combo) + item.Bind(wx.EVT_CHAR_HOOK, self.onEditor_charHook) + items.append(item) + gbSizer.Add(item, pos=(row, 2), flag=wx.EXPAND) + row += 1 items = hideable["hideIfNotTEXT"] = [] item = self.editor_text_label = wx.StaticText(self, label="") @@ -418,7 +462,7 @@ def makeSettings(self, settingsSizer): item.Bind(wx.EVT_CHAR_HOOK, self.onEditor_charHook) items.append(item) gbSizer.Add(item, pos=(row, 2), flag=wx.EXPAND) - + for item in ( hideable["hideIfNoneSupported"] + hideable["hideIfNotCHECKBOX"] @@ -426,7 +470,7 @@ def makeSettings(self, settingsSizer): + hideable["hideIfNotTEXT"] ): item.Show(False) - + gbSizer.AddGrowableCol(2) gbSizer.FitInside(self) self.gbSizer = gbSizer @@ -501,7 +545,7 @@ def onEditor_charHook(self, evt): return elif keycode == wx.WXK_DELETE and not mods: prop = self.prop - if prop.editorType is not EditorType.TEXT: + if prop.editorType not in (EditorType.COMBO, EditorType.TEXT): self.prop_reset() return evt.Skip() diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index f362e2f9..061abe81 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -56,7 +56,13 @@ class PropertySpecValue: """ __slots__ = ( - "ruleTypes", "valueType", "default", "displayName", "displayValueIfUndefined", "isRestrictedChoice" + "ruleTypes", + "valueType", + "default", + "displayName", + "displayValueIfUndefined", + "isRestrictedChoice", + "hasSuggestions", ) ruleTypes: Sequence[str] # Rule types for which the property is supported @@ -64,7 +70,8 @@ class PropertySpecValue: default: PropertyValue displayName: str | Mapping[Sequence[str], str] # Can be different by rule type displayValueIfUndefined: str - isRestrictedChoice: bool # Currently applies only in the editor + isRestrictedChoice: bool # Applies only in the editor + hasSuggestions: bool # Applies only in the editor def getDisplayName(self, ruleType) -> str: displayName = self.displayName @@ -83,7 +90,8 @@ class PropertySpec(Enum): displayName=pgettext("webAccess.ruleProperty", "Auto Actions"), # Translators: Displayed if no value is set for the "Auto Actions" property displayValueIfUndefined=pgettext("webAccess.action", "No action"), - isRestrictedChoice=True + isRestrictedChoice=True, + hasSuggestions=False, ) multiple = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.PARENT, ruleTypes.ZONE), @@ -92,7 +100,8 @@ class PropertySpec(Enum): # Translators: The display name for a rule property displayName=pgettext("webAccess.ruleProperty", "Multiple results"), displayValueIfUndefined=None, # Does not apply as there is a sensible default - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) formMode = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), @@ -101,7 +110,8 @@ class PropertySpec(Enum): # Translators: The display name for a rule property displayName=pgettext("webAccess.ruleProperty", "Activate form mode"), displayValueIfUndefined=None, # Does not apply as there is a sensible default - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) skip = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), @@ -110,7 +120,8 @@ class PropertySpec(Enum): # Translators: The display name for a rule property displayName=pgettext("webAccess.ruleProperty", "Skip with Page Down"), displayValueIfUndefined=None, # Does not apply as there is a sensible default - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) sayName = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), @@ -119,7 +130,8 @@ class PropertySpec(Enum): # Translators: The display name for a rule property displayName=pgettext("webAccess.ruleProperty", "Speak rule name"), displayValueIfUndefined=None, # Does not apply as there is a sensible default - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) customName = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), @@ -129,7 +141,8 @@ class PropertySpec(Enum): displayName=pgettext("webAccess.ruleProperty", "Custom name"), # Translators: Displayed if no value is set for a given rule property displayValueIfUndefined=pgettext("webAccess.ruleProperty", ""), - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) customValue = PropertySpecValue( ruleTypes=( @@ -151,7 +164,8 @@ class PropertySpec(Enum): }, # Translators: Displayed if no value is set for a given rule property displayValueIfUndefined=pgettext("webAccess.ruleProperty", ""), - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=False, ) mutation = PropertySpecValue( ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), @@ -161,7 +175,8 @@ class PropertySpec(Enum): displayName=pgettext("webAccess.ruleProperty", "Transform"), # Translators: Displayed if no value is set for the "Transform" rule property displayValueIfUndefined=pgettext("webAccess.ruleProperty.mutation", "None"), - isRestrictedChoice=True + isRestrictedChoice=True, + hasSuggestions=False, ) subModule = PropertySpecValue( ruleTypes=(ruleTypes.ZONE,), @@ -171,7 +186,8 @@ class PropertySpec(Enum): displayName=pgettext("webAccess.ruleProperty", "Load sub-module"), # Translators: The displayed text if there is no value for the "Load sub-module" property displayValueIfUndefined=pgettext("webAccess.ruleProperty.subModule", "No"), - isRestrictedChoice=False + isRestrictedChoice=False, + hasSuggestions=True, ) def __getattr__(self, name: str): From 0a4248025398a15d27ef6c77080575d340fcd1a1 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 30 Aug 2024 17:41:37 +0200 Subject: [PATCH 19/54] SubModules: GUI: Editor: Prompt for creation of a missing SubModule - WebModule Editor: Do not require a trigger when creating from the Rule Editor - WebModule Manager: Add key shortcuts `F2`, `F3` and `delete` to edit, manage rules and delete, respectively (#38) - WebModule Manager: Set "no" as the default when prompting for deletion - Refactor `gui.actions` into `gui.rule.actions` - Refactor `gui.criteriaEditor` into `gui.rule.criteriaEditor` - Refactor `gui.gestureBinding` into `gui.rule.gestureBinding` - Refactor `gui.properties` into `gui.rule.properties` - Refactor `gui.webModulesManager` into `gui.webModule.manager` - Refactor `gui.webModuleEditor` into `gui.webModule.editor` - Refactor `gui.webModuleManager.promptDelete` into `gui.webModule.promptDelete` - GUI: Refactor `gui.webModuleManager.promptMask` into `gui.webModule.promptMask` --- addon/globalPlugins/webAccess/gui/menu.py | 17 ++-- .../webAccess/gui/rule/__init__.py | 89 ++++++++++++++++++ .../webAccess/gui/{ => rule}/actions.py | 11 ++- .../gui/{ => rule}/criteriaEditor.py | 31 +++++-- .../webAccess/gui/rule/editor.py | 22 +++-- .../gui/{ => rule}/gestureBinding.py | 6 +- .../webAccess/gui/{ => rule}/properties.py | 12 +-- .../webAccess/gui/webModule/__init__.py | 88 ++++++++++++++++++ .../editor.py} | 63 +++++++++---- .../manager.py} | 93 +++++++------------ .../webAccess/webModuleHandler/__init__.py | 8 +- 11 files changed, 315 insertions(+), 125 deletions(-) rename addon/globalPlugins/webAccess/gui/{ => rule}/actions.py (97%) rename addon/globalPlugins/webAccess/gui/{ => rule}/criteriaEditor.py (97%) rename addon/globalPlugins/webAccess/gui/{ => rule}/gestureBinding.py (97%) rename addon/globalPlugins/webAccess/gui/{ => rule}/properties.py (97%) create mode 100644 addon/globalPlugins/webAccess/gui/webModule/__init__.py rename addon/globalPlugins/webAccess/gui/{webModuleEditor.py => webModule/editor.py} (82%) rename addon/globalPlugins/webAccess/gui/{webModulesManager.py => webModule/manager.py} (79%) diff --git a/addon/globalPlugins/webAccess/gui/menu.py b/addon/globalPlugins/webAccess/gui/menu.py index 7bd23ae8..b02f1cbe 100644 --- a/addon/globalPlugins/webAccess/gui/menu.py +++ b/addon/globalPlugins/webAccess/gui/menu.py @@ -38,7 +38,6 @@ from ... import webAccess from .. import ruleHandler from ..utils import guarded -from . import webModulesManager addonHandler.initTranslation() @@ -150,21 +149,21 @@ def onRulesManager(self, evt): show(self.context, gui.mainFrame) @guarded - def onWebModuleEdit(self, evt, webModule=None): - if webModule is not None: - self.context["webModule"] = webModule - from .webModuleEditor import show + def onWebModuleCreate(self, evt, webModule=None): + self.context["new"] = True + from .webModule.editor import show show(self.context) @guarded - def onWebModuleCreate(self, evt, webModule=None): - self.context["new"] = True - from .webModuleEditor import show + def onWebModuleEdit(self, evt, webModule=None): + if webModule is not None: + self.context["webModule"] = webModule + from .webModule.editor import show show(self.context) @guarded def onWebModulesManager(self, evt): - from .webModulesManager import show + from .webModule.manager import show show(self.context) @guarded diff --git a/addon/globalPlugins/webAccess/gui/rule/__init__.py b/addon/globalPlugins/webAccess/gui/rule/__init__.py index e69de29b..79f0f180 100644 --- a/addon/globalPlugins/webAccess/gui/rule/__init__.py +++ b/addon/globalPlugins/webAccess/gui/rule/__init__.py @@ -0,0 +1,89 @@ +# globalPlugins/webAccess/gui/rule/__init__.py +# -*- coding: utf-8 -*- + +# This file is part of Web Access for NVDA. +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See the file COPYING.txt at the root of this distribution for more details. + + +__author__ = "Julien Cochuyt " + + +import sys +from typing import Any + +import wx + +import addonHandler +import gui +from logHandler import log + + +if sys.version_info[1] < 9: + from typing import Mapping +else: + from collections.abc import Mapping + + +addonHandler.initTranslation() + + +def createMissingSubModule( + context: Mapping[str, Any], + data: Mapping[str, Any], + parent: wx.Window + ) -> bool: + """Create the missing SubModule from Rule or Criteria data + + If a SubModule is specified in the provided data, it is looked-up in the catalog. + If it is missing from the catalog, the user is prompted for creating it. + + This function returns: + - `None` if no creation was necessary or if the user declined the prompt. + - `False` if the user canceled the prompt or if the creation failed or has been canceled. + - `True` if the creation succeeded. + """ + name = data.get("properties", {}).get("subModule") + if not name: + return None + from ...webModuleHandler import getCatalog + if any(meta["name"] == name for ref, meta in getCatalog()): + return True + res = gui.messageBox( + message=( + # Translators: A prompt for creation of a missing SubModule + _(f"""SubModule {name} could not be found. + +Do you want to create it now?""") + ), + style=wx.YES_NO | wx.CANCEL | wx.ICON_QUESTION, + parent=parent, + ) + if res is wx.NO: + return None + elif res is wx.CANCEL: + return False + context = context.copy() + context["new"] = True + context["data"] = {"webModule": {"name": name, "subModule": True}} + from ..webModule.editor import show + res = show(context, parent) + if res: + newName = context["webModule"].name + if newName != name: + data["properties"]["subModule"] = newName + return res diff --git a/addon/globalPlugins/webAccess/gui/actions.py b/addon/globalPlugins/webAccess/gui/rule/actions.py similarity index 97% rename from addon/globalPlugins/webAccess/gui/actions.py rename to addon/globalPlugins/webAccess/gui/rule/actions.py index 5955340b..d21c47d5 100644 --- a/addon/globalPlugins/webAccess/gui/actions.py +++ b/addon/globalPlugins/webAccess/gui/rule/actions.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/actions.py +# globalPlugins/webAccess/gui/rule/actions.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -38,10 +38,11 @@ import gui from gui import guiHelper -from ..ruleHandler import ruleTypes -from ..utils import guarded -from . import ContextualSettingsPanel, Change, gestureBinding -from .rule.abc import RuleAwarePanelBase +from ...ruleHandler import ruleTypes +from ...utils import guarded +from .. import ContextualSettingsPanel, Change +from . import gestureBinding +from .abc import RuleAwarePanelBase if sys.version_info[1] < 9: diff --git a/addon/globalPlugins/webAccess/gui/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py similarity index 97% rename from addon/globalPlugins/webAccess/gui/criteriaEditor.py rename to addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py index 81242c90..d43d1552 100644 --- a/addon/globalPlugins/webAccess/gui/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/criteriaEditor.py +# globalPlugins/webAccess/gui/rule/criteria.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -21,7 +21,7 @@ -__version__ = "2024.08.29" +__version__ = "2024.08.30" __authors__ = ( "Shirley Noël ", "Julien Cochuyt ", @@ -48,9 +48,9 @@ import ui import addonHandler -from ..ruleHandler import builtinRuleActions, ruleTypes -from ..utils import guarded, notifyError, updateOrDrop -from . import ( +from ...ruleHandler import builtinRuleActions, ruleTypes +from ...utils import guarded, notifyError, updateOrDrop +from .. import ( ContextualMultiCategorySettingsDialog, ContextualSettingsPanel, DropDownWithHideableChoices, @@ -61,8 +61,9 @@ stripAccel, stripAccelAndColon, ) +from . import createMissingSubModule +from .abc import RuleAwarePanelBase from .actions import ActionsPanelBase -from .rule.abc import RuleAwarePanelBase from .properties import Properties, PropertiesPanelBase, Property @@ -245,7 +246,7 @@ def testCriteria(context): ruleData.setdefault("properties", {})['multiple'] = True critData.setdefault("properties", {}).pop("multiple", None) mgr = context["webModule"].ruleManager - from ..ruleHandler import Rule + from ...ruleHandler import Rule rule = Rule(mgr, ruleData) import time start = time.time() @@ -263,7 +264,9 @@ def testCriteria(context): class CriteriaEditorPanel(RuleAwarePanelBase): def getData(self): - return self.context["data"].setdefault("criteria", {}) + # Should always be initialized, as the Rule Editor populates it with at least + # the index of this Alternative Criteria Set ("criteriaIndex"). + return self.context["data"]["criteria"] class GeneralPanel(CriteriaEditorPanel): @@ -1023,6 +1026,11 @@ class CriteriaEditorDialog(ContextualMultiCategorySettingsDialog): categoryClasses = [GeneralPanel, CriteriaPanel, ActionsPanel, PropertiesPanel] INITIAL_SIZE = (900, 580) + def getData(self): + # Should always be initialized, as the Rule Editor populates it with at least + # the index of this Alternative Criteria Set ("criteriaIndex"). + return self.context["data"]["criteria"] + def makeSettings(self, settingsSizer): super().makeSettings(settingsSizer) idTestCriteria = wx.NewId() @@ -1034,8 +1042,13 @@ def makeSettings(self, settingsSizer): def onTestCriteria(self, evt): self.currentCategory.updateData() testCriteria(self.context) + + def _saveAllPanels(self): + super()._saveAllPanels() + if createMissingSubModule(self.context, self.getData(), self) is False: + raise ValidationError() # Cancels closing of the dialog def show(context, parent=None): - from . import showContextualDialog + from .. import showContextualDialog return showContextualDialog(CriteriaEditorDialog, context, parent) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index a9bd0433..40c19a01 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -62,22 +62,21 @@ TreeMultiCategorySettingsDialog, TreeNodeInfo, ValidationError, - criteriaEditor, - gestureBinding, showContextualDialog, stripAccel, stripAccelAndColon, stripAccelAndColon, ) -from ..actions import ActionsPanelBase -from ..properties import ( +from . import createMissingSubModule, criteriaEditor, gestureBinding +from .abc import RuleAwarePanelBase +from .actions import ActionsPanelBase +from .properties import ( EditorType, Property, Properties, PropertiesPanelBase, SinglePropertyEditorPanelBase, ) -from .abc import RuleAwarePanelBase if sys.version_info[1] < 9: @@ -939,9 +938,9 @@ def initData(self, context: Mapping[str, Any]) -> None: break node = node.parent super().initData(context) - - def _doSave(self): - super()._doSave() + + def _saveAllPanels(self): + super()._saveAllPanels() context = self.context data = self.getData() mgr = context["webModule"].ruleManager @@ -952,14 +951,17 @@ def _doSave(self): layerName = rule.layer webModule = webModuleHandler.getEditableWebModule(mgr.webModule, layerName=layerName) if not webModule: - return + raise ValidationError() # Cancels closing of the dialog + if createMissingSubModule(context, data, self) is False: + raise ValidationError() # Cancels closing of the dialog if context.get("new"): layerName = webModule.getWritableLayer().name else: mgr.removeRule(rule) context["rule"] = mgr.loadRule(layerName, data["name"], data) webModule.getLayer(layerName, raiseIfMissing=True).dirty = True - webModuleHandler.save(webModule, layerName=layerName) + if not webModuleHandler.save(webModule, layerName=layerName): + raise ValidationError() # Cancels closing of the dialog def show(context, parent=None): diff --git a/addon/globalPlugins/webAccess/gui/gestureBinding.py b/addon/globalPlugins/webAccess/gui/rule/gestureBinding.py similarity index 97% rename from addon/globalPlugins/webAccess/gui/gestureBinding.py rename to addon/globalPlugins/webAccess/gui/rule/gestureBinding.py index d994c593..08e3d485 100644 --- a/addon/globalPlugins/webAccess/gui/gestureBinding.py +++ b/addon/globalPlugins/webAccess/gui/rule/gestureBinding.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/gestureBinding.py +# globalPlugins/webAccess/gui/rule/gestureBinding.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -41,8 +41,8 @@ import speech import ui -from ..utils import guarded, logException -from . import ScalingMixin, showContextualDialog +from ...utils import guarded, logException +from .. import ScalingMixin, showContextualDialog if sys.version_info[1] < 9: diff --git a/addon/globalPlugins/webAccess/gui/properties.py b/addon/globalPlugins/webAccess/gui/rule/properties.py similarity index 97% rename from addon/globalPlugins/webAccess/gui/properties.py rename to addon/globalPlugins/webAccess/gui/rule/properties.py index 2075c979..4bf47fa0 100644 --- a/addon/globalPlugins/webAccess/gui/properties.py +++ b/addon/globalPlugins/webAccess/gui/rule/properties.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/properties.py +# globalPlugins/webAccess/gui/rule/properties.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -36,10 +36,10 @@ import speech import ui -from ..ruleHandler.controlMutation import MUTATIONS_BY_RULE_TYPE, mutationLabels -from ..ruleHandler.properties import PropertiesBase, PropertySpec, PropertySpecValue, PropertyValue -from ..utils import guarded, logException -from . import ContextualSettingsPanel, EditorType, ListCtrlAutoWidth, SingleFieldEditorMixin +from ...ruleHandler.controlMutation import MUTATIONS_BY_RULE_TYPE, mutationLabels +from ...ruleHandler.properties import PropertiesBase, PropertySpec, PropertySpecValue, PropertyValue +from ...utils import guarded, logException +from .. import ContextualSettingsPanel, EditorType, ListCtrlAutoWidth, SingleFieldEditorMixin if sys.version_info[1] < 9: @@ -151,7 +151,7 @@ def suggestions(self): if name in cache: return cache[name] if name == "subModule": - from ..webModuleHandler import getCatalog + from ...webModuleHandler import getCatalog suggestions = tuple(sorted({meta["name"] for ref, meta in getCatalog()})) else: raise ValueError(f"prop.name: {name!r}") diff --git a/addon/globalPlugins/webAccess/gui/webModule/__init__.py b/addon/globalPlugins/webAccess/gui/webModule/__init__.py new file mode 100644 index 00000000..5e1a81d7 --- /dev/null +++ b/addon/globalPlugins/webAccess/gui/webModule/__init__.py @@ -0,0 +1,88 @@ +# globalPlugins/webAccess/gui/rule/__init__.py +# -*- coding: utf-8 -*- + +# This file is part of Web Access for NVDA. +# 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 +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# See the file COPYING.txt at the root of this distribution for more details. + + +__author__ = "Julien Cochuyt " + + +import sys +from typing import Any + +import os +import wx + +import addonHandler +import config +import gui + +from ...webModuleHandler import WebModule + + +if sys.version_info[1] < 9: + from typing import Mapping +else: + from collections.abc import Mapping + + +addonHandler.initTranslation() + + +def promptDelete(webModule: WebModule): + msg = ( + # Translators: Prompt before deleting a web module. + _("Do you really want to delete this web module?") + + os.linesep + + str(webModule.name) + ) + if config.conf["webAccess"]["devMode"]: + msg += " ({})".format("/".join((layer.name for layer in webModule.layers))) + return gui.messageBox( + parent=gui.mainFrame, + message=msg, + style=wx.YES_NO | wx.CANCEL | wx.NO_DEFAULT | wx.ICON_WARNING + ) == wx.YES + + +def promptMask(webModule: WebModule): + ref = webModule.getLayer("addon", raiseIfMissing=True).storeRef + if ref[0] != "addons": + raise ValueError("ref={!r}".format(ref)) + addonName = ref[1] + for addon in addonHandler.getRunningAddons(): + if addon.name == addonName: + addonSummary = addon.manifest["summary"] + break + else: + raise LookupError("addonName={!r}".format(addonName)) + log.info("Proposing to mask {!r} from addon {!r}".format(webModule, addonName)) + msg = _( + """This web module comes with the add-on {addonSummary}. +It cannot be modified at its current location. + +Do you want to make a copy in your scratchpad? +""" + ).format(addonSummary=addonSummary) + return gui.messageBox( + parent=gui.mainFrame, + message=msg, + caption=_("Warning"), + style=wx.ICON_WARNING | wx.YES | wx.NO + ) == wx.YES diff --git a/addon/globalPlugins/webAccess/gui/webModuleEditor.py b/addon/globalPlugins/webAccess/gui/webModule/editor.py similarity index 82% rename from addon/globalPlugins/webAccess/gui/webModuleEditor.py rename to addon/globalPlugins/webAccess/gui/webModule/editor.py index e834be42..a2c0ff54 100644 --- a/addon/globalPlugins/webAccess/gui/webModuleEditor.py +++ b/addon/globalPlugins/webAccess/gui/webModule/editor.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/webModuleEditor.py +# globalPlugins/webAccess/gui/webModule/editor.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -23,12 +23,16 @@ __author__ = ( "Yannick Plassiard " "Frédéric Brugnot " - "Julien Cochuyt " + "Julien Cochuyt ", + "André-Abush Clause ", + "Gatien Bouyssou ", ) import itertools import os +import sys +from typing import Any import wx from NVDAObjects import NVDAObject, IAccessible @@ -41,8 +45,15 @@ from logHandler import log import ui -from ..webModuleHandler import WebModule, getEditableWebModule, getUrl, getWindowTitle, save -from . import ContextualDialog, showContextualDialog +from ...utils import guarded +from ...webModuleHandler import WebModule, getEditableWebModule, getUrl, getWindowTitle, save, store +from .. import ContextualDialog, showContextualDialog + + +if sys.version_info[1] < 9: + from typing import Mapping +else: + from collections.abc import Mapping addonHandler.initTranslation() @@ -140,30 +151,38 @@ def __init__(self, parent): self.SetSizer(mainSizer) self.CentreOnScreen() self.webModuleName.SetFocus() - + + def getData(self) -> Mapping[str, Any]: + return self.context["data"]["webModule"] + def initData(self, context): super().initData(context) - data = context.setdefault("data", {})["webModule"] = {} + data = context.setdefault("data", {}).setdefault("webModule", {}) if not context.get("new"): webModule = context.get("webModule") data.update(webModule.dump(webModule.layers[-1].name).data["WebModule"]) + # Could not have been saved without triggers from this editor unless invoked + # from the Rule or Criteria editor. + subModule = data["subModule"] = not (data.get("url") or data.get("windowTitle")) # Translators: Web module edition dialog title title = _("Edit Web Module") if config.conf["webAccess"]["devMode"]: title += " ({})".format("/".join((layer.name for layer in webModule.layers))) else: + subModule = data.get("subModule", False) # Translators: Web module creation dialog title title = _("New Web Module") if config.conf["webAccess"]["devMode"]: - from .. import webModuleHandler try: guineaPig = getEditableWebModule(WebModule(), prompt=False) - store = next(iter(webModuleHandler.store.getSupportingStores( + supportingStore = next(iter(store.getSupportingStores( "create", item=guineaPig ))) if guineaPig is not None else None title += " ({})".format( - store and ("user" if store.name == "userConfig" else store.name) + supportingStore and ( + "user" if supportingStore.name == "userConfig" else supportingStore.name + ) ) except Exception: log.exception() @@ -217,9 +236,7 @@ def initData(self, context): ] self.webModuleUrl.SetItems(urlsChoices) self.webModuleUrl.Selection = ( - urlsChoices.index(selectedUrl) - if selectedUrl - else 0 + urlsChoices.index(selectedUrl) if selectedUrl else 0 if not subModule else -1 ) windowTitleChoices = [] @@ -240,9 +257,18 @@ def initData(self, context): item.Value = "" self.help.Value = data.get("help", "") - + + def updateData(self): + data = self.getData() + data["name"] = self.webModuleName.Value.strip() + data["url"] = [url.strip() for url in self.webModuleUrl.Value.split(",") if url.strip()] + data["windowTitle"] = self.webModuleWindowTitle.Value.strip() + + @guarded def onOk(self, evt): - name = self.webModuleName.Value.strip() + self.updateData() + data = self.getData() + name = data["name"] if len(name) < 1: gui.messageBox( _("You must enter a name for this web module"), @@ -253,10 +279,11 @@ def onOk(self, evt): self.webModuleName.SetFocus() return - url = [url.strip() for url in self.webModuleUrl.Value.split(",") if url.strip()] - windowTitle = self.webModuleWindowTitle.Value.strip() + subModule: bool = data.get("subModule", False) + url = data["url"] + windowTitle = data["windowTitle"] help = self.help.Value.strip() - if not (url or windowTitle): + if not (url or windowTitle or subModule): gui.messageBox( _("You must specify at least a URL or a window title."), _("Error"), @@ -283,7 +310,7 @@ def onOk(self, evt): if not save(webModule, prompt=self.Title): return - + context["webModule"] = webModule self.DestroyLater() self.SetReturnCode(wx.ID_OK) diff --git a/addon/globalPlugins/webAccess/gui/webModulesManager.py b/addon/globalPlugins/webAccess/gui/webModule/manager.py similarity index 79% rename from addon/globalPlugins/webAccess/gui/webModulesManager.py rename to addon/globalPlugins/webAccess/gui/webModule/manager.py index 011fc130..70817063 100644 --- a/addon/globalPlugins/webAccess/gui/webModulesManager.py +++ b/addon/globalPlugins/webAccess/gui/webModule/manager.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/webModulesManager.py +# globalPlugins/webAccess/gui/webModule/manager.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -27,64 +27,16 @@ ) -import os import wx import addonHandler addonHandler.initTranslation() -import config -import core -import globalVars import gui from gui import guiHelper -import languageHandler from logHandler import log -from ..utils import guarded -from . import ContextualDialog, ListCtrlAutoWidth, showContextualDialog - - -def promptDelete(webModule): - msg = ( - # Translators: Prompt before deleting a web module. - _("Do you really want to delete this web module?") - + os.linesep - + str(webModule.name) - ) - if config.conf["webAccess"]["devMode"]: - msg += " ({})".format("/".join((layer.name for layer in webModule.layers))) - return gui.messageBox( - parent=gui.mainFrame, - message=msg, - style=wx.YES_NO | wx.ICON_WARNING - ) == wx.YES - - -def promptMask(webModule): - ref = webModule.getLayer("addon", raiseIfMissing=True).storeRef - if ref[0] != "addons": - raise ValueError("ref={!r}".format(ref)) - addonName = ref[1] - for addon in addonHandler.getRunningAddons(): - if addon.name == addonName: - addonSummary = addon.manifest["summary"] - break - else: - raise LookupError("addonName={!r}".format(addonName)) - log.info("Proposing to mask {!r} from addon {!r}".format(webModule, addonName)) - msg = _( - """This web module comes with the add-on {addonSummary}. -It cannot be modified at its current location. - -Do you want to make a copy in your scratchpad? -""" - ).format(addonSummary=addonSummary) - return gui.messageBox( - parent=gui.mainFrame, - message=msg, - caption=_("Warning"), - style=wx.ICON_WARNING | wx.YES | wx.NO - ) == wx.YES +from ...utils import guarded +from .. import ContextualDialog, ListCtrlAutoWidth, showContextualDialog class Dialog(ContextualDialog): @@ -181,24 +133,42 @@ def __init__(self, parent): flag=wx.EXPAND | wx.BOTTOM | wx.LEFT | wx.RIGHT, border=scale(guiHelper.BORDER_FOR_DIALOGS), ) + self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) self.SetSize(scale(790, 400)) self.SetSizer(mainSizer) self.CentreOnScreen() self.modulesList.SetFocus() - def __del__(self): - Dialog._instance = None - def initData(self, context): super().initData(context) module = context["webModule"] if "webModule" in context else None self.refreshModulesList(selectItem=module) + @guarded + def onCharHook(self, evt): + keycode = evt.GetKeyCode() + if keycode == wx.WXK_ESCAPE: + # Try to limit the difficulty of closing the dialog using the keyboard + # in the event of an error later in this function + evt.Skip() + return + elif keycode == wx.WXK_DELETE: + self.onModuleDelete(None) + return + elif keycode == wx.WXK_F2: + self.onModuleEdit(None) + return + elif keycode == wx.WXK_F3: + self.onRulesManager(None) + return + evt.Skip() + + @guarded def onModuleCreate(self, evt=None): context = self.context.copy() context["new"] = True - from .webModuleEditor import show + from .editor import show if show(context, self): self.refreshModulesList(selectItem=context["webModule"]) @@ -206,10 +176,11 @@ def onModuleCreate(self, evt=None): def onModuleDelete(self, evt=None): index = self.modulesList.GetFirstSelected() if index < 0: + wx.Bell() return webModule = self.modules[index] - from .. import webModuleHandler - if webModuleHandler.delete(webModule=webModule): + from ...webModuleHandler import delete + if delete(webModule=webModule): self.refreshModulesList() @guarded @@ -221,7 +192,7 @@ def onModuleEdit(self, evt=None): context = self.context context.pop("new", None) context["webModule"] = self.modules[index] - from .webModuleEditor import show + from .editor import show if show(context, self): self.refreshModulesList(selectIndex=index) @@ -240,7 +211,7 @@ def onRulesManager(self, evt=None): if not webModule.equals(context.get("webModule")): context["webModule"] = webModule context.pop("result", None) - from .rule.manager import show + from ..rule.manager import show show(context, self) def refreshButtons(self): @@ -259,8 +230,8 @@ def refreshModulesList(self, selectIndex: int = None, selectItem: "WebModule" = for module in list(reversed(contextModule.ruleManager.subModules.all())) + [contextModule] } if contextModule else {} modules = self.modules = [] - from .. import webModuleHandler - for index, module in enumerate(webModuleHandler.getWebModules()): + from ...webModuleHandler import getWebModules + for index, module in enumerate(getWebModules()): if selectIndex is None and module.equals(selectItem): selectIndex = index module = contextModules.get((module.name, module.layers[0].storeRef), module) diff --git a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py index ce709d6d..a7a90d60 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py @@ -56,7 +56,7 @@ def delete(webModule, prompt=True): if prompt: - from ..gui.webModulesManager import promptDelete + from ..gui.webModule import promptDelete if not promptDelete(webModule): return False store.delete(webModule) @@ -227,8 +227,8 @@ def save(webModule, layerName=None, prompt=True, force=False, fromRuleEditor=Fal except DuplicateRefError as e: if not prompt or force: return False - from ..gui import webModuleEditor - if webModuleEditor.promptOverwrite(): + from ..gui.webModule.editor import promptOverwrite + if promptOverwrite(): return save(webModule, layerName=layerName, prompt=prompt, force=True) return False except MalformedRefError: @@ -347,7 +347,7 @@ def _getEditableScratchpadWebModule(webModule, layerName=None, prompt=True): if layerName != "addon": return None if prompt: - from ..gui.webModulesManager import promptMask + from ..gui.webModule import promptMask if not promptMask(webModule): return False data = webModule.dump(layerName).data From 4ad71774b37c5a3e0710bd95e499ce193ab8501b Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 30 Aug 2024 19:53:33 +0200 Subject: [PATCH 20/54] WebAccessBmdti: Force re-analysis of the nodes when manually refreshing results --- addon/globalPlugins/webAccess/nodeHandler.py | 5 +++-- addon/globalPlugins/webAccess/overlay.py | 8 +++++-- .../webAccess/ruleHandler/__init__.py | 22 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 2f3682ef..b953b234 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -19,6 +19,7 @@ # # See the file COPYING.txt at the root of this distribution for more details. + __authors__ = ( "Frédéric Brugnot ", "Julien Cochuyt ", @@ -224,7 +225,7 @@ def afficheNode(self, node, level=0): s += self.afficheNode(child, level + 1) return s - def update(self): + def update(self, force=False): # t = logTimeStart() if self.treeInterceptor is None or not self.treeInterceptor.isReady: self._ready = False @@ -239,7 +240,7 @@ def update(self): except Exception: self._ready = False return False - if size == self.treeInterceptorSize: + if not force and size == self.treeInterceptorSize: # probably not changed return False self.treeInterceptorSize = size diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index 123695a6..bdc6171d 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -23,6 +23,7 @@ WebAccess overlay classes """ + __authors__ = ( "Julien Cochuyt ", "André-Abush Clause ", @@ -51,6 +52,7 @@ import ui import virtualBuffers +from .utils import guarded from six import iteritems @@ -1085,11 +1087,13 @@ def script_quickNavToPreviousResultLevel3(self, gesture): category=SCRCAT_WEBACCESS, gesture="kb:NVDA+shift+f5" ) + @guarded def script_refreshResults(self, gesture): # Translators: Notified when manually refreshing results ui.message(_("Refresh results")) - self.webAccess.ruleManager.update(force=True) - + self.webAccess.ruleManager.clear() + self.webAccess.nodeManager.update(force=True) + script_refreshResults.ignoreTreeInterceptorPassThrough = True script_refreshResults.passThroughIfNoWebModule = True diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index a6c8621a..f49458c8 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -189,8 +189,8 @@ def getRules(self, layer=None): return tuple(self._layers[layer].values()) return tuple([ rule - for ruleLayers in list(self._rules.values()) - for rule in reversed(list(ruleLayers.values())) + for ruleLayers in self._rules.values() + for rule in reversed(ruleLayers.values()) ]) def getRule(self, name, layer=None): @@ -335,14 +335,17 @@ def terminate(self): pass self.timerCheckAutoAction = None self._nodeManager = None + self.clear() + + def clear(self): + self._ready = False self._results.clear() self._subModuleResults.clear() self._mutatedControlsById.clear() self._mutatedControlsByOffset.clear() - for ruleLayers in list(self._rules.values()): - for rule in list(ruleLayers.values()): - rule.resetResults() - + for rule in self.getRules(): + rule.resetResults() + @logException def update(self, nodeManager=None, force=False): if self.webModule is None: @@ -364,12 +367,7 @@ def update(self, nodeManager=None, force=False): self._ready = True return False t = logTimeStart() - self._results.clear() - self._subModuleResults.clear() - self._mutatedControlsById.clear() - self._mutatedControlsByOffset.clear() - for rule in self.getRules(): - rule.resetResults() + self.clear() for rule in (rule for rule in self.getRules() if rule.properties.subModule): results = rule.getResults() From b46b3d71edc656fa5bafc22de931947a560f14b3 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 1 Sep 2024 20:30:18 +0200 Subject: [PATCH 21/54] GUI: Rules Manager: Widen the "filter" field and make it enlarge with the dialog --- .../webAccess/gui/rule/manager.py | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index cb5a6e9c..262c4c49 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -308,32 +308,39 @@ def __init__(self, parent): # Translator: A label on the RulesManager dialog. label=_("Group by: "), choices=tuple((groupBy.label for groupBy in GROUP_BY)), - majorDimension=len(GROUP_BY) + 3 # +1 for the label ) item.Bind(wx.EVT_RADIOBOX, self.onGroupByRadio) contentsSizer.Add(item, flag=wx.EXPAND) contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) - filtersSizer = wx.GridSizer(1, 2, 10, 10) + filtersSizer = wx.GridBagSizer() + filtersSizer.SetEmptyCellSize((0, 0)) - labeledCtrlHelper = guiHelper.LabeledControlHelper( - self, - # Translator: A label on the RulesManager dialog. - _("&Filter: "), - wx.TextCtrl, size=scale(250, -1), style=wx.TE_PROCESS_ENTER - ) - item = self.filterEdit = labeledCtrlHelper.control + row = 0 + col = 0 + # Translator: A label on the RulesManager dialog. + item = wx.StaticText(self, label=_("&Filter: ")) + filtersSizer.Add(item, (row, col)) + col += 1 + filtersSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), (row, col)) + col += 1 + item = self.filterEdit = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER) item.Bind(wx.EVT_TEXT, lambda evt: self.refreshRuleList()) item.Bind(wx.EVT_TEXT_ENTER, lambda evt: self.tree.SetFocus()) - filtersSizer.Add(labeledCtrlHelper.sizer, flag=wx.EXPAND) + filtersSizer.Add(item, (row, col), flag=wx.EXPAND) + filtersSizer.AddGrowableCol(col) + + col += 1 + filtersSizer.Add(scale(20, 0), (row, col)) - self.activeOnlyCheckBox = wx.CheckBox( + col += 1 + item = self.activeOnlyCheckBox = wx.CheckBox( self, # Translator: A label on the RulesManager dialog. label=_("Include only rules &active on the current page") ) - self.activeOnlyCheckBox.Bind(wx.EVT_CHECKBOX, self.onActiveOnlyCheckBox) - filtersSizer.Add(self.activeOnlyCheckBox) + item.Bind(wx.EVT_CHECKBOX, self.onActiveOnlyCheckBox) + filtersSizer.Add(item, (row, col)) contentsSizer.Add(filtersSizer, flag=wx.EXPAND) contentsSizer.AddSpacer(scale(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS)) @@ -436,7 +443,7 @@ def initData(self, context): self.activeOnlyCheckBox.Value = lastActiveOnly mgr = context["webModule"].ruleManager # disableGroupByPosition returns True if it triggered refresh - not mgr.isReady and self.disableGroupByPosition() or self.refreshRuleList() + not mgr.isReady and self.disableGroupByPosition() or self.onGroupByRadio(None) def getSelectedObject(self): selection = self.tree.Selection @@ -482,15 +489,6 @@ def disableGroupByPosition(self) -> bool: return True return False - def refreshGroupByRadio(self): - radioBox = self.groupByRadio - index = next(i for i, g in enumerate(GROUP_BY) if g.id == lastGroupBy) - if not radioBox.IsItemEnabled(index): - index = next(i for i in range(radioBox.Count) if radioBox.IsItemEnabled(i)) - if radioBox.Selection != index: - radioBox.SetSelection(index) - self.onGroupByRadio(None) - def refreshRuleList(self): context = self.context result = context.pop("initialSelectedResult", None) @@ -601,7 +599,7 @@ def onCharHook(self, evt: wx.KeyEvent): evt.Skip() @guarded - def onGroupByRadio(self, evt, report=False): + def onGroupByRadio(self, evt=None, report=False): global lastGroupBy, lastActiveOnly self.refreshTitle() groupBy = GROUP_BY[self.groupByRadio.GetSelection()] From 0590dc12901c7d3cf6f08bc65df4bfcb6f106e3a Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 1 Sep 2024 22:38:47 +0200 Subject: [PATCH 22/54] GUI: Rules Manager: Group by Position: Fix rendering of nested zones --- .../webAccess/gui/rule/manager.py | 71 +++++++------------ .../webAccess/ruleHandler/__init__.py | 6 +- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index 262c4c49..5e13f58f 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -110,7 +110,7 @@ def rule_getResults_safe(rule): return [] -def getRulesByGesture(ruleManager, filter=None, active=False): +def iterRulesByGesture(ruleManager, filter=None, active=False): gestures = {} noGesture = [] @@ -165,11 +165,11 @@ def getRulesByName(ruleManager, filter=None, active=False): ) for rule in getRules(ruleManager) if ( - (not filter or filter.lower() in rule.name.lower()) + (not filter or filter in rule.name.casefold()) and (not active or rule_getResults_safe(rule)) ) ), - key=lambda tid: tid.label.lower() + key=lambda tid: tid.label.casefold() ) @@ -179,28 +179,15 @@ def getRulesByPosition(ruleManager, filter=None, active=True): As position depends on result, the `active` criteria is ignored. """ - Parent = namedtuple("Parent", ("parent", "tid", "zone")) - - def filterChildlessParent(parent): - if ( - not filter - or parent.tid.children - or filter.lower() in parent.tid.obj.name.lower() - ): - return False - if parent.parent: - parent.parent.tid.children.remove(parent) - return True - webModule = ruleManager.webModule if not webModule.isReadOnly(): layer = webModule.getWritableLayer().name elif config.conf["webAccess"]["devMode"]: layer = None else: - return - - parent = None + return [] + roots: list[TreeItemData] = [] + ancestors: list[TreeItemData] = [] for result in ruleManager.getResults(): rule = result.rule if layer and rule.layer != layer: @@ -210,37 +197,31 @@ def filterChildlessParent(parent): obj=result, children=[] ) - zone = None - if rule.type in (ruleTypes.PARENT, ruleTypes.ZONE): - zone = Zone(result) - elif filter and filter.lower() not in rule.name.lower(): - continue - while parent: - if parent.zone.containsResult(result): - parent.tid.children.append(tid) - if zone: - parent = Parent(parent, tid, zone) + while ancestors: + candidate = ancestors[-1] + if candidate.obj.containsResult(result): + candidate.children.append(tid) break - elif not filterChildlessParent(parent): - yield parent.tid - parent = parent.parent - else: # no parent - assert parent is None - if zone: - parent = Parent(None, tid, zone) - else: - yield tid - while parent: - if not filterChildlessParent(parent): - yield parent.tid - parent = parent.parent + ancestors.pop() + else: + roots.append(tid) + if result.rule.type in (ruleTypes.PARENT, ruleTypes.ZONE): + ancestors.append(tid) + + def passesFilter(tid) -> bool: + for index, child in enumerate(tid.children.copy()): + if not passesFilter(child) : + del tid.children[index] + return tid.children or filter in tid.obj.name.casefold() + + return tuple(tid for tid in roots if passesFilter(tid)) def getRulesByType(ruleManager, filter=None, active=False): types = {} for rule in getRules(ruleManager): if ( - (filter and filter.lower() not in rule.name.lower()) + (filter and filter not in rule.name.casefold()) or (active and not rule_getResults_safe(rule)) ): continue @@ -279,7 +260,7 @@ def getRulesByType(ruleManager, filter=None, active=False): id="gestures", # Translator: Grouping option on the RulesManager dialog. label=pgettext("webAccess.rulesGroupBy", "&Gestures"), - func=getRulesByGesture + func=iterRulesByGesture ), GroupBy( id="name", @@ -499,7 +480,7 @@ def refreshRuleList(self): # Pop the just created or edited rule in order to avoid keeping it selected # when later cycling through groupBy selectObj = context.pop("rule", result.rule if result else None) - filter = self.filterEdit.GetValue() + filter = self.filterEdit.Value.casefold() active = self.activeOnlyCheckBox.Value tree = self.tree root = self.treeRoot diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index f49458c8..91da5983 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -984,6 +984,9 @@ def containsNode(self, node): offset = node.offset return self.startOffset <= offset and self.endOffset >= offset + node.size + def containsResult(self, result): + return self.startOffset <= result.startOffset and self.endOffset >= result.endOffset + def getDisplayString(self): return " ".join( [self.name] @@ -1620,7 +1623,8 @@ def containsOffsets(self, startOffset, endOffset): ) def containsResult(self, result): - return self.containsOffsets(result.startOffset, result.endOffset) + r = self.result + return r and r.containsResult(result) def containsTextInfo(self, info): try: From 2d0a3ff177f6804978b3b160fb499a8340675d68 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Sun, 1 Sep 2024 23:01:20 +0200 Subject: [PATCH 23/54] Nodes: Fix test for boundary check Seems it only affected, in the Criteria Editor, populating of the Context Parent combo. Fix-up of 073eb6a2 --- addon/globalPlugins/webAccess/nodeHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index b953b234..a353270e 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -983,7 +983,7 @@ def __contains__(self, node): Check whether the given node belongs to the subtree of this node, based on their offset. """ - if self <= node: + if self > node: return False if not self.children: return False From c4b5e06749bf187df6af35d8d0ffdb7838d902d4 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 2 Sep 2024 09:17:01 +0200 Subject: [PATCH 24/54] SubModules: Control Mutation - Introduce a link to the root RuleManager, which solely holds control mutation entries - Fix quick nav stuck into SubModule even after canceling zone restriction - Overlay: Helper: Return the WebModule at caret by default - Zones: When canceling zone restriction, restrict to the first encompassing zone - GUI: Rules Manager: Group By Position: List results for all WebModules active on the document - GUI: Rules Manager: Make dialog's title even more explicit about the current listing --- addon/globalPlugins/webAccess/__init__.py | 23 +- .../webAccess/gui/rule/manager.py | 30 +- addon/globalPlugins/webAccess/overlay.py | 170 +++++++---- .../webAccess/ruleHandler/__init__.py | 271 ++++++++++++------ .../webAccess/webAppScheduler.py | 4 +- 5 files changed, 329 insertions(+), 169 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index ab19307c..60983a8b 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -214,7 +214,7 @@ def showWebAccessGui(self): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("You must be in a web browser to use Web Access.")) return - if obj.treeInterceptor is None or not isinstance(obj, overlay.WebAccessObject): + if not isinstance(obj.treeInterceptor, overlay.WebAccessBmdti): # Translators: Error message when attempting to show the Web Access GUI. ui.message(_("You must be on the web page to use Web Access.")) return @@ -223,16 +223,21 @@ def showWebAccessGui(self): "webAccess": self, "focusObject": obj, } - if obj.webAccess.webModule: - webModule = obj.treeInterceptor.webAccess.webModuleAtCaret + # Use the helper of the TreeInterceptor to get the WebModule at caret + # rather than the one where the focus is stuck. + webModule = obj.treeInterceptor.webAccess.webModule + if webModule: context["webModule"] = webModule context["pageTitle"] = webModule.pageTitle mgr = webModule.ruleManager - context["result"] = mgr.getResultAtCaret(focus=obj) + context["result"] = mgr.getResultAtCaret() stack = [] - while webModule: + while True: stack.append(webModule) - webModule = webModule.ruleManager.parentModule + try: + webModule = webModule.ruleManager.parentRuleManager.webModule + except AttributeError: + break if len(stack) > 1: context["webModuleStackAtCaret"] = stack menu.show(context) @@ -347,8 +352,8 @@ def eventExecuter_gen(self, eventName, obj): if func: yield func, (obj, self.next) - # webApp level - if not canHaveWebAccessSupport(obj) and eventName in ["gainFocus"] and activeWebModule is not None: + # WebModule level. + if not canHaveWebAccessSupport(obj) and eventName in ["gainFocus"] and activeWebModule is not None: # log.info("Received event %s on a non-hosted object" % eventName) webAppLoseFocus(obj) else: @@ -361,7 +366,7 @@ def eventExecuter_gen(self, eventName, obj): # log.info("Getting method %s -> %s" %(webApp.name, funcName)) func = getattr(webModule, funcName, None) if func: - yield func,(obj, self.next) + yield func, (obj, self.next) # App module level. app = obj.appModule diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index 5e13f58f..5111df7a 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -177,6 +177,7 @@ def getRulesByPosition(ruleManager, filter=None, active=True): """ Yield rules by position. + Includes results from all active WebModules on the document. As position depends on result, the `active` criteria is ignored. """ webModule = ruleManager.webModule @@ -188,7 +189,7 @@ def getRulesByPosition(ruleManager, filter=None, active=True): return [] roots: list[TreeItemData] = [] ancestors: list[TreeItemData] = [] - for result in ruleManager.getResults(): + for result in ruleManager.rootRuleManager.getAllResults(): rule = result.rule if layer and rule.layer != layer: continue @@ -520,12 +521,26 @@ def addToTree(parent, tids): tree.EnsureVisible(selectTreeItem) def refreshTitle(self): - webModule = self.context["webModule"] - # Translators: The title of the Rules Manager dialog - title = "Web Module {} - Rules by {}".format( - webModule.name, - stripAccel(GROUP_BY[self.groupByRadio.GetSelection()].label).lower(), - ) + context = self.context + webModule = context["webModule"] + groupBy = GROUP_BY[self.groupByRadio.GetSelection()] + if self.filterEdit.Value: + if groupBy.id != "position" and lastActiveOnly: + # Translators: A possible title of the Rules Manager dialog + title = "Web Module {} - Filtered active rules by {}" + else: + # Translators: A possible title of the Rules Manager dialog + title = "Web Module {} - Filtered rules by {}" + else: + if groupBy.id != "position" and lastActiveOnly: + # Translators: A possible title of the Rules Manager dialog + title = "Web Module {} - Active rules by {}" + else: + # Translators: A possible title of the Rules Manager dialog + title = "Web Module {} - Rules by {}" + title = title.format(webModule.name, stripAccel(groupBy.label).lower()) + if groupBy.id == "position" and webModule.ruleManager.rootRuleManager.subModules.all(): + title += " for all active WebModules on this page" if config.conf["webAccess"]["devMode"]: title += " ({})".format("/".join((layer.name for layer in webModule.layers))) self.Title = title @@ -653,6 +668,7 @@ def onRuleEdit(self, evt): context = self.context.copy() context["new"] = False context["rule"] = rule + context["webModule"] = rule.ruleManager.webModule from .editor import show if show(context, parent=self): rule = self.context["rule"] = context["rule"] diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index bdc6171d..a422a0d7 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -34,6 +34,8 @@ import weakref import wx +import NVDAObjects +from NVDAObjects.IAccessible import IAccessible import addonHandler import baseObject import browseMode @@ -41,24 +43,19 @@ import controlTypes import core import cursorManager +from garbageHandler import TrackedObject import gui from logHandler import log from scriptHandler import script -import NVDAObjects -from NVDAObjects.IAccessible import IAccessible import speech import textInfos import treeInterceptorHandler import ui import virtualBuffers -from .utils import guarded +from .utils import guarded, logException -from six import iteritems -from six.moves import xrange - -from garbageHandler import TrackedObject REASON_CARET = controlTypes.OutputReason.CARET @@ -151,22 +148,23 @@ class WebAccessBmdtiHelper(TrackedObject): Utility methods and properties. """ WALK_ALL_TREES = False - + def __init__(self, treeInterceptor): self.caretHitZoneBorder = False self._nodeManager = None self._treeInterceptor = weakref.ref(treeInterceptor) - self._webModule = None - + self._rootWebModule = None + def terminate(self): - if self._webModule is not None: - self._webModule.terminate() - self._webModule = None + if self._rootWebModule is not None: + self._rootWebModule.terminate() + self._rootWebModule = None if self._nodeManager is not None: self._nodeManager.terminate() self._nodeManager = None - + @property + @logException def nodeManager(self): nodeManager = self._nodeManager ti = self.treeInterceptor @@ -178,61 +176,81 @@ def nodeManager(self): from .webAppScheduler import scheduler nodeManager = self._nodeManager = NodeManager(ti, scheduler.onNodeMoveto) return nodeManager - + @property - def ruleManager(self): - if not self.webModule: + @logException + def rootRuleManager(self): + webModule = self.rootWebModule + if not webModule: return None - return self.webModule.ruleManager - - @property - def treeInterceptor(self): - if hasattr(self, "_treeInterceptor"): - ti = self._treeInterceptor - if isinstance(ti, weakref.ReferenceType): - ti = ti() - if ti and ti in treeInterceptorHandler.runningTable: - return ti - else: - return None - + return webModule.ruleManager + @property - def webModule(self): + @logException + def rootWebModule(self): from . import canHaveWebAccessSupport, webAccessEnabled if not webAccessEnabled: return None ti = self.treeInterceptor if not ti: - self._webModule = None + self._rootWebModule = None return None - webModule = self._webModule + webModule = self._rootWebModule if not webModule: obj = ti.rootNVDAObject if not canHaveWebAccessSupport(obj): return None from . import webModuleHandler try: - webModule = self._webModule = webModuleHandler.getWebModuleForTreeInterceptor(ti) + webModule = self._rootWebModule = webModuleHandler.getWebModuleForTreeInterceptor(ti) except Exception: log.exception() return webModule @property - def webModuleAtCaret(self): - rootModule = self.webModule - if not rootModule: + @logException + @logException + def ruleManager(self): + webModule = self.webModule + if not webModule: return None - info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) - return self.ruleManager.subModules.atPosition(info._startOffset) or rootModule + return webModule.ruleManager + + @property + @logException + def treeInterceptor(self): + if hasattr(self, "_treeInterceptor"): + ti = self._treeInterceptor + if isinstance(ti, weakref.ReferenceType): + ti = ti() + if ti and ti in treeInterceptorHandler.runningTable: + return ti + else: + return None + + @property + @logException + def webModule(self): + root = self.rootWebModule + if not root: + return None + try: + info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) + except Exception: + #log.exception(stack_info=True) + return root + return root.ruleManager.subModules.atPosition(info._startOffset) or root @property + @logException def zone(self): ruleManager = self.ruleManager if not ruleManager: return None return ruleManager.zone - + @zone.setter + @logException def zone(self, value): if value is None: # Avoid raising an AttributeError if we only want to ensure @@ -243,6 +261,13 @@ def zone(self, value): return # Properly raise AttributeError if there is no RuleManager. self.ruleManager.zone = value + + @logException + def getWebModuleAtTextInfo(self, info): + rootModule = self.webModule + if not rootModule: + return None + return self.ruleManager.subModules.atPosition(info._startOffset) or rootModule class WebAccessBmdtiTextInfo(textInfos.offsets.OffsetsTextInfo): @@ -332,7 +357,7 @@ def _getControlFieldAttribs(self, docHandle, controlId): break else: raise LookupError - mgr = self.obj.webAccess.ruleManager + mgr = self.obj.webAccess.rootRuleManager if not mgr: return attrs mutated = mgr.getMutatedControl(controlId) @@ -344,7 +369,7 @@ def _getFieldsInRange(self, start, end): fields = super()._getFieldsInRange( start, end ) - mgr = self.obj.webAccess.ruleManager + mgr = self.obj.webAccess.rootRuleManager if not mgr or not mgr.isReady: return fields for field in fields: @@ -470,7 +495,7 @@ def _get_TextInfo(self): return getDynamicClass((WebAccessBmdtiTextInfo, superCls)) def _set_selection(self, info, reason=REASON_CARET): - webModule = self.webAccess.webModule + webModule = self.webAccess.getWebModuleAtTextInfo(info) if webModule and hasattr(webModule, "_set_selection"): webModule._set_selection(self, info, reason=reason) return @@ -504,7 +529,7 @@ def _caretMovementScriptHelper( self.webAccess.caretHitZoneBorder = True elif ( posUnit == textInfos.POSITION_LAST - and zone.isTextInfoAtEnd(info) + and zone.isTextInfoAtOfAfterEnd(info) ) or ( posUnit == textInfos.POSITION_FIRST and zone.isTextInfoAtStart(info) @@ -546,7 +571,7 @@ def _iterNodesByType(self, itemType, direction="next", pos=None): yield next(superIter) except StopIteration: return - mgr = self.webAccess.ruleManager + mgr = self.webAccess.rootRuleManager if not mgr: for item in superIter: yield item @@ -587,7 +612,7 @@ def _iterNodesByType(self, itemType, direction="next", pos=None): elif isinstance(item, browseMode.TextInfoQuickNavItem): try: obj = item.textInfo.NVDAObjectAtStart - controlId = obj.IA2UniqueID + controlId = str(self.getIdentifierFromNVDAObject(obj)[1]) except Exception: log.exception() if controlId is None: @@ -728,7 +753,7 @@ def __iterMutatedControlsByCriteria( See `__mutatedControlMatchesCriteria` for details on criteria format. """ - mgr = self.webAccess.ruleManager + mgr = self.webAccess.rootRuleManager if not mgr: return if not criteria: @@ -788,7 +813,7 @@ def __mutatedControlMatchesCriteria(self, criteria, mutated, info=None): parentAttrs = None # Fetch lazily as seldom needed parentNode = mutated.node.parent for alternative in criteria: - for key, values in iteritems(alternative): + for key, values in alternative.items(): if key.endswith("::not"): negate = True key = key[:-len("::not")] @@ -877,6 +902,7 @@ def doFindText(self, text, reverse=False, caseSensitive=False, willSayAllResume= if not willSayAllResume: speech.speakTextInfo(info, reason=REASON_CARET) elif self.webAccess.zone: + def ask(): if gui.messageBox( "\n".join(( @@ -893,6 +919,7 @@ def ask(): reverse=reverse, caseSensitive=caseSensitive ) + wx.CallAfter(ask) else: wx.CallAfter( @@ -940,7 +967,7 @@ class Break(Exception): return super().getAlternativeScript(gesture, script) def getScript(self, gesture): - webModule = self.webAccess.webModuleAtCaret + webModule = self.webAccess.webModule if webModule: script = webModule.getScript(gesture) if script: @@ -962,7 +989,12 @@ def script_disablePassThrough(self, gesture): and self.webAccess.zone ): self.webAccess.zone = None - ui.message(_("Zone restriction cancelled")) + if self.webAccess.zone is None: + # Translators: Reported when cancelling zone restriction + ui.message(_("Zone restriction cancelled")) + else: + # Translators: Reported when cancelling zone restriction + ui.message(_("Zone restriction enlarged to a wider zone")) else: super().script_disablePassThrough(gesture) @@ -1091,7 +1123,7 @@ def script_quickNavToPreviousResultLevel3(self, gesture): def script_refreshResults(self, gesture): # Translators: Notified when manually refreshing results ui.message(_("Refresh results")) - self.webAccess.ruleManager.clear() + self.webAccess.rootRuleManager.clear() self.webAccess.nodeManager.update(force=True) script_refreshResults.ignoreTreeInterceptorPassThrough = True @@ -1120,10 +1152,12 @@ class WebAccessObjectHelper(TrackedObject): """ Utility methods and properties. """ + def __init__(self, obj): self._obj = weakref.ref(obj) - + @property + @logException def nodeManager(self): ti = self.treeInterceptor if not ti: @@ -1131,17 +1165,28 @@ def nodeManager(self): return ti.webAccess.nodeManager @property + @logException def obj(self): return self._obj() @property + @logException def ruleManager(self): + webModule = self.webModule + if not webModule: + return None + return webModule.ruleManager + + @property + @logException + def rootRuleManager(self): ti = self.treeInterceptor - if not ti: + if not isinstance(ti, WebAccessBmdti): return None - return ti.webAccess.ruleManager - + return ti.webAccess.rootRuleManager + @property + @logException def treeInterceptor(self): obj = self.obj while True: @@ -1156,16 +1201,23 @@ def treeInterceptor(self): obj = ti.rootNVDAObject.parent except Exception: return None - + @property + @logException def webModule(self): ti = self.treeInterceptor if not ti: return None - return ti.webAccess.webModule - + try: + info = ti.makeTextInfo(self.obj) + except Exception: + log.exception(stack_info=True) + return ti.webAccess.webModule + return ti.webAccess.getWebModuleAtTextInfo(info) + + @logException def getMutatedControlAttribute(self, attr, default=None): - mgr = self.ruleManager + mgr = self.rootRuleManager if not mgr: return default obj = self.obj diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 91da5983..44771963 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -128,18 +128,64 @@ def __init__(self, webModule): self.lastAutoMovetoTime = 0 self.defaultScripts = DefaultScripts("Aucun marqueur associé à cette touche") self.timerCheckAutoAction = None - self.zone: "Zone" = None - self.parentModule: "WebModule" = None - self.parentNode: "NodeField" = None + self._zone: Zone = None self.subModules: SubModules = SubModules(self) - self._subModuleResults: Sequence["SingleNodeResult"] = [] + self._allResults: Sequence["Result"] = [] + """Results for this WebModule and all of its SubModules. + """ + self.parentZone: Zone = None + """The zone containing this SubModule in its parent WebModule + """ + def _get_rootRuleManager(self): + parent = self.parentRuleManager + if parent: + return parent.rootRuleManager + return self + + def _get_nodeManager(self): + root = self.rootRuleManager + if root is not self: + return root.nodeManager + return self._nodeManager() if self._nodeManager else None + + def _get_parentNode(self): + parentZone = self.parentZone + if parentZone is not None: + return parentZone.result.Node + return self.nodeManager.mainNode + + def _get_parentRuleManager(self): + try: + return self.parentZone.ruleManager + except AttributeError: + return None + def _get_webModule(self): return self._webModule() - def _get_nodeManager(self): - return self._nodeManager and self._nodeManager() - + @logException + def _get_zone(self): + if self.parentZone is not None: + return self.parentRuleManager.zone + else: + return self._zone + + @logException + def _set_zone(self, value, force=False): + if value is None and not force: + curZone = self.zone + curResult = curZone.result if curZone is not None else None + if curResult is not None: + for candidate in self.iterResultsAtCaret(): + if candidate.zone not in (None, curZone) and candidate.containsResult(curResult): + value = candidate.zone + break + if self.parentZone is not None: + self.parentRuleManager.zone = value + else: + self._zone = value + def dump(self, layer): return {name: rule.dump() for name, rule in list(self._layers[layer].items())} @@ -206,10 +252,19 @@ def getRule(self, name, layer=None): except StopIteration: raise LookupError({"name": name, "layer": layer}) - def getResults(self): + def getResults(self) -> tuple["Result"]: + """Get the results for all the layers of this WebModule, excluding SubModules. + """ if not self.isReady: return [] - return self._results + return tuple(self._results) + + def getAllResults(self) -> tuple["Result"]: + """Get the results for all the layers of this WebModule and all its SubModules. + """ + if not self.isReady: + return [] + return tuple(self._allResults) def getResultsByName(self, name, layer=None): return list(self.iterResultsByName(name, layer=layer)) @@ -232,6 +287,8 @@ def iterResultsByName(self, name, layer=None): yield result def iterMutatedControls(self, direction="next", offset=None): + if self.parentZone is not None: + raise Exception("Supported on the root RuleManager only") for entry in ( self._mutatedControlsByOffset if direction == "next" else reversed(self._mutatedControlsByOffset) @@ -253,6 +310,8 @@ def iterMutatedControls(self, direction="next", offset=None): yield entry def getMutatedControl(self, controlId): + if self.parentZone is not None: + raise Exception("Supported on the root RuleManager only") return self._mutatedControlsById.get(controlId) def removeResults(self, rule): @@ -274,7 +333,7 @@ def getActions(self) -> Mapping[str, str]: def getGlobalScript(self, gesture, caret=None, fromParent=False): if caret is None: - webModuleAtCaret = self.nodeManager.treeInterceptor.webAccess.webModuleAtCaret + webModuleAtCaret = self.nodeManager.treeInterceptor.webAccess.webModule return webModuleAtCaret.ruleManager.getGlobalScript(gesture, caret=webModuleAtCaret) def gen(): @@ -340,7 +399,7 @@ def terminate(self): def clear(self): self._ready = False self._results.clear() - self._subModuleResults.clear() + self._allResults.clear() self._mutatedControlsById.clear() self._mutatedControlsByOffset.clear() for rule in self.getRules(): @@ -366,13 +425,17 @@ def update(self, nodeManager=None, force=False): # already updated self._ready = True return False + self.nodeManagerIdentifier = self.nodeManager.identifier t = logTimeStart() self.clear() + # Do not clear the other mappings in subModules to avoid reloading + # modules that were already loaded on the last update. + self.subModules._results.clear() for rule in (rule for rule in self.getRules() if rule.properties.subModule): results = rule.getResults() self._results.extend(results) - self._subModuleResults.extend(results) + self.subModules._results.extend(results) for rule in sorted( (rule for rule in self.getRules() if not rule.properties.subModule), key=lambda rule: ( @@ -382,7 +445,13 @@ def update(self, nodeManager=None, force=False): ) ): self._results.extend(rule.getResults()) - self._results.sort() + + def resultSortKey(result): + # If two Results start at the same offset, sort first the widest + return result.startOffset#, -result.endOffset + + self._results.sort(key=resultSortKey) + self._allResults.extend(self._results) for result in self._results: if not (hasattr(result, "node") and result.properties.mutation): @@ -395,9 +464,13 @@ def update(self, nodeManager=None, force=False): self._mutatedControlsByOffset.append(entry) else: entry.apply(result) - self._ready = True - self.nodeManagerIdentifier = self.nodeManager.identifier self.subModules.update() + self._allResults.sort(key=resultSortKey) + if self is self.rootRuleManager: + # This should have been populated only on the rootRuleManager + self._mutatedControlsByOffset.sort(key=lambda m: m.start) + self._ready = True + # Zone update check can be performed only once ready if self.zone is not None: if not self.zone.update() or not self.zone.containsTextInfo( self.nodeManager.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) @@ -541,9 +614,9 @@ def getPageTypes(self): def _getIncrementalResult( self, + caret: textInfos.offsets.OffsetsTextInfo, previous=False, relative=True, - caret=None, types=None, name=None, respectZone=False, @@ -560,10 +633,8 @@ def _getIncrementalResult( zone = result.zone if not zone.containsTextInfo(caret): skippedZones.append(zone) - for result in ( - reversed(self.getResults()) - if previous else self.getResults() - ): + results = self.getResults() if respectZone and self.zone else self.rootRuleManager.getAllResults() + for result in (reversed(results) if previous else results): rule = result.rule if types and rule.type not in types: continue @@ -605,16 +676,15 @@ def _getIncrementalResult( return result return None - def getResultAtCaret(self, focus=None): - return next(self.iterResultsAtCaret(focus), None) + def getResultAtCaret(self): + return next(self.iterResultsAtCaret(), None) - def iterResultsAtCaret(self, focus=None): - if focus is None: - focus = api.getFocusObject() + def iterResultsAtCaret(self): try: - info = focus.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) + ti = self.nodeManager.treeInterceptor except AttributeError: return + info = ti.makeTextInfo(textInfos.POSITION_CARET) for result in self.iterResultsAtTextInfo(info): yield result @@ -634,19 +704,8 @@ def iterResultsAtTextInfo(self, info): if not isinstance(info, textInfos.offsets.OffsetsTextInfo): raise ValueError("Not supported {}".format(type(info))) offset = info._startOffset -# for result in self.iterResultsAtOffset(offset): -# yield result -# -# def iterResultsAtOffset(self, offset): -# if not self.isReady: -# return -# if not self.results: -# return for r in reversed(self.getResults()): - if ( - hasattr(r, "node") - and r.node.offset <= offset < r.node.offset + r.node.size - ): + if r.startOffset <= offset <= r.endOffset: yield r def quickNav( @@ -680,8 +739,8 @@ def quickNav( # return the first/last result. for relative in ((True, False) if cycle else (True,)): result = self._getIncrementalResult( - previous=previous, caret=position, + previous=previous, relative=relative, types=types, name=name, @@ -763,47 +822,70 @@ def quickNavToPreviousLevel3(self): class SubModules(AutoPropertyObject): - def __init__(self, parent: RuleManager): - self._parent = weakref.ref(parent) - self.modulesByNameAndIndex: Mapping[tuple(str, int), "WebModule"] = {} - self.modulesByPosition: Mapping[tuple(int, int), "WebModule"] = {} + def __init__(self, ruleManager: RuleManager): + self._ruleManager = weakref.ref(ruleManager) + self._webModulesByNameAndIndex: Mapping[tuple(str, int), "WebModule"] = {} + """Ensure SubModules are not re-instanciated upon update + """ + self._webModulesByPosition: Mapping[tuple(int, int), "WebModule"] = {} + self._results: Sequence["SingleNodeResult"] = [] + """Results for this WebModule that should load a SubModule (ie. `result.properties.subModule` is set) + + Used in `Criteria.iterResults` to not search for nested matches. + """ - def _get_parent(self): - return self._parent and self._parent() + def _get_ruleManager(self): + return self._ruleManager() def all(self) -> Sequence["WebModule"]: - return tuple(self.modulesByNameAndIndex.values()) + return tuple(self._webModulesByPosition.values()) @logException def atPosition(self, offset) -> "WebModule": - for (start, end), module in self.modulesByPosition.items(): - if start <= offset <= end: - return module.ruleManager.subModules.atPosition(offset) or module - + for (start, end), webModule in self._webModulesByPosition.items(): + if start <= offset < end: + subModule = webModule.ruleManager.subModules.atPosition(offset) + return subModule if subModule is not None else webModule + @logException def update(self): from ..webModuleHandler import getWebModule - previous = self.modulesByNameAndIndex.copy() - self.modulesByNameAndIndex.clear() - for result in self.parent.getResults(): - if result.properties.subModule: - key = (result.rule.name, result.index) - webModule = previous.get(key) - if not webModule: - webModule = getWebModule(result.properties.subModule) - if not webModule: - log.error(f"WebModule not found: {result.properties.subModule!r}") - continue - self.modulesByNameAndIndex[key] = webModule - self.modulesByPosition[(result.startOffset, result.endOffset)] = webModule - webModule.ruleManager.parentModule = self.parent.webModule - webModule.ruleManager.parentNode = result.node - webModule.ruleManager.update(self.parent.nodeManager) + webModulesByNameAndIndex = self._webModulesByNameAndIndex + webModulesByPosition = self._webModulesByPosition + previousByNameAndIndex = webModulesByNameAndIndex.copy() + webModulesByNameAndIndex.clear() + webModulesByPosition.clear() + ruleManager = self.ruleManager + rootRuleManager = ruleManager.rootRuleManager + nodeManager = rootRuleManager.nodeManager + mutatedControlsById = rootRuleManager._mutatedControlsById + mutatedControlsByOffset = rootRuleManager._mutatedControlsByOffset + for result in self._results: + key = (result.rule.name, result.index) + webModule = previousByNameAndIndex.get(key) + if not webModule: + webModule = getWebModule(result.properties.subModule) + else: + webModule.ruleManager.parentZone.update(result=result) + if not webModule: + log.error(f"WebModule not found: {result.properties.subModule!r}") + continue + webModulesByNameAndIndex[key] = webModule + webModulesByPosition[(result.startOffset, result.endOffset)] = webModule + subRuleManager = webModule.ruleManager + subRuleManager.parentZone = result.zone + subRuleManager.update(nodeManager) + ruleManager._allResults.extend(subRuleManager.getAllResults()) + mutatedControlsById.update(subRuleManager._mutatedControlsById) + mutatedControlsByOffset.extend(subRuleManager._mutatedControlsByOffset) def terminate(self): - self.modulesByNameAndIndex.clear() - self.modulesByPosition.clear() - self._parent = None + self._ruleManager = None + for webModule in self._webModulesByNameAndIndex.values(): + webModule.terminate() + self._results.clear() + self._webModulesByNameAndIndex.clear() + self._webModulesByPosition.clear() class CustomActionDispatcher(object): @@ -978,7 +1060,10 @@ def __bool__(self): raise NotImplementedError def __lt__(self, other): - raise NotImplementedError + 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): offset = node.offset @@ -1035,21 +1120,20 @@ def script_moveto(self, gesture, fromQuickNav=False, fromSpeak=False): return treeInterceptor.passThrough = self.properties.formMode browseMode.reportPassThrough.last = treeInterceptor.passThrough - if rule.type == ruleTypes.ZONE: - rule.ruleManager.zone = Zone(self) + if self.zone: + rule.ruleManager.zone = self.zone # Ensure the focus does not remain on a control out of the zone treeInterceptor.rootNVDAObject.setFocus() else: for result in reversed(rule.ruleManager.getResults()): - if result.rule.type != ruleTypes.ZONE: + zone = result.zone + if zone is None: continue - zone = Zone(result) if zone.containsResult(self): - if zone != rule.ruleManager.zone: - rule.ruleManager.zone = zone + rule.ruleManager.zone = zone break else: - rule.ruleManager.zone = None + rule.ruleManager._set_zone(rule.ruleManager.parentZone, force=True) offset = self.startOffset info = treeInterceptor.makeTextInfo(textInfos.offsets.Offsets(offset, offset)) treeInterceptor.selection = info @@ -1130,12 +1214,6 @@ def getTextInfo(self): def __bool__(self): return bool(self.node) - def __lt__(self, other): - 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 @@ -1301,8 +1379,6 @@ def iterResults(self): return if not self.checkContextPageType(): return - # Restrict to current SubModule - parentNode = mgr.parentNode or mgr.nodeManager.mainNode # Handle contextParent rootNodes = set() # Set of possible parent nodes excludedNodes = set() # Set of excluded parent nodes @@ -1335,7 +1411,7 @@ def iterResults(self): else: multipleContext = False if results: - nodes = [result.node for result in results if result.node in parentNode] + nodes = (result.node for result in results) if exclude: excludedNodes.update(nodes) else: @@ -1358,7 +1434,7 @@ def iterResults(self): rootNodes = newRootNodes kwargs = getSimpleSearchKwargs(self) excludedNodes.update({ - result.node for result in self.rule.ruleManager._subModuleResults + result.node for result in self.rule.ruleManager.subModules._results }) if excludedNodes: kwargs["exclude"] = excludedNodes @@ -1367,7 +1443,13 @@ def iterResults(self): limit = self.index or 1 # 1-based index = 0 - for root in rootNodes or (mgr.nodeManager.mainNode,): + if not rootNodes: + parentZone = mgr.parentZone + if parentZone is not None: + rootNodes = (parentZone.result.node,) + else: + rootNodes = (mgr.nodeManager.mainNode,) + for root in rootNodes or (parentNode,): rootLimit = limit if multipleContext: index = 0 @@ -1689,13 +1771,18 @@ def restrictTextInfo(self, info): info._startOffset = result.endOffset return res - def update(self): + def update(self, result=None): + if result is not None: + self._result = result + return True try: # Result index is 1-based - self.result = self.ruleManager.getResultsByName( - self.name, layer=self.layer - )[self.index - 1] + self.result = self.getRule().getResults()[self.index - 1] except IndexError: self._result = None return False + except Exception: + log.exception() + self._result = None + return False return True diff --git a/addon/globalPlugins/webAccess/webAppScheduler.py b/addon/globalPlugins/webAccess/webAppScheduler.py index a12e940a..4d56ddd8 100644 --- a/addon/globalPlugins/webAccess/webAppScheduler.py +++ b/addon/globalPlugins/webAccess/webAppScheduler.py @@ -148,10 +148,10 @@ def event_nodeManagerUpdated(self, nodeManager): nodeManager and nodeManager.treeInterceptor and isinstance(nodeManager.treeInterceptor, WebAccessBmdti) - and nodeManager.treeInterceptor.webAccess.ruleManager + and nodeManager.treeInterceptor.webAccess.rootWebModule ): return - nodeManager.treeInterceptor.webAccess.ruleManager.update(nodeManager) + nodeManager.treeInterceptor.webAccess.rootRuleManager.update(nodeManager) def event_ruleManagerUpdated(self, ruleManager): # Doesn't work outside of the main thread for Google Chrome 83 From d315283b667cf3c3eec297376dcec978562cbd50 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 2 Sep 2024 10:10:47 +0200 Subject: [PATCH 25/54] SubModules: RuleManager.iterResultsAtTextInfo: Include all WebModules active on the document This change also impacts: - `RuleManager.getResultAtCaret` - `RuleManager.iterResultsAtCaret` - `RuleManager.iterResultsAtObject` --- .../webAccess/ruleHandler/__init__.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 44771963..0dafec31 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -677,9 +677,13 @@ def _getIncrementalResult( return None def getResultAtCaret(self): + """Includes Results from all WebModules active on the document + """ return next(self.iterResultsAtCaret(), None) def iterResultsAtCaret(self): + """Includes Results from all WebModules active on the document + """ try: ti = self.nodeManager.treeInterceptor except AttributeError: @@ -689,6 +693,8 @@ def iterResultsAtCaret(self): yield result def iterResultsAtObject(self, obj): + """Includes Results from all WebModules active on the document + """ try: info = obj.treeInterceptor.makeTextInfo(obj) except AttributeError: @@ -697,14 +703,18 @@ def iterResultsAtObject(self, obj): yield result def iterResultsAtTextInfo(self, info): - if not self.isReady: + """Includes Results from all WebModules active on the document + """ + root = self.rootRuleManager + if not root.isReady: return - if not self.getResults(): + results = root.getAllResults() + if not results: return if not isinstance(info, textInfos.offsets.OffsetsTextInfo): raise ValueError("Not supported {}".format(type(info))) offset = info._startOffset - for r in reversed(self.getResults()): + for r in reversed(results): if r.startOffset <= offset <= r.endOffset: yield r From 13bbbf08982285d1ea22d4866349237adfc30442 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 2 Sep 2024 15:17:07 +0200 Subject: [PATCH 26/54] SubModules: Helper: Cache the WebModule retrieved by position for an object --- addon/globalPlugins/webAccess/overlay.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index a422a0d7..f8e10139 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -1153,8 +1153,9 @@ class WebAccessObjectHelper(TrackedObject): Utility methods and properties. """ - def __init__(self, obj): - self._obj = weakref.ref(obj) + def __init__(self, obj: NVDAObjects.NVDAObject): + self._obj: weakref.ref[NVDAObjects.NVDAObject] = weakref.ref(obj) + self._webModule: weakref.ref[WebModule] = None @property @logException @@ -1205,6 +1206,8 @@ def treeInterceptor(self): @property @logException def webModule(self): + if self._webModule: + return self._webModule() ti = self.treeInterceptor if not ti: return None @@ -1213,7 +1216,10 @@ def webModule(self): except Exception: log.exception(stack_info=True) return ti.webAccess.webModule - return ti.webAccess.getWebModuleAtTextInfo(info) + webModule = ti.webAccess.getWebModuleAtTextInfo(info) + if webModule: + self._webModule = weakref.ref(webModule) + return webModule @logException def getMutatedControlAttribute(self, attr, default=None): From 5bb2625cfd41b8e26267549a5d16b90e05a8ea9a Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 2 Sep 2024 16:51:49 +0200 Subject: [PATCH 27/54] SubModules: GUI: Rule Editor: Set a more explicit dialog title --- .../webAccess/gui/rule/editor.py | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index 40c19a01..9145dce7 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -31,6 +31,7 @@ from abc import abstractmethod from collections import OrderedDict +import config from dataclasses import dataclass from enum import Enum from functools import partial @@ -835,8 +836,7 @@ def delete(self): class RuleEditorDialog(TreeMultiCategorySettingsDialog): - # Translators: The title of the rule editor - title = _("WebAccess Rule editor") + INITIAL_SIZE = (750, 520) categoryInitList = [ (GeneralPanel, 'getGeneralChildren'), @@ -925,13 +925,46 @@ def getData(self): def initData(self, context: Mapping[str, Any]) -> None: context.setdefault("data", {}) + webModule = context["webModule"] + ruleManager = webModule.ruleManager if context.get("new"): data = context["data"]["rule"] = {"type": ruleTypes.MARKER} + if ruleManager.parentZone is not None: + # Translators: A title of the rule editor + title = (_("Sub Module {} - New Rule").format(webModule.name)) + elif ruleManager.subModules.all(): + # Translators: A title of the rule editor + title = (_("Root Module {} - New Rule").format(webModule.name)) + else: + # Translators: A title of the rule editor + title = (_("Web Module {} - New Rule").format(webModule.name)) else: data = context["data"]["rule"] = context["rule"].dump() - mgr = context["webModule"].ruleManager.nodeManager - if mgr: - node = mgr.getCaretNode() + if ruleManager.parentZone is not None: + # Translators: A title of the rule editor + title = (_("Sub Module {} - Edit Rule {}").format(webModule.name, data.get("name"))) + elif ruleManager.subModules.all(): + # Translators: A title of the rule editor + title = (_("Root Module {} - Edit Rule {}").format(webModule.name, data.get("name"))) + else: + # Translators: A title of the rule editor + title = (_("Web Module {} - Edit Rule {}").format(webModule.name, data.get("name"))) + if config.conf["webAccess"]["devMode"]: + layerName = None + if context.get("new"): + try: + webModule = webModuleHandler.getEditableWebModule(webModule, prompt=False) + if webModule: + layerName = webModule.getWritableLayer().name + except Exception: + log.exception() + else: + layerName = context["rule"].layer + title += f" ({layerName})" + self.SetTitle(title) + nodeManager = ruleManager.nodeManager + if nodeManager: + node = nodeManager.getCaretNode() while node is not None: if node.role in formModeRoles: data.setdefault("properties", {})["formMode"] = True From 0a0fd728bf85b27f6ca9909594942402e5f8b921 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 4 Sep 2024 11:44:44 +0200 Subject: [PATCH 28/54] GUI: Context: Reword "Global" to "General" to avoid confusion with "Global Marker" --- addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py index d43d1552..6e50b932 100644 --- a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py @@ -21,7 +21,6 @@ -__version__ = "2024.08.30" __authors__ = ( "Shirley Noël ", "Julien Cochuyt ", @@ -176,7 +175,7 @@ def getSummary_context(data) -> Sequence[str]: parts.append("{} {}".format(stripAccel(label), value)) if not parts: # Translators: A mention on the Criteria summary report - parts.append(_("Global - Applies to the whole web module")) + parts.append(_("General - Applies to the whole web module")) return parts @@ -430,7 +429,7 @@ def makeSettings(self, settingsSizer): item = self.contextMacroDropDown = DropDownWithHideableChoices(self) item.setChoices(( # Translator: A selection value for the Context field on the Criteria editor - ("global", _("Global - Applies to the whole web module")), + ("general", _("General - Applies to the whole web module")), # Translator: A selection value for the Context field on the Criteria editor ("contextPageTitle", _("Page title - Applies only to pages with the given title")), # Translator: A selection value for the Context field on the Criteria editor @@ -746,11 +745,11 @@ def refreshContextMacroChoices(self, initial=False): if data.get(field) ] if not filled: - dropDown.setSelectedChoiceKey("global") + dropDown.setSelectedChoiceKey("general") elif len(filled) > 1: dropDown.setSelectedChoiceKey("advanced") else: - dropDown.setSelectedChoiceKey(filled[0], default="global") + dropDown.setSelectedChoiceKey(filled[0], default="general") self.onContextMacroChoice(None) def onContextMacroChoice(self, evt): From f864da5bf0a4d266a4338f1b94ce7c0abb54b498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9-Abush=20Clause?= Date: Mon, 9 Sep 2024 16:17:12 +0200 Subject: [PATCH 29/54] Restore Element Description gesture --- addon/globalPlugins/webAccess/__init__.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 60983a8b..67c9180b 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -261,13 +261,6 @@ def script_showWebAccessSettings(self, gesture): # @UnusedVariable # Now part of the public API as of NVDA PR #15121 gui.mainFrame.popupSettingsDialog(WebAccessSettingsDialog) - @script( - # Translators: Input help mode message for a command. - description=_("Toggle debug mode."), - category=SCRIPT_CATEGORY, - gesture="kb:nvda+control+shift+w" - ) - @script( # Translators: Input help mode message for show Web Access menu command. description=_("Show the element description."), From 10328d5d68ba0aaaa896efb3cac5cbd9f9001fe4 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 10 Sep 2024 11:50:26 +0200 Subject: [PATCH 30/54] GUI: Properties: Fix display value of custom autoAction This fix would also cover cases of custom mutation template, even if there are no such thing yet. --- addon/globalPlugins/webAccess/gui/rule/properties.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/properties.py b/addon/globalPlugins/webAccess/gui/rule/properties.py index 4bf47fa0..d0383cca 100644 --- a/addon/globalPlugins/webAccess/gui/rule/properties.py +++ b/addon/globalPlugins/webAccess/gui/rule/properties.py @@ -20,7 +20,12 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Sendhil Randon " +__authors__ = ( + "Sendhil Randon ", + "André-Abush Clause ", + "Gatien Bouyssou ", + "Julien Cochuyt ", +) from collections import ChainMap @@ -171,7 +176,10 @@ def getDisplayValue(self, value): if value in (None, ""): return self.displayValueIfUndefined if self.isRestrictedChoice: - return self.choices[value] + try: + return self.choices[value] + except LookupError: + return f"*{value}" if self.valueType is bool: if value: # Translators: The displayed value of a yes/no rule property From 1d4a25345d6fb6b1241c6bfdea034c49d7b4e991 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 10 Sep 2024 12:10:44 +0200 Subject: [PATCH 31/54] Control Mutation: Fix error handling --- .../webAccess/ruleHandler/controlMutation.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py b/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py index 6a9d546e..b175b7c9 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py +++ b/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py @@ -20,7 +20,10 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Julien Cochuyt " +__authors__ = ( + "Julien Cochuyt ", + "André-Abush Clause ", +) import addonHandler @@ -69,11 +72,13 @@ def end(self): def apply(self, result): rule = result.rule - mutation = MUTATIONS[result.properties.mutation] - if mutation is None: - raise ValueError("No mutation defined for this rule: {}".format( - rule.name - )) + try: + mutation = MUTATIONS[result.properties.mutation] + except LookupError as e: + if not result.properties.mutation: + raise ValueError(f"No mutation defined for this rule: {rule.name}") from e + else: + raise ValueError(f"Unknown mutation in rule {rule.name!r}: {result.properties.mutation!r}") from e self.attrs.update(mutation.attrs) if mutation.mutateName: self.attrs["name"] = rule.label From f85d07b1ef34da7c7a86e519dae012d1b28d34c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9-Abush=20Clause?= Date: Wed, 11 Sep 2024 09:19:05 +0200 Subject: [PATCH 32/54] fix(rule/manager): resolve AttributeError in Dialog.onRuleDelete by using 'rule.ruleManager' --- addon/globalPlugins/webAccess/gui/rule/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index 5111df7a..d9dcd66f 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -651,7 +651,7 @@ def onRuleDelete(self, evt): webModule = getEditableWebModule(self.context["webModule"], layerName=rule.layer) if not webModule: return - self.ruleManager.removeRule(rule) + rule.ruleManager.removeRule(rule) save( webModule=self.context["webModule"], layerName=rule.layer, From 93b890b7a151af4049d79a5218e344ef2bb05da7 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 10 Sep 2024 16:57:22 +0200 Subject: [PATCH 33/54] SubModules: Try fix handling of custom event handlers --- addon/globalPlugins/webAccess/__init__.py | 23 ++++----- addon/globalPlugins/webAccess/nodeHandler.py | 34 +++++++++++++- addon/globalPlugins/webAccess/overlay.py | 47 ++++++++++++++----- .../webAccess/ruleHandler/__init__.py | 32 ++++++++++--- 4 files changed, 103 insertions(+), 33 deletions(-) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 67c9180b..11d658e1 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -346,20 +346,17 @@ def eventExecuter_gen(self, eventName, obj): yield func, (obj, self.next) # WebModule level. - if not canHaveWebAccessSupport(obj) and eventName in ["gainFocus"] and activeWebModule is not None: - # log.info("Received event %s on a non-hosted object" % eventName) - webAppLoseFocus(obj) + webModule = obj.webAccess.webModule if isinstance(obj, overlay.WebAccessObject) else None + if webModule is None: + # Currently dead code, but will likely be revived for issue #17. + if activeWebModule is not None and obj.hasFocus: + #log.info("Disabling active webApp event %s" % eventName) + webAppLoseFocus(obj) else: - webModule = obj.webAccess.webModule if isinstance(obj, overlay.WebAccessObject) else None - if webModule is None: - if activeWebModule is not None and obj.hasFocus: - #log.info("Disabling active webApp event %s" % eventName) - webAppLoseFocus(obj) - else: - # log.info("Getting method %s -> %s" %(webApp.name, funcName)) - func = getattr(webModule, funcName, None) - if func: - yield func, (obj, self.next) + # log.info("Getting method %s -> %s" %(webApp.name, funcName)) + func = getattr(webModule, funcName, None) + if func: + yield func, (obj, self.next) # App module level. app = obj.appModule diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index a353270e..92a3a34e 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -30,6 +30,7 @@ import gc import re +import sys import time import weakref from ast import literal_eval @@ -49,6 +50,12 @@ from .webAppLib import * +if sys.version_info[1] < 9: + from typing import Mapping +else: + from collections.abc import Mapping + + TRACE = lambda *args, **kwargs: None # noqa: E731 # TRACE = log.info # noqa: E731 # TRACE = lambda *args, **kwargs: trace.append((args, kwargs)) # noqa: E731 @@ -275,7 +282,7 @@ def update(self, force=False): # logTime ("Update node manager %d nodes" % len(fields), t) self.updating = False # playWebAccessSound("tick") - self._curNode = self.caretNode = self.getCaretNode() + self._curNode = self.caretNode = self.getCaretNode() # FIXME: Dead code try: info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_LAST) except Exception: @@ -348,7 +355,25 @@ def getCaretNode(self): return self.searchOffset(info._startOffset) except Exception: return None - + + def getControlIdToPosition(self): + if not self.isReady: + return {} + map: Mapping[str, int] = {} + + def walk(node): + id = node.controlIdentifier + if id: + if id in map and map[id] != node.offset: + log.warning(f"ControlId double: {id} at {map[id]} and {node.offset}") + map[id] = node.offset + for child in node.children: + walk(child) + + walk(self.mainNode) + return map + + # FIXME: Dead code def getCurrentNode(self): if not self.isReady: return None @@ -356,18 +381,21 @@ def getCurrentNode(self): self._curNode = self.getCaretNode() return self._curNode + # FIXME: Dead code def setCurrentNode(self, node): if hasattr(node, 'control') is False: self._curNode = node.parent else: self._curNode = node + # FIXME: Dead code from day 1 (probably used with presenters) def event_caret(self, obj, nextHandler): # @UnusedVariable if not self.isReady: return self.display(self._curNode) nextHandler() + # FIXME: Dead code from here to end of class def script_nextItem(self, gesture): if not self.isReady: return @@ -920,6 +948,7 @@ def mouseMove(self): winUser.setCursorPos(x, y) mouseHandler.executeMouseMoveEvent(x, y) + # FIXME: Dead code def getPresentationString(self): """Returns the current node text and role for speech and Braille. @param None @@ -934,6 +963,7 @@ def getPresentationString(self): return "_innerText_ _role_ de niveau %s" % self.control["level"] return "_innerText_ _role_" + # FIXME: Dead code def getBraillePresentationString(self): return False diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index f8e10139..e7bbfcf0 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -1156,6 +1156,14 @@ class WebAccessObjectHelper(TrackedObject): def __init__(self, obj: NVDAObjects.NVDAObject): self._obj: weakref.ref[NVDAObjects.NVDAObject] = weakref.ref(obj) self._webModule: weakref.ref[WebModule] = None + """Cached to try sustain operations during RuleManager update. + """ + + @property + @logException + def controlId(self): + obj = self.obj + return str(obj.treeInterceptor.getIdentifierFromNVDAObject(obj)[1]) @property @logException @@ -1206,18 +1214,36 @@ def treeInterceptor(self): @property @logException def webModule(self): - if self._webModule: - return self._webModule() - ti = self.treeInterceptor - if not ti: + mgr = self.rootRuleManager + if mgr is None: return None try: - info = ti.makeTextInfo(self.obj) + controlId = self.controlId except Exception: - log.exception(stack_info=True) - return ti.webAccess.webModule - webModule = ti.webAccess.getWebModuleAtTextInfo(info) - if webModule: + log.exception() + return None + try: + webModule = mgr.getWebModuleForControlId(controlId) + except LookupError: + log.warning(f"Unknown controlId: {controlId}") + webModule = None + if webModule is None and self._webModule is not None: + # The RumeManager is not ready, return the last cached value + return self._webModule() + if webModule is None: + # Lookup by controlId failed and there is no cached value. + # Attempt lookup by position. + ti = self.treeInterceptor + if not ti: + return None + try: + info = ti.makeTextInfo(self.obj) + except Exception: + log.exception(stack_info=True) + # Failback to the WebModule at caret (not cached) + return ti.webAccess.webModule + webModule = ti.webAccess.getWebModuleAtTextInfo(info) + if webModule is not None: self._webModule = weakref.ref(webModule) return webModule @@ -1226,9 +1252,8 @@ def getMutatedControlAttribute(self, attr, default=None): mgr = self.rootRuleManager if not mgr: return default - obj = self.obj try: - controlId = obj.treeInterceptor.getIdentifierFromNVDAObject(obj)[1] + controlId = self.controlId except Exception: log.exception() return default diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 0dafec31..35647922 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -130,6 +130,9 @@ def __init__(self, webModule): self.timerCheckAutoAction = None self._zone: Zone = None self.subModules: SubModules = SubModules(self) + self._controlIdToPosition: Mapping[str, int] = {} + """Used for safer retrieval of the WebModule associated to an object in focus mode + """ self._allResults: Sequence["Result"] = [] """Results for this WebModule and all of its SubModules. """ @@ -378,7 +381,19 @@ def getScript(self, gesture): # if func is not None: # return func return self.defaultScripts.getScript(gesture) - + + def getWebModuleForControlId(self, controlId): + if self.parentZone is not None: + raise Exception("Supported on the root RuleManager only") + if not self.isReady: + return None + # Raises LookupError on purpose, to distinguish from returning None meaning "Not Ready" + offset = self._controlIdToPosition[controlId] + webModule = self.subModules.atPosition(offset) + if webModule is None: + webModule = self.webModule + return webModule + def _get_isReady(self): if not self._ready or not self.nodeManager or not self.nodeManager.isReady or self.nodeManager.identifier != self.nodeManagerIdentifier: return False @@ -402,6 +417,7 @@ def clear(self): self._allResults.clear() self._mutatedControlsById.clear() self._mutatedControlsByOffset.clear() + self._controlIdToPosition.clear() for rule in self.getRules(): rule.resetResults() @@ -417,15 +433,17 @@ def update(self, nodeManager=None, force=False): except AttributeError: pass self.timerCheckAutoAction = None - if nodeManager is not None: + if nodeManager is None: + nodeManager = self.nodeManager + else: self._nodeManager = weakref.ref(nodeManager) - if self.nodeManager is None or not self.nodeManager.isReady: + if nodeManager is None or not nodeManager.isReady: return False - if not force and self.nodeManagerIdentifier == self.nodeManager.identifier: + if not force and self.nodeManagerIdentifier == nodeManager.identifier: # already updated self._ready = True return False - self.nodeManagerIdentifier = self.nodeManager.identifier + self.nodeManagerIdentifier = nodeManager.identifier t = logTimeStart() self.clear() # Do not clear the other mappings in subModules to avoid reloading @@ -467,13 +485,13 @@ def resultSortKey(result): self.subModules.update() self._allResults.sort(key=resultSortKey) if self is self.rootRuleManager: - # This should have been populated only on the rootRuleManager self._mutatedControlsByOffset.sort(key=lambda m: m.start) + self._controlIdToPosition = nodeManager.getControlIdToPosition() self._ready = True # Zone update check can be performed only once ready if self.zone is not None: if not self.zone.update() or not self.zone.containsTextInfo( - self.nodeManager.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) + nodeManager.treeInterceptor.makeTextInfo(textInfos.POSITION_CARET) ): self.zone = None #logTime("update marker", t) From 55b928cbd876f15b1476e432ee61a39f0a0fa0d7 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 11 Sep 2024 13:22:32 +0200 Subject: [PATCH 34/54] Switch SubModules and Control Mutation indexing from id to (docHandle, id) --- addon/globalPlugins/webAccess/nodeHandler.py | 23 +++++++---- addon/globalPlugins/webAccess/overlay.py | 40 ++++++++----------- .../webAccess/ruleHandler/__init__.py | 2 +- .../webAccess/ruleHandler/controlMutation.py | 2 +- addon/globalPlugins/webAccess/utils.py | 16 +++++++- 5 files changed, 48 insertions(+), 35 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 92a3a34e..93ba17bf 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -47,6 +47,7 @@ import winUser from garbageHandler import TrackedObject +from .utils import tryInt from .webAppLib import * @@ -359,19 +360,22 @@ def getCaretNode(self): def getControlIdToPosition(self): if not self.isReady: return {} - map: Mapping[str, int] = {} + map: Mapping[tuple[int, int], tuple[int, int]] = {} def walk(node): - id = node.controlIdentifier - if id: - if id in map and map[id] != node.offset: - log.warning(f"ControlId double: {id} at {map[id]} and {node.offset}") - map[id] = node.offset + controlId = node.controlIdentifier + span = (node.offset, node.offset + node.size) + if controlId: + if controlId in map: + prev = map[controlId] + if not(prev[0] <= span[0] and span[1] <= prev[1]): + log.warning(f"ControlId double: {controlId} at {prev} and {span}") + map[controlId] = span for child in node.children: walk(child) walk(self.mainNode) - return map + return {key: startOffset for key, (startOffset, endOffset) in map.items()} # FIXME: Dead code def getCurrentNode(self): @@ -492,7 +496,10 @@ def __init__(self, nodeType, attrs, parent, offset, nodeManager): self.name = attrs.get("name", "") self.role = attrs["role"] self.states = attrs["states"] - self.controlIdentifier = attrs.get("controlIdentifier_ID", 0) + self.controlIdentifier = ( + tryInt(attrs.get("controlIdentifier_docHandle", 0)), + tryInt(attrs.get("controlIdentifier_ID", 0)), + ) self.tag = attrs.get("IAccessible2::attribute_tag") if not self.tag: self.tag = attrs.get("IHTMLDOMNode::nodeName") diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index e7bbfcf0..c28dc284 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -53,7 +53,7 @@ import ui import virtualBuffers -from .utils import guarded, logException +from .utils import guarded, logException, tryInt @@ -340,7 +340,7 @@ def updateSelection(self): self.obj.webAccess.zone = None super().updateSelection() - def _getControlFieldAttribs(self, docHandle, controlId): + def _getControlFieldAttribs(self, docHandle, id): info = self.copy() info.expand(textInfos.UNIT_CHARACTER) for field in reversed(info.getTextWithFields()): @@ -351,8 +351,8 @@ def _getControlFieldAttribs(self, docHandle, controlId): continue attrs = field.field if ( - int(attrs["controlIdentifier_docHandle"]) == docHandle - and int(attrs["controlIdentifier_ID"]) == controlId + tryInt(attrs["controlIdentifier_docHandle"]) == docHandle + and tryInt(attrs["controlIdentifier_ID"]) == id ): break else: @@ -360,7 +360,7 @@ def _getControlFieldAttribs(self, docHandle, controlId): mgr = self.obj.webAccess.rootRuleManager if not mgr: return attrs - mutated = mgr.getMutatedControl(controlId) + mutated = mgr.getMutatedControl((docHandle, id)) if mutated: attrs.update(mutated.attrs) return attrs @@ -379,8 +379,9 @@ def _getFieldsInRange(self, start, end): ): continue attrs = field.field - controlId = int(attrs["controlIdentifier_ID"]) - mutated = mgr.getMutatedControl(controlId) + mutated = mgr.getMutatedControl(( + tryInt(attrs["controlIdentifier_docHandle"]), tryInt(attrs["controlIdentifier_ID"]) + )) if mutated: attrs.update(mutated.attrs) return fields @@ -394,16 +395,13 @@ def __init__(self, itemType, document, textInfo, controlId): super().__init__( itemType, document, textInfo ) - self.controlId = controlId # Support for `virtualBuffers.VirtualBufferQuickNavItem.isChild` # so that the Elements List dialog can relate nested headings. - self.vbufFieldIdentifier = (document.rootDocHandle, controlId) + self.vbufFieldIdentifier = controlId @property def obj(self): - return self.document.getNVDAObjectFromIdentifier( - self.document.rootDocHandle, self.controlId - ) + return self.document.getNVDAObjectFromIdentifier(self.vbufFieldIdentifier) def isChild(self, parent): if self.itemType == "heading": @@ -430,10 +428,7 @@ def propertyGetter(prop): # that is, in the Elements List dialog. info = self.textInfo.copy() info.expand(textInfos.UNIT_CHARACTER) - attrs.update(info._getControlFieldAttribs( - self.document.rootDocHandle, - self.controlId - )) + attrs.update(info._getControlFieldAttribs(*self.vbufFieldIdentifier)) return attrs.get(prop) return self._getLabelForProperties(propertyGetter) @@ -608,11 +603,11 @@ def _iterNodesByType(self, itemType, direction="next", pos=None): if not isinstance(item, WebAccessMutatedQuickNavItem): controlId = None if isinstance(item, virtualBuffers.VirtualBufferQuickNavItem): - docHandle, controlId = item.vbufFieldIdentifier + controlId = item.vbufFieldIdentifier elif isinstance(item, browseMode.TextInfoQuickNavItem): try: obj = item.textInfo.NVDAObjectAtStart - controlId = str(self.getIdentifierFromNVDAObject(obj)[1]) + controlId = self.getIdentifierFromNVDAObject(obj) except Exception: log.exception() if controlId is None: @@ -807,8 +802,7 @@ def __mutatedControlMatchesCriteria(self, criteria, mutated, info=None): if info is None: info = mutated.node.getTextInfo() docHandle = self.rootDocHandle - controlId = mutated.controlId - controlAttrs = info._getControlFieldAttribs(docHandle, controlId) + controlAttrs = info._getControlFieldAttribs(*mutated.controlId) controlNode = mutated.node parentAttrs = None # Fetch lazily as seldom needed parentNode = mutated.node.parent @@ -824,9 +818,7 @@ def __mutatedControlMatchesCriteria(self, criteria, mutated, info=None): if parentAttrs is None: parent = mutated.node.parent parentInfo = parent.getTextInfo() - parentAttrs = parentInfo._getControlFieldAttribs( - docHandle, int(parent.controlIdentifier) - ) + parentAttrs = parentInfo._getControlFieldAttribs(*parent.controlIdentifier) attrs = parentAttrs node = parentNode else: @@ -1163,7 +1155,7 @@ def __init__(self, obj: NVDAObjects.NVDAObject): @logException def controlId(self): obj = self.obj - return str(obj.treeInterceptor.getIdentifierFromNVDAObject(obj)[1]) + return obj.treeInterceptor.getIdentifierFromNVDAObject(obj) @property @logException diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 35647922..80b4a3c9 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -474,7 +474,7 @@ def resultSortKey(result): for result in self._results: if not (hasattr(result, "node") and result.properties.mutation): continue - controlId = int(result.node.controlIdentifier) + controlId = result.node.controlIdentifier entry = self._mutatedControlsById.get(controlId) if entry is None: entry = MutatedControl(result) diff --git a/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py b/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py index b175b7c9..04330cb9 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py +++ b/addon/globalPlugins/webAccess/ruleHandler/controlMutation.py @@ -60,7 +60,7 @@ def __init__(self, result): @property def controlId(self): - return int(self.node.controlIdentifier) + return self.node.controlIdentifier @property def start(self): diff --git a/addon/globalPlugins/webAccess/utils.py b/addon/globalPlugins/webAccess/utils.py index 65391f61..17e09e66 100644 --- a/addon/globalPlugins/webAccess/utils.py +++ b/addon/globalPlugins/webAccess/utils.py @@ -20,7 +20,11 @@ # See the file COPYING.txt at the root of this distribution for more details. -__author__ = "Julien Cochuyt " +__authors__ = ( + "Julien Cochuyt ", + "Frédéric Brugnot ", + "André-Abush Clause ", +) from functools import wraps @@ -103,3 +107,13 @@ def wrapper(*args, **kwargs): return wrapper + +def tryInt(value): + """Try to convert the given value to `int` + + If the conversion fails, the value is returned unchanged. + """ + try: + return int(value) + except ValueError: + return value From b68aaa509db3208f96a28f044f5eb1409ad76c28 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 12 Sep 2024 14:22:35 +0200 Subject: [PATCH 35/54] GUI: Rules Manager: When grouping by position, mention which alternative if more than one --- .../webAccess/gui/rule/manager.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index d9dcd66f..9c18218d 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -82,6 +82,22 @@ def getGestureLabel(gesture): return "{main} ({source})".format(source=source, main=main) +def getResultLabel(result): + rule = result.rule + label = rule.name + if len(rule.criteria) > 1: + if result.criteria.name: + label += f" - {result.criteria.name}" + else: + label += f" - #{rule.criteria.index(result.criteria) + 1}" + if rule._gestureMap: + label += " ({gestures})".format(gestures=", ".join( + inputCore.getDisplayTextForGestureIdentifier(identifier)[1] + for identifier in list(rule._gestureMap.keys()) + )) + return label + + def getRuleLabel(rule): label = rule.name if rule._gestureMap: @@ -194,7 +210,7 @@ def getRulesByPosition(ruleManager, filter=None, active=True): if layer and rule.layer != layer: continue tid = TreeItemData( - label=getRuleLabel(rule), + label=getResultLabel(result), obj=result, children=[] ) From aa2b0c9e3ef83c7850ff97171c1723adbd94da85 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 13 Sep 2024 08:30:06 +0200 Subject: [PATCH 36/54] GUI: Rules Manager: Add grouping by context --- .../webAccess/gui/rule/manager.py | 92 ++++++++++++++++--- 1 file changed, 77 insertions(+), 15 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index 9c18218d..2441be21 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -30,6 +30,7 @@ from collections import namedtuple +import sys import wx import addonHandler @@ -52,6 +53,13 @@ from .. import ContextualDialog, showContextualDialog, stripAccel from .editor import getSummary + +if sys.version_info[1] < 9: + from typing import Mapping +else: + from collections.abc import Mapping + + try: from six import iteritems except ImportError: @@ -73,23 +81,14 @@ def show(context, parent): TreeItemData = namedtuple("TreeItemData", ("label", "obj", "children")) -def getGestureLabel(gesture): - source, main = inputCore.getDisplayTextForGestureIdentifier( - inputCore.normalizeGestureIdentifier(gesture) - ) - if gesture.startswith("kb:"): - return main - return "{main} ({source})".format(source=source, main=main) - - -def getResultLabel(result): - rule = result.rule +def getCriteriaLabel(criteria): + rule = criteria.rule label = rule.name if len(rule.criteria) > 1: - if result.criteria.name: - label += f" - {result.criteria.name}" + if criteria.name: + label += f" - {criteria.name}" else: - label += f" - #{rule.criteria.index(result.criteria) + 1}" + label += f" - #{rule.criteria.index(criteria) + 1}" if rule._gestureMap: label += " ({gestures})".format(gestures=", ".join( inputCore.getDisplayTextForGestureIdentifier(identifier)[1] @@ -98,6 +97,15 @@ def getResultLabel(result): return label +def getGestureLabel(gesture): + source, main = inputCore.getDisplayTextForGestureIdentifier( + inputCore.normalizeGestureIdentifier(gesture) + ) + if gesture.startswith("kb:"): + return main + return "{main} ({source})".format(source=source, main=main) + + def getRuleLabel(rule): label = rule.name if rule._gestureMap: @@ -171,6 +179,54 @@ def iterRulesByGesture(ruleManager, filter=None, active=False): ) +def getRulesByContext(ruleManager, filter=None, active=False): + contexts: Mapping[tuple[str, str, str], Rule] = {} + for rule in getRules(ruleManager): + if filter and filter not in rule.name.casefold(): + continue + if active: + results = rule_getResults_safe(rule) + if not results: + continue + alternatives = (result.criteria for result in results) + else: + alternatives = (criteria for criteria in rule.criteria) + for criteria in alternatives: + contexts.setdefault(( + criteria.contextPageTitle or "", # Avoiding None eases later sorting + criteria.contextPageType or "", + criteria.contextParent or "", + ), []).append(TreeItemData( + label=getCriteriaLabel(criteria), + obj=rule, + children=[] + )) + for context, tids in sorted( + contexts.items(), + key=lambda item: (item[0] == ("", "", ""), item[0]) # Move "General" to the end + ): + parts = [] + if context[0]: + # Translators: A part of a context grouping label on the Rules Manager + parts.append(_("Page Title: {contextPageTitle}").format(contextPageTitle=context[0])) + if context[1]: + # Translators: A part of a context grouping label on the Rules Manager + parts.append(_("Page Type: {contextPageTitle}").format(contextPageTitle=context[1])) + if context[2]: + # Translators: A part of a context grouping label on the Rules Manager + parts.append(_("Parent: {contextPageTitle}").format(contextPageTitle=context[2])) + if parts: + label = ", ".join(parts) + else: + # Translators: A context grouping label on the Rules Manager + label = "General" + yield TreeItemData( + label=label, + obj=None, + children=sorted(tids, key=lambda tid: tid.label) + ) + + def getRulesByName(ruleManager, filter=None, active=False): return sorted( ( @@ -210,7 +266,7 @@ def getRulesByPosition(ruleManager, filter=None, active=True): if layer and rule.layer != layer: continue tid = TreeItemData( - label=getResultLabel(result), + label=getCriteriaLabel(result.criteria), obj=result, children=[] ) @@ -273,6 +329,12 @@ def getRulesByType(ruleManager, filter=None, active=False): label=pgettext("webAccess.rulesGroupBy", "&Type"), func=getRulesByType ), + GroupBy( + id="type", + # Translator: Grouping option on the RulesManager dialog. + label=pgettext("webAccess.rulesGroupBy", "&Context"), + func=getRulesByContext + ), GroupBy( id="gestures", # Translator: Grouping option on the RulesManager dialog. From e06659b2cecaa58e5c46cae414399467662922f6 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 17 Sep 2024 11:17:16 +0200 Subject: [PATCH 37/54] RuleManager: Restore `script_notFound` fallback - Bump version to 2024.09.17-dev+subModules --- .../webAccess/ruleHandler/__init__.py | 19 ++++++++++--------- buildVars.py | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index 80b4a3c9..d67c8223 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -371,15 +371,16 @@ def getScript(self, gesture): script = result.getScript(gesture) if script: return script -# for rules in reversed(list(self._layers.values())): -# for rule in list(rules.values()): -# for criterion in rule.criteria: -# func = rule.getScript(gesture) -# if func is not None: -# return func -# func = rule.getScript(gesture) -# if func is not None: -# return func + # Handle script_notFound fallback + for rules in reversed(list(self._layers.values())): + for rule in list(rules.values()): + for criterion in rule.criteria: + func = rule.getScript(gesture) + if func is not None: + return func + func = rule.getScript(gesture) + if func is not None: + return func return self.defaultScripts.getScript(gesture) def getWebModuleForControlId(self, controlId): diff --git a/buildVars.py b/buildVars.py index 1f6381ef..36a71fa1 100644 --- a/buildVars.py +++ b/buildVars.py @@ -26,7 +26,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Web application modules support for modern or complex web sites."""), # version - "addon_version" : "2024.08.31-dev+subModules", + "addon_version" : "2024.09.17-dev+subModules", # Author(s) "addon_author" : ( "Accessolutions (https://accessolutions.fr), " From e790e05439b749cb9d44c9bb01fedf397c115b21 Mon Sep 17 00:00:00 2001 From: BOUYSSOU Gatien Date: Tue, 17 Sep 2024 19:13:46 +0200 Subject: [PATCH 38/54] fix criteria not found by searchNode when text surounded with spaces --- addon/globalPlugins/webAccess/ruleHandler/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index d67c8223..d31bac60 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1285,6 +1285,8 @@ def load(self, data): self.relativePath = data.pop("relativePath", None) self.index = data.pop("index", None) self.gestures = data.pop("gestures", {}) + if self.text: + self.text = self.text.strip() gesturesMap = {} for gestureIdentifier in list(self.gestures.keys()): gesturesMap[gestureIdentifier] = "notFound" From 8ddb6f29e8d1bced62ddc21467efa690bb92cc50 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 18 Sep 2024 11:05:35 +0200 Subject: [PATCH 39/54] NodeManager: Refine detection of control ID doubles --- addon/globalPlugins/webAccess/nodeHandler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 93ba17bf..9f71d82f 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -368,7 +368,11 @@ def walk(node): if controlId: if controlId in map: prev = map[controlId] - if not(prev[0] <= span[0] and span[1] <= prev[1]): + if prev[1] == span[0]: + # Consecutive spans for the same control. Expand the recorded span. + span = (prev[0], span[1]) + elif not(prev[0] <= span[0] and span[1] <= prev[1]): + # Neither consecutive nor nested log.warning(f"ControlId double: {controlId} at {prev} and {span}") map[controlId] = span for child in node.children: From 5f7cf2e2a043482485203be5956879075183fc2f Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 18 Sep 2024 11:17:25 +0200 Subject: [PATCH 40/54] SubModules: Fix loading the proper SubModule on a nested dialog --- addon/globalPlugins/webAccess/overlay.py | 2 +- .../webAccess/webModuleHandler/__init__.py | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index c28dc284..b2c8d12d 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -183,7 +183,7 @@ def rootRuleManager(self): webModule = self.rootWebModule if not webModule: return None - return webModule.ruleManager + return webModule.ruleManager.rootRuleManager @property @logException diff --git a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py index a7a90d60..6429387c 100644 --- a/addon/globalPlugins/webAccess/webModuleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/webModuleHandler/__init__.py @@ -86,7 +86,39 @@ def getWebModule(name: str) -> WebModule: def getWebModuleForTreeInterceptor(treeInterceptor): + from ..overlay import WebAccessObject obj = treeInterceptor.rootNVDAObject + + # Dialogs and Applications are handled by NVDA in a separate TreeInterceptor + # (while, sadly, IFrames are not). + # If the Dialog or Application belongs to a SubModule, we need to pass it here + # as it can't be loaded using the usual triggers (URL or WindowTitle). + # Still, if we simply set the same SubModule on the new TreeInterceptor, its current + # results will be invalidated by the upcoming update and it will finally get terminated + # when the TreeInterceptor gets killed when the modal is discarded. + if ( + isinstance(obj, WebAccessObject) + and obj._get_role(original=True) in (controlTypes.ROLE_DIALOG, controlTypes.ROLE_APPLICATION) + ): + class Break(Exception): + """Block-level break.""" + + try: + parent = obj.parent + if isinstance(parent, WebAccessObject): + webModule = parent.webAccess.webModule + if webModule is None: + raise Break() + # Not testing on purpose if this is a SubModule: Applications/dialogs can be nested in + # one another and, while we could walk up the ancestry to check this, it wouldn't give + # much benefits as in a single WebModule scenario, a new instance would be loaded for + # the new TreeInterceptor anyway. + return store.get(webModule.layers[-1].storeRef) + except Break: + pass + except Exception: + log.exception() + windowTitle = getWindowTitle(obj) if windowTitle: mod = getWebModuleForWindowTitle(windowTitle) From 455b0e6735efbe232cdd710c202fedec4d0ca636 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 13 Sep 2024 09:23:59 +0200 Subject: [PATCH 41/54] GUI: Editors: Remove autoAction from Actions panels - Replace the "Actions" category by "Input Gestures" - Rename the corresponding module and classes - Bump version to addon_version" : "2024.09.18-dev+subModules --- .../webAccess/gui/rule/criteriaEditor.py | 46 ++--------- .../webAccess/gui/rule/editor.py | 44 +++------- .../gui/rule/{actions.py => gestures.py} | 82 ++++--------------- .../webAccess/gui/rule/manager.py | 10 +-- .../webAccess/ruleHandler/__init__.py | 27 +++--- .../webAccess/ruleHandler/properties.py | 2 +- buildVars.py | 2 +- 7 files changed, 50 insertions(+), 163 deletions(-) rename addon/globalPlugins/webAccess/gui/rule/{actions.py => gestures.py} (73%) diff --git a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py index 6e50b932..2a71778d 100644 --- a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py @@ -47,7 +47,7 @@ import ui import addonHandler -from ...ruleHandler import builtinRuleActions, ruleTypes +from ...ruleHandler import ruleTypes from ...utils import guarded, notifyError, updateOrDrop from .. import ( ContextualMultiCategorySettingsDialog, @@ -62,7 +62,7 @@ ) from . import createMissingSubModule from .abc import RuleAwarePanelBase -from .actions import ActionsPanelBase +from .gestures import GesturesPanelBase from .properties import Properties, PropertiesPanelBase, Property @@ -859,44 +859,8 @@ def isValid(self): return True -class ActionsPanel(ActionsPanelBase, CriteriaEditorPanel): - - def makeSettings(self, settingsSizer): - super().makeSettings(settingsSizer) - self.autoActionChoice.Bind(wx.EVT_CHAR_HOOK, self.onAutoActionChoice_charHook) - - def getAutoAction(self): - return self.getData().get("properties", {}).get( - "autoAction", self.getRuleAutoAction() - ) - - def getRuleAutoAction(self): - return self.getRuleData().get("properties", {}).get("autoAction") - - def getAutoActionChoices(self): - choices = super().getAutoActionChoices() - ruleValue = self.getRuleAutoAction() - # Translators: An entry in the Automatic Action list on the Criteria Editor denoting the rule value - choices[ruleValue] = "{action} (default)".format( - action=choices.get(ruleValue, f"*{ruleValue}") - ) - return choices - - @guarded - def onAutoActionChoice_charHook(self, evt): - keycode = evt.GetKeyCode() - mods = evt.GetModifiers() - if keycode == wx.WXK_DELETE and not mods: - self.resetAutoAction() - return - evt.Skip() - - def resetAutoAction(self): - data = self.getData().setdefault("properties", {}) - data["autoAction"] = self.getRuleAutoAction() - self.updateAutoActionChoice(refreshChoices=False) - # Translators: Announced when resetting a property to its default value in the editor - ui.message(_("Reset to {value}").format(value=self.autoActionChoice.StringSelection)) +class GesturesPanel(GesturesPanelBase, CriteriaEditorPanel): + pass class PropertyOverrideSelectMenu(wx.Menu): @@ -1022,7 +986,7 @@ def prop_reset(self): class CriteriaEditorDialog(ContextualMultiCategorySettingsDialog): # Translators: The title of the Criteria Editor dialog. title = _("WebAccess Criteria Set editor") - categoryClasses = [GeneralPanel, CriteriaPanel, ActionsPanel, PropertiesPanel] + categoryClasses = [GeneralPanel, CriteriaPanel, GesturesPanel, PropertiesPanel] INITIAL_SIZE = (900, 580) def getData(self): diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index 9145dce7..748f9442 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -49,7 +49,7 @@ import ui from ... import webModuleHandler -from ...ruleHandler import RuleManager, builtinRuleActions, ruleTypes +from ...ruleHandler import RuleManager, ruleTypes from ...ruleHandler.controlMutation import ( MUTATIONS_BY_RULE_TYPE, mutationLabels @@ -70,7 +70,7 @@ ) from . import createMissingSubModule, criteriaEditor, gestureBinding from .abc import RuleAwarePanelBase -from .actions import ActionsPanelBase +from .gestures import GesturesPanelBase from .properties import ( EditorType, Property, @@ -158,7 +158,7 @@ def getData(self): def onRuleType_change(self): prm = self.categoryParams categoryClasses = tuple(nodeInfo.categoryClass for nodeInfo in self.Parent.Parent.categoryClasses) - for index in (categoryClasses.index(cls) for cls in (GeneralPanel, ActionsPanel, PropertiesPanel)): + for index in (categoryClasses.index(cls) for cls in (GeneralPanel, GesturesPanel, PropertiesPanel)): category = prm.tree.getXChild(prm.tree.GetRootItem(), index) self.refreshParent(category) @@ -545,7 +545,7 @@ def onSave(self): data.pop("gestures", None) -class ActionsPanel(ActionsPanelBase, RuleEditorTreeContextualPanel): +class GesturesPanel(GesturesPanelBase, RuleEditorTreeContextualPanel): def delete(self): wx.Bell() @@ -557,24 +557,6 @@ def onGestureChange(self, change: Change, id: str): def spaceIsPressedOnTreeNode(self, withShift=False): self.gesturesListBox.SetFocus() - - @guarded - def onAutoActionChoice(self, evt): - super().onAutoActionChoice(evt) - # Refresh ChildProperty tree node label - index = tuple( - nodeInfo.categoryClass - for nodeInfo in self.Parent.Parent.categoryClasses - ).index(PropertiesPanel) - prm = self.categoryParams - propsCat = prm.tree.getXChild(prm.tree.GetRootItem(), index) - data = super().getData() - props = Properties(self.context, data) - index = tuple(p.name for p in props).index("autoAction") - prm.tree.SetItemText( - prm.tree.getXChild(propsCat, index), - ChildPropertyPanel.getTreeNodeLabelForProp(props[index]) - ) class PropertiesPanel(PropertiesPanelBase, RuleEditorTreeContextualPanel): @@ -699,7 +681,7 @@ def onCriteriaChange(self, change: Change, index: int): prm.tree.SetFocus() -class ChildActionPanel(RuleEditorTreeContextualPanel): +class ChildGesturePanel(RuleEditorTreeContextualPanel): @dataclass class CategoryParams(TreeContextualPanel.CategoryParams): @@ -841,14 +823,14 @@ class RuleEditorDialog(TreeMultiCategorySettingsDialog): categoryInitList = [ (GeneralPanel, 'getGeneralChildren'), (AlternativesPanel, 'getAlternativeChildren'), - (ActionsPanel, 'getActionsChildren'), + (GesturesPanel, 'getGesturesChildren'), #FIXME PropertiesPanel, 'getPropertiesChildren'), (PropertiesPanel, 'getPropertiesChildren'), ] categoryClasses = [ GeneralPanel, AlternativesPanel, - ActionsPanel, + GesturesPanel, #FIXME PropertiesPanel, PropertiesPanel, ] @@ -894,17 +876,17 @@ def getAlternativeChildren(self): for data in self.getData().get("criteria", []) ) - def getActionsChildren(self): + def getGesturesChildren(self): data = self.getData() if data["type"] not in [ruleTypes.ACTION_TYPES]: return [] mgr = self.context["webModule"].ruleManager - actionsPanel = [] + panels = [] for key, value in data.get('gestures', {}).items(): - title = ChildActionPanel.getTreeNodeLabel(mgr, key, value) - prm = ChildActionPanel.CategoryParams(title=title, gestureIdentifier=key) - actionsPanel.append(TreeNodeInfo(ChildActionPanel, title=title, categoryParams=prm)) - return actionsPanel + title = ChildGesturePanel.getTreeNodeLabel(mgr, key, value) + prm = ChildGesturePanel.CategoryParams(title=title, gestureIdentifier=key) + panels.append(TreeNodeInfo(ChildGesturePanel, title=title, categoryParams=prm)) + return panels def getPropertiesChildren(self) -> Sequence[TreeNodeInfo]: context = self.context diff --git a/addon/globalPlugins/webAccess/gui/rule/actions.py b/addon/globalPlugins/webAccess/gui/rule/gestures.py similarity index 73% rename from addon/globalPlugins/webAccess/gui/rule/actions.py rename to addon/globalPlugins/webAccess/gui/rule/gestures.py index d21c47d5..cc493967 100644 --- a/addon/globalPlugins/webAccess/gui/rule/actions.py +++ b/addon/globalPlugins/webAccess/gui/rule/gestures.py @@ -1,4 +1,4 @@ -# globalPlugins/webAccess/gui/rule/actions.py +# globalPlugins/webAccess/gui/rule/gestures.py # -*- coding: utf-8 -*- # This file is part of Web Access for NVDA. @@ -54,22 +54,22 @@ addonHandler.initTranslation() -class ActionsPanelBase(RuleAwarePanelBase, metaclass=guiHelper.SIPABCMeta): - """ABC for Actions panels +class GesturesPanelBase(RuleAwarePanelBase, metaclass=guiHelper.SIPABCMeta): + """ABC for Gestures panels Sub-classes must implement the methods `getData` (inherited from `ContextualSettingsPanel`) and `getRuleType` (inherited from `RuleTypeAware`). Known sub-classes: - - `criteriaEditor.ActionsPanel` - - `ruleEditor.ActionsPanel` + - `criteriaEditor.GesturesPanel` + - `ruleEditor.GesturesPanel` """ # Translators: The label for a category in the Rule and Criteria editors - title = _("Actions") + title = _("Input Gestures") - # Translators: Displayed when the selected rule type doesn't support any action - descriptionIfNoneSupported = _("No action available for the selected rule type.") + # Translators: Displayed when the selected rule type doesn't support input gestures + descriptionIfNoneSupported = _("The selected Rule Type does not support Input Gestures.") def __init__(self, *args, **kwargs): self.hideable: Mapping[str, Sequence[wx.Window]] = {} @@ -87,14 +87,14 @@ def makeSettings(self, settingsSizer): item = wx.StaticText(self, label=self.descriptionIfNoneSupported) item.Hide() items.append(item) - gbSizer.Add(item, pos=(row, 0), span=(1, 5), flag=wx.EXPAND) + gbSizer.Add(item, pos=(row, 0), span=(1, 3), flag=wx.EXPAND) row += 1 items = self.hideable["IfSupported"] = [] # Translators: The label for a list on the Rule Editor dialog item = wx.StaticText(self, label=_("&Gestures")) items.append(item) - gbSizer.Add(item, pos=(row, col), span=(1, 3), flag=wx.EXPAND) + gbSizer.Add(item, pos=(row, col), flag=wx.EXPAND) row += 1 item = gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_VERTICAL), pos=(row, 0)) @@ -103,9 +103,11 @@ def makeSettings(self, settingsSizer): row += 1 item = self.gesturesListBox = wx.ListBox(self, size=scale(-1, 100)) items.append(item) - gbSizer.Add(item, pos=(row, col), span=(6, 3), flag=wx.EXPAND) + gbSizer.Add(item, pos=(row, col), span=(6, 1), flag=wx.EXPAND) + gbSizer.AddGrowableCol(col) + gbSizer.AddGrowableRow(row + 5) - col += 3 + col += 1 item = gbSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), pos=(row, col)) items.append(item) @@ -137,63 +139,21 @@ def makeSettings(self, settingsSizer): item.Bind(wx.EVT_BUTTON, self.onDeleteGesture) items.append(item) gbSizer.Add(item, pos=(row, col), flag=wx.EXPAND) - - row += 2 - col = 0 - item = gbSizer.Add(scale(0, guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS), pos=(row, col)) - items.append(item) - - row += 1 - # Translators: Automatic action at rule detection input label for the rule dialog's action panel. - item = wx.StaticText(self, label=_("A&utomatic action at rule detection:")) - items.append(item) - gbSizer.Add(item, pos=(row, col)) - - col += 1 - item = gbSizer.Add(scale(guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL, 0), pos=(row, col)) - items.append(item) - - col += 1 - item = self.autoActionChoice = wx.Choice(self, choices=[]) - item.Bind(wx.EVT_CHOICE, self.onAutoActionChoice) - items.append(item) - gbSizer.Add(item, pos=(row, col), span=(1, 3), flag=wx.EXPAND) - - gbSizer.AddGrowableCol(2) def initData(self, context: Mapping[str, Any]) -> None: super().initData(context) data = self.getData() self.gesturesMap = data.setdefault("gestures", {}) self.updateGesturesListBox() - self.updateAutoActionChoice(refreshChoices=True) def updateData(self): # Nothing to update: This panel writes directly into the data map. pass - def getAutoAction(self): - return self.getData().get("properties", {}).get("autoAction") - - def getAutoActionChoices(self) -> Mapping[str, str]: - mgr = self.context["webModule"].ruleManager - # Translators: An entry in the Automatic Action selection list. - choices = {None: pgettext("webAccess.action", "No action")} - choices.update(mgr.getActions()) - action = self.getAutoAction() - if action not in choices: - choices[action] = f"*{action}" - return choices - def getSelectedGesture(self): index = self.gesturesListBox.Selection return self.gesturesListBox.GetClientData(index) if index > -1 else None - @guarded - def onAutoActionChoice(self, evt): - action = evt.EventObject.GetClientData(evt.Selection) - self.getData().setdefault("properties", {})["autoAction"] = action - @guarded def onAddGesture(self, evt): context = self.context.copy() @@ -224,19 +184,6 @@ def onGestureChange(self, change: Change, id: str): index = None self.updateGesturesListBox(selectId=id, focus=True) - def updateAutoActionChoice(self, refreshChoices: bool): - ctrl = self.autoActionChoice - value = self.getAutoAction() - if refreshChoices: - choices = self.getAutoActionChoices() - ctrl.Clear() - for action, displayName in choices.items(): - ctrl.Append(displayName, action) - index = tuple(choices.keys()).index(self.getAutoAction()) - else: - index = tuple(ctrl.GetClientData(i) for i in range(ctrl.Count)).index(value) - ctrl.SetSelection(index) - def updateGesturesListBox(self, selectId: str = None, focus: bool = False): mgr = self.getRuleManager() map = self.gesturesMap @@ -281,7 +228,6 @@ def onSave(self): data = self.getData() if self.getRuleType() not in ruleTypes.ACTION_TYPES: data.pop("gestures", None) - data.get("properties", {}).pop("autoAction", None) elif not data.get("gestures"): data.pop("gestures", None) diff --git a/addon/globalPlugins/webAccess/gui/rule/manager.py b/addon/globalPlugins/webAccess/gui/rule/manager.py index 2441be21..3598686e 100644 --- a/addon/globalPlugins/webAccess/gui/rule/manager.py +++ b/addon/globalPlugins/webAccess/gui/rule/manager.py @@ -41,13 +41,7 @@ import queueHandler import ui -from ...ruleHandler import ( - Rule, - Result, - Zone, - builtinRuleActions, - ruleTypes, -) +from ...ruleHandler import Rule, Result, Zone, ruleTypes from ...utils import guarded from ...webModuleHandler import getEditableWebModule, save from .. import ContextualDialog, showContextualDialog, stripAccel @@ -149,7 +143,7 @@ def iterRulesByGesture(ruleManager, filter=None, active=False): label=( "{rule} - {action}".format( rule=rule.name, - action=builtinRuleActions.get(action, action) + action=rule.ruleManager.getActions().get(action, f"*{action}") ) if action != "moveto" else rule.name diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index d31bac60..b3a46eeb 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -79,17 +79,18 @@ SCRIPT_CATEGORY = "WebAccess" -builtinRuleActions = {} -# Translators: Action name -builtinRuleActions["moveto"] = pgettext("webAccess.action", "Move to") -# Translators: Action name -builtinRuleActions["sayall"] = pgettext("webAccess.action", "Say all") -# Translators: Action name -builtinRuleActions["speak"] = pgettext("webAccess.action", "Speak") -# Translators: Action name -builtinRuleActions["activate"] = pgettext("webAccess.action", "Activate") -# Translators: Action name -builtinRuleActions["mouseMove"] = pgettext("webAccess.action", "Mouse move") +builtinActions = { + # Translators: Action name + "moveto": pgettext("webAccess.action", "Move to"), + # Translators: Action name + "sayall": pgettext("webAccess.action", "Say all"), + # Translators: Action name + "speak": pgettext("webAccess.action", "Speak"), + # Translators: Action name + "activate": pgettext("webAccess.action", "Activate"), + # Translators: Action name + "mouseMove": pgettext("webAccess.action", "Mouse move"), +} class DefaultScripts(ScriptableObject): @@ -323,13 +324,13 @@ def removeResults(self, rule): del self._results[index] def getActions(self) -> Mapping[str, str]: - actions = builtinRuleActions.copy() + actions = builtinActions.copy() prefix = "action_" for key in dir(self.webModule): if key[:len(prefix)] == prefix: actionId = key[len(prefix):] actionLabel = getattr(self.webModule, key).__doc__ or actionId - # Prefix to denote customized action + # Prefix to denote custom action actionLabel = "*" + actionLabel actions.setdefault(actionId, actionLabel) return actions diff --git a/addon/globalPlugins/webAccess/ruleHandler/properties.py b/addon/globalPlugins/webAccess/ruleHandler/properties.py index 061abe81..6d5f1543 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/properties.py +++ b/addon/globalPlugins/webAccess/ruleHandler/properties.py @@ -83,7 +83,7 @@ def getDisplayName(self, ruleType) -> str: class PropertySpec(Enum): autoAction = PropertySpecValue( - ruleTypes=(ruleTypes.GLOBAL_MARKER, ruleTypes.MARKER, ruleTypes.ZONE), + ruleTypes=ruleTypes.ACTION_TYPES, valueType=str, default=None, # Translators: The display name for a rule property diff --git a/buildVars.py b/buildVars.py index 36a71fa1..fda36d1f 100644 --- a/buildVars.py +++ b/buildVars.py @@ -26,7 +26,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Web application modules support for modern or complex web sites."""), # version - "addon_version" : "2024.09.17-dev+subModules", + "addon_version" : "2024.09.18-dev+subModules", # Author(s) "addon_author" : ( "Accessolutions (https://accessolutions.fr), " From e28d9029ca2e8177741f62bc00092548c04baffb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9-Abush=20Clause?= Date: Thu, 19 Sep 2024 15:43:53 +0200 Subject: [PATCH 42/54] Criteria Editor: testCriteria: Drop properties For the test, the "multiple" property is forced to ensure the user is informed about all potential matches. Hence, the rule type "marker" is forced as the "multiple" property is not supported for every rule types. Rather than filtering out properties that are not supported for markers, simply drop them all as they have no impact on result matching anyway. --- .../webAccess/gui/rule/criteriaEditor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py index 2a71778d..34bfd2a5 100644 --- a/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py +++ b/addon/globalPlugins/webAccess/gui/rule/criteriaEditor.py @@ -236,14 +236,18 @@ def getSummary(context, data, indent="", condensed=False) -> str: def testCriteria(context): ruleData = deepcopy(context["data"]["rule"]) ruleData["name"] = "__tmp__" - # Other rule types might not support the "multiple" property we are forcing for the test - ruleData["type"] = ruleTypes.MARKER critData = context["data"]["criteria"].copy() critData.pop("new", None) critData.pop("criteriaIndex", None) ruleData["criteria"] = [critData] - ruleData.setdefault("properties", {})['multiple'] = True - critData.setdefault("properties", {}).pop("multiple", None) + # Ensure the user is informed about all the match occurrences, even if only + # the first is retained by a disabled "multiple" property. + # All rule types do not support this property, hence force the rule type "marker". + # Rather than filtering out properties not supported for this type, simply drop them all + # as they have no impact on the actual search. + ruleData["type"] = ruleTypes.MARKER + ruleData["properties"] = {"multiple": True} + critData["properties"] = {"multiple": True} mgr = context["webModule"].ruleManager from ...ruleHandler import Rule rule = Rule(mgr, ruleData) From e7814201995a701bd47bccd60a899e34d4cdbeba Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 26 Sep 2024 12:12:03 +0200 Subject: [PATCH 43/54] Overlay: Add support for application root elements Follow-up of 735eff8e --- addon/globalPlugins/webAccess/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/addon/globalPlugins/webAccess/__init__.py b/addon/globalPlugins/webAccess/__init__.py index 11d658e1..100e3f4c 100644 --- a/addon/globalPlugins/webAccess/__init__.py +++ b/addon/globalPlugins/webAccess/__init__.py @@ -192,6 +192,7 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): if cls in clsList ): if obj.role in ( + controlTypes.ROLE_APPLICATION, controlTypes.ROLE_DIALOG, controlTypes.ROLE_DOCUMENT, ): From 82c18466a24ffaf10fa6b0dee0ff89e6552a051c Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 15 Oct 2024 10:48:18 +0200 Subject: [PATCH 44/54] TableHandler integration: Move `createResult` from `Criteria` to `Rule` Eases creation of subclassed Results by custom WebModules. --- .../webAccess/ruleHandler/__init__.py | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/addon/globalPlugins/webAccess/ruleHandler/__init__.py b/addon/globalPlugins/webAccess/ruleHandler/__init__.py index b3a46eeb..a4e0abd6 100644 --- a/addon/globalPlugins/webAccess/ruleHandler/__init__.py +++ b/addon/globalPlugins/webAccess/ruleHandler/__init__.py @@ -1038,6 +1038,12 @@ def __init__(self, criteria, context, index): }) self.bindGestures(criteria.gestures) + def __repr__(self): + try: + return f"<{type(self).__name__} of {self.rule.name!r} at {self.startOffset, self.endOffset}>" + except Exception: + return super().__repr__() + def _get_criteria(self): return self._criteria() @@ -1399,13 +1405,11 @@ def checkContextPageType(self): if not (found or exclude): return False return True - - def createResult(self, node, context, index): - return SingleNodeResult(self, node, context, index) - + def iterResults(self): t = logTimeStart() - mgr = self.rule.ruleManager + rule = self.rule + mgr = rule.ruleManager text = self.text if not self.checkContextPageTitle(): return @@ -1429,14 +1433,18 @@ def iterResults(self): name = name.strip() if not name: continue - rule = mgr.getRule(name, layer=self.layer) - if rule is None: - log.error(( - "In rule \"{rule}\".contextParent: " - "Rule not found: \"{parent}\"" - ).format(rule=self.name, parent=name)) + parentRule = mgr.getRule(name, layer=self.layer) + if parentRule is None: + log.error( + 'In rule "{rule}", alternative "{alternative}" .contextParent: ' + 'Rule not found: "{parent}"' + ).format( + rule=rule.name, + alternative=self.name or f"#{rule.criteria.index(self)}", + parent=name, + ) return - results = rule.getResults() + results = parentRule.getResults() if not exclude and any(r.properties.multiple for r in results): if multipleContext is None: multipleContext = True @@ -1466,7 +1474,7 @@ def iterResults(self): rootNodes = newRootNodes kwargs = getSimpleSearchKwargs(self) excludedNodes.update({ - result.node for result in self.rule.ruleManager.subModules._results + result.node for result in mgr.subModules._results }) if excludedNodes: kwargs["exclude"] = excludedNodes @@ -1497,8 +1505,8 @@ def iterResults(self): context = textInfos.offsets.Offsets( startOffset=root.offset, endOffset=root.offset + root.size - ) if root is not self.ruleManager.nodeManager.mainNode else None - yield self.createResult(node, context, index) + ) if root is not mgr.nodeManager.mainNode else None + yield rule.createResult(self, node, context, index) if not self.properties.multiple and not multipleContext: return @@ -1567,6 +1575,9 @@ def load(self, data): + ", ".join(list(data.keys())) ) + def createResult(self, criteria, node, context, index): + return SingleNodeResult(criteria, node, context, index) + def resetResults(self): self._results = None From fe24a807f345d25caaef0888e60a8fbaa846cb7c Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 15 Oct 2024 16:06:34 +0200 Subject: [PATCH 45/54] WebAccessBmdti: Manual refresh is now synchronous --- addon/globalPlugins/webAccess/nodeHandler.py | 28 +++++++++++++++++++- addon/globalPlugins/webAccess/overlay.py | 14 ++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 9f71d82f..8d0ea43e 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -233,23 +233,36 @@ def afficheNode(self, node, level=0): s += self.afficheNode(child, level + 1) return s - def update(self, force=False): + def update(self, force=False, ruleManager=None, debug=False): + """Analyze the VirtualBuffer + + If a RuleManager is specified, it is immediately updated and no + event is sent to the scheduler. + """ # t = logTimeStart() if self.treeInterceptor is None or not self.treeInterceptor.isReady: self._ready = False + if debug: + log.info(f"No TreeInterceptor") return False try: info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_LAST) except Exception: self._ready = False + if debug: + log.exception() return False try: size = info._endOffset + 1 except Exception: self._ready = False + if debug: + log.exception() return False if not force and size == self.treeInterceptorSize: # probably not changed + if debug: + log.info(f"Size unchanged: {size}") return False self.treeInterceptorSize = size if True: @@ -260,6 +273,8 @@ def update(self, force=False): end = info._endOffset if start == end: self._ready = False + if debug: + log.info("The VirtualBuffer is empty") return False text = NVDAHelper.VBuf_getTextInRange( info.obj.VBufHandle, start, end, True) @@ -278,22 +293,33 @@ def update(self, force=False): if self.mainNode is None: self.updating = False self._ready = False + if debug: + # @@@ + log.info("NodeManager.update: ") return False self.identifier = time.time() # logTime ("Update node manager %d nodes" % len(fields), t) self.updating = False # playWebAccessSound("tick") self._curNode = self.caretNode = self.getCaretNode() # FIXME: Dead code + if ruleManager: + # Synchronous update, eg. from WebAccessBmdti.script_refreshResults + self._ready = True + return ruleManager.update(nodeManager=self, force=force) try: info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_LAST) except Exception: self._ready = False + if debug: + log.exception() return False size = info._endOffset + 1 from . import webAppScheduler if size != self.treeInterceptorSize: # treeInterceptor has changed during analyze self._ready = False + if debug: + log.info("VirtualBuffer has changed during analysis") webAppScheduler.scheduler.send( eventName="updateNodeManager", treeInterceptor=self.treeInterceptor diff --git a/addon/globalPlugins/webAccess/overlay.py b/addon/globalPlugins/webAccess/overlay.py index b2c8d12d..77fd0423 100644 --- a/addon/globalPlugins/webAccess/overlay.py +++ b/addon/globalPlugins/webAccess/overlay.py @@ -1115,8 +1115,18 @@ def script_quickNavToPreviousResultLevel3(self, gesture): def script_refreshResults(self, gesture): # Translators: Notified when manually refreshing results ui.message(_("Refresh results")) - self.webAccess.rootRuleManager.clear() - self.webAccess.nodeManager.update(force=True) + try: + res = self.webAccess.nodeManager.update( + force=True, + ruleManager=self.webAccess.rootRuleManager, + ) + except Exception: + log.exception() + res = False + if res: + ui.message(_("Updated")) + else: + ui.message(_("Update failed")) script_refreshResults.ignoreTreeInterceptorPassThrough = True script_refreshResults.passThroughIfNoWebModule = True From a2b0e54ed0f926168c849d473ca1a95e1c7aa1ae Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 15 Oct 2024 16:18:41 +0200 Subject: [PATCH 46/54] NodeManager: Clean-up Follow-up of d6afdfc4a --- addon/globalPlugins/webAccess/nodeHandler.py | 148 +++---------------- 1 file changed, 17 insertions(+), 131 deletions(-) diff --git a/addon/globalPlugins/webAccess/nodeHandler.py b/addon/globalPlugins/webAccess/nodeHandler.py index 8d0ea43e..d782b06c 100644 --- a/addon/globalPlugins/webAccess/nodeHandler.py +++ b/addon/globalPlugins/webAccess/nodeHandler.py @@ -71,7 +71,7 @@ nodeManagerIndex = 0 -class NodeManager(baseObject.ScriptableObject): +class NodeManager(baseObject.AutoPropertyObject): def __init__(self, treeInterceptor, callbackNodeMoveto=None): super().__init__() @@ -132,7 +132,6 @@ def terminate(self): self.devNode = None self.callbackNodeMoveto = None self.updating = False - self._curNode = self.caretNode = None def formatAttributes(self, attrs): s = "" @@ -265,31 +264,24 @@ def update(self, force=False, ruleManager=None, debug=False): log.info(f"Size unchanged: {size}") return False self.treeInterceptorSize = size - if True: - self.updating = True - info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_ALL) - self.info = info - start = info._startOffset - end = info._endOffset - if start == end: - self._ready = False - if debug: - log.info("The VirtualBuffer is empty") - return False - text = NVDAHelper.VBuf_getTextInRange( - info.obj.VBufHandle, start, end, True) - if self.mainNode is not None: - self.mainNode.recursiveDelete() - self.parseXML(text) - # logTime("Update node manager %d, text=%d" % (self.index, len(text)), t) - self.info = None - gc.collect() - else: - self.updating = False + self.updating = True + info = self.treeInterceptor.makeTextInfo(textInfos.POSITION_ALL) + self.info = info + start = info._startOffset + end = info._endOffset + if start == end: self._ready = False - log.info("reading vBuff error") + if debug: + log.info("The VirtualBuffer is empty") return False - # self.info = info + text = NVDAHelper.VBuf_getTextInRange( + info.obj.VBufHandle, start, end, True) + if self.mainNode is not None: + self.mainNode.recursiveDelete() + self.parseXML(text) + # logTime("Update node manager %d, text=%d" % (self.index, len(text)), t) + self.info = None + gc.collect() if self.mainNode is None: self.updating = False self._ready = False @@ -301,7 +293,6 @@ def update(self, force=False, ruleManager=None, debug=False): # logTime ("Update node manager %d nodes" % len(fields), t) self.updating = False # playWebAccessSound("tick") - self._curNode = self.caretNode = self.getCaretNode() # FIXME: Dead code if ruleManager: # Synchronous update, eg. from WebAccessBmdti.script_refreshResults self._ready = True @@ -406,83 +397,6 @@ def walk(node): walk(self.mainNode) return {key: startOffset for key, (startOffset, endOffset) in map.items()} - - # FIXME: Dead code - def getCurrentNode(self): - if not self.isReady: - return None - if self._curNode is None: - self._curNode = self.getCaretNode() - return self._curNode - - # FIXME: Dead code - def setCurrentNode(self, node): - if hasattr(node, 'control') is False: - self._curNode = node.parent - else: - self._curNode = node - - # FIXME: Dead code from day 1 (probably used with presenters) - def event_caret(self, obj, nextHandler): # @UnusedVariable - if not self.isReady: - return - self.display(self._curNode) - nextHandler() - - # FIXME: Dead code from here to end of class - def script_nextItem(self, gesture): - if not self.isReady: - return - if self.treeInterceptor.passThrough is True: - gesture.send() - return - c = self.searchOffset(self._curNode.offset + self._curNode.size + 0) - if c == self._curNode or c is None: - ui.message("Bas du document") - self._curNode.moveto() - return - if c.parent.role not in ( - controlTypes.ROLE_SECTION, controlTypes.ROLE_PARAGRAPH - ): - c = c.parent - # log.info("C set to %s" % c) - self._curNode = c - c.moveto() - - def script_previousItem(self, gesture): - if not self.isReady: - return - if self.treeInterceptor.passThrough is True: - gesture.send() - return - c = self.searchOffset(self._curNode.offset - 1) - # log.info("C is %s" % c) - if c is None: - ui.message("Début du document") - self._curNode.moveto() - return - if c.parent.role not in ( - controlTypes.ROLE_SECTION, controlTypes.ROLE_PARAGRAPH - ): - c = c.parent - # log.info("C set to %s" % c) - self._curNode = c - c.moveto() - - def script_enter(self, gesture): - if not self.isReady: - return - if self.treeInterceptor.passThrough is True: - gesture.send() - return - self._curNode.moveto() - self._curNode.activate() - - __gestures = { - "kb:downarrow": "nextItem", - "kb:uparrow": "previousItem", - "kb:enter": "enter", - } class NodeField(TrackedObject): @@ -985,34 +899,6 @@ def mouseMove(self): winUser.setCursorPos(x, y) mouseHandler.executeMouseMoveEvent(x, y) - # FIXME: Dead code - def getPresentationString(self): - """Returns the current node text and role for speech and Braille. - @param None - @returns a presentation string - @rtype str - """ - if hasattr(self, 'text'): - return self.text - elif self.role is controlTypes.ROLE_EDITABLETEXT: - return "_name_ _role_" - elif self.role is controlTypes.ROLE_HEADING: - return "_innerText_ _role_ de niveau %s" % self.control["level"] - return "_innerText_ _role_" - - # FIXME: Dead code - def getBraillePresentationString(self): - return False - - # TODO: Thoroughly check this wasn't used anywhere - # In Python 3, all classes defining __eq__ must also define __hash__ -# def __eq__(self, node): -# if node is None: -# return False -# if self.offset == node.offset: -# return True -# return False - def __lt__(self, node): """ Compare nodes based on their offset. From c2abec9262034db4abc72ac8645b592abc39660c Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Wed, 6 Nov 2024 16:00:35 +0100 Subject: [PATCH 47/54] Bump version to 2024.10.15-dev+subModules --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index fda36d1f..539c82e4 100644 --- a/buildVars.py +++ b/buildVars.py @@ -26,7 +26,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Web application modules support for modern or complex web sites."""), # version - "addon_version" : "2024.09.18-dev+subModules", + "addon_version" : "2024.10.15-dev+subModules", # Author(s) "addon_author" : ( "Accessolutions (https://accessolutions.fr), " From 73c329c9ec8be21449b7f90b7f71170172bc992a Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Thu, 14 Nov 2024 18:53:45 +0100 Subject: [PATCH 48/54] GUI: Input Gestures: Fix adding multiple gesture bindings in a row --- .../webAccess/gui/rule/gestures.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/gestures.py b/addon/globalPlugins/webAccess/gui/rule/gestures.py index cc493967..04b1e23f 100644 --- a/addon/globalPlugins/webAccess/gui/rule/gestures.py +++ b/addon/globalPlugins/webAccess/gui/rule/gestures.py @@ -156,11 +156,15 @@ def getSelectedGesture(self): @guarded def onAddGesture(self, evt): - context = self.context.copy() - context["data"]["gestures"] = self.gesturesMap + context = self.context + data = context["data"] + data["gestureBinding"] = {} + data["gestures"] = self.gesturesMap if gestureBinding.show(context, self): - id = context["data"]["gestureBinding"]["gestureIdentifier"] + id = data["gestureBinding"]["gestureIdentifier"] self.onGestureChange(Change.CREATION, id) + del data["gestureBinding"] + del data["gestures"] @guarded def onDeleteGesture(self, evt): @@ -171,13 +175,18 @@ def onDeleteGesture(self, evt): @guarded def onEditGesture(self, evt): - context = self.context.copy() - gestures = context["data"]["gestures"] = self.gesturesMap + context = self.context id = self.getSelectedGesture() - context["data"]["gestureBinding"] = {"gestureIdentifier": id, "action": gestures[id]} + data = context["data"] + data["gestureBinding"] = { + "gestureIdentifier": id, "action": self.gesturesMap[id] + } + data["gestures"] = self.gesturesMap if gestureBinding.show(context=context, parent=self): - id = context["data"]["gestureBinding"]["gestureIdentifier"] + id = data["gestureBinding"]["gestureIdentifier"] self.onGestureChange(Change.UPDATE, id) + del data["gestureBinding"] + del data["gestures"] def onGestureChange(self, change: Change, id: str): if change is Change.DELETION: From e7f4fcdfb5192294930b80be5cbb1de4a0161ead Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 3 Dec 2024 11:03:19 +0100 Subject: [PATCH 49/54] GUI: Rule Summary: Display criteria set index as 1-based --- addon/globalPlugins/webAccess/gui/rule/editor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index 748f9442..a2062e55 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -140,10 +140,10 @@ def getSummary(context, data): name = alternative.get("name") if name: # Translators: The label for a section on the Rule Summary report - altHeader = _('Alternative #{index} "{name}":').format(index=index, name=name) + altHeader = _('Alternative #{index} "{name}":').format(index=index + 1, name=name) else: # Translators: The label for a section on the Rule Summary report - altHeader = _("Alternative #{index}:").format(index=index) + altHeader = _("Alternative #{index}:").format(index=index + 1) subParts.append(" " + altHeader) subParts.append(criteriaEditor.getSummary(context, alternative, indent=" ")) parts.extend(subParts) From b303e2039fadfdfec080e4a355918a65022163da Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Fri, 6 Dec 2024 18:01:38 +0100 Subject: [PATCH 50/54] GUI: Editors: Do not attempt to save gesture bindings or properties not supported for the selected rule type --- .../webAccess/gui/rule/editor.py | 24 ++++++++++++++----- .../webAccess/gui/rule/gestures.py | 9 ------- .../webAccess/gui/rule/properties.py | 7 +----- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index a2062e55..6a142b9b 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -538,12 +538,6 @@ def updateCriteriaList(self, index=None): self.editButton.Disable() self.deleteButton.Disable() - def onSave(self): - super().onSave() - data = super().getData() - if not data.get("gestures"): - data.pop("gestures", None) - class GesturesPanel(GesturesPanelBase, RuleEditorTreeContextualPanel): @@ -958,6 +952,24 @@ def _saveAllPanels(self): super()._saveAllPanels() context = self.context data = self.getData() + + # Remove gesture bindings and properties not supported for the selected Rule Type. + # This needs to be done here rather than + # - upon changing the Rule Type for easier non-destructive cycle-through + # the different types, + # - in the panel's doSave because the Rule Type might have been changed + # without even instantiating any other panel. + cnts = [data] + [crit for crit in data.get("criteria", tuple())] + for cnt in cnts: + if data["type"] not in ruleTypes.ACTION_TYPES or not cnt.get("gestures"): + cnt.pop("gestures", None) + updateOrDrop( + cnt, + "properties", + Properties(context, cnt.get("properties", {}), iterOnlyFirstMap=True).dump(), + {} + ) + mgr = context["webModule"].ruleManager if context.get("new"): layerName = None diff --git a/addon/globalPlugins/webAccess/gui/rule/gestures.py b/addon/globalPlugins/webAccess/gui/rule/gestures.py index 04b1e23f..bc76c72b 100644 --- a/addon/globalPlugins/webAccess/gui/rule/gestures.py +++ b/addon/globalPlugins/webAccess/gui/rule/gestures.py @@ -231,12 +231,3 @@ def onPanelActivated(self): item.Show(not supported) self.Thaw() self._sendLayoutUpdatedEvent() - - def onSave(self): - super().onSave() - data = self.getData() - if self.getRuleType() not in ruleTypes.ACTION_TYPES: - data.pop("gestures", None) - elif not data.get("gestures"): - data.pop("gestures", None) - diff --git a/addon/globalPlugins/webAccess/gui/rule/properties.py b/addon/globalPlugins/webAccess/gui/rule/properties.py index d0383cca..e1908369 100644 --- a/addon/globalPlugins/webAccess/gui/rule/properties.py +++ b/addon/globalPlugins/webAccess/gui/rule/properties.py @@ -43,7 +43,7 @@ from ...ruleHandler.controlMutation import MUTATIONS_BY_RULE_TYPE, mutationLabels from ...ruleHandler.properties import PropertiesBase, PropertySpec, PropertySpecValue, PropertyValue -from ...utils import guarded, logException +from ...utils import guarded, logException, updateOrDrop from .. import ContextualSettingsPanel, EditorType, ListCtrlAutoWidth, SingleFieldEditorMixin @@ -304,11 +304,6 @@ def setFieldValue(self, value): # @@@ self.prop.value = value - def onSave(self): - super().onSave() - if not self.getData(): - del super().getData()["properties"] - def prop_reset(self): self.prop.reset() self.updateEditor() From 72de3de7bb94d3bd8c9d9c90f2c69e275aaaff61 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 9 Dec 2024 10:53:33 +0100 Subject: [PATCH 51/54] Bump version to 2024.12.06-dev+subModules --- buildVars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildVars.py b/buildVars.py index 539c82e4..8261549b 100644 --- a/buildVars.py +++ b/buildVars.py @@ -26,7 +26,7 @@ def _(arg): # Translators: Long description to be shown for this add-on on add-on information from add-ons manager "addon_description" : _("""Web application modules support for modern or complex web sites."""), # version - "addon_version" : "2024.10.15-dev+subModules", + "addon_version" : "2024.12.06-dev+subModules", # Author(s) "addon_author" : ( "Accessolutions (https://accessolutions.fr), " From fdb309926ad18f89ae035ab23e34fb85854ce5af Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Mon, 9 Dec 2024 11:30:51 +0100 Subject: [PATCH 52/54] GUI: Rule Editor: Initially select the alternative matched at caret --- addon/globalPlugins/webAccess/gui/rule/editor.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index 6a142b9b..bc48b7e3 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -524,7 +524,18 @@ def updateCriteriaList(self, index=None): data = self.getData() ctrl = self.criteriaList if index is None: - index = max(ctrl.Selection, 0) + index = ctrl.Selection + if index < 0: + # When first displaying the list, attempt to select the + # alternative corresponding to the result at caret, if any. + try: + result = self.context["result"] + if self.getRuleData()["name"] == result.rule.name: + index = self.getData().index(result.criteria.dump()) + except Exception: + pass + if index < 0: + index = 0 ctrl.Clear() for criteria in data: ctrl.Append(self.getCriteriaName(criteria)) From ff8c20e3dfa5fed8636ca741a5aae4d575e39068 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 10 Dec 2024 17:07:51 +0100 Subject: [PATCH 53/54] Update fr translation --- addon/locale/fr/LC_MESSAGES/nvda.po | 1342 +++++++++++++++------------ 1 file changed, 726 insertions(+), 616 deletions(-) diff --git a/addon/locale/fr/LC_MESSAGES/nvda.po b/addon/locale/fr/LC_MESSAGES/nvda.po index 2d93123d..2d1102b5 100644 --- a/addon/locale/fr/LC_MESSAGES/nvda.po +++ b/addon/locale/fr/LC_MESSAGES/nvda.po @@ -20,10 +20,10 @@ msgid "" msgstr "" "Project-Id-Version: \n" -"Report-Msgid-Bugs-To: nvda-translations@groups.io\n" -"POT-Creation-Date: 2024-10-10 09:27+0200\n" -"PO-Revision-Date: 2024-10-10 09:48+0200\n" -"Last-Translator: Julien Cochuyt \n" +"Report-Msgid-Bugs-To: 'nvda-translations@groups.io'\n" +"POT-Creation-Date: 2024-12-10 15:36+0100\n" +"PO-Revision-Date: 2024-12-10 16:00+0100\n" +"Last-Translator: Julien Cochuyt \n" "Language-Team: \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" @@ -31,129 +31,137 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Generated-By: pygettext.py 1.5\n" -"X-Generator: Poedit 3.4.4\n" +"X-Generator: Poedit 3.4.1\n" "X-Poedit-Basepath: ../../..\n" "X-Poedit-KeywordsList: pgettext:1c,2\n" "X-Poedit-Bookmarks: -1,2,-1,-1,-1,-1,-1,-1,-1,-1\n" "X-Poedit-SearchPath-0: .\n" -#: addon\globalPlugins\webAccess\overlay.py:500 +#: addon\globalPlugins\webAccess\overlay.py:534 msgid "Zone border" msgstr "Bordure de zone" #. Translators: Hint on how to cancel zone restriction. -#: addon\globalPlugins\webAccess\overlay.py:504 -#: addon\globalPlugins\webAccess\overlay.py:828 -#: addon\globalPlugins\webAccess\overlay.py:849 -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:713 +#: addon\globalPlugins\webAccess\overlay.py:538 +#: addon\globalPlugins\webAccess\overlay.py:859 +#: addon\globalPlugins\webAccess\overlay.py:880 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:817 msgid "Press escape to cancel zone restriction." msgstr "Appuyez sur échappement pour ne plus être restreint à la zone." #. Translators: Complement to quickNav error message in zone. -#: addon\globalPlugins\webAccess\overlay.py:825 -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:710 +#: addon\globalPlugins\webAccess\overlay.py:856 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:814 msgid "in this zone." msgstr "dans cette zone." -#: addon\globalPlugins\webAccess\overlay.py:840 +#: addon\globalPlugins\webAccess\overlay.py:871 msgid "No more focusable element in this zone." msgstr "Pas d'élément focusable suivant dans cette zone." -#: addon\globalPlugins\webAccess\overlay.py:842 +#: addon\globalPlugins\webAccess\overlay.py:873 msgid "No previous focusable element in this zone." msgstr "Pas d'élément focusable précédant dans cette zone." #. Translators: Hint on how to cancel zone restriction. -#: addon\globalPlugins\webAccess\overlay.py:846 +#: addon\globalPlugins\webAccess\overlay.py:877 msgid "Press escape twice to cancel zone restriction." msgstr "" "Appuyez deux fois sur échappement pour ne plus être restreint à la zone." -#: addon\globalPlugins\webAccess\overlay.py:869 -#: addon\globalPlugins\webAccess\overlay.py:886 +#: addon\globalPlugins\webAccess\overlay.py:901 +#: addon\globalPlugins\webAccess\overlay.py:919 #, python-format msgid "text \"%s\" not found" msgstr "texte \"%s\" non trouvé" -#: addon\globalPlugins\webAccess\overlay.py:871 +#: addon\globalPlugins\webAccess\overlay.py:903 msgid "Cancel zone restriction and retry?" msgstr "Lever la restriction à la zone et réessayer ?" -#: addon\globalPlugins\webAccess\overlay.py:873 -#: addon\globalPlugins\webAccess\overlay.py:887 +#: addon\globalPlugins\webAccess\overlay.py:905 +#: addon\globalPlugins\webAccess\overlay.py:920 msgid "Find Error" msgstr "Chaîne non trouvée" -#: addon\globalPlugins\webAccess\overlay.py:958 +#. Translators: Reported when cancelling zone restriction +#: addon\globalPlugins\webAccess\overlay.py:986 msgid "Zone restriction cancelled" msgstr "Restriction au sein de la zone annulée" +#. Translators: Reported when cancelling zone restriction +#: addon\globalPlugins\webAccess\overlay.py:989 +msgid "Zone restriction enlarged to a wider zone" +msgstr "Restriction élargie à la zone environnante" + #. Translators: The description for the elementsListInZone script -#: addon\globalPlugins\webAccess\overlay.py:991 +#: addon\globalPlugins\webAccess\overlay.py:1022 msgid "Lists various types of elements in the current zone" msgstr "Dresser la liste de différents types d'éléments dans la zone courante" #. Translators: The description for the quickNavToNextResultLevel1 script -#: addon\globalPlugins\webAccess\overlay.py:1003 +#: addon\globalPlugins\webAccess\overlay.py:1034 msgid "Move to next zone." msgstr "Aller à la zone suivante." #. Translators: The description for the quickNavToPreviousResultLevel1 script -#: addon\globalPlugins\webAccess\overlay.py:1015 +#: addon\globalPlugins\webAccess\overlay.py:1046 msgid "Move to previous zone." msgstr "Aller à la zone précédente." #. Translators: The description for the quickNavToNextResultLevel2 script -#: addon\globalPlugins\webAccess\overlay.py:1027 +#: addon\globalPlugins\webAccess\overlay.py:1058 msgid "Move to next global marker." msgstr "Aller au marqueur global suivant." #. Translators: The description for the quickNavToPreviousResultLevel2 script -#: addon\globalPlugins\webAccess\overlay.py:1040 +#: addon\globalPlugins\webAccess\overlay.py:1071 msgid "Move to previous global marker." msgstr "Aller au marqueur global précédent." #. Translators: The description for the quickNavToNextResultLevel3 script -#: addon\globalPlugins\webAccess\overlay.py:1054 +#: addon\globalPlugins\webAccess\overlay.py:1085 msgid "Move to next local marker." msgstr "Aller au marqueur local suivant." #. Translators: The description for the quickNavToPreviousResultLevel3 script -#: addon\globalPlugins\webAccess\overlay.py:1067 +#: addon\globalPlugins\webAccess\overlay.py:1098 msgid "Move to previous local marker." msgstr "Aller au marqueur local précédent." #. Translators: The description for the refreshResults script #. Translators: Notified when manually refreshing results -#: addon\globalPlugins\webAccess\overlay.py:1079 -#: addon\globalPlugins\webAccess\overlay.py:1085 +#: addon\globalPlugins\webAccess\overlay.py:1110 +#: addon\globalPlugins\webAccess\overlay.py:1117 msgid "Refresh results" msgstr "Réactualisé les résultats" +#: addon\globalPlugins\webAccess\overlay.py:1127 +msgid "Updated" +msgstr "Mise à jour terminée" + +#: addon\globalPlugins\webAccess\overlay.py:1129 +msgid "Update failed" +msgstr "Échec de la mise à jour" + #. Translators: A generic error message -#: addon\globalPlugins\webAccess\utils.py:59 +#: addon\globalPlugins\webAccess\utils.py:63 msgid "An error occured. See NVDA log for more details." msgstr "Une erreur s'est produite. Voir le journal NVDA pour plus de détails." #. Translators: Input help mode message for show Web Access menu command. -#: addon\globalPlugins\webAccess\__init__.py:214 +#: addon\globalPlugins\webAccess\__init__.py:205 msgid "Show the Web Access menu." msgstr "Afficher le menu Web Access." #. Translators: Error message when attempting to show the Web Access GUI. -#: addon\globalPlugins\webAccess\__init__.py:225 -#: addon\globalPlugins\webAccess\__init__.py:394 -msgid "The current object does not support Web Access." -msgstr "Cet objet n'est pas pris en charge par Web Access." - -#. Translators: Error message when attempting to show the Web Access GUI. -#: addon\globalPlugins\webAccess\__init__.py:229 +#: addon\globalPlugins\webAccess\__init__.py:216 msgid "You must be in a web browser to use Web Access." msgstr "Placez-vous dans un navigateur web pour utiliser Web Access." #. Translators: Error message when attempting to show the Web Access GUI. -#: addon\globalPlugins\webAccess\__init__.py:233 -#: addon\globalPlugins\webAccess\__init__.py:398 +#: addon\globalPlugins\webAccess\__init__.py:220 +#: addon\globalPlugins\webAccess\__init__.py:279 msgid "You must be on the web page to use Web Access." msgstr "Placez-vous sur la page web pour utiliser Web Access." @@ -162,29 +170,29 @@ msgstr "Placez-vous sur la page web pour utiliser Web Access." msgid "Open the Web Access Settings." msgstr "Ouvrir les préférences Web Access." -#. Translators: Input help mode message for a command. -#: addon\globalPlugins\webAccess\__init__.py:267 -msgid "Toggle debug mode." -msgstr "Basculer le mode de débogage." - #. Translators: Input help mode message for show Web Access menu command. -#: addon\globalPlugins\webAccess\__init__.py:386 +#: addon\globalPlugins\webAccess\__init__.py:267 msgid "Show the element description." msgstr "Afficher la description de l'élément." -#: addon\globalPlugins\webAccess\__init__.py:405 +#. Translators: Error message when attempting to show the Web Access GUI. +#: addon\globalPlugins\webAccess\__init__.py:275 +msgid "The current object does not support Web Access." +msgstr "Cet objet n'est pas pris en charge par Web Access." + +#: addon\globalPlugins\webAccess\__init__.py:286 msgid "Toggle Web Access support." msgstr "Basculer le support de Web Access." -#: addon\globalPlugins\webAccess\__init__.py:414 +#: addon\globalPlugins\webAccess\__init__.py:295 msgid "Web Access support disabled." msgstr "Web Access désactivé." -#: addon\globalPlugins\webAccess\__init__.py:417 +#: addon\globalPlugins\webAccess\__init__.py:298 msgid "Web Access support enabled." msgstr "Web Access activé." -#: addon\globalPlugins\webAccess\__init__.py:549 +#: addon\globalPlugins\webAccess\__init__.py:427 msgid "" "This web module has been created by a newer version of Web Access for NVDA " "and could not be loaded:" @@ -192,7 +200,7 @@ msgstr "" "Ce module web ne peut pas être utilisé car il a été créé à l'aide d'une " "version plus récente de Web Access pour NVDA :" -#: addon\globalPlugins\webAccess\__init__.py:553 +#: addon\globalPlugins\webAccess\__init__.py:431 msgid "" "These web modules have been created by a newer version of Web Access for " "NVDA and could not be loaded:" @@ -200,7 +208,7 @@ msgstr "" "Ces modules web ne peuvent pas être utilisé car ils ont été créé à l'aide " "d'une version plus récente de Web Access pour NVDA :" -#: addon\globalPlugins\webAccess\__init__.py:560 +#: addon\globalPlugins\webAccess\__init__.py:438 msgid "" "This Python web module uses a different API version of Web Access for NVDA " "and could not be loaded:" @@ -208,7 +216,7 @@ msgstr "" "Ce module web Python ne peut pas être utilisé car il cible une version " "différente de l'API de Web Access pour NVDA :" -#: addon\globalPlugins\webAccess\__init__.py:564 +#: addon\globalPlugins\webAccess\__init__.py:442 msgid "" "These Python web modules use a different API version of Web Access for NVDA " "and could not be loaded:" @@ -216,1162 +224,1237 @@ msgstr "" "Ces modules web Python ne peuvent pas être utilisé car ils ciblent une " "version différente de l'API de Web Access pour NVDA :" -#: addon\globalPlugins\webAccess\__init__.py:571 +#: addon\globalPlugins\webAccess\__init__.py:449 msgid "An unexpected error occurred while attempting to load this web module:" msgstr "Une erreur s'est produite au chargement de ce module web :" -#: addon\globalPlugins\webAccess\__init__.py:575 +#: addon\globalPlugins\webAccess\__init__.py:453 msgid "" "An unexpected error occurred while attempting to load these web modules:" msgstr "Une erreur s'est produite au chargement de ces modules web :" #. Translators: The title of an error message dialog -#: addon\globalPlugins\webAccess\__init__.py:610 -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:248 +#: addon\globalPlugins\webAccess\__init__.py:488 msgid "Web Access for NVDA" msgstr "Web Access pour NVDA" -#. Translators: The label for a category in the Rule and Criteria editors -#: addon\globalPlugins\webAccess\gui\actions.py:59 -msgid "Actions" -msgstr "Actions" +#: addon\globalPlugins\webAccess\gui\elementDescription.py:54 +msgctxt "webAccess.elementDescription" +msgid "elements" +msgstr "éléments" -#. Translators: Displayed when the selected rule type doesn't support any action -#: addon\globalPlugins\webAccess\gui\actions.py:62 -msgid "No action available for the selected rule type." -msgstr "Aucune action disponible pour le type de règle sélectionné." +#: addon\globalPlugins\webAccess\gui\elementDescription.py:56 +msgctxt "webAccess.elementDescription" +msgid "element" +msgstr "élément" -#. Translators: The label for a list on the Rule Editor dialog -#: addon\globalPlugins\webAccess\gui\actions.py:85 -msgid "&Gestures" -msgstr "&Gestes" +#: addon\globalPlugins\webAccess\gui\elementDescription.py:69 +msgctxt "webAccess.elementDescription" +msgid "from:" +msgstr "de :" -#. Translators: The label for a button in the Rule Editor dialog -#. Translators: The label for a button on the Rule Editor dialog -#. Translators: New criteria button label -#. Translators: The label for a button on the Rule Editor dialog -#: addon\globalPlugins\webAccess\gui\actions.py:104 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:417 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:661 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:754 -msgid "&New..." -msgstr "&Nouvelle..." +#: addon\globalPlugins\webAccess\gui\elementDescription.py:73 +msgctxt "webAccess.elementDescription" +msgid "to:" +msgstr "à :" -#. Translators: The label for a button in the Rule Editor dialog -#. Translators: The label for a button in the Web Modules Manager dialog -#. Translators: The label for a button on the Rule Editor dialog -#. Translators: Edit criteria button label -#. Translator: The label for a button on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\actions.py:115 -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:147 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:426 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:645 -#: addon\globalPlugins\webAccess\gui\rule\manager.py:399 -msgid "&Edit..." -msgstr "&Modifier..." +#: addon\globalPlugins\webAccess\gui\elementDescription.py:113 +msgid "No NodeManager" +msgstr "Pas de NodeManager" -#. Translators: The label for a button in the Rule Editor dialog -#. Translators: The label for a button on the Criteria Editor dialog -#. Translators: The label for a button in the -#. Web Modules Manager dialog -#. Translators: The label for a button on the Rule Editor dialog -#. Translators: Delete criteria button label -#. Translators: The label for a button on the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\elementDescription.py:167 +msgid "Element description" +msgstr "Description de l'élément" + +#. Translators: Web Access menu item label. #. Translator: The label for a button on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\actions.py:126 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:910 -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:160 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:435 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:654 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:747 -#: addon\globalPlugins\webAccess\gui\rule\manager.py:407 -msgid "&Delete" -msgstr "&Supprimer" +#: addon\globalPlugins\webAccess\gui\menu.py:64 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:456 +msgid "&New rule..." +msgstr "&Nouvelle règle..." -#. Translators: Automatic action at rule detection input label for the rule dialog's action panel. -#: addon\globalPlugins\webAccess\gui\actions.py:138 -msgid "A&utomatic action at rule detection:" -msgstr "Action &automatique à la détection d'une règle :" +#. Translators: Web Access menu item label. +#. Translators: The label for a button in the Web Modules Manager dialog +#: addon\globalPlugins\webAccess\gui\menu.py:70 +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:113 +msgid "Manage &rules..." +msgstr "Gérer les &règles..." -#. Translators: An entry in the Automatic Action selection list. -#. Translators: Displayed if no value is set for the "Auto Actions" property -#: addon\globalPlugins\webAccess\gui\actions.py:171 -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:77 -msgctxt "webAccess.action" -msgid "No action" -msgstr "Ne rien faire" +#. Translators: Web Access menu item label. +#. Translators: The label for a button in the Web Modules Manager +#: addon\globalPlugins\webAccess\gui\menu.py:79 +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:93 +msgid "&New web module..." +msgstr "&Nouveau module web..." + +#. Translators: Web Access menu item label. +#: addon\globalPlugins\webAccess\gui\menu.py:93 +msgid "Edit &web module" +msgstr "Modifier le module &web" + +#. Translators: Web Access menu item label. +#: addon\globalPlugins\webAccess\gui\menu.py:99 +#, python-format +msgid "Edit &web module %s..." +msgstr "Modifier le module &web %s..." + +#. Translators: Web Access menu item label. +#: addon\globalPlugins\webAccess\gui\menu.py:106 +msgid "Manage web &modules..." +msgstr "Gérer les &modules web..." + +#. Translators: Web Access menu item label. +#: addon\globalPlugins\webAccess\gui\menu.py:116 +msgid "&Element description..." +msgstr "D&escription de l'élément..." + +#. Translators: Web Access menu item label. +#: addon\globalPlugins\webAccess\gui\menu.py:125 +msgid "Temporarily &disable all web modules" +msgstr "&Désactiver temporairement tous les modules web" + +#. Translators: The title of a dialog +#: addon\globalPlugins\webAccess\gui\settings.py:53 +msgid "WebAccess Preferences" +msgstr "Paramètres de Web Access" + +#. Translators: The label for a category in the settings dialog +#: addon\globalPlugins\webAccess\gui\settings.py:91 +msgid "WebAccess" +msgstr "WebAccess" + +#. Translators: The label for a settings in the WebAccess settings panel +#: addon\globalPlugins\webAccess\gui\settings.py:97 +msgid "&Developer mode" +msgstr "Mode développeur" + +#. Translators: The label for a settings in the WebAccess settings panel +#: addon\globalPlugins\webAccess\gui\settings.py:102 +msgid "Disable all &user WebModules (activate only scratchpad and addons)" +msgstr "" +"Désactiver tous les WebModules utilisateur (ne charger que répertoire Bloc-" +"notes du Développeur et extensions)" + +#. Translators: The label for a settings in the WebAccess settings panel +#: addon\globalPlugins\webAccess\gui\settings.py:107 +msgid "Write into add-ons' \"webModules\" folder (not recommended)" +msgstr "" +"Écrire directement dans le dossier \"webModules\" des extensions (non " +"recommandé)" + +#. Translators: Announced when a list is empty +#: addon\globalPlugins\webAccess\gui\__init__.py:174 +msgid "Empty" +msgstr "Vide" + +#. Translator: The placeholder for an invalid value in summary reports +#: addon\globalPlugins\webAccess\gui\__init__.py:217 +msgid "" +msgstr "" + +#. Translators: The label for the list of categories in a multi category settings dialog. +#: addon\globalPlugins\webAccess\gui\__init__.py:540 +msgid "&Categories:" +msgstr "&Catégories :" + +#. Translators: The display value of a yes/no field +#. Translators: The displayed value of a yes/no rule property +#: addon\globalPlugins\webAccess\gui\__init__.py:948 +#: addon\globalPlugins\webAccess\gui\rule\properties.py:186 +msgid "Yes" +msgstr "Oui" + +#. Translators: The displayed value of a yes/no field +#. Translators: The displayed value of a yes/no rule property +#: addon\globalPlugins\webAccess\gui\__init__.py:951 +#: addon\globalPlugins\webAccess\gui\rule\properties.py:189 +msgid "No" +msgstr "Non" + +#. Translators: A field label. French typically adds a space before the colon. +#: addon\globalPlugins\webAccess\gui\__init__.py:1076 +#, python-brace-format +msgid "{field}:" +msgstr "{field} :" + +#. Translators: The label for a node in the category tree on a multi-category dialog +#. Translators: A mention on the Rule Summary report +#: addon\globalPlugins\webAccess\gui\__init__.py:1113 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:121 +#, python-brace-format +msgid "{field}: {value}" +msgstr "{field} : {value}" #. Translators: A mention on the Criteria summary report #. Translator: A selection value for the Context field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:165 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:414 -msgid "Global - Applies to the whole web module" -msgstr "Global - S'applique à l'ensemble du module web" +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:178 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:436 +msgid "General - Applies to the whole web module" +msgstr "Général - S'applique à l'ensemble du module web" #. Translators: A mention on the Criteria Summary report -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:202 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:215 #, python-brace-format msgid "{indent}{field}: {value}" msgstr "{indent}{field} : {value}" #. Translators: The label for a section on the Criteria Summary report #. Translators: The label for a section on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:210 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:118 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:223 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:125 #, python-brace-format msgid "{section}:" msgstr "{section} :" #. Translators: A mention on the Criteria Summary report -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:219 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:232 msgid "No criteria" msgstr "Pas de critère" -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:242 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:259 msgid "Found 1 result in {:.3f} seconds." msgstr "Trouvé 1 résultat en {:.3f} secondes." -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:244 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:261 msgid "Found {} results in {:.3f} seconds." msgstr "Trouvé {} résultats en {:.3f} secondes." -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:246 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:263 msgid "No result found on the current page." msgstr "Aucun résultat trouvé sur la page actuelle." -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:247 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:264 msgid "Criteria test" msgstr "Critères d'évaluation" #. Translators: The label for a Criteria editor category. #. Translators: The label for the General settings panel. -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:258 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:165 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:277 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:172 msgid "General" msgstr "Général" #. Translator: The label for a field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:272 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:291 msgid "Criteria Set &name:" msgstr "Nom du jeu de critères :" #. Translator: The label for a field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:284 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:303 msgid "&Sequence order:" msgstr "Ordre de &séquence :" #. Translator: The label for a field on the Criteria editor #. Translators: The label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:298 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:203 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:392 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:317 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:210 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:386 msgid "Summar&y:" msgstr "&Résumé :" #. Translator: The label for a field on the Criteria editor #. Translator: The label for a field on the Rule editor #. Translators: The label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:310 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:215 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:404 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:329 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:222 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:398 msgid "Technical n&otes:" msgstr "N&otes techniques :" #. Translators: The label for a Criteria editor category. #. Translators: The label for a category in the rule editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:367 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:360 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:387 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:354 msgid "Criteria" msgstr "Critères" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:373 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:393 msgctxt "webAccess.ruleCriteria" msgid "Page &title:" msgstr "&Titre de page :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:375 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:395 msgctxt "webAccess.ruleCriteria" msgid "Page t&ype" msgstr "T&ype de page" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:377 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:397 msgctxt "webAccess.ruleCriteria" msgid "&Parent element" msgstr "Élément &parent" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:379 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:399 msgctxt "webAccess.ruleCriteria" msgid "&Text:" msgstr "&Texte :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:381 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:401 msgctxt "webAccess.ruleCriteria" msgid "&Role:" msgstr "&Rôle :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:383 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:403 msgctxt "webAccess.ruleCriteria" msgid "T&ag:" msgstr "B&alise :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:385 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:405 msgctxt "webAccess.ruleCriteria" msgid "&ID:" msgstr "&ID :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:387 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:407 msgctxt "webAccess.ruleCriteria" msgid "&Class:" msgstr "&Classe :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:389 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:409 msgctxt "webAccess.ruleCriteria" msgid "&States:" msgstr "État&s :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:391 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:411 msgctxt "webAccess.ruleCriteria" msgid "Ima&ge source:" msgstr "Source de l'ima&ge :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:393 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:413 +msgctxt "webAccess.ruleCriteria" +msgid "Document &URL:" +msgstr "&URL du document :" + +#. Translator: The label for a Rule Criteria field +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:415 msgctxt "webAccess.ruleCriteria" msgid "R&elative path:" msgstr "Ch&emin relatif :" #. Translator: The label for a Rule Criteria field -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:395 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:417 msgctxt "webAccess.ruleCriteria" msgid "Inde&x:" msgstr "Inde&x :" -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:409 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:431 msgid "Context:" msgstr "Contexte :" #. Translator: A selection value for the Context field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:416 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:438 msgid "Page title - Applies only to pages with the given title" msgstr "Titre de page - S'applique uniquement aux pages portant le titre donné" #. Translator: A selection value for the Context field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:418 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:440 msgid "Page type - Applies only to pages with the given type" msgstr "Type de page - S'applique uniquement aux pages du type donné" #. Translator: A selection value for the Context field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:420 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:442 msgid "Parent element - Applies only within the results of another rule" msgstr "" "Élément parent - S'applique uniquement dans le cadre des résultats d'une " "autre règle" #. Translator: A selection value for the Context field on the Criteria editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:422 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:444 msgid "Advanced" msgstr "Avancé" #. Translators: The label for a button in the Criteria Editor dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:579 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:611 msgid "Test these criteria (F5)" msgstr "Tester ces critères (F5)" #. Translators: Error message when the field doesn't meet the required syntax -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:757 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:786 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:794 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:823 #, python-brace-format msgid "Syntax error in the field \"{field}\"" msgstr "Erreur de syntaxe dans le champ \"{field}\"" #. Translators: The title of an error message dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:759 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:773 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:788 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:802 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:819 -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:267 -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:280 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:307 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:319 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:350 -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:390 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:796 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:810 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:825 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:839 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:856 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:303 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:315 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:344 +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:275 +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:289 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:357 msgid "Error" msgstr "Erreur" #. Translators: Error message when the field doesn't match any known identifier -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:771 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:800 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:808 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:837 #, python-brace-format msgid "Unknown identifier in the field \"{field}\"" msgstr "Identifiant inconnu dans le champ \"{field}\"" #. Translators: Error message when the index is not positive -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:818 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:855 msgid "Index, if set, must be a positive integer." msgstr "Index, si renseigné, doit être un nombre entier strictement positif." -#. Translators: Announced when resetting a property to its default value in the editor -#. Avoid announcing the whole eventual control refresh -#. Translators: Announced when resetting a property to its default value in the editor -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:866 -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:986 -#: addon\globalPlugins\webAccess\gui\properties.py:277 -#, python-brace-format -msgid "Reset to {value}" -msgstr "Réinitialiser à {value}" - -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:874 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:875 msgid "Select a property to override" msgstr "Sélectionnez une propriété à surcharger" #. Translators: The label for a list on the Criteria Editor dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:887 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:888 msgid "Properties specific to this criteria set" msgstr "Propriétés spécifiques à cet ensemble de critères" #. Translators: A hint stating a list is empty and how to populate it on the Criteria Editor dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:890 +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:891 msgid "None. Press alt+n to override a property." msgstr "Aucune. Appuyez sur alt+n pour remplacer une propriété." -#. Translators: A column header in the Criteria Editor dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:892 -msgid "Rule value" -msgstr "Valeur de la règle" - -#. Translators: The label for a button on the Criteria Editor dialog -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:900 -msgid "&New" -msgstr "&Nouveau" - -#. Translators: The title of the Criteria Editor dialog. -#: addon\globalPlugins\webAccess\gui\criteriaEditor.py:991 -msgid "WebAccess Criteria Set editor" -msgstr "Éditeur de jeux de critères WebAccess" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:49 -msgctxt "webAccess.elementDescription" -msgid "elements" -msgstr "éléments" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:51 -msgctxt "webAccess.elementDescription" -msgid "element" -msgstr "élément" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:64 -msgctxt "webAccess.elementDescription" -msgid "from:" -msgstr "de :" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:68 -msgctxt "webAccess.elementDescription" -msgid "to:" -msgstr "à :" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:106 -msgid "No NodeManager" -msgstr "Pas de NodeManager" - -#: addon\globalPlugins\webAccess\gui\elementDescription.py:160 -msgid "Element description" -msgstr "Description de l'élément" - -#. Translators: The title for a dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:54 -msgid "Input Gesture" -msgstr "Gestes de saisie" - -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:69 -msgid "Gesture: " -msgstr "Geste : " - -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:78 -msgid "&Action to execute" -msgstr "&Action à exécuter" - -#. Translators: A prompt for selection in the action list on the Input Gesture dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:114 -msgid "Select an action" -msgstr "Sélectionner une action" - -#. Translators: The prompt to enter a gesture on the Input Gesture dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:129 -msgid "Type now the desired key combination" -msgstr "Pressez maintenant la combinaison de touches souhaitée" - -#. Translators: A message on the Input Gesture dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:145 -msgid "You must define a shortcut" -msgstr "Vous devez entrer un geste de commande" - -#. Translators: A message on the Input Gesture dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:155 -msgid "You must choose an action" -msgstr "Vous devez choisir une action" - -#. Translators: Displayed when no gesture as been entered on the Input Gesture dialog -#: addon\globalPlugins\webAccess\gui\gestureBinding.py:188 -msgid "None" -msgstr "Aucune" - -#. Translators: Web Access menu item label. -#. Translator: The label for a button on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\menu.py:61 -#: addon\globalPlugins\webAccess\gui\rule\manager.py:392 -msgid "&New rule..." -msgstr "&Nouvelle règle..." - -#. Translators: Web Access menu item label. -#. Translators: The label for a button in the Web Modules Manager dialog -#: addon\globalPlugins\webAccess\gui\menu.py:69 -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:152 -msgid "Manage &rules..." -msgstr "Gérer les &règles..." - -#. Translators: Web Access menu item label. -#. Translators: The label for a button in the Web Modules Manager -#: addon\globalPlugins\webAccess\gui\menu.py:78 -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:142 -msgid "&New web module..." -msgstr "&Nouveau module web..." - -#. Translators: Web Access menu item label. -#: addon\globalPlugins\webAccess\gui\menu.py:84 -#, python-format -msgid "Edit &web module %s..." -msgstr "Modifier le module &web %s..." - -#. Translators: Web Access menu item label. -#: addon\globalPlugins\webAccess\gui\menu.py:91 -msgid "Manage web &modules..." -msgstr "Gérer les &modules web..." - -#. Translators: Web Access menu item label. -#: addon\globalPlugins\webAccess\gui\menu.py:100 -msgid "Temporarily &disable all web modules" -msgstr "&Désactiver temporairement tous les modules web" - -#. Translators: The displayed value of a yes/no rule property -#. Translators: The display value of a yes/no field -#: addon\globalPlugins\webAccess\gui\properties.py:149 -#: addon\globalPlugins\webAccess\gui\__init__.py:919 -msgid "Yes" -msgstr "Oui" - -#. Translators: The displayed value of a yes/no rule property -#. Translators: The displayed value of a yes/no field -#: addon\globalPlugins\webAccess\gui\properties.py:152 -#: addon\globalPlugins\webAccess\gui\__init__.py:922 -msgid "No" -msgstr "Non" - -#. Translators: The label for a category in the rule editor -#: addon\globalPlugins\webAccess\gui\properties.py:218 -msgid "Properties" -msgstr "Propriétés" - -#. Translators: Displayed when the selected rule type doesn't support any property -#: addon\globalPlugins\webAccess\gui\properties.py:292 -msgid "No property available for this rule type." -msgstr "Aucune propriété n'est disponible pour ce type de règle." - -#. Translators: The label for a list on the Rule Editor dialog -#: addon\globalPlugins\webAccess\gui\properties.py:357 -msgid "&Properties" -msgstr "&Propriétés" - -#. Translators: A column header on the Rule Editor dialog -#: addon\globalPlugins\webAccess\gui\properties.py:369 -msgctxt "webAccess.ruleEditor" -msgid "Property" -msgstr "Propriété" - -#. Translators: A column header on the Rule Editor dialog -#: addon\globalPlugins\webAccess\gui\properties.py:371 -msgctxt "webAccess.ruleEditor" -msgid "Value" -msgstr "Valeur" - -#. Translators: The title of a dialog -#: addon\globalPlugins\webAccess\gui\settings.py:53 -msgid "WebAccess Preferences" -msgstr "Paramètres de Web Access" - -#. Translators: The label for a category in the settings dialog -#: addon\globalPlugins\webAccess\gui\settings.py:91 -msgid "WebAccess" -msgstr "WebAccess" - -#. Translators: The label for a settings in the WebAccess settings panel -#: addon\globalPlugins\webAccess\gui\settings.py:97 -msgid "&Developer mode" -msgstr "Mode développeur" - -#. Translators: The label for a settings in the WebAccess settings panel -#: addon\globalPlugins\webAccess\gui\settings.py:102 -msgid "Disable all &user WebModules (activate only scratchpad and addons)" -msgstr "" -"Désactiver tous les WebModules utilisateur (ne charger que répertoire Bloc-" -"notes du Développeur et extensions)" - -#. Translators: The label for a settings in the WebAccess settings panel -#: addon\globalPlugins\webAccess\gui\settings.py:107 -msgid "Write into add-ons' \"webModules\" folder (not recommended)" -msgstr "" -"Écrire directement dans le dossier \"webModules\" des extensions (non " -"recommandé)" - -#. Translators: Error message while naming a web module. -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:56 -msgid "A web module already exists with this name." -msgstr "Un autre module web porte déjà ce nom." - -#. Translators: Prompt before overwriting a web module. -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:59 -msgid "Do you want to overwrite it?" -msgstr "Voulez-vous le remplacer ?" - -#. Translators: The label for a field in the WebModule editor -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:103 -msgid "Web module name:" -msgstr "Nom du module web :" - -#. Translators: The label for a field in the WebModule editor -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:114 -msgid "URL:" -msgstr "URL :" - -#. Translators: The label for a field in the WebModule editor -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:125 -msgid "Window title:" -msgstr "Titre de la fenêtre :" - -#. Translators: The label for a field in the WebModule editor -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:136 -msgid "Help (in Markdown):" -msgstr "Aide du module (format Markdown)" - -#. Translators: Web module creation dialog title -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:169 -msgid "New Web Module" -msgstr "Nouveau Module web" - -#. Translators: Web module edition dialog title -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:185 -msgid "Edit Web Module" -msgstr "Modifier le module web" - -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:205 -msgid "URL not found" -msgstr "URL introuvable" - -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:266 -msgid "You must enter a name for this web module" -msgstr "Vous devez entrer un nom pour ce module web" - -#: addon\globalPlugins\webAccess\gui\webModuleEditor.py:279 -msgid "You must specify at least a URL or a window title." -msgstr "Vous devez spécifier au moins une URL ou un titre de fenêtre." - -#. Translators: Prompt before deleting a web module. -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:45 -msgid "Do you really want to delete this web module?" -msgstr "Êtes-vous sûr de vouloir supprimer ce module web ?" - -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:71 -#, python-brace-format -msgid "" -"This web module comes with the add-on {addonSummary}.\n" -"It cannot be modified at its current location.\n" -"\n" -"Do you want to make a copy in your scratchpad?\n" -msgstr "" -"Ce module web est fourni par le module complémentaire {addonSummary}.\n" -"Il ne peut être modifié à son emplacement actuel.\n" -"\n" -"Voulez-vous en faire une copie dans votre répertoire Bloc-notes du " -"Développeur (scratchpad) ?\n" - -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:80 -msgid "Warning" -msgstr "Avertissement" - -#. Translators: The title of the Web Modules Manager dialog -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:107 -msgid "Web Modules Manager" -msgstr "Gestion des modules web" - -#. Translators: The label for the modules list in the -#. Web Modules dialog. -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:116 -msgid "Available Web Modules:" -msgstr "Modules web disponibles :" - -#. Translators: The label for a column of the web modules list -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:125 -msgid "Name" -msgstr "Nom" - -#. Translators: The label for a column of the web modules list -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:127 -msgid "Trigger" -msgstr "Déclencheur" - -#: addon\globalPlugins\webAccess\gui\webModulesManager.py:255 -msgid "and" -msgstr "et" - -#. Translators: Announced when a list is empty -#: addon\globalPlugins\webAccess\gui\__init__.py:165 -msgid "Empty" -msgstr "Vide" +#. Translators: A column header in the Criteria Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:893 +msgid "Rule value" +msgstr "Valeur de la règle" -#. Translator: The placeholder for an invalid value in summary reports -#: addon\globalPlugins\webAccess\gui\__init__.py:208 -msgid "" -msgstr "" +#. Translators: The label for a button on the Criteria Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:901 +msgid "&New" +msgstr "&Nouveau" -#. Translators: The label for the list of categories in a multi category settings dialog. -#: addon\globalPlugins\webAccess\gui\__init__.py:523 -msgid "&Categories:" -msgstr "&Catégories :" +#. Translators: The label for a button on the Criteria Editor dialog +#. Translators: The label for a button on the Rule Editor dialog +#. Translators: Delete criteria button label +#. Translators: The label for a button on the Rule Editor dialog +#. Translators: The label for a button in the Rule Editor dialog +#. Translator: The label for a button on the RulesManager dialog. +#. Translators: The label for a button in the +#. Web Modules Manager dialog +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:911 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:429 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:637 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:730 +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:138 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:471 +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:126 +msgid "&Delete" +msgstr "&Supprimer" -#. Translators: A field label. French typically adds a space before the colon. -#: addon\globalPlugins\webAccess\gui\__init__.py:1029 +#. Avoid announcing the whole eventual control refresh +#. Translators: Announced when resetting a property to its default value in the editor +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:987 +#: addon\globalPlugins\webAccess\gui\rule\properties.py:313 #, python-brace-format -msgid "{field}:" -msgstr "{field} :" +msgid "Reset to {value}" +msgstr "Réinitialiser à {value}" -#. Translators: The label for a node in the category tree on a multi-category dialog -#. Translators: A mention on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\__init__.py:1064 -#: addon\globalPlugins\webAccess\gui\rule\editor.py:114 -#, python-brace-format -msgid "{field}: {value}" -msgstr "{field} : {value}" +#. Translators: The title of the Criteria Editor dialog. +#: addon\globalPlugins\webAccess\gui\rule\criteriaEditor.py:992 +msgid "WebAccess Criteria Set editor" +msgstr "Éditeur de jeux de critères WebAccess" #. Translators: The Label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:91 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:98 msgid "Rule &type:" msgstr "&Type de règle :" #. Translators: The Label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:93 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:100 msgid "Rule &name:" msgstr "&Nom de la règle :" #. Translators: A mention on the Rule summary report -#: addon\globalPlugins\webAccess\gui\rule\editor.py:101 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:108 msgid "No rule type selected." msgstr "Aucun type de règle n'est sélectionné." #. Translators: The label for a section on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\rule\editor.py:127 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:134 msgid "Criteria:" msgstr "Critères :" #. Translators: The label for a section on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\rule\editor.py:131 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:138 msgid "Multiple criteria sets:" msgstr "Jeux de critères multiples :" #. Translators: The label for a section on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\rule\editor.py:136 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:143 #, python-brace-format msgid "Alternative #{index} \"{name}\":" msgstr "Alternative #{index} \"{name}\" :" #. Translators: The label for a section on the Rule Summary report -#: addon\globalPlugins\webAccess\gui\rule\editor.py:139 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:146 #, python-brace-format msgid "Alternative #{index}:" msgstr "Alternative #{index} :" #. todo: change tooltip's text #. Translators: Tooltip for rule type choice list. -#: addon\globalPlugins\webAccess\gui\rule\editor.py:184 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:191 msgid "TOOLTIP EXEMPLE" msgstr "EXEMPLE DE BULLE D'AIDE" #. Translators: Error message when no type is chosen before saving the rule -#: addon\globalPlugins\webAccess\gui\rule\editor.py:306 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:302 msgid "You must choose a type for this rule" msgstr "Vous devez choisir un type pour cette règle" #. Translators: Error message when no name is entered before saving the rule -#: addon\globalPlugins\webAccess\gui\rule\editor.py:318 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:314 msgid "You must choose a name for this rule" msgstr "Vous devez choisir un nom pour cette règle" #. Translators: Error message when another rule with the same name already exists -#: addon\globalPlugins\webAccess\gui\rule\editor.py:349 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:343 msgid "There already is another rule with the same name." msgstr "Il existe déjà une autre règle portant le même nom." #. Translators: Label for a control in the Rule Editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:374 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:368 msgid "&Alternatives" msgstr "&Alternatives" +#. Translators: The label for a button on the Rule Editor dialog +#. Translators: New criteria button label +#. Translators: The label for a button on the Rule Editor dialog +#. Translators: The label for a button in the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\editor.py:411 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:644 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:737 +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:116 +msgid "&New..." +msgstr "&Nouveau..." + +#. Translators: The label for a button on the Rule Editor dialog +#. Translators: Edit criteria button label +#. Translators: The label for a button in the Rule Editor dialog +#. Translator: The label for a button on the RulesManager dialog. +#. Translators: The label for a button in the Web Modules Manager dialog +#: addon\globalPlugins\webAccess\gui\rule\editor.py:420 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:628 +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:127 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:463 +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:103 +msgid "&Edit..." +msgstr "&Modifier..." + #. Translator: A confirmation prompt on the Rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:507 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:503 msgid "Are you sure you want to delete this alternative?" msgstr "Êtes-vous sûr de vouloir supprimer cette alternative ?" #. Translator: The title for a confirmation prompt on the Rule editor #. Translator: The title for a confirmation prompt on the #. RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\editor.py:509 -#: addon\globalPlugins\webAccess\gui\rule\manager.py:578 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:505 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:720 msgid "Confirm Deletion" msgstr "Confirmation d'effacement" #. Translators: The label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:610 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:593 msgid "Summar&y" msgstr "&Résumé" #. Translators: The label for a field on the Rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:627 +#: addon\globalPlugins\webAccess\gui\rule\editor.py:610 msgid "Technical n&otes" msgstr "&Notes techniques" -#. Translators: The title of the rule editor -#: addon\globalPlugins\webAccess\gui\rule\editor.py:848 -msgid "WebAccess Rule editor" -msgstr "Éditeur de règles WebAccess" +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:921 +msgid "Sub Module {} - New Rule" +msgstr "Sous-module {} - Nouvelle règle" + +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:924 +msgid "Root Module {} - New Rule" +msgstr "Module racine {} - Nouvelle règle" + +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:927 +msgid "Web Module {} - New Rule" +msgstr "Module web {} - Nouvelle règle" + +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:932 +msgid "Sub Module {} - Edit Rule {}" +msgstr "Sous-module {} - Modifier la règle {}" + +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:935 +msgid "Root Module {} - Edit Rule {}" +msgstr "Module racine - Modifier la règle {}" + +#. Translators: A title of the rule editor +#: addon\globalPlugins\webAccess\gui\rule\editor.py:938 +msgid "Web Module {} - Edit Rule {}" +msgstr "Module web {} - Modifier la règle {}" + +#. Translators: The title for a dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:67 +msgid "Input Gesture" +msgstr "Gestes de saisie" + +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:82 +msgid "Gesture: " +msgstr "Geste : " + +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:91 +msgid "&Action to execute" +msgstr "&Action à exécuter" + +#. Translators: A prompt for selection in the action list on the Input Gesture dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:127 +msgid "Select an action" +msgstr "Sélectionner une action" + +#. Translators: The prompt to enter a gesture on the Input Gesture dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:142 +msgid "Type now the desired key combination" +msgstr "Pressez maintenant la combinaison de touches souhaitée" + +#. Translators: A message on the Input Gesture dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:158 +msgid "You must define a shortcut" +msgstr "Vous devez entrer un geste de commande" + +#. Translators: A message on the Input Gesture dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:168 +msgid "You must choose an action" +msgstr "Vous devez choisir une action" + +#. Translators: Displayed when no gesture as been entered on the Input Gesture dialog +#: addon\globalPlugins\webAccess\gui\rule\gestureBinding.py:201 +msgid "None" +msgstr "Aucune" + +#. Translators: The label for a category in the Rule and Criteria editors +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:69 +msgid "Input Gestures" +msgstr "Gestes de commande" + +#. Translators: Displayed when the selected rule type doesn't support input gestures +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:72 +msgid "The selected Rule Type does not support Input Gestures." +msgstr "" +"Le type de règle sélectionné ne prend pas en charge les gestes de commande." + +#. Translators: The label for a list on the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\gestures.py:95 +msgid "&Gestures" +msgstr "&Gestes" #. Translator: TreeItem label on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:149 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:170 msgctxt "webAccess.ruleGesture" msgid "" msgstr "" +#. Translators: A part of a context grouping label on the Rules Manager +#: addon\globalPlugins\webAccess\gui\rule\manager.py:205 +#, python-brace-format +msgid "Page Title: {contextPageTitle}" +msgstr "Titre de la page : {contextPageTitle}" + +#. Translators: A part of a context grouping label on the Rules Manager +#: addon\globalPlugins\webAccess\gui\rule\manager.py:208 +#, python-brace-format +msgid "Page Type: {contextPageTitle}" +msgstr "Type de page : {contextPageTitle}" + +#. Translators: A part of a context grouping label on the Rules Manager +#: addon\globalPlugins\webAccess\gui\rule\manager.py:211 +#, python-brace-format +msgid "Parent: {contextPageTitle}" +msgstr "Parent : {contextPageTitle}" + #. Translator: Grouping option on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:266 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:317 msgctxt "webAccess.rulesGroupBy" msgid "&Position" msgstr "&Position" #. Translator: Grouping option on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:272 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:323 msgctxt "webAccess.rulesGroupBy" msgid "&Type" msgstr "&Type" #. Translator: Grouping option on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:278 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:329 +msgctxt "webAccess.rulesGroupBy" +msgid "&Context" +msgstr "&Contexte :" + +#. Translator: Grouping option on the RulesManager dialog. +#: addon\globalPlugins\webAccess\gui\rule\manager.py:335 msgctxt "webAccess.rulesGroupBy" msgid "&Gestures" msgstr "&Geste de commande" #. Translator: Grouping option on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:284 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:341 msgctxt "webAccess.rulesGroupBy" msgid "Nam&e" msgstr "N&om" #. Translator: A label on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:306 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:363 msgid "Group by: " msgstr "Regrouper par : " #. Translator: A label on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:319 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:376 msgid "&Filter: " msgstr "&Filtre : " #. Translator: A label on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:330 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:394 msgid "Include only rules &active on the current page" msgstr "N'inclure que les règles &actives sur la page courante" #. contentsSizer.Add(descSizer, flag=wx.EXPAND) #. Translator: The label for a field on the Rules manager -#: addon\globalPlugins\webAccess\gui\rule\manager.py:356 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:420 msgid "Summary" msgstr "Résumé" #. Translator: The label for a field on the Rules manager -#: addon\globalPlugins\webAccess\gui\rule\manager.py:367 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:431 msgid "Technical notes" msgstr "Notes techniques" #. Translator: The label for a button on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:383 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:447 msgid "Move to" msgstr "Aller à" +#. Translators: Reported when cycling through rules grouping on the Rules Manager dialog +#: addon\globalPlugins\webAccess\gui\rule\manager.py:529 +#, fuzzy +#| msgid "Group by: " +msgid "Group by: {}" +msgstr "Regrouper par : " + #. Translator: A confirmation prompt on the RulesManager dialog. -#: addon\globalPlugins\webAccess\gui\rule\manager.py:572 +#: addon\globalPlugins\webAccess\gui\rule\manager.py:714 msgid "Are you sure you want to delete this rule?" msgstr "Êtes-vous sûr de vouloir supprimer cette règle ?" +#. Translators: The label for a category in the rule editor +#: addon\globalPlugins\webAccess\gui\rule\properties.py:255 +msgid "Properties" +msgstr "Propriétés" + +#. Translators: Displayed when the selected rule type doesn't support any property +#: addon\globalPlugins\webAccess\gui\rule\properties.py:328 +msgid "No property available for this rule type." +msgstr "Aucune propriété n'est disponible pour ce type de règle." + +#. Translators: The label for a list on the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\properties.py:397 +msgid "&Properties" +msgstr "&Propriétés" + +#. Translators: A column header on the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\properties.py:409 +msgctxt "webAccess.ruleEditor" +msgid "Property" +msgstr "Propriété" + +#. Translators: A column header on the Rule Editor dialog +#: addon\globalPlugins\webAccess\gui\rule\properties.py:411 +msgctxt "webAccess.ruleEditor" +msgid "Value" +msgstr "Valeur" + +#. Translators: A prompt for creation of a missing SubModule +#: addon\globalPlugins\webAccess\gui\rule\__init__.py:69 +#, python-brace-format +msgid "" +"SubModule {name} could not be found.\n" +"\n" +"Do you want to create it now?" +msgstr "" +"Le sous-module {name} n'existe pas encore.\n" +"\n" +"Voulez-vous le créer maintenant ?" + +#. Translators: Error message while naming a web module. +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:67 +msgid "A web module already exists with this name." +msgstr "Un autre module web porte déjà ce nom." + +#. Translators: Prompt before overwriting a web module. +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:70 +msgid "Do you want to overwrite it?" +msgstr "Voulez-vous le remplacer ?" + +#. Translators: The label for a field in the WebModule editor +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:102 +msgid "Web module name:" +msgstr "Nom du module web :" + +#. Translators: The label for a field in the WebModule editor +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:113 +msgid "URL:" +msgstr "URL :" + +#. Translators: The label for a field in the WebModule editor +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:124 +msgid "Window title:" +msgstr "Titre de la fenêtre :" + +#. Translators: The label for a field in the WebModule editor +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:135 +msgid "Help (in Markdown):" +msgstr "Aide du module (format Markdown)" + +#. Translators: Web module edition dialog title +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:168 +msgid "Edit Web Module" +msgstr "Modifier le module web" + +#. Translators: Web module creation dialog title +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:174 +msgid "New Web Module" +msgstr "Nouveau Module web" + +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:206 +msgid "URL not found" +msgstr "URL introuvable" + +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:274 +msgid "You must enter a name for this web module" +msgstr "Vous devez entrer un nom pour ce module web" + +#: addon\globalPlugins\webAccess\gui\webModule\editor.py:288 +msgid "You must specify at least a URL or a window title." +msgstr "Vous devez spécifier au moins une URL ou un titre de fenêtre." + +#. Translators: The title of the Web Modules Manager dialog +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:48 +msgid "Web Modules Manager" +msgstr "Gestion des modules web" + +#. Translators: The label for the modules list in the +#. Web Modules dialog. +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:66 +msgid "Available Web Modules:" +msgstr "Modules web disponibles :" + +#. Translators: The label for a column of the web modules list +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:76 +msgid "Name" +msgstr "Nom" + +#. Translators: The label for a column of the web modules list +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:78 +msgid "Trigger" +msgstr "Déclencheur" + +#: addon\globalPlugins\webAccess\gui\webModule\manager.py:238 +msgid "and" +msgstr "et" + +#. Translators: Prompt before deleting a web module. +#: addon\globalPlugins\webAccess\gui\webModule\__init__.py:51 +msgid "Do you really want to delete this web module?" +msgstr "Êtes-vous sûr de vouloir supprimer ce module web ?" + +#: addon\globalPlugins\webAccess\gui\webModule\__init__.py:77 +#, python-brace-format +msgid "" +"This web module comes with the add-on {addonSummary}.\n" +"It cannot be modified at its current location.\n" +"\n" +"Do you want to make a copy in your scratchpad?\n" +msgstr "" +"Ce module web est fourni par le module complémentaire {addonSummary}.\n" +"Il ne peut être modifié à son emplacement actuel.\n" +"\n" +"Voulez-vous en faire une copie dans votre répertoire Bloc-notes du " +"Développeur (scratchpad) ?\n" + +#: addon\globalPlugins\webAccess\gui\webModule\__init__.py:86 +msgid "Warning" +msgstr "Avertissement" + #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:144 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:149 msgctxt "webAccess.controlMutation" msgid "Button" msgstr "Bouton" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:146 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:151 msgctxt "webAccess.controlMutation" msgid "Header level 1" msgstr "Titre de niveau 1" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:148 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:153 msgctxt "webAccess.controlMutation" msgid "Header level 2" msgstr "Titre de niveau 2" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:150 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:155 msgctxt "webAccess.controlMutation" msgid "Header level 3" msgstr "Titre de niveau 3" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:152 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:157 msgctxt "webAccess.controlMutation" msgid "Header level 4" msgstr "Titre de niveau 4" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:154 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:159 msgctxt "webAccess.controlMutation" msgid "Header level 5" msgstr "Titre de niveau 5" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:156 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:161 msgctxt "webAccess.controlMutation" msgid "Header level 6" msgstr "Titre de niveau 6" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:158 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:163 msgctxt "webAccess.controlMutation" msgid "Add a label" msgstr "Ajouter un libellé" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:160 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:165 msgctxt "webAccess.controlMutation" msgid "Section" msgstr "Section" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:162 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:167 msgctxt "webAccess.controlMutation" msgid "Region" msgstr "Région" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:164 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:169 msgctxt "webAccess.controlMutation" msgid "Navigation (named)" msgstr "Navigation (nommée)" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:166 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:171 msgctxt "webAccess.controlMutation" msgid "Navigation (unnamed)" msgstr "Navigation (anonyme)" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:168 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:173 msgctxt "webAccess.controlMutation" msgid "Link" msgstr "Lien" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:170 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:175 msgctxt "webAccess.controlMutation" msgid "Data table (Internet Explorer only)" msgstr "Tableau de données (Internet Explorer uniquement)" #. Translators: The label for a control mutation. -#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:172 +#: addon\globalPlugins\webAccess\ruleHandler\controlMutation.py:177 msgctxt "webAccess.controlMutation" msgid "Layout table" msgstr "Tableau de présentation" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:75 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:90 msgctxt "webAccess.ruleProperty" msgid "Auto Actions" msgstr "Actions automatiques" +#. Translators: Displayed if no value is set for the "Auto Actions" property +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:92 +msgctxt "webAccess.action" +msgid "No action" +msgstr "Ne rien faire" + #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:85 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:101 msgctxt "webAccess.ruleProperty" msgid "Multiple results" msgstr "Résultats multiples" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:94 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:111 msgctxt "webAccess.ruleProperty" msgid "Activate form mode" msgstr "Activer le mode formulaire" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:103 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:121 msgctxt "webAccess.ruleProperty" msgid "Skip with Page Down" msgstr "Ignorer avec Page Suivante" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:112 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:131 msgctxt "webAccess.ruleProperty" msgid "Speak rule name" msgstr "Énoncer le nom de la règle" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:121 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:141 msgctxt "webAccess.ruleProperty" msgid "Custom name" msgstr "Nom personnalisé" #. Translators: Displayed if no value is set for a given rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:123 -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:139 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:143 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:166 msgctxt "webAccess.ruleProperty" msgid "" msgstr "" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:133 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:160 msgctxt "webAccess.ruleProperty" msgid "Custom message" msgstr "Message personnalisé" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:136 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:163 msgctxt "webAccess.ruleProperty" msgid "Custom page title" msgstr "Titre de page personnalisé" #. Translators: The display name for a rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:147 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:175 msgctxt "webAccess.ruleProperty" msgid "Transform" msgstr "Transformation" #. Translators: Displayed if no value is set for the "Transform" rule property -#: addon\globalPlugins\webAccess\ruleHandler\properties.py:149 +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:177 msgctxt "webAccess.ruleProperty.mutation" msgid "None" msgstr "Aucune" +#. Translators: The display name for a rule property +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:186 +msgctxt "webAccess.ruleProperty" +msgid "Load sub-module" +msgstr "Charger le sous-module" + +#. Translators: The displayed text if there is no value for the "Load sub-module" property +#: addon\globalPlugins\webAccess\ruleHandler\properties.py:188 +#, fuzzy +#| msgid "No" +msgctxt "webAccess.ruleProperty.subModule" +msgid "No" +msgstr "Non" + +#. Translators: The label for a rule type. +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:45 +msgid "Global Marker" +msgstr "Marqueur global" + #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:43 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:47 msgid "Marker" msgstr "Marqueur" #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:45 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:49 msgid "Zone" msgstr "Zone" #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:47 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:51 msgid "Page type" msgstr "Type de page" #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:49 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:53 msgid "Parent element" msgstr "Élément parent" #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:51 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:55 msgid "Page main title" msgstr "Titre de page - principal" #. Translators: The label for a rule type. -#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:53 +#: addon\globalPlugins\webAccess\ruleHandler\ruleTypes.py:57 msgid "Page secondary title" msgstr "Titre de page - secondaire" #. Translators: Action name -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:75 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:84 msgctxt "webAccess.action" msgid "Move to" msgstr "Aller à" #. Translators: Action name -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:77 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:86 msgctxt "webAccess.action" msgid "Say all" msgstr "Dire tout" #. Translators: Action name -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:79 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:88 msgctxt "webAccess.action" msgid "Speak" msgstr "Énoncer" #. Translators: Action name -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:81 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:90 msgctxt "webAccess.action" msgid "Activate" msgstr "Activer" #. Translators: Action name -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:83 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:92 msgctxt "webAccess.action" msgid "Mouse move" msgstr "Déplacer la souris" -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:106 -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:652 -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:661 +#. Translators: Reported when attempting an action while WebAccess is not ready +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:755 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:765 msgid "Not ready" msgstr "Pas encore prêt" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:690 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:794 msgid "No previous zone" msgstr "Pas de zone précédente" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:693 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:797 msgid "No next zone" msgstr "Pas de zone suivante" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:696 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:800 msgid "No zone" msgstr "Pas de zone" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:700 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:804 msgid "No marker" msgstr "Pas de marqueur" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:703 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:807 msgid "No previous marker" msgstr "Pas de marqueur précédent" #. Translator: Error message in quickNav (page up/down) -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:706 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:810 msgid "No next marker" msgstr "Pas de marqueur suivant" #. Translators: Speak rule name on "Move to" action -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:944 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:1149 #, python-brace-format msgid "Move to {ruleName}" msgstr "Aller à {ruleName}" -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:1296 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:1514 #, python-brace-format msgid "{criteriaName} not found" msgstr "{criteriaName} non trouvé" -#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:1373 +#: addon\globalPlugins\webAccess\ruleHandler\__init__.py:1594 #, python-brace-format msgid "{ruleName} not found" msgstr "{ruleName} introuvable" #. Translators: Presented when requesting a missing contextual help -#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:310 +#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:339 msgid "No contextual help available." msgstr "Aucune aide contextuelle disponible." #. Translators: Title of the Contextual Help dialog -#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:315 +#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:344 msgid "Contextual Help" msgstr "Aide contextuelle" -#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:328 +#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:357 #, python-format msgid "%s copied to clipboard" msgstr "%s copié dans le presse-papier" #. Translators: Speak name of current web module -#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:332 +#: addon\globalPlugins\webAccess\webModuleHandler\webModule.py:361 #, python-brace-format msgid "Current web module is: {name}" msgstr "Le module web actuel est : {name}" #. Translators: Confirmation message after web module creation. -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:215 -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:294 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:255 #, python-brace-format msgid "Your new web module {name} has been created." msgstr "Votre nouveau module web {name} a été créé." -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:229 -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:316 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:269 msgid "The web module name should be a valid file name." msgstr "Le nom d'un web module doit être un nom de fichier valide." -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:231 -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:318 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:271 msgid "It should not contain any of the following:" msgstr "Il ne doit contenir aucun des caractères suivants :" -#. Translators: The text of a generic error message dialog -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:246 -msgid "" -"An error occured.\n" -"\n" -"Please consult NVDA's log." -msgstr "" -"Une erreur s'est produite.\n" -"\n" -"Consultez le journal de NVDA pour plus de détails." - -#. Translator: Canceling web module creation. -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:332 -msgid "Cancel" -msgstr "Annuler" - #. Translators: An error message upon attempting to save a modification -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:359 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:326 msgid "This modification cannot be saved under the current configuration." msgstr "" "Cette modification ne peut pas être enregistrée de par la configuration " "actuelle." #. Translators: A hint on how to allow to save a modification -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:363 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:330 msgid "• In the WebAccess category, enable User WebModules." msgstr "• Dans la catégorie WebAccess, activer les WebModules utilisateur." #. Translators: A hint on how to allow to save a modification -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:369 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:336 msgid "• In the WebAccess category, enable Developper Mode." msgstr "• Dans la catégorie WebAccess, activer le mode développeur." #. Translators: A hint on how to allow to save a modification -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:378 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:345 msgid "• In the Advanced category, enable loading of the Scratchpad directory" msgstr "" "• Dans la catégorie Avancé, activer le chargement de code personnalisé " "depuis le répertoire Bloc-notes du Développeur" -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:381 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:348 msgid "or" msgstr "ou" #. Translators: An introduction to hints on how to allow to save a modification -#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:384 +#: addon\globalPlugins\webAccess\webModuleHandler\__init__.py:351 msgid "You may, in NVDA Preferences:" msgstr "Vous pouvez, dans les paramètres de NVDA :" @@ -1394,3 +1477,30 @@ msgid "Web application modules support for modern or complex web sites." msgstr "" "Gestion des modules d'applications web pour la navigation sur sites web " "riches ou complexes." + +#~ msgid "Toggle debug mode." +#~ msgstr "Basculer le mode de débogage." + +#~ msgid "Actions" +#~ msgstr "Actions" + +#~ msgid "No action available for the selected rule type." +#~ msgstr "Aucune action disponible pour le type de règle sélectionné." + +#~ msgid "A&utomatic action at rule detection:" +#~ msgstr "Action &automatique à la détection d'une règle :" + +#~ msgid "WebAccess Rule editor" +#~ msgstr "Éditeur de règles WebAccess" + +#~ msgid "" +#~ "An error occured.\n" +#~ "\n" +#~ "Please consult NVDA's log." +#~ msgstr "" +#~ "Une erreur s'est produite.\n" +#~ "\n" +#~ "Consultez le journal de NVDA pour plus de détails." + +#~ msgid "Cancel" +#~ msgstr "Annuler" From d63ba00257fdd0e679e43f946089b003e8038371 Mon Sep 17 00:00:00 2001 From: Julien Cochuyt Date: Tue, 10 Dec 2024 17:08:11 +0100 Subject: [PATCH 54/54] GUI: Summary: Condense alternatives --- addon/globalPlugins/webAccess/gui/rule/editor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/addon/globalPlugins/webAccess/gui/rule/editor.py b/addon/globalPlugins/webAccess/gui/rule/editor.py index bc48b7e3..af1c666f 100644 --- a/addon/globalPlugins/webAccess/gui/rule/editor.py +++ b/addon/globalPlugins/webAccess/gui/rule/editor.py @@ -145,7 +145,9 @@ def getSummary(context, data): # Translators: The label for a section on the Rule Summary report altHeader = _("Alternative #{index}:").format(index=index + 1) subParts.append(" " + altHeader) - subParts.append(criteriaEditor.getSummary(context, alternative, indent=" ")) + subParts.append(criteriaEditor.getSummary( + context, alternative, indent=" ", condensed=True + )) parts.extend(subParts) return "\n".join(parts)