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:
@CyrilleB79 Could you attach a log snippet for the error please?
Yes. Sorry. I had prepared the log but have forgotten to attach it to this issue.
I'll attach it when I am back on my home's computer tonight.

Here is the log: 
[nvda.log](https://github.com/nvaccess/nvda/files/6869875/nvda.log)

The first error is line 403.

Thanks @CyrilleB79
Here is the error / stack for convenience:
```
DEBUG - gui.inputGestures._GesturesTree.getData (23:10:05.261) - MainThread (18736):
Getting data for item indexes (0, None, None)
ERROR - gui.inputGestures._GesturesTree.getData (23:10:05.264) - MainThread (18736):
Exceeded expected categories bounds, trying to access category with index 0, but only 0 exist.
Stack trace:
  File "nvda.pyw", line 359, in <module>
  File "core.pyc", line 750, in main
  File "wx\core.pyc", line 2237, in MainLoop
  File "gui\inputGestures.pyc", line 626, in onFilterChange
  File "gui\inputGestures.pyc", line 634, in filter
  File "gui\inputGestures.pyc", line 541, in doRefresh
  File "wx\lib\mixins\treemixin.pyc", line 371, in RefreshItems
  File "wx\lib\mixins\treemixin.pyc", line 397, in RefreshChildrenRecursively
  File "gui\inputGestures.pyc", line 639, in onTreeSelect
  File "gui\inputGestures.pyc", line 642, in _refreshButtonState
  File "gui\inputGestures.pyc", line 489, in getSelectedItemData
  File "gui\inputGestures.pyc", line 505, in getData
ERROR - unhandled exception (23:10:05.265) - MainThread (18736):
Traceback (most recent call last):
  File "gui\inputGestures.pyc", line 639, in onTreeSelect
  File "gui\inputGestures.pyc", line 642, in _refreshButtonState
  File "gui\inputGestures.pyc", line 489, in getSelectedItemData
  File "gui\inputGestures.pyc", line 499, in getData
IndexError: list index out of range
```

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: