Repository: nvaccess/nvda

Issue:
focus existing NVDA Settings dialog if the same dialog is attempted to be opened again instead of presenting Error dialog
**Reported by bhavyashah on 2015-09-21 16:57**
STR
- Press NVDA + Ctrl + V to open the NVDA Voice Settings dialog.
- Now, retain your position or go elsewhere, and press NVDA + Ctrl + V once again.
  Expected Result: Focus should move to the Voice Settings dialog itself, because, apparently, the user wishes to access the same dialog.
  Actual Result: The Error dialog is presented informing the user that an NVDA settings dialog is already open and only upon it closure can another dialog be opened.
  My conclusive request here is that the focus move to the dialog itself when one tries to open it, unaware that it is already open. The user's ultimate intention is to access that dialog and he might still prefer the keystroke over Alt Tabbing to it if too many windows are open.
  In my case, I prefer to always keep the Voice Settings dialog open alongside any work I do, because background noise around me tends to fluctuate. Accordingly, I continuously and conveniently change the volume of NVDA's speech. While closing multiple windows, or due to other reasons, I sometimes close the dialog, but, when it is already open, the Error dialog quite a bit annoys me, because it simply informs me that it is already open, and I still need to navigate to it manually.
  I hope my request has been clearly expressed and substantially justified.



Discussion:
Comment 1 by jteh on 2015-09-22 06:18
Sounds reasonable. Could be tricky to implement, though.

Comment 2 by jteh on 2015-09-22 11:44
This request is still valid, but just in case you're not aware, you could quickly change the volume without using the Voice Settings dialog at all using the Synth Settings Ring.

Comment 3 by briang1 on 2015-09-22 11:47
Hmm, you would need a new variable  unless the  opened dialogue already has a persistant  one for this purpose and check it in the error generation code and decide what to do from that.

I'd not know how to do this in Python though.

Comment 4 by jteh on 2015-09-22 11:51
Having a variable with the current instance is relatively trivial. My concern is that we may end up with a circular reference and circular references can cause memory leaks in Python if not managed properly. Thus, this would need some pretty careful testing.

@feerrenrut, any thoughts about this?
I haven't thought this through at all, but my first thought on how to approach this would be replacing the boolean `_hasInstance` with a composite type, including the class type of the instance (and `None` when there is none) and a function to give it focus, perhaps even a function to close it. Then the check can tell if the dialog attempting to be opened is the same as already opened and give it focus. This approach could also help when exiting of NVDA is blocked due to an open dialog. Care will need to be taken in the exit case, to avoid losing unsaved changes.
Now that we have a combined settings dialog, does that make implementing this easier?  I can see a situation where even with the dialog still focussed, you might be in the keyboard settings section and want to go to the voice settings, so you press NVDA+control+v.  Ideally this should simply change to that settings page.
I think it shouldn't be very difficult to do indeed.
UX proposal:
When attempting to open an NVDA dialog while one is already open:
- If this is the same dialog (including a different category in settings), just bring it forward and focus it (eventually switching categories)
- if this is a different dialog, show the current error message, but offer the possibility to focus the previously opened dialog with OK or to cancel the request - as is currently done by pressing OK.

What do you think?
Is there any new progress on this issue? I think this kind of error prompt will make people feel unacceptable and confused. If this is improved, it will undoubtedly improve the user experience to a large extent.

Resolving patch:
diff --git a/source/gui/__init__.py b/source/gui/__init__.py
--- a/source/gui/__init__.py
+++ b/source/gui/__init__.py
@@ -145,10 +145,8 @@ def _popupSettingsDialog(self, dialog, *args, **kwargs):
 		self.prePopup()
 		try:
 			dialog(self, *args, **kwargs).Show()
-		except SettingsDialog.MultiInstanceError:
-			# Translators: Message shown when attempting to open another NVDA settings dialog when one is already open
-			# (example: when trying to open keyboard settings when general settings dialog is open).
-			messageBox(_("An NVDA settings dialog is already open. Please close it first."),_("Error"),style=wx.OK | wx.ICON_ERROR)
+		except SettingsDialog.MultiInstanceErrorWithDialog as errorWithDialog:
+			errorWithDialog.dialog.SetFocus()
 		except MultiCategorySettingsDialog.CategoryUnavailableError:
 			# Translators: Message shown when trying to open an unavailable category of a multi category settings dialog
 			# (example: when trying to open touch interaction settings on an unsupported system).
diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py
--- a/source/gui/settingsDialogs.py
+++ b/source/gui/settingsDialogs.py
@@ -85,6 +85,13 @@ class SettingsDialog(
 
 	class MultiInstanceError(RuntimeError): pass
 
+	class MultiInstanceErrorWithDialog(MultiInstanceError):
+		dialog: 'SettingsDialog'
+
+		def __init__(self, dialog: 'SettingsDialog', *args: object) -> None:
+			self.dialog = dialog
+			super().__init__(*args)
+
 	class DialogState(IntEnum):
 		CREATED = 0
 		DESTROYED = 1
@@ -110,7 +117,10 @@ def __new__(cls, *args, **kwargs):
 				"State of _instances {!r}".format(multiInstanceAllowed, instancesState)
 			)
 		if state is cls.DialogState.CREATED and not multiInstanceAllowed:
-			raise SettingsDialog.MultiInstanceError("Only one instance of SettingsDialog can exist at a time")
+			raise SettingsDialog.MultiInstanceErrorWithDialog(
+				firstMatchingInstance,
+				"Only one instance of SettingsDialog can exist at a time",
+			)
 		if state is cls.DialogState.DESTROYED and not multiInstanceAllowed:
 			# the dialog has been destroyed by wx, but the instance is still available. This indicates there is something
 			# keeping it alive.


Test patch: