Repository: nvaccess/nvda

Issue:
Gesture overrides broken in NVDA 2022.2.1
### Steps to reproduce:
Add the following gesture overrides for the brailliant driver. Providing the gestures.ini entries for convenience
```ini
[globalCommands.GlobalCommands]
braille_scrollForward = br(brailliantb):down
braille_scrollBack = br(brailliantb):up
braille_previousLine = br(brailliantb):left
braille_nextLine = br(brailliantb):right
```

### Actual behavior:
Overrides are ignored arbitrarily. For example, on several restarts of nvda:

| n | up | left | right | down |
| --- | --- | --- | --- | --- |
| 1 | braille_previousLine | braille_scrollBack | braille_scrollForward | braille_scrollForward |
| 2 | braille_previousLine | braille_previousLine | braille_nextLine | braille_nextLine| 
| 3 | braille_previousLine | braille_previousLine | braille_scrollForward | braille_scrollForward| 

### Expected behavior:
Gestures as provided in gestures.ini are always mapped

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
2022.2.1 and recent alpha

#### Windows version:
Windows 11 22621.105

#### Name and version of other software in use when reproducing the issue:
n/a

#### Other information about your system:
Humanware Mantis connected via USB. Also reported by @dkager when using a Brailliant.

### Other questions
#### Does the issue still occur after restarting your computer?
Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
Yes, does not occur with 2022.2

#### If NVDA add-ons are disabled, is your problem still occurring?
Yes

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
Yes


Discussion:
Probably related to changing globalMapScripts.extend to globalMapScripts.update in scriptHandler.

Resolving patch:
diff --git a/source/braille.py b/source/braille.py
--- a/source/braille.py
+++ b/source/braille.py
@@ -2599,8 +2599,7 @@ def _getTryPorts(
 					yield match
 
 	#: Global input gesture map for this display driver.
-	#: @type: L{inputCore.GlobalGestureMap}
-	gestureMap = None
+	gestureMap: Optional[inputCore.GlobalGestureMap] = None
 
 	@classmethod
 	def _getModifierGestures(cls, model=None):
diff --git a/source/inputCore.py b/source/inputCore.py
--- a/source/inputCore.py
+++ b/source/inputCore.py
@@ -13,7 +13,16 @@
 import os
 import weakref
 import time
-from typing import Dict, Any, Tuple, List, Union
+from typing import (
+	Any,
+	Dict,
+	Generator,
+	List,
+	Optional,
+	Tuple,
+	TypeVar,
+	Union,
+)
 from gui import blockAction
 import configobj
 from speech import sayAll
@@ -32,6 +41,16 @@
 import controlTypes
 import winKernel
 
+
+InputGestureBindingClassT = TypeVar("InputGestureBindingClassT")
+ScriptNameT = str
+InputGestureScriptT = Tuple[InputGestureBindingClassT, Optional[ScriptNameT]]
+"""
+The Python class and script name for each script;
+the script name may be C{None} indicating that the gesture should be unbound for this class.
+"""
+
+
 #: Script category for emulated keyboard keys.
 # Translators: The name of a category of NVDA commands.
 SCRCAT_KBEMU = _("Emulated system keyboard keys")
@@ -321,13 +340,11 @@ def update(self, entries):
 						self.lastUpdateContainedError = True
 						continue
 
-	def getScriptsForGesture(self, gesture):
+	def getScriptsForGesture(self, gesture: str) -> Generator[InputGestureScriptT, None, None]:
 		"""Get the scripts associated with a particular gesture.
 		@param gesture: The gesture identifier.
-		@type gesture: str
 		@return: The Python class and script name for each script;
 			the script name may be C{None} indicating that the gesture should be unbound for this class.
-		@rtype: generator of (class, str)
 		"""
 		try:
 			scripts = self._map[gesture]
@@ -845,9 +862,10 @@ def getDisplayTextForGestureIdentifier(identifier):
 		raise
 		raise LookupError("Couldn't get display text for identifier: %s" % identifier)
 
+
 #: The singleton input manager instance.
-#: @type: L{InputManager}
-manager = None
+manager: Optional[InputManager] = None
+
 
 def initialize():
 	"""Initializes input core, creating a global L{InputManager} singleton.
diff --git a/source/scriptHandler.py b/source/scriptHandler.py
--- a/source/scriptHandler.py
+++ b/source/scriptHandler.py
@@ -1,5 +1,5 @@
 # A part of NonVisual Desktop Access (NVDA)
-# Copyright (C) 2007-2022 NV Access Limited, Babbage B.V., Julien Cochuyt
+# Copyright (C) 2007-2022 NV Access Limited, Babbage B.V., Julien Cochuyt, Leonard de Ruijter
 # This file is covered by the GNU General Public License.
 # See the file COPYING for more details.
 
@@ -8,7 +8,6 @@
 	Optional,
 	Iterator,
 	List,
-	Set,
 )
 import time
 import weakref
@@ -49,8 +48,13 @@ def _makeKbEmulateScript(scriptName):
 def _getObjScript(
 		obj: "NVDAObjects.NVDAObject",
 		gesture: "inputCore.InputGesture",
-		globalMapScripts: Set[_ScriptFunctionT],
+		globalMapScripts: List["inputCore.InputGestureScriptT"],
 ) -> Optional[_ScriptFunctionT]:
+	"""
+	@param globalMapScripts: An ordered list of scripts.
+	The list is ordered by resolution priority,
+	the first map in the list should be used to resolve the script first.
+	"""
 	# Search the scripts from the global gesture maps.
 	for cls, scriptName in globalMapScripts:
 		if isinstance(obj, cls):
@@ -72,15 +76,20 @@ def _getObjScript(
 		log.exception()
 
 
-def getGlobalMapScripts(gesture: "inputCore.InputGesture") -> Set[_ScriptFunctionT]:
-	globalMapScripts: Set[_ScriptFunctionT] = set()
+def getGlobalMapScripts(gesture: "inputCore.InputGesture") -> List["inputCore.InputGestureScriptT"]:
+	"""
+	@returns: An ordered list of scripts.
+	The list is ordered by resolution priority,
+	the first map in the list should be used to resolve scripts first.
+	"""
+	globalMapScripts: List["inputCore.InputGestureScriptT"] = []
 	globalMaps = [inputCore.manager.userGestureMap, inputCore.manager.localeGestureMap]
 	globalMap = braille.handler.display.gestureMap if braille.handler and braille.handler.display else None
 	if globalMap:
 		globalMaps.append(globalMap)
 	for globalMap in globalMaps:
 		for identifier in gesture.normalizedIdentifiers:
-			globalMapScripts.update(globalMap.getScriptsForGesture(identifier))
+			globalMapScripts.extend(globalMap.getScriptsForGesture(identifier))
 	return globalMapScripts
 
 


Test patch: