Repository: nvaccess/nvda

Issue:
NVDA2019.3beta2 - Cannot close settings window on Vision panel

### Steps to reproduce:

* Open the Vision panel in the settings window
* Check the "Enable Highlighting" checkbox
* Click "Apply" button (without closing the window)
* Uncheck the "Enable Highlighting" checkbox
* Press "Escape" to cancel last action (i.e. unchecking the checkbox)

### Actual behavior:

NVDA error sound is heard and the settings Window does not close. There is then no way to close it unless restarting NVDA.

Moreover, we are then in an unexpected state where you can open many instances of the setting windows, what is usually forbidden.

The error is the following:

> IO - inputCore.InputManager.executeGesture (15:25:06.255) - winInputHook (2852):
> Input: kb(desktop):escape
> DEBUG - gui.settingsDialogs.SettingsDialog._setInstanceDestroyedState (15:25:06.255) - MainThread (11732):
> Setting state to destroyed for instance: <gui.settingsDialogs.NVDASettingsDialog object at 0x036B88A0>
> Current _instances {<gui.settingsDialogs.NVDASettingsDialog object at 0x036B88A0>: 0}
> DEBUG - autoSettingsUtils.autoSettings.AutoSettings._loadSpecificSettings (15:25:06.271) - MainThread (11732):
> loading vision screenCurtain
> DEBUG - autoSettingsUtils.autoSettings.AutoSettings._loadSpecificSettings (15:25:06.271) - MainThread (11732):
> Loaded settings for ScreenCurtainSettings
> DEBUG - autoSettingsUtils.autoSettings.AutoSettings._loadSpecificSettings (15:25:06.271) - MainThread (11732):
> loading vision NVDAHighlighter
> DEBUG - autoSettingsUtils.autoSettings.AutoSettings._loadSpecificSettings (15:25:06.271) - MainThread (11732):
> Loaded settings for NVDAHighlighterSettings
> ERROR - unhandled exception (15:25:06.271) - MainThread (11732):
> Traceback (most recent call last):
>   File "gui\settingsDialogs.pyc", line 633, in onCancel
>   File "gui\settingsDialogs.pyc", line 3344, in onDiscard
>   File "gui\settingsDialogs.pyc", line 3298, in safeInitProviders
> AttributeError: __enter__

### Expected behavior:

Settings window should be closed correctly

### Additional information

There are other ways to reproduce the issue, e.g.:

* Clicking on "Cancel" instead of pressing "Escape"
* Closing the settings window with "OK" after checking the checkbox and re-open it to make the cancelled uncheck.
* Use the Screen curtain checkbox instead of the Highlighter one.


### Analysis

The error occurs in settingsDialogs.py file on the following line:

`with VisionProviderStateControl(self, provider) as control:`

Indeed the class VisionProviderStateControl does not implement a context manager (__enter__ and __exit__ methods), and thus, cannot be used with the "with" keyword. The goal seemed to avoid any possible exception. If this is the case, a try/except would be more suitable. Moreover, we should guarantee that the "success" variable is assigned even if an exception occurs.

I am not totally sure how to fix the issue so I did not provide a PR to fix it.


### System configuration
#### NVDA installed/portable/running from source:

Installed and portable

#### NVDA version:

NVDA 2019.3beta2

#### Windows version:

Win 10 1809

#### Name and version of other software in use when reproducing the issue:

N/A

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your PC?

Yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.

No



Discussion:
Cc @feerrenrut 


Resolving patch:
diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py
--- a/source/gui/settingsDialogs.py
+++ b/source/gui/settingsDialogs.py
@@ -3163,7 +3163,7 @@ def __init__(
 			providerInfo: vision.providerInfo.ProviderInfo
 	):
 		self._providerInfo = providerInfo
-		self._parent = parent
+		self._parent = weakref.ref(parent)  # don't keep parent dialog alive with a circular reference.
 
 	def getProviderInfo(self) -> vision.providerInfo.ProviderInfo:
 		return self._providerInfo
@@ -3181,7 +3181,7 @@ def startProvider(
 		"""
 		success = self._doStartProvider()
 		if not success and shouldPromptOnError:
-			showStartErrorForProviders(self._parent, [self._providerInfo, ])
+			showStartErrorForProviders(self._parent(), [self._providerInfo, ])
 		return success
 
 	def terminateProvider(
@@ -3194,7 +3194,7 @@ def terminateProvider(
 		"""
 		success = self._doTerminate()
 		if not success and shouldPromptOnError:
-			showTerminationErrorForProviders(self._parent, [self._providerInfo, ])
+			showTerminationErrorForProviders(self._parent(), [self._providerInfo, ])
 		return success
 
 	def _doStartProvider(self) -> bool:
@@ -3295,8 +3295,7 @@ def safeInitProviders(
 		"""
 		errorProviders: List[vision.providerInfo.ProviderInfo] = []
 		for provider in providers:
-			with VisionProviderStateControl(self, provider) as control:
-				success = control.startProvider(shouldPromptOnError=False)
+			success = VisionProviderStateControl(self, provider).startProvider(shouldPromptOnError=False)
 			if not success:
 				errorProviders.append(provider)
 		showStartErrorForProviders(self, errorProviders)
diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py
--- a/source/vision/visionHandler.py
+++ b/source/vision/visionHandler.py
@@ -99,10 +99,30 @@ def postGuiInit(self) -> None:
 
 	_allProviders: List[providerInfo.ProviderInfo] = []
 
+	def _getBuiltInProviderIds(self):
+		from visionEnhancementProviders.NVDAHighlighter import NVDAHighlighterSettings
+		from visionEnhancementProviders.screenCurtain import ScreenCurtainSettings
+		return [
+			NVDAHighlighterSettings.getId(),
+			ScreenCurtainSettings.getId()
+		]
+
 	def _updateAllProvidersList(self):
-		self._allProviders = list(_getProvidersFromFileSystem())
-		# Sort the providers alphabetically by name.
-		self._allProviders.sort(key=lambda info: info.displayName.lower())
+		# Sort the providers alphabetically by id.
+		# id is used because it will not vary by locale
+		allProviders = sorted(
+			_getProvidersFromFileSystem(),
+			key=lambda info: info.providerId.lower()
+		)
+		# Built in providers should come first
+		# Python list.sort is stable sort again by 'built-in'
+		builtInProviderIds = self._getBuiltInProviderIds()
+		allProviders = sorted(
+			allProviders,
+			key=lambda info: info.providerId in builtInProviderIds,
+			reverse=True  # Because False comes before True, we want built-ins first.
+		)
+		self._allProviders = list(allProviders)
 
 	def getProviderList(
 			self,
@@ -231,8 +251,14 @@ def initializeProvider(
 				raise exceptions.ProviderInitException(
 					f"Trying to initialize provider {providerId} which reported being unable to start"
 				)
-			# Initialize the provider.
-			providerInst = providerCls()
+			try:
+				# Initialize the provider.
+				providerInst = providerCls()
+			except Exception as e:
+				# Disable the provider, so that it does not error every startup.
+				providerCls.enableInConfig(False)
+				log.warning(f"Error initialising {providerId}. Disabling in config.")
+				raise e
 			# Register extension points.
 			try:
 				providerInst.registerEventExtensionPoints(self.extensionPoints)
diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py
--- a/source/visionEnhancementProviders/NVDAHighlighter.py
+++ b/source/visionEnhancementProviders/NVDAHighlighter.py
@@ -253,7 +253,7 @@ def __init__(
 			providerControl: VisionProviderStateControl
 	):
 		self._providerControl = providerControl
-		initiallyEnabledInConfig = providerControl._providerInfo.providerClass.isEnabledInConfig()
+		initiallyEnabledInConfig = NVDAHighlighter.isEnabledInConfig()
 		if not initiallyEnabledInConfig:
 			settingsStorage = self._getSettingsStorage()
 			settingsToCheck = [
@@ -316,50 +316,57 @@ def onPanelActivated(self):
 		self.lastControl = self.optionsText
 
 	def _updateEnabledState(self):
-		settings = self._getSettingsStorage()
+		settingsStorage = self._getSettingsStorage()
 		settingsToTriggerActivation = [
-			settings.highlightBrowseMode,
-			settings.highlightFocus,
-			settings.highlightNavigator,
+			settingsStorage.highlightBrowseMode,
+			settingsStorage.highlightFocus,
+			settingsStorage.highlightNavigator,
 		]
-		if any(settingsToTriggerActivation):
-			if all(settingsToTriggerActivation):
-				self._enabledCheckbox.Set3StateValue(wx.CHK_CHECKED)
-			else:
-				self._enabledCheckbox.Set3StateValue(wx.CHK_UNDETERMINED)
-			self._ensureEnableState(True)
+		isAnyEnabled = any(settingsToTriggerActivation)
+		if all(settingsToTriggerActivation):
+			self._enabledCheckbox.Set3StateValue(wx.CHK_CHECKED)
+		elif isAnyEnabled:
+			self._enabledCheckbox.Set3StateValue(wx.CHK_UNDETERMINED)
 		else:
 			self._enabledCheckbox.Set3StateValue(wx.CHK_UNCHECKED)
-			self._ensureEnableState(False)
 
-	def _ensureEnableState(self, shouldBeEnabled: bool):
+		if not self._ensureEnableState(isAnyEnabled) and isAnyEnabled:
+			self._onEnableFailure()
+
+	def _onEnableFailure(self):
+		""" Initialization of Highlighter failed. Reset settings / GUI
+		"""
+		settingsStorage = self._getSettingsStorage()
+		settingsStorage.highlightBrowseMode = False
+		settingsStorage.highlightFocus = False
+		settingsStorage.highlightNavigator = False
+		self.updateDriverSettings()
+		self._updateEnabledState()
+
+	def _ensureEnableState(self, shouldBeEnabled: bool) -> bool:
 		currentlyEnabled = bool(self._providerControl.getProviderInstance())
 		if shouldBeEnabled and not currentlyEnabled:
-			self._providerControl.startProvider()
+			return self._providerControl.startProvider()
 		elif not shouldBeEnabled and currentlyEnabled:
-			self._providerControl.terminateProvider()
+			return self._providerControl.terminateProvider()
+		return True
 
 	def _onCheckEvent(self, evt: wx.CommandEvent):
 		settingsStorage = self._getSettingsStorage()
 		if evt.GetEventObject() is self._enabledCheckbox:
-			settingsStorage.highlightBrowseMode = evt.IsChecked()
-			settingsStorage.highlightFocus = evt.IsChecked()
-			settingsStorage.highlightNavigator = evt.IsChecked()
-			self._ensureEnableState(evt.IsChecked())
+			isEnableAllChecked = evt.IsChecked()
+			settingsStorage.highlightBrowseMode = isEnableAllChecked
+			settingsStorage.highlightFocus = isEnableAllChecked
+			settingsStorage.highlightNavigator = isEnableAllChecked
+			if not self._ensureEnableState(isEnableAllChecked) and isEnableAllChecked:
+				self._onEnableFailure()
 			self.updateDriverSettings()
 		else:
 			self._updateEnabledState()
+
 		providerInst: Optional[NVDAHighlighter] = self._providerControl.getProviderInstance()
 		if providerInst:
 			providerInst.refresh()
-		elif evt.IsChecked():
-			# One or more check boxes are enabled, so the provider instance must be there.
-			# Yet, there is no instance. This must be a case where initialization failed.
-			settingsStorage.highlightBrowseMode = False
-			settingsStorage.highlightFocus = False
-			settingsStorage.highlightNavigator = False
-			self.updateDriverSettings()
-			self._updateEnabledState()
 
 
 class NVDAHighlighter(providerBase.VisionEnhancementProvider):
@@ -432,11 +439,10 @@ def _run(self):
 		try:
 			if vision._isDebug():
 				log.debug("Starting NVDAHighlighter thread")
-			try:
-				window = self._window = self.customWindowClass(self)
-				timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None)
-			finally:
-				self._highlighterRunningEvent.set()
+
+			window = self._window = self.customWindowClass(self)
+			timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None)
+			self._highlighterRunningEvent.set()  # notify main thread that initialisation was successful
 			msg = MSG()
 			# Python 3.8 note, Change this to use an Assignment expression to catch a return value of -1.
 			# See the remarks section of


Test patch: