Repository: nvaccess/nvda

Issue:
Error in the Input gesture manager allowing to open many instances of them
### Steps to reproduce:

* Open the Input gesture manager
* Shift-tab back in the filter field
* Type a string with a typo leading to no result in the tree, e.g. "zzzz"
* Press Escape to close the dialog
* Open the input gesture manager dialog
* Open again the input gesture manager dialog, without closing the previous instance.

### Actual behavior:

* First errors in the log appear as soon as you type "zzzz"
* Two or more instances of the input gesture manager are opened

### Expected behavior:

* No error in the log
* There should not be a way to open two instances of the input gesture manager and the usual message should appear.

### System configuration
#### NVDA installed/portable/running from source:
Installed

#### NVDA version:
2021.1

#### Windows version:
Windows 10 2004 (64-bit) build 19041.388

#### Name and version of other software in use when reproducing the issue:

#### Other information about your system:

### 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.
2020.4 does not have this issue.

#### If 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?
Not tested


Maybe a regression with wxPython upgrade?
Cc @seanbudd 



Discussion:

Resolving patch:
diff --git a/source/gui/inputGestures.py b/source/gui/inputGestures.py
--- a/source/gui/inputGestures.py
+++ b/source/gui/inputGestures.py
@@ -432,7 +432,7 @@ def __init__(self, parent, gesturesVM: _InputGesturesViewModel):
 
 	def OnGetChildrenCount(self, index: Tuple[int, ...]) -> int:
 		filteredGesturesVM = self.gesturesVM.filteredGestures
-		if not index:  # Root node
+		if not index or not len(filteredGesturesVM):  # An empty index indicates the root node is requested
 			return len(filteredGesturesVM)
 		catIndex = index[0]
 		categoryVM = filteredGesturesVM[catIndex]
@@ -452,7 +452,7 @@ def OnGetChildrenCount(self, index: Tuple[int, ...]) -> int:
 	def OnGetItemText(self, index: Tuple[int, ...], column: int = 0) -> str:
 		filteredGesturesVM = self.gesturesVM.filteredGestures
 
-		assert len(index) >= 1
+		assert len(index) >= 1 and len(filteredGesturesVM) >= 1
 		catIndex = index[0]
 		catVM = filteredGesturesVM[catIndex]
 		if len(index) == 1:  # Get the display name of a category
@@ -480,9 +480,17 @@ def OnGetItemText(self, index: Tuple[int, ...], column: int = 0) -> str:
 		gesture = scriptVm.gestures[gestureIndex]
 		return gesture.displayName
 
-	def getSelectedItemData(self) -> _VmSelection:
+	def getSelectedItemData(self) -> Optional[_VmSelection]:
 		selection = self.GetSelection()
-		selIdx: Tuple[int, ...] = self.GetIndexOfItem(selection)
+		try:
+			selIdx: Tuple[int, ...] = self.GetIndexOfItem(selection)
+		except AssertionError:
+			# If item.IsOK() fails on this item or any parents indexed, an assertion error is thrown. (#12673)
+			log.debugWarning(
+				"",
+				exc_info=True,
+			)
+			return None
 		# ensure that the length of tuple is 3, missing elements replaced with None
 		nonesForMissingElements = ((None, ) * (3 - len(selIdx)))
 		selIdx: Tuple[int, Optional[int], Optional[int]] = selIdx + nonesForMissingElements
@@ -491,8 +499,11 @@ def getSelectedItemData(self) -> _VmSelection:
 	def getData(
 			self,
 			index: Tuple[int, Optional[int], Optional[int]]
-	) -> _VmSelection:
+	) -> Optional[_VmSelection]:
 		assert 3 == len(index) and index[0] is not None
+		if len(self.gesturesVM.filteredGestures) == 0:
+			log.debug("No filtered gestures available.")
+			return None
 		catIndex, scriptIndex, gestureIndex = index
 		log.debug(f"Getting data for item indexes {index}")
 		try:
@@ -640,8 +651,11 @@ def onTreeSelect(self, evt):
 
 	def _refreshButtonState(self):
 		selectedItems = self.tree.getSelectedItemData()
-		# get the leaf of the selection
-		item = next((item for item in reversed(selectedItems) if item is not None), None)
+		if selectedItems is None:
+			item = None
+		else:
+			# get the leaf of the selection
+			item = next((item for item in reversed(selectedItems) if item is not None), None)
 		pendingAdd = self.gesturesVM.isExpectingNewEmuGesture or self.gesturesVM.isExpectingNewGesture
 		self.addButton.Enabled = bool(item and item.canAdd and not pendingAdd)
 		self.removeButton.Enabled = bool(item and item.canRemove and not pendingAdd)
@@ -651,7 +665,9 @@ def onAdd(self, evt):
 			# don't add while already in process of adding.
 			return
 
-		catVM, scriptVM, gestureVM = self.tree.getSelectedItemData()
+		selectedItems = self.tree.getSelectedItemData()
+		assert selectedItems is not None
+		catVM, scriptVM, gestureVM = selectedItems
 		log.debug(f"selection: {catVM}, {scriptVM}, {gestureVM}")
 
 		if scriptVM is None and isinstance(catVM, _EmuCategoryVM):
@@ -740,7 +756,9 @@ def _addCapturedKbEmu(self, gesture: inputCore.InputGesture, catVM: _EmulatedGes
 		self._refreshButtonState()
 
 	def onRemove(self, evt):
-		catVM, scriptVM, gestureVM = self.tree.getSelectedItemData()
+		selectedItems = self.tree.getSelectedItemData()
+		assert selectedItems is not None
+		catVM, scriptVM, gestureVM = selectedItems
 		if gestureVM is not None:  # removing a gesture
 			scriptVM.removeGesture(gestureVM)
 		elif isinstance(scriptVM, _EmulatedGestureVM):  # removing a emulated KB gesture


Test patch: