From 95842084024fc4ec46022a91e3588cc44162899f Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 20 Jul 2024 17:20:46 -0400 Subject: [PATCH 01/29] first draft at dark mode support for settings dialog --- source/gui/settingsDialogs.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9e762329a35..c1a43ddcc56 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -56,6 +56,7 @@ from typing import ( Any, Callable, + Generator, List, Optional, Set, @@ -85,6 +86,24 @@ PANEL_DESCRIPTION_WIDTH = 544 +def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: + yield widget + if hasattr(widget, "GetChildren"): + for child in widget.GetChildren(): + for descendant in _getDescendants(child): + yield descendant + + +def _applyDarkMode(widget : wx.Window): + if wx.SystemSettings.GetAppearance().IsDark(): + fgColor, bgColor = "White", "Dark Grey" + else: + fgColor, bgColor = "Black", "White" + for child in _getDescendants(widget): + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) + + class SettingsDialog( DpiScalingHelperMixinWithoutInit, gui.contextHelp.ContextHelpMixin, @@ -248,6 +267,8 @@ def __init__( # destroyed. self.Bind(wx.EVT_WINDOW_DESTROY, self._onWindowDestroy) + _applyDarkMode(self) + self.postInit() if resizeable: self.SetMinSize(self.mainSizer.GetMinSize()) @@ -368,6 +389,7 @@ def __init__(self, parent: wx.Window): super().__init__(parent) self._buildGui() + _applyDarkMode(self) if gui._isDebug(): elapsedSeconds = time.time() - startTime @@ -391,7 +413,7 @@ def makeSettings(self, sizer: wx.BoxSizer): raise NotImplementedError def onPanelActivated(self): - """Called after the panel has been activated (i.e. de corresponding category is selected in the list of categories). + """Called after the panel has been activated (i.e. the corresponding category is selected in the list of categories). For example, this might be used for resource intensive tasks. Sub-classes should extend this method. """ From c1abae382c9544b5f48cef331e7e6a1ce51b8e0c Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 20 Jul 2024 17:25:59 -0400 Subject: [PATCH 02/29] respect custom themes that have bg darker than fg, even if they aren't officially "dark" --- source/gui/settingsDialogs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index c1a43ddcc56..f386cf1d423 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -95,7 +95,8 @@ def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: def _applyDarkMode(widget : wx.Window): - if wx.SystemSettings.GetAppearance().IsDark(): + systemAppearance : wx.SystemAppearance = wx.SystemSettings.GetAppearance() + if systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground(): fgColor, bgColor = "White", "Dark Grey" else: fgColor, bgColor = "Black", "White" From af236924ed045ed8361ec506628d84a41c864f70 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 20 Jul 2024 19:43:57 -0400 Subject: [PATCH 03/29] generalize dark mode beyond the settings dialogs to several other dialogs --- source/gui/configProfiles.py | 4 ++++ source/gui/exit.py | 1 + source/gui/guiHelper.py | 20 ++++++++++++++++++++ source/gui/logViewer.py | 2 ++ source/gui/settingsDialogs.py | 23 ++--------------------- source/gui/speechDict.py | 2 ++ source/gui/startupDialogs.py | 4 ++++ 7 files changed, 35 insertions(+), 21 deletions(-) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index 7fcc16421a2..4b051ff6aa9 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -120,6 +120,7 @@ def __init__(self, parent): self.Sizer = mainSizer self.profileList.SetFocus() self.CentreOnScreen() + guiHelper.enableDarkMode(self) def __del__(self): ProfilesDialog._instance = None @@ -407,6 +408,7 @@ def __init__(self, parent): mainSizer.Fit(self) self.Sizer = mainSizer self.CentreOnScreen() + guiHelper.enableDarkMode(self) def onTriggerListChoice(self, evt): trig = self.triggers[self.triggerList.Selection] @@ -482,6 +484,8 @@ def __init__(self, parent): self.Sizer = mainSizer self.profileName.SetFocus() self.CentreOnScreen() + # Note: we don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support + # changing the foreground color def onOk(self, evt): confTrigs = config.conf.triggersToProfiles diff --git a/source/gui/exit.py b/source/gui/exit.py index 53d95425458..95fe47d1483 100644 --- a/source/gui/exit.py +++ b/source/gui/exit.py @@ -120,6 +120,7 @@ def __init__(self, parent): self.Sizer = mainSizer self.actionsList.SetFocus() self.CentreOnScreen() + guiHelper.enableDarkMode(self) def onOk(self, evt): action = [a for a in _ExitAction if a.displayString == self.actionsList.GetStringSelection()][0] diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index eb8a05ed1bf..80ccd7df4e9 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -46,6 +46,7 @@ def __init__(self, parent): from contextlib import contextmanager import weakref from typing import ( + Generator, Generic, Optional, Type, @@ -84,6 +85,25 @@ def autoThaw(control: wx.Window): control.Thaw() +def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: + yield widget + if hasattr(widget, "GetChildren"): + for child in widget.GetChildren(): + for descendant in _getDescendants(child): + yield descendant + + +def enableDarkMode(widget : wx.Window): + systemAppearance : wx.SystemAppearance = wx.SystemSettings.GetAppearance() + if systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground(): + fgColor, bgColor = "White", "Dark Grey" + else: + fgColor, bgColor = "Black", "White" + for child in _getDescendants(widget): + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) + + class ButtonHelper(object): """Class used to ensure that the appropriate space is added between each button, whether in horizontal or vertical arrangement. This class should be used for groups of buttons. While it won't cause problems to use this class with a diff --git a/source/gui/logViewer.py b/source/gui/logViewer.py index 7e7b973a3fe..fe00bd53220 100755 --- a/source/gui/logViewer.py +++ b/source/gui/logViewer.py @@ -10,6 +10,7 @@ import gui import gui.contextHelp from gui import blockAction +import gui.guiHelper #: The singleton instance of the log viewer UI. @@ -65,6 +66,7 @@ def __init__(self, parent): self.refresh() self.outputCtrl.SetFocus() + gui.guiHelper.enableDarkMode(self) def refresh(self, evt=None): # Ignore if log is not initialized diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index f386cf1d423..77374390899 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -86,25 +86,6 @@ PANEL_DESCRIPTION_WIDTH = 544 -def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: - yield widget - if hasattr(widget, "GetChildren"): - for child in widget.GetChildren(): - for descendant in _getDescendants(child): - yield descendant - - -def _applyDarkMode(widget : wx.Window): - systemAppearance : wx.SystemAppearance = wx.SystemSettings.GetAppearance() - if systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground(): - fgColor, bgColor = "White", "Dark Grey" - else: - fgColor, bgColor = "Black", "White" - for child in _getDescendants(widget): - child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) - - class SettingsDialog( DpiScalingHelperMixinWithoutInit, gui.contextHelp.ContextHelpMixin, @@ -268,7 +249,7 @@ def __init__( # destroyed. self.Bind(wx.EVT_WINDOW_DESTROY, self._onWindowDestroy) - _applyDarkMode(self) + guiHelper.enableDarkMode(self) self.postInit() if resizeable: @@ -390,7 +371,7 @@ def __init__(self, parent: wx.Window): super().__init__(parent) self._buildGui() - _applyDarkMode(self) + guiHelper.enableDarkMode(self) if gui._isDebug(): elapsedSeconds = time.time() - startTime diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index 090ce5192cd..617ac92f1e6 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -13,6 +13,7 @@ import globalVars import gui import gui.contextHelp +import gui.guiHelper from logHandler import log import speechDictHandler @@ -78,6 +79,7 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): self.setType(speechDictHandler.ENTRY_TYPE_ANYWHERE) self.patternTextCtrl.SetFocus() self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) + # Note: don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support changing the foreground color def getType(self): typeRadioValue = self.typeRadioBox.GetSelection() diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index bf3d236a6c7..7a88c5869e2 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -15,6 +15,7 @@ import globalVars import gui from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit +import gui.guiHelper import keyboardHandler from logHandler import log import versionInfo @@ -106,6 +107,7 @@ def __init__(self, parent): self.SetSizer(mainSizer) self.kbdList.SetFocus() self.CentreOnScreen() + gui.guiHelper.enableDarkMode(self) def onOk(self, evt): layout = self.kbdNames[self.kbdList.GetSelection()] @@ -220,6 +222,7 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() + gui.guiHelper.enableDarkMode(self) def _createLicenseAgreementGroup(self) -> wx.StaticBoxSizer: # Translators: The label of the license text which will be shown when NVDA installation program starts. @@ -326,6 +329,7 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() + gui.guiHelper.enableDarkMode(self) def onYesButton(self, evt): log.debug("Usage stats gathering has been allowed") From 5fe2f7e74436a33a085ff1eaaa182e83720bfa08 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 20 Jul 2024 19:50:12 -0400 Subject: [PATCH 04/29] tidy up the diff --- source/gui/logViewer.py | 1 - source/gui/settingsDialogs.py | 3 +-- source/gui/speechDict.py | 1 - source/gui/startupDialogs.py | 1 - 4 files changed, 1 insertion(+), 5 deletions(-) diff --git a/source/gui/logViewer.py b/source/gui/logViewer.py index fe00bd53220..20c9bf3dbfe 100755 --- a/source/gui/logViewer.py +++ b/source/gui/logViewer.py @@ -10,7 +10,6 @@ import gui import gui.contextHelp from gui import blockAction -import gui.guiHelper #: The singleton instance of the log viewer UI. diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 77374390899..f2453940de2 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -249,12 +249,11 @@ def __init__( # destroyed. self.Bind(wx.EVT_WINDOW_DESTROY, self._onWindowDestroy) - guiHelper.enableDarkMode(self) - self.postInit() if resizeable: self.SetMinSize(self.mainSizer.GetMinSize()) self.CentreOnScreen() + guiHelper.enableDarkMode(self) if gui._isDebug(): log.debug("Loading %s took %.2f seconds" % (self.__class__.__name__, time.time() - startTime)) diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index 617ac92f1e6..c8b7fd05901 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -13,7 +13,6 @@ import globalVars import gui import gui.contextHelp -import gui.guiHelper from logHandler import log import speechDictHandler diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index 7a88c5869e2..85e2eefa6b2 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -15,7 +15,6 @@ import globalVars import gui from gui.dpiScalingHelper import DpiScalingHelperMixinWithoutInit -import gui.guiHelper import keyboardHandler from logHandler import log import versionInfo From 74c8134557ac3ef528980c5da355e63ab2e4d87f Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 20 Jul 2024 19:51:21 -0400 Subject: [PATCH 05/29] lint --- source/gui/guiHelper.py | 4 ++-- source/gui/settingsDialogs.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index 80ccd7df4e9..f645457ab24 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -93,8 +93,8 @@ def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: yield descendant -def enableDarkMode(widget : wx.Window): - systemAppearance : wx.SystemAppearance = wx.SystemSettings.GetAppearance() +def enableDarkMode(widget: wx.Window): + systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() if systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground(): fgColor, bgColor = "White", "Dark Grey" else: diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index f2453940de2..8eeae3dcd5d 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -56,7 +56,6 @@ from typing import ( Any, Callable, - Generator, List, Optional, Set, From 127dd936b60100b005fe5f3959d5710b5ffdf1da Mon Sep 17 00:00:00 2001 From: Tristan Date: Wed, 24 Jul 2024 21:40:43 -0400 Subject: [PATCH 06/29] Added darkmode to changes.md. --- user_docs/en/changes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index c7babad78b0..7abdbc2f0e5 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -69,6 +69,7 @@ Unicode CLDR has also been updated. * NVDA checks daily for add-on updates. * Only updates within the same channel will be checked (e.g. installed beta add-ons will only notify for updates in the beta channel). * Added support for the Help Tech Activator Pro displays. (#16668) +* Added support for dark mode. (#16683) ### Changes From 8ca57af4850649db173da40ea7e1b1f6d1aea084 Mon Sep 17 00:00:00 2001 From: Tristan Date: Sun, 28 Jul 2024 14:34:20 -0400 Subject: [PATCH 07/29] The python console, add-on store, braille viewer, and speech viewer are correctly in dark mode while the system is in dark mode. --- source/brailleViewer/brailleViewerGui.py | 1 + source/pythonConsole.py | 1 + source/speechViewer.py | 1 + 3 files changed, 3 insertions(+) diff --git a/source/brailleViewer/brailleViewerGui.py b/source/brailleViewer/brailleViewerGui.py index 93c07a92122..7204a1ee265 100644 --- a/source/brailleViewer/brailleViewerGui.py +++ b/source/brailleViewer/brailleViewerGui.py @@ -485,6 +485,7 @@ def _updateGui(self): """Ensure all GUI updates happen in one place to create a smooth update, all changes should happen between freeze and thaw. """ + gui.guiHelper.enableDarkMode(self) self.Freeze() if self._newBraille is not None: self._brailleOutput.SetValue(self._newBraille) diff --git a/source/pythonConsole.py b/source/pythonConsole.py index 80e12429e72..30d77dc6203 100755 --- a/source/pythonConsole.py +++ b/source/pythonConsole.py @@ -330,6 +330,7 @@ def __init__(self, parent): mainSizer.Add(inputSizer, proportion=1, flag=wx.EXPAND) self.SetSizer(mainSizer) mainSizer.Fit(self) + gui.guiHelper.enableDarkMode(self) self.console = PythonConsole( outputFunc=self.output, diff --git a/source/speechViewer.py b/source/speechViewer.py index e4a62860438..9b690e5b711 100644 --- a/source/speechViewer.py +++ b/source/speechViewer.py @@ -65,6 +65,7 @@ def __init__(self, onDestroyCallBack: Callable[[], None]): # Don't let speech viewer to steal keyboard focus when opened self.ShowWithoutActivating() + gui.guiHelper.enableDarkMode(self) def onSessionLockStateChange(self, isNowLocked: bool): """ From ae9599ee24819957bfd7c075df5f6f29ac03b00e Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 28 Jul 2024 14:52:14 -0400 Subject: [PATCH 08/29] link to wxPython issue for RadioButton not supporting SetForegroundColour() --- source/gui/configProfiles.py | 2 +- source/gui/speechDict.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index 4b051ff6aa9..cac38c6d525 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -485,7 +485,7 @@ def __init__(self, parent): self.profileName.SetFocus() self.CentreOnScreen() # Note: we don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support - # changing the foreground color + # changing the foreground color (https://github.com/wxWidgets/Phoenix/issues/1512) def onOk(self, evt): confTrigs = config.conf.triggersToProfiles diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index c8b7fd05901..790e0859a2d 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -78,7 +78,8 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): self.setType(speechDictHandler.ENTRY_TYPE_ANYWHERE) self.patternTextCtrl.SetFocus() self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - # Note: don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support changing the foreground color + # Note: don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support + # changing the foreground color (https://github.com/wxWidgets/Phoenix/issues/1512) def getType(self): typeRadioValue = self.typeRadioBox.GetSelection() From 81db549b968ecba43f49c6170a151e96d4271e86 Mon Sep 17 00:00:00 2001 From: Tristan Date: Sun, 28 Jul 2024 19:59:15 -0400 Subject: [PATCH 09/29] Added dark mode capabilities for create portable copy and NVDA installer in the tools menu. --- source/gui/installerGui.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index c73107459ad..209042134df 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -276,6 +276,7 @@ def __init__(self, parent, isUpdate): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() + gui.guiHelper.enableDarkMode(self) def onInstall(self, evt): self.Hide() @@ -487,6 +488,7 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() + gui.guiHelper.enableDarkMode(self) def onCreatePortable(self, evt): if not self.portableDirectoryEdit.Value: From 754e6cdd66db999f504888b10e0f4eb31c428ae8 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 28 Jul 2024 21:33:06 -0400 Subject: [PATCH 10/29] Make dark mode configurable --- source/config/configFlags.py | 21 +++++++++++++++++++++ source/config/configSpec.py | 1 + source/gui/guiHelper.py | 12 ++++++++++-- source/gui/settingsDialogs.py | 20 ++++++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/source/config/configFlags.py b/source/config/configFlags.py index 21354abcca6..0784e350cff 100644 --- a/source/config/configFlags.py +++ b/source/config/configFlags.py @@ -251,3 +251,24 @@ def _displayStringLabels(self): # Translators: A label for an option to choose a method of reporting information, e.g. font attributes. self.SPEECH_AND_BRAILLE: _("Speech and braille"), } + + +@unique +class ColorTheme(DisplayStringStrEnum): + """Enumeration for what foreground and background colors to use. + """ + + AUTO = "auto" + DARK = "dark" + LIGHT = "light" + + @property + def _displayStringLabels(self): + return { + # Translators: One of the color theme choices in the visual settings category panel (this choice uses the system's Dark Mode setting). + self.AUTO: _("Auto"), + # Translators: One of the color theme choices in the visual settings category panel (this choice uses light background with dark text). + self.LIGHT: _("Light"), + # Translators: One of the color theme choices in the visual settings category panel (this choice uses dark background with light text). + self.DARK: _("Dark"), + } diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 82a5de7eabf..03937deaaea 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -98,6 +98,7 @@ # Vision enhancement provider settings [vision] + colorTheme = option("auto", "light", "dark", default="auto") # Vision enhancement provider settings [[__many__]] diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index f645457ab24..0d32f6a4e2d 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -58,6 +58,8 @@ def __init__(self, parent): import wx from wx.lib import scrolledpanel, newevent from abc import ABCMeta +import config +from config.configFlags import ColorTheme #: border space to be used around all controls in dialogs BORDER_FOR_DIALOGS = 10 @@ -94,8 +96,14 @@ def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: def enableDarkMode(widget: wx.Window): - systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() - if systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground(): + curTheme = config.conf["vision"]["colorTheme"] + if curTheme == ColorTheme.AUTO: + systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() + isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() + else: + isDark = curTheme == ColorTheme.DARK + + if isDark: fgColor, bgColor = "White", "Dark Grey" else: fgColor, bgColor = "Black", "White" diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 8eeae3dcd5d..38906dac526 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -28,6 +28,7 @@ import config from config.configFlags import ( AddonsAutomaticUpdate, + ColorTheme, NVDAKey, ShowMessages, TetherTo, @@ -4687,6 +4688,21 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): providerSizer.Add(settingsPanel, flag=wx.EXPAND) self.providerPanelInstances.append(settingsPanel) + # Translators: label for a choice in the vision settings category panel + colorThemeLabelText = _("&Color theme") + self.colorThemeList = self.settingsSizerHelper.addLabeledControl( + colorThemeLabelText, + wx.Choice, + choices=[theme.displayString for theme in ColorTheme], + ) + self.bindHelpEvent("VisionSettingsColorTheme", self.colorThemeList) + curTheme = config.conf["vision"]["colorTheme"] + for i, theme in enumerate(ColorTheme): + if theme == curTheme: + self.colorThemeList.SetSelection(i) + else: + log.debugWarning("Could not set color theme list to current theme") + def safeInitProviders( self, providers: List[vision.providerInfo.ProviderInfo], @@ -4760,6 +4776,10 @@ def onSave(self): except Exception: log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = vision.handler.getActiveProviderInfos() + colorTheme = list(ColorTheme)[self.colorThemeList.GetSelection()] + config.conf["vision"]["colorTheme"] = colorTheme.value + guiHelper.enableDarkMode(self.TopLevelParent) + self.TopLevelParent.Refresh() class VisionProviderSubPanel_Settings( From ea49b57f1b5554432899afce03701219f06685ec Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 3 Aug 2024 12:38:05 -0400 Subject: [PATCH 11/29] use FilterEvent to apply color theme, rather than explicitly enabling it in every dialog --- source/brailleViewer/brailleViewerGui.py | 1 - source/core.py | 34 ++++++++++++++++++++++++ source/gui/configProfiles.py | 17 +++++------- source/gui/exit.py | 1 - source/gui/guiHelper.py | 25 ----------------- source/gui/installerGui.py | 2 -- source/gui/logViewer.py | 1 - source/gui/settingsDialogs.py | 3 --- source/gui/startupDialogs.py | 3 --- source/pythonConsole.py | 1 - source/speechViewer.py | 1 - 11 files changed, 40 insertions(+), 49 deletions(-) diff --git a/source/brailleViewer/brailleViewerGui.py b/source/brailleViewer/brailleViewerGui.py index 7204a1ee265..93c07a92122 100644 --- a/source/brailleViewer/brailleViewerGui.py +++ b/source/brailleViewer/brailleViewerGui.py @@ -485,7 +485,6 @@ def _updateGui(self): """Ensure all GUI updates happen in one place to create a smooth update, all changes should happen between freeze and thaw. """ - gui.guiHelper.enableDarkMode(self) self.Freeze() if self._newBraille is not None: self._brailleOutput.SetValue(self._newBraille) diff --git a/source/core.py b/source/core.py index cada5f1a45d..a4593a46e71 100644 --- a/source/core.py +++ b/source/core.py @@ -10,9 +10,11 @@ from typing import ( TYPE_CHECKING, Any, + Generator, List, Optional, ) +from config.configFlags import (ColorTheme) import comtypes import sys import winVersion @@ -589,6 +591,29 @@ def _setUpWxApp() -> "wx.App": # This is due to the wx.LogSysError dialog allowing a file explorer dialog to be opened. wx.Log.EnableLogging(not globalVars.appArgs.secure) + def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: + yield window + if hasattr(window, "GetChildren"): + for child in window.GetChildren(): + for descendant in _getDescendants(child): + yield descendant + + def _enableDarkMode(window: wx.Window): + curTheme = config.conf["vision"]["colorTheme"] + if curTheme == ColorTheme.AUTO: + systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() + isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() + else: + isDark = curTheme == ColorTheme.DARK + + if isDark: + fgColor, bgColor = "White", "Dark Grey" + else: + fgColor, bgColor = "Black", "White" + for child in _getDescendants(window): + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) + class App(wx.App): def OnAssert(self, file: str, line: str, cond: str, msg: str): message = f"{file}, line {line}:\nassert {cond}: {msg}" @@ -605,6 +630,15 @@ def InitLocale(self): """ pass + def FilterEvent(self, event:wx.Event): + """FilterEvent is called for every UI event in the entire application. Keep it quick to + avoid slowing everything down.""" + if isinstance(event, wx.ShowEvent): + window : wx.Window = event.EventObject + _enableDarkMode(window.GetTopLevelParent()) + window.Refresh() + return -1 + app = App(redirect=False) # We support queryEndSession events, but in general don't do anything for them. diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index cac38c6d525..d6358324fae 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -120,7 +120,6 @@ def __init__(self, parent): self.Sizer = mainSizer self.profileList.SetFocus() self.CentreOnScreen() - guiHelper.enableDarkMode(self) def __del__(self): ProfilesDialog._instance = None @@ -408,7 +407,6 @@ def __init__(self, parent): mainSizer.Fit(self) self.Sizer = mainSizer self.CentreOnScreen() - guiHelper.enableDarkMode(self) def onTriggerListChoice(self, evt): trig = self.triggers[self.triggerList.Selection] @@ -462,14 +460,13 @@ def __init__(self, parent): # in the new configuration profile dialog. self.triggers = triggers = [(None, _("Manual activation"), True)] triggers.extend(parent.getSimpleTriggers()) - self.triggerChoice = sHelper.addItem( - wx.RadioBox( - self, - label=_("Use this profile for:"), - choices=[trig[1] for trig in triggers], - ), + self.triggerChoice = sHelper.addLabeledControl( + _("Use this profile for:"), + wx.Choice, + choices=[trig[1] for trig in triggers], ) - self.triggerChoice.Bind(wx.EVT_RADIOBOX, self.onTriggerChoice) + self.triggerChoice.Bind(wx.EVT_CHOICE, self.onTriggerChoice) + self.triggerChoice.SetSelection(0) self.autoProfileName = "" self.onTriggerChoice(None) @@ -484,8 +481,6 @@ def __init__(self, parent): self.Sizer = mainSizer self.profileName.SetFocus() self.CentreOnScreen() - # Note: we don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support - # changing the foreground color (https://github.com/wxWidgets/Phoenix/issues/1512) def onOk(self, evt): confTrigs = config.conf.triggersToProfiles diff --git a/source/gui/exit.py b/source/gui/exit.py index 95fe47d1483..53d95425458 100644 --- a/source/gui/exit.py +++ b/source/gui/exit.py @@ -120,7 +120,6 @@ def __init__(self, parent): self.Sizer = mainSizer self.actionsList.SetFocus() self.CentreOnScreen() - guiHelper.enableDarkMode(self) def onOk(self, evt): action = [a for a in _ExitAction if a.displayString == self.actionsList.GetStringSelection()][0] diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index 0d32f6a4e2d..4a0614bf699 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -87,31 +87,6 @@ def autoThaw(control: wx.Window): control.Thaw() -def _getDescendants(widget: wx.Window) -> Generator[wx.Window, None, None]: - yield widget - if hasattr(widget, "GetChildren"): - for child in widget.GetChildren(): - for descendant in _getDescendants(child): - yield descendant - - -def enableDarkMode(widget: wx.Window): - curTheme = config.conf["vision"]["colorTheme"] - if curTheme == ColorTheme.AUTO: - systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() - isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() - else: - isDark = curTheme == ColorTheme.DARK - - if isDark: - fgColor, bgColor = "White", "Dark Grey" - else: - fgColor, bgColor = "Black", "White" - for child in _getDescendants(widget): - child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) - - class ButtonHelper(object): """Class used to ensure that the appropriate space is added between each button, whether in horizontal or vertical arrangement. This class should be used for groups of buttons. While it won't cause problems to use this class with a diff --git a/source/gui/installerGui.py b/source/gui/installerGui.py index 209042134df..c73107459ad 100644 --- a/source/gui/installerGui.py +++ b/source/gui/installerGui.py @@ -276,7 +276,6 @@ def __init__(self, parent, isUpdate): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() - gui.guiHelper.enableDarkMode(self) def onInstall(self, evt): self.Hide() @@ -488,7 +487,6 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() - gui.guiHelper.enableDarkMode(self) def onCreatePortable(self, evt): if not self.portableDirectoryEdit.Value: diff --git a/source/gui/logViewer.py b/source/gui/logViewer.py index 20c9bf3dbfe..7e7b973a3fe 100755 --- a/source/gui/logViewer.py +++ b/source/gui/logViewer.py @@ -65,7 +65,6 @@ def __init__(self, parent): self.refresh() self.outputCtrl.SetFocus() - gui.guiHelper.enableDarkMode(self) def refresh(self, evt=None): # Ignore if log is not initialized diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 38906dac526..0311c4a9423 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -253,7 +253,6 @@ def __init__( if resizeable: self.SetMinSize(self.mainSizer.GetMinSize()) self.CentreOnScreen() - guiHelper.enableDarkMode(self) if gui._isDebug(): log.debug("Loading %s took %.2f seconds" % (self.__class__.__name__, time.time() - startTime)) @@ -370,7 +369,6 @@ def __init__(self, parent: wx.Window): super().__init__(parent) self._buildGui() - guiHelper.enableDarkMode(self) if gui._isDebug(): elapsedSeconds = time.time() - startTime @@ -4778,7 +4776,6 @@ def onSave(self): self.initialProviders = vision.handler.getActiveProviderInfos() colorTheme = list(ColorTheme)[self.colorThemeList.GetSelection()] config.conf["vision"]["colorTheme"] = colorTheme.value - guiHelper.enableDarkMode(self.TopLevelParent) self.TopLevelParent.Refresh() diff --git a/source/gui/startupDialogs.py b/source/gui/startupDialogs.py index 85e2eefa6b2..bf3d236a6c7 100644 --- a/source/gui/startupDialogs.py +++ b/source/gui/startupDialogs.py @@ -106,7 +106,6 @@ def __init__(self, parent): self.SetSizer(mainSizer) self.kbdList.SetFocus() self.CentreOnScreen() - gui.guiHelper.enableDarkMode(self) def onOk(self, evt): layout = self.kbdNames[self.kbdList.GetSelection()] @@ -221,7 +220,6 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() - gui.guiHelper.enableDarkMode(self) def _createLicenseAgreementGroup(self) -> wx.StaticBoxSizer: # Translators: The label of the license text which will be shown when NVDA installation program starts. @@ -328,7 +326,6 @@ def __init__(self, parent): self.Sizer = mainSizer mainSizer.Fit(self) self.CentreOnScreen() - gui.guiHelper.enableDarkMode(self) def onYesButton(self, evt): log.debug("Usage stats gathering has been allowed") diff --git a/source/pythonConsole.py b/source/pythonConsole.py index 30d77dc6203..80e12429e72 100755 --- a/source/pythonConsole.py +++ b/source/pythonConsole.py @@ -330,7 +330,6 @@ def __init__(self, parent): mainSizer.Add(inputSizer, proportion=1, flag=wx.EXPAND) self.SetSizer(mainSizer) mainSizer.Fit(self) - gui.guiHelper.enableDarkMode(self) self.console = PythonConsole( outputFunc=self.output, diff --git a/source/speechViewer.py b/source/speechViewer.py index 9b690e5b711..e4a62860438 100644 --- a/source/speechViewer.py +++ b/source/speechViewer.py @@ -65,7 +65,6 @@ def __init__(self, onDestroyCallBack: Callable[[], None]): # Don't let speech viewer to steal keyboard focus when opened self.ShowWithoutActivating() - gui.guiHelper.enableDarkMode(self) def onSessionLockStateChange(self, isNowLocked: bool): """ From 76fd24dd24d3cefc85a1f580cca61d57c0ce957f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 16:49:28 +0000 Subject: [PATCH 12/29] Pre-commit auto-fix --- source/config/configFlags.py | 8 ++++---- source/core.py | 6 +++--- source/gui/guiHelper.py | 3 --- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/source/config/configFlags.py b/source/config/configFlags.py index d41bdac2003..a975f0942b8 100644 --- a/source/config/configFlags.py +++ b/source/config/configFlags.py @@ -255,13 +255,12 @@ def _displayStringLabels(self): @unique class ColorTheme(DisplayStringStrEnum): - """Enumeration for what foreground and background colors to use. - """ + """Enumeration for what foreground and background colors to use.""" AUTO = "auto" DARK = "dark" LIGHT = "light" - + @property def _displayStringLabels(self): return { @@ -273,12 +272,13 @@ def _displayStringLabels(self): self.DARK: _("Dark"), } + @unique class ParagraphStartMarker(DisplayStringStrEnum): NONE = "" SPACE = " " PILCROW = "¶" - + @property def _displayStringLabels(self): return { diff --git a/source/core.py b/source/core.py index d3f4a9151c6..e4382d04f50 100644 --- a/source/core.py +++ b/source/core.py @@ -14,7 +14,7 @@ List, Optional, ) -from config.configFlags import (ColorTheme) +from config.configFlags import ColorTheme import comtypes import sys import winVersion @@ -636,11 +636,11 @@ def InitLocale(self): """ pass - def FilterEvent(self, event:wx.Event): + def FilterEvent(self, event: wx.Event): """FilterEvent is called for every UI event in the entire application. Keep it quick to avoid slowing everything down.""" if isinstance(event, wx.ShowEvent): - window : wx.Window = event.EventObject + window: wx.Window = event.EventObject _enableDarkMode(window.GetTopLevelParent()) window.Refresh() return -1 diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index 4a0614bf699..eb8a05ed1bf 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -46,7 +46,6 @@ def __init__(self, parent): from contextlib import contextmanager import weakref from typing import ( - Generator, Generic, Optional, Type, @@ -58,8 +57,6 @@ def __init__(self, parent): import wx from wx.lib import scrolledpanel, newevent from abc import ABCMeta -import config -from config.configFlags import ColorTheme #: border space to be used around all controls in dialogs BORDER_FOR_DIALOGS = 10 From e16e79da7ae713c40b2c8158662c2734c6048be2 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 3 Aug 2024 16:07:46 -0400 Subject: [PATCH 13/29] Change speechDict.py so it works in dark mode --- source/gui/speechDict.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index 790e0859a2d..4968277894d 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -27,12 +27,12 @@ class DictionaryEntryDialog( helpId = "SpeechDictionaries" TYPE_LABELS = { - # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_ANYWHERE: _("&Anywhere"), - # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_WORD: _("Whole &word"), - # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_REGEXP: _("Regular &expression"), + # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + speechDictHandler.ENTRY_TYPE_ANYWHERE: _("Anywhere"), + # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + speechDictHandler.ENTRY_TYPE_WORD: _("Whole word"), + # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + speechDictHandler.ENTRY_TYPE_REGEXP: _("Regular expression"), } TYPE_LABELS_ORDERING = ( speechDictHandler.ENTRY_TYPE_ANYWHERE, @@ -63,12 +63,13 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): caseSensitiveText = _("Case &sensitive") self.caseSensitiveCheckBox = sHelper.addItem(wx.CheckBox(self, label=caseSensitiveText)) - # Translators: This is a label for a set of radio buttons in add dictionary entry dialog. + # Translators: This is a label for a dropdown list in add dictionary entry dialog. typeText = _("&Type") typeChoices = [ DictionaryEntryDialog.TYPE_LABELS[i] for i in DictionaryEntryDialog.TYPE_LABELS_ORDERING ] - self.typeRadioBox = sHelper.addItem(wx.RadioBox(self, label=typeText, choices=typeChoices)) + self.typeList = sHelper.addLabeledControl(typeText, wx.Choice, choices=typeChoices) + self.typeList.SetSelection(0) sHelper.addDialogDismissButtons(wx.OK | wx.CANCEL, separated=True) @@ -78,14 +79,12 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): self.setType(speechDictHandler.ENTRY_TYPE_ANYWHERE) self.patternTextCtrl.SetFocus() self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) - # Note: don't call guiHelper.enableDarkMode() here because wx.RadioBox doesn't support - # changing the foreground color (https://github.com/wxWidgets/Phoenix/issues/1512) def getType(self): - typeRadioValue = self.typeRadioBox.GetSelection() - if typeRadioValue == wx.NOT_FOUND: + selection = self.typeList.GetSelection() + if selection == wx.NOT_FOUND: return speechDictHandler.ENTRY_TYPE_ANYWHERE - return DictionaryEntryDialog.TYPE_LABELS_ORDERING[typeRadioValue] + return DictionaryEntryDialog.TYPE_LABELS_ORDERING[selection] def onOk(self, evt): if not self.patternTextCtrl.GetValue(): @@ -144,7 +143,7 @@ def onOk(self, evt): evt.Skip() def setType(self, type): - self.typeRadioBox.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type)) + self.typeList.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type)) class DictionaryDialog( From faaf99a73b52d6c9838ce25791e9f328aab7e273 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 3 Aug 2024 17:03:21 -0400 Subject: [PATCH 14/29] make the Apply button work again when changing color theme --- source/core.py | 28 ++-------------------------- source/gui/guiHelper.py | 27 +++++++++++++++++++++++++++ source/gui/settingsDialogs.py | 1 + 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/source/core.py b/source/core.py index e4382d04f50..45083bf8d14 100644 --- a/source/core.py +++ b/source/core.py @@ -10,11 +10,9 @@ from typing import ( TYPE_CHECKING, Any, - Generator, List, Optional, ) -from config.configFlags import ColorTheme import comtypes import sys import winVersion @@ -590,6 +588,7 @@ def _setUpWxApp() -> "wx.App": import config import nvwave import speech + import gui.guiHelper log.info(f"Using wx version {wx.version()} with six version {six.__version__}") @@ -597,29 +596,6 @@ def _setUpWxApp() -> "wx.App": # This is due to the wx.LogSysError dialog allowing a file explorer dialog to be opened. wx.Log.EnableLogging(not globalVars.appArgs.secure) - def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: - yield window - if hasattr(window, "GetChildren"): - for child in window.GetChildren(): - for descendant in _getDescendants(child): - yield descendant - - def _enableDarkMode(window: wx.Window): - curTheme = config.conf["vision"]["colorTheme"] - if curTheme == ColorTheme.AUTO: - systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() - isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() - else: - isDark = curTheme == ColorTheme.DARK - - if isDark: - fgColor, bgColor = "White", "Dark Grey" - else: - fgColor, bgColor = "Black", "White" - for child in _getDescendants(window): - child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) - class App(wx.App): def OnAssert(self, file: str, line: str, cond: str, msg: str): message = f"{file}, line {line}:\nassert {cond}: {msg}" @@ -641,7 +617,7 @@ def FilterEvent(self, event: wx.Event): avoid slowing everything down.""" if isinstance(event, wx.ShowEvent): window: wx.Window = event.EventObject - _enableDarkMode(window.GetTopLevelParent()) + gui.guiHelper.applyColorTheme(window.GetTopLevelParent()) window.Refresh() return -1 diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index eb8a05ed1bf..c57b24273df 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -46,6 +46,7 @@ def __init__(self, parent): from contextlib import contextmanager import weakref from typing import ( + Generator, Generic, Optional, Type, @@ -57,6 +58,8 @@ def __init__(self, parent): import wx from wx.lib import scrolledpanel, newevent from abc import ABCMeta +import config +from config.configFlags import ColorTheme #: border space to be used around all controls in dialogs BORDER_FOR_DIALOGS = 10 @@ -458,3 +461,27 @@ class SIPABCMeta(wx.siplib.wrappertype, ABCMeta): """Meta class to be used for wx subclasses with abstract methods.""" pass + +def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: + yield window + if hasattr(window, "GetChildren"): + for child in window.GetChildren(): + for descendant in _getDescendants(child): + yield descendant + + +def applyColorTheme(window: wx.Window): + curTheme = config.conf["vision"]["colorTheme"] + if curTheme == ColorTheme.AUTO: + systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() + isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() + else: + isDark = curTheme == ColorTheme.DARK + + if isDark: + fgColor, bgColor = "White", "Dark Grey" + else: + fgColor, bgColor = "Black", "White" + for child in _getDescendants(window): + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) \ No newline at end of file diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 56a9f0ec0a1..6b9b64b1913 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -4835,6 +4835,7 @@ def onSave(self): self.initialProviders = vision.handler.getActiveProviderInfos() colorTheme = list(ColorTheme)[self.colorThemeList.GetSelection()] config.conf["vision"]["colorTheme"] = colorTheme.value + guiHelper.applyColorTheme(self.TopLevelParent) self.TopLevelParent.Refresh() From d71db375b10e27754fbd2c5781ead134e0c4ecfa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 21:26:37 +0000 Subject: [PATCH 15/29] Pre-commit auto-fix --- source/gui/guiHelper.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index c57b24273df..7d4f74fdbf8 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -462,6 +462,7 @@ class SIPABCMeta(wx.siplib.wrappertype, ABCMeta): pass + def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: yield window if hasattr(window, "GetChildren"): @@ -484,4 +485,4 @@ def applyColorTheme(window: wx.Window): fgColor, bgColor = "Black", "White" for child in _getDescendants(window): child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) \ No newline at end of file + child.SetForegroundColour(fgColor) From 28ea90090944af6af008d7181ab3ea8fe7744ff4 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 3 Aug 2024 22:05:58 -0400 Subject: [PATCH 16/29] update user guide --- user_docs/en/userGuide.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/user_docs/en/userGuide.md b/user_docs/en/userGuide.md index c651d48ffe9..609ba24470e 100644 --- a/user_docs/en/userGuide.md +++ b/user_docs/en/userGuide.md @@ -2463,7 +2463,7 @@ The check boxes in the Visual Highlight grouping control the behaviour of NVDA's * Highlight navigator object: toggles whether the [navigator object](#ObjectNavigation) will be highlighted. * Highlight browse mode cursor: Toggles whether the [virtual browse mode cursor](#BrowseMode) will be highlighted. -Note that checking and unchecking the "Enable Highlighting" check box wil also change the state of the tree other check boxes accordingly. +Note that checking and unchecking the "Enable Highlighting" check box will also change the state of the tree other check boxes accordingly. Therefore, if "Enable Highlighting" is off and you check this check box, the other tree check boxes will also be checked automatically. If you only want to highlight the focus and leave the navigator object and browse mode check boxes unchecked, the state of the "Enable Highlighting" check box will be half checked. @@ -2480,6 +2480,14 @@ You can always restore the warning by checking the "Always show a warning when l By default, sounds are played when the Screen Curtain is toggled. When you want to change this behaviour, you can uncheck the "Play sound when toggling Screen Curtain" check box. +##### Color Theme {#VisionSettingsColorTheme} + +You can control whether NVDA dialogs use a light background (the "Light" theme) or a dark background (the "Dark" theme). +The default theme is "Auto", which causes NVDA to match your operating system's "dark mode" setting. + +You can try out a different theme by changing the selected theme and then pressing the "Apply" button. +This will cause the color of the settings dialog to update immediately. + ##### Settings for third party visual aids {#VisionSettingsThirdPartyVisualAids} Additional vision enhancement providers can be provided in [NVDA add-ons](#AddonsManager). From 35cfaa116ef327f48491082c19806efea0908174 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sat, 3 Aug 2024 22:11:10 -0400 Subject: [PATCH 17/29] update changes.md to indicate that dark mode is now configurable --- user_docs/en/changes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index c79f033c1ab..71441f60299 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -89,7 +89,9 @@ Unicode CLDR has also been updated. * NVDA checks daily for add-on updates. * Only updates within the same channel will be checked (e.g. installed beta add-ons will only notify for updates in the beta channel). * Added support for the Help Tech Activator Pro displays. (#16668) -* Added support for dark mode. (#16683) +* Added support for Light and Dark color themes. (#16683) + * The default behavior is to match your system's "dark mode" setting. + * You can change this behavior in the Visual category of the NVDA Settings dialog. ### Changes From e5e46543c578d1057f259448a2e752a1526c1a19 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 18 Aug 2024 12:10:50 -0400 Subject: [PATCH 18/29] Major refactor to use actual windows APIs --- source/config/configSpec.py | 3 +- source/core.py | 4 +- source/gui/__init__.py | 6 ++ source/gui/darkMode.py | 137 ++++++++++++++++++++++++++++++++++ source/gui/guiHelper.py | 28 ------- source/gui/settingsDialogs.py | 33 ++++++-- 6 files changed, 171 insertions(+), 40 deletions(-) create mode 100644 source/gui/darkMode.py diff --git a/source/config/configSpec.py b/source/config/configSpec.py index b515385e114..8344a69d902 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -27,6 +27,8 @@ #possible log levels are DEBUG, IO, DEBUGWARNING, INFO loggingLevel = string(default="INFO") showWelcomeDialogAtStartup = boolean(default=true) + colorTheme = option("auto", "light", "dark", default="light") + darkModeCanUseUndocumentedAPIs = boolean(default=false) # Speech settings [speech] @@ -102,7 +104,6 @@ # Vision enhancement provider settings [vision] - colorTheme = option("auto", "light", "dark", default="auto") # Vision enhancement provider settings [[__many__]] diff --git a/source/core.py b/source/core.py index 45083bf8d14..98155c1119a 100644 --- a/source/core.py +++ b/source/core.py @@ -616,9 +616,7 @@ def FilterEvent(self, event: wx.Event): """FilterEvent is called for every UI event in the entire application. Keep it quick to avoid slowing everything down.""" if isinstance(event, wx.ShowEvent): - window: wx.Window = event.EventObject - gui.guiHelper.applyColorTheme(window.GetTopLevelParent()) - window.Refresh() + gui.darkMode.applyColorTheme(event.EventObject) return -1 app = App(redirect=False) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 72d9b4fb148..0ba5d8d805b 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -810,6 +810,12 @@ def initialize(): global mainFrame if mainFrame: raise RuntimeError("GUI already initialized") + + from gui import darkMode + # Dark mode must be initialized before creating main frame + # otherwise context menus will not be styled correctly + darkMode.initialize() + mainFrame = MainFrame() wxLang = core.getWxLangOrNone() if wxLang: diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py new file mode 100644 index 00000000000..cc27d2d5b02 --- /dev/null +++ b/source/gui/darkMode.py @@ -0,0 +1,137 @@ +# A part of NonVisual Desktop Access (NVDA) +# Copyright (C) 2016-2024 NV Access Limited, Łukasz Golonka +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. + +"""Dark mode makes UI elements have a dark background with light text. + +If the darkModeCanUseUndocumentedAPIs config setting is true, then we are +able to get proper styling for popup context menus. +""" + +import ctypes.wintypes +from typing import ( + Generator, +) + +import config +from config.configFlags import ColorTheme +import ctypes +import ctypes.wintypes as wintypes +import logging +import wx + + +_initialized = False + + +# Documented windows APIs +_DwmSetWindowAttribute = None +_SetWindowTheme = None +_SendMessageW = None +DWMWA_USE_IMMERSIVE_DARK_MODE = 20 +WM_THEMECHANGED = 0x031A + + +# Undocumented windows APIs adapted from https://github.com/ysc3839/win32-darkmode +_SetPreferredAppMode = None + + +def initialize(): + global _initialized + _initialized = True + + global _SetWindowTheme + uxtheme = ctypes.cdll.LoadLibrary("uxtheme") + _SetWindowTheme = uxtheme.SetWindowTheme + _SetWindowTheme.restype = ctypes.HRESULT + _SetWindowTheme.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR] + + global _DwmSetWindowAttribute + dwmapi = ctypes.cdll.LoadLibrary("dwmapi") + _DwmSetWindowAttribute = dwmapi.DwmSetWindowAttribute + _DwmSetWindowAttribute.restype = ctypes.HRESULT + _DwmSetWindowAttribute.argtypes = [wintypes.HWND, wintypes.DWORD, wintypes.LPCVOID, wintypes.DWORD] + + global _SendMessageW + user32 = ctypes.cdll.LoadLibrary("user32") + _SendMessageW = user32.SendMessageW + _SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] + + try: + global _SetPreferredAppMode + _SetPreferredAppMode = uxtheme[135] + _SetPreferredAppMode.restype = wintypes.INT + _SetPreferredAppMode.argtypes = [wintypes.INT] + except Exception as err: + logging.debug("Will not use undocumented windows api SetPreferredAppMode: " + str(err)) + + +def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: + yield window + if hasattr(window, "GetChildren"): + for child in window.GetChildren(): + for descendant in _getDescendants(child): + yield descendant + + +def applyColorTheme(window: wx.Window): + if not _initialized: + return + curTheme = config.conf["general"]["colorTheme"] + if curTheme == ColorTheme.AUTO: + systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() + isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() + else: + isDark = (curTheme == ColorTheme.DARK) + + if isDark: + fgColor, bgColor, themePrefix = "White", "Dark Grey", "DarkMode" + else: + fgColor, bgColor, themePrefix = "Black", "Very Light Grey", "LightMode" + + # This config setting MUST be in a non-profile-specific config section, otherwise it + # won't be available until after the main window is created, which is too late. + canUseUndocumentedAPIs = config.conf["general"]["darkModeCanUseUndocumentedAPIs"] + if _SetPreferredAppMode and canUseUndocumentedAPIs: + # This makes context menus dark. + if curTheme == ColorTheme.AUTO: + _SetPreferredAppMode(1) + elif curTheme == ColorTheme.DARK: + _SetPreferredAppMode(2) + else: + _SetPreferredAppMode(0) + + descendants = list(_getDescendants(window)) + for child in descendants: + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) + + if isinstance(child, wx.Frame) or isinstance(child, wx.Dialog): + # This makes title bars dark + useDarkMode = ctypes.wintypes.BOOL(isDark) + _DwmSetWindowAttribute( + child.Handle, + DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(useDarkMode), + ctypes.sizeof(ctypes.c_int32)) + elif ( + isinstance(child, wx.Button) + or isinstance(child, wx.ScrolledWindow) + or isinstance(child, wx.ToolTip) + or isinstance(child, wx.TextEntry) + ): + _SetWindowTheme(child.Handle, themePrefix + "_Explorer", None) + _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) + elif isinstance(child, wx.Choice): + _SetWindowTheme(child.Handle, themePrefix + "_CFD", None) + _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) + elif isinstance(child, wx.ListCtrl): + _SetWindowTheme(child.Handle, themePrefix + "_ItemsView", None) + _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) + else: + print(child.ClassName) + _SetWindowTheme(child.Handle, themePrefix, None) + _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) + + window.Refresh() \ No newline at end of file diff --git a/source/gui/guiHelper.py b/source/gui/guiHelper.py index 7d4f74fdbf8..eb8a05ed1bf 100644 --- a/source/gui/guiHelper.py +++ b/source/gui/guiHelper.py @@ -46,7 +46,6 @@ def __init__(self, parent): from contextlib import contextmanager import weakref from typing import ( - Generator, Generic, Optional, Type, @@ -58,8 +57,6 @@ def __init__(self, parent): import wx from wx.lib import scrolledpanel, newevent from abc import ABCMeta -import config -from config.configFlags import ColorTheme #: border space to be used around all controls in dialogs BORDER_FOR_DIALOGS = 10 @@ -461,28 +458,3 @@ class SIPABCMeta(wx.siplib.wrappertype, ABCMeta): """Meta class to be used for wx subclasses with abstract methods.""" pass - - -def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: - yield window - if hasattr(window, "GetChildren"): - for child in window.GetChildren(): - for descendant in _getDescendants(child): - yield descendant - - -def applyColorTheme(window: wx.Window): - curTheme = config.conf["vision"]["colorTheme"] - if curTheme == ColorTheme.AUTO: - systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() - isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() - else: - isDark = curTheme == ColorTheme.DARK - - if isDark: - fgColor, bgColor = "White", "Dark Grey" - else: - fgColor, bgColor = "Black", "White" - for child in _getDescendants(window): - child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 6b9b64b1913..a3fafe06ff6 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -43,6 +43,7 @@ import systemUtils import gui import gui.contextHelp +import gui.darkMode import globalVars from logHandler import log import nvwave @@ -4725,6 +4726,10 @@ def _createProviderSettingsPanel( return None def makeSettings(self, settingsSizer: wx.BoxSizer): + self.makeVisionProviderSettings(settingsSizer) + self.makeDarkModeSettings(settingsSizer) + + def makeVisionProviderSettings(self, settingsSizer: wx.BoxSizer): self.initialProviders = vision.handler.getActiveProviderInfos() self.providerPanelInstances = [] self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) @@ -4745,15 +4750,27 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): providerSizer.Add(settingsPanel, flag=wx.EXPAND) self.providerPanelInstances.append(settingsPanel) - # Translators: label for a choice in the vision settings category panel - colorThemeLabelText = _("&Color theme") - self.colorThemeList = self.settingsSizerHelper.addLabeledControl( - colorThemeLabelText, + def makeDarkModeSettings(self, settingsSizer: wx.BoxSizer): + sizer = self.settingsSizerHelper.addItem( + wx.StaticBoxSizer(wx.VERTICAL, self, label=_("Dark Mode")), + flag=wx.EXPAND, + ) + sHelper = guiHelper.BoxSizerHelper(self, sizer=sizer) + self.colorThemeList = sHelper.addLabeledControl( + # Translators: label for a choice in the vision settings category panel + _("&Color theme"), wx.Choice, choices=[theme.displayString for theme in ColorTheme], ) + self.darkModeCanUseUnsupportedAPIs = wx.CheckBox( + sizer.GetStaticBox(), + # Translators: label for a checkbox in the vision settings category panel + label=_("Allow use of undocumented windows APIs (unsafe)"), + ) + self.darkModeCanUseUnsupportedAPIs.Value = config.conf["general"]["darkModeCanUseUndocumentedAPIs"] + sHelper.addItem(self.darkModeCanUseUnsupportedAPIs) self.bindHelpEvent("VisionSettingsColorTheme", self.colorThemeList) - curTheme = config.conf["vision"]["colorTheme"] + curTheme = config.conf["general"]["colorTheme"] for i, theme in enumerate(ColorTheme): if theme == curTheme: self.colorThemeList.SetSelection(i) @@ -4834,9 +4851,9 @@ def onSave(self): log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = vision.handler.getActiveProviderInfos() colorTheme = list(ColorTheme)[self.colorThemeList.GetSelection()] - config.conf["vision"]["colorTheme"] = colorTheme.value - guiHelper.applyColorTheme(self.TopLevelParent) - self.TopLevelParent.Refresh() + config.conf["general"]["colorTheme"] = colorTheme.value + config.conf["general"]["darkModeCanUseUndocumentedAPIs"] = self.darkModeCanUseUnsupportedAPIs.Value + gui.darkMode.applyColorTheme(self.TopLevelParent) class VisionProviderSubPanel_Settings( From 7c7c8445fbb7165d3a7ad358abb1abdd33b0a7f2 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 18 Aug 2024 16:34:26 -0400 Subject: [PATCH 19/29] tidy up code, include TODOs --- source/core.py | 6 +- source/gui/darkMode.py | 117 ++++++++++++++++++++-------------- source/gui/settingsDialogs.py | 8 +-- 3 files changed, 77 insertions(+), 54 deletions(-) diff --git a/source/core.py b/source/core.py index 98155c1119a..bd9a14f91c2 100644 --- a/source/core.py +++ b/source/core.py @@ -615,8 +615,10 @@ def InitLocale(self): def FilterEvent(self, event: wx.Event): """FilterEvent is called for every UI event in the entire application. Keep it quick to avoid slowing everything down.""" - if isinstance(event, wx.ShowEvent): - gui.darkMode.applyColorTheme(event.EventObject) + if isinstance(event, wx.WindowCreateEvent): + gui.darkMode.handleEvent(event.EventObject, event.EventType) + elif isinstance(event, wx.ShowEvent) and event.IsShown: + gui.darkMode.handleEvent(event.EventObject, event.EventType) return -1 app = App(redirect=False) diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index cc27d2d5b02..68d27f03885 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -5,8 +5,13 @@ """Dark mode makes UI elements have a dark background with light text. -If the darkModeCanUseUndocumentedAPIs config setting is true, then we are -able to get proper styling for popup context menus. +Note: Config settings must be in a non-profile-specific config section (e.g. "general"). +Profile-specific config sections (e.g. "vision") aren't available to read until after +the main app window is created. But _SetPreferredAppMode must be called BEFORE the main +window is created in order for popup context menus to be properly styled. + +TODO: dictionary dialogs and the add-on store look bad because column titles aren't styled. +These are wx.Notebook controls. """ import ctypes.wintypes @@ -67,6 +72,32 @@ def initialize(): logging.debug("Will not use undocumented windows api SetPreferredAppMode: " + str(err)) +def DwmSetWindowAttribute_ImmersiveDarkMode(window: wx.Window, isDark: bool): + """This makes title bars dark""" + useDarkMode = ctypes.wintypes.BOOL(isDark) + _DwmSetWindowAttribute( + window.Handle, + DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(useDarkMode), + ctypes.sizeof(ctypes.c_int32)) + + +def SetPreferredAppMode(curTheme: ColorTheme): + """This makes popup context menus dark""" + if _SetPreferredAppMode and config.conf["general"]["darkModeCanUseUndocumentedAPIs"]: + if curTheme == ColorTheme.AUTO: + _SetPreferredAppMode(1) + elif curTheme == ColorTheme.DARK: + _SetPreferredAppMode(2) + else: + _SetPreferredAppMode(0) + + +def SetWindowTheme(window: wx.Window, theme: str): + _SetWindowTheme(window.Handle, theme, None) + _SendMessageW(window.Handle, WM_THEMECHANGED, 0, 0) + + def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: yield window if hasattr(window, "GetChildren"): @@ -75,7 +106,7 @@ def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: yield descendant -def applyColorTheme(window: wx.Window): +def handleEvent(window: wx.Window, eventType): if not _initialized: return curTheme = config.conf["general"]["colorTheme"] @@ -84,54 +115,44 @@ def applyColorTheme(window: wx.Window): isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() else: isDark = (curTheme == ColorTheme.DARK) - if isDark: fgColor, bgColor, themePrefix = "White", "Dark Grey", "DarkMode" else: fgColor, bgColor, themePrefix = "Black", "Very Light Grey", "LightMode" - # This config setting MUST be in a non-profile-specific config section, otherwise it - # won't be available until after the main window is created, which is too late. - canUseUndocumentedAPIs = config.conf["general"]["darkModeCanUseUndocumentedAPIs"] - if _SetPreferredAppMode and canUseUndocumentedAPIs: - # This makes context menus dark. - if curTheme == ColorTheme.AUTO: - _SetPreferredAppMode(1) - elif curTheme == ColorTheme.DARK: - _SetPreferredAppMode(2) - else: - _SetPreferredAppMode(0) - - descendants = list(_getDescendants(window)) - for child in descendants: - child.SetBackgroundColour(bgColor) - child.SetForegroundColour(fgColor) - - if isinstance(child, wx.Frame) or isinstance(child, wx.Dialog): - # This makes title bars dark - useDarkMode = ctypes.wintypes.BOOL(isDark) - _DwmSetWindowAttribute( - child.Handle, - DWMWA_USE_IMMERSIVE_DARK_MODE, - ctypes.byref(useDarkMode), - ctypes.sizeof(ctypes.c_int32)) - elif ( - isinstance(child, wx.Button) - or isinstance(child, wx.ScrolledWindow) - or isinstance(child, wx.ToolTip) - or isinstance(child, wx.TextEntry) + if eventType == wx.wxEVT_CREATE: + SetPreferredAppMode(curTheme) + if ( + # Necessary for background of ListBoxes such as Settings >> Audio >> Cycle sound split mode. + # TODO: this breaks lists of checkboxes + isinstance(window, wx.ListBox) + + # Necessary for Add-on store >> Documentation >> Other details. + # TODO: this fixes Add-on store >> Documentation >> Other details, but breaks the + # ExpandoTextCtrl used by the debug log, python console, etc + #or isinstance(window, wx.TextCtrl) ): - _SetWindowTheme(child.Handle, themePrefix + "_Explorer", None) - _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) - elif isinstance(child, wx.Choice): - _SetWindowTheme(child.Handle, themePrefix + "_CFD", None) - _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) - elif isinstance(child, wx.ListCtrl): - _SetWindowTheme(child.Handle, themePrefix + "_ItemsView", None) - _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) - else: - print(child.ClassName) - _SetWindowTheme(child.Handle, themePrefix, None) - _SendMessageW(child.Handle, WM_THEMECHANGED, 0, 0) - - window.Refresh() \ No newline at end of file + window.SetBackgroundColour(bgColor) + window.SetForegroundColour(fgColor) + elif eventType == wx.wxEVT_SHOW: + for child in _getDescendants(window): + child.SetBackgroundColour(bgColor) + child.SetForegroundColour(fgColor) + + if isinstance(child, wx.Frame) or isinstance(child, wx.Dialog): + DwmSetWindowAttribute_ImmersiveDarkMode(child, isDark) + elif ( + isinstance(child, wx.Button) + or isinstance(child, wx.ScrolledWindow) + or isinstance(child, wx.ToolTip) + or isinstance(child, wx.TextEntry) + ): + SetWindowTheme(child, themePrefix + "_Explorer") + elif isinstance(child, wx.Choice): + SetWindowTheme(child, themePrefix + "_CFD") + elif isinstance(child, wx.ListCtrl): + SetWindowTheme(child, themePrefix + "_ItemsView") + else: + SetWindowTheme(child, themePrefix) + + window.Refresh() \ No newline at end of file diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index a3fafe06ff6..eb5e1a8b0bb 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -4726,14 +4726,14 @@ def _createProviderSettingsPanel( return None def makeSettings(self, settingsSizer: wx.BoxSizer): - self.makeVisionProviderSettings(settingsSizer) + self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) self.makeDarkModeSettings(settingsSizer) + self.makeVisionProviderSettings(settingsSizer) def makeVisionProviderSettings(self, settingsSizer: wx.BoxSizer): self.initialProviders = vision.handler.getActiveProviderInfos() self.providerPanelInstances = [] - self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) for providerInfo in vision.handler.getProviderList(reloadFromSystem=True): providerSizer = self.settingsSizerHelper.addItem( @@ -4853,7 +4853,7 @@ def onSave(self): colorTheme = list(ColorTheme)[self.colorThemeList.GetSelection()] config.conf["general"]["colorTheme"] = colorTheme.value config.conf["general"]["darkModeCanUseUndocumentedAPIs"] = self.darkModeCanUseUnsupportedAPIs.Value - gui.darkMode.applyColorTheme(self.TopLevelParent) + gui.darkMode.handleEvent(self.TopLevelParent, wx.wxEVT_SHOW) class VisionProviderSubPanel_Settings( From 9c723acbe4d4395db0a1397e32691766788b342b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 18 Aug 2024 20:35:42 +0000 Subject: [PATCH 20/29] Pre-commit auto-fix --- source/gui/__init__.py | 1 + source/gui/darkMode.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/source/gui/__init__.py b/source/gui/__init__.py index 0ba5d8d805b..1b9cb652e0e 100644 --- a/source/gui/__init__.py +++ b/source/gui/__init__.py @@ -812,6 +812,7 @@ def initialize(): raise RuntimeError("GUI already initialized") from gui import darkMode + # Dark mode must be initialized before creating main frame # otherwise context menus will not be styled correctly darkMode.initialize() diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index 68d27f03885..dc458785f0b 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -58,7 +58,7 @@ def initialize(): _DwmSetWindowAttribute.restype = ctypes.HRESULT _DwmSetWindowAttribute.argtypes = [wintypes.HWND, wintypes.DWORD, wintypes.LPCVOID, wintypes.DWORD] - global _SendMessageW + global _SendMessageW user32 = ctypes.cdll.LoadLibrary("user32") _SendMessageW = user32.SendMessageW _SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] @@ -79,7 +79,8 @@ def DwmSetWindowAttribute_ImmersiveDarkMode(window: wx.Window, isDark: bool): window.Handle, DWMWA_USE_IMMERSIVE_DARK_MODE, ctypes.byref(useDarkMode), - ctypes.sizeof(ctypes.c_int32)) + ctypes.sizeof(ctypes.c_int32), + ) def SetPreferredAppMode(curTheme: ColorTheme): @@ -114,7 +115,7 @@ def handleEvent(window: wx.Window, eventType): systemAppearance: wx.SystemAppearance = wx.SystemSettings.GetAppearance() isDark = systemAppearance.IsDark() or systemAppearance.IsUsingDarkBackground() else: - isDark = (curTheme == ColorTheme.DARK) + isDark = curTheme == ColorTheme.DARK if isDark: fgColor, bgColor, themePrefix = "White", "Dark Grey", "DarkMode" else: @@ -126,11 +127,10 @@ def handleEvent(window: wx.Window, eventType): # Necessary for background of ListBoxes such as Settings >> Audio >> Cycle sound split mode. # TODO: this breaks lists of checkboxes isinstance(window, wx.ListBox) - # Necessary for Add-on store >> Documentation >> Other details. - # TODO: this fixes Add-on store >> Documentation >> Other details, but breaks the + # TODO: this fixes Add-on store >> Documentation >> Other details, but breaks the # ExpandoTextCtrl used by the debug log, python console, etc - #or isinstance(window, wx.TextCtrl) + # or isinstance(window, wx.TextCtrl) ): window.SetBackgroundColour(bgColor) window.SetForegroundColour(fgColor) @@ -155,4 +155,4 @@ def handleEvent(window: wx.Window, eventType): else: SetWindowTheme(child, themePrefix) - window.Refresh() \ No newline at end of file + window.Refresh() From 9bf17c0e9130ded5923fe2a9f6de11ab025341e0 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 25 Aug 2024 14:17:08 -0400 Subject: [PATCH 21/29] Fix TextCtrl foreground colors --- source/gui/darkMode.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index dc458785f0b..ecbb10303ca 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -123,17 +123,36 @@ def handleEvent(window: wx.Window, eventType): if eventType == wx.wxEVT_CREATE: SetPreferredAppMode(curTheme) - if ( - # Necessary for background of ListBoxes such as Settings >> Audio >> Cycle sound split mode. - # TODO: this breaks lists of checkboxes - isinstance(window, wx.ListBox) - # Necessary for Add-on store >> Documentation >> Other details. - # TODO: this fixes Add-on store >> Documentation >> Other details, but breaks the - # ExpandoTextCtrl used by the debug log, python console, etc - # or isinstance(window, wx.TextCtrl) - ): - window.SetBackgroundColour(bgColor) + + # For some controls, colors must be set in EVT_CREATE otherwise it has no effect. + if isinstance(window, wx.CheckListBox): + # Unfortunately CheckListBoxes always seem to use a black foreground color for the labels, + # which means they become illegible if you make the background too dark. So we compromise + # by setting the background to be a little bit darker while still being readable. + if isDark: + window.SetBackgroundColour("Light Grey") + else: + window.SetBackgroundColour("White") window.SetForegroundColour(fgColor) + elif isinstance(window, wx.TextCtrl): + window.SetBackgroundColour(bgColor) + # Foreground colors for TextCtrls are surprisingly tricky, because their behavior is + # inconsistent. In particular, the Add-On Store Details pane behaves differently than + # the Debug Log, Python Console, etc. Here is a table of what happens with different + # possibilites: + # + # Color Add-on Store Everything else Usable? + # ----- ------------ --------------- ------- + # white white black no + # light grey black white no + # yellow yellow white no + # 0xFEFEFE white white YES + # black black black YES + if isDark: + window.SetForegroundColour(wx.Colour(254, 254, 254)) + else: + window.SetForegroundColour("Black") + elif eventType == wx.wxEVT_SHOW: for child in _getDescendants(window): child.SetBackgroundColour(bgColor) From 65ed8b8bfc499fb5aac59a2b141e29b26030680b Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 25 Aug 2024 15:02:21 -0400 Subject: [PATCH 22/29] Add comment for translators --- source/gui/settingsDialogs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index eb5e1a8b0bb..613182b8fa6 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -4752,6 +4752,8 @@ def makeVisionProviderSettings(self, settingsSizer: wx.BoxSizer): def makeDarkModeSettings(self, settingsSizer: wx.BoxSizer): sizer = self.settingsSizerHelper.addItem( + # Translators: this is a label for a group of controls appearing on + # the vision settings panel. wx.StaticBoxSizer(wx.VERTICAL, self, label=_("Dark Mode")), flag=wx.EXPAND, ) From b3fefa932590e9c2aaaf0b268fad22bff57e4c27 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 25 Aug 2024 16:19:55 -0400 Subject: [PATCH 23/29] ListCtrl needs the same foregroundColor logic as TextCtrl --- source/gui/darkMode.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index ecbb10303ca..e835d309b38 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -5,13 +5,28 @@ """Dark mode makes UI elements have a dark background with light text. +This is a best-effort attempt to implement dark mode. There are some remaining known issues: + +1) MessageBox'es are not themed. An example is the NVDA About dialog. + +2) Menu bars are not themed. An example can be seen in the Debug Log. Supporting themed +menu bars would require intercepting several undocumented events and drawing the menu items +ourselves. An example implementation is described in +https://github.com/adzm/win32-custom-menubar-aero-theme + +3) Column titles are not themed. An example can be seen in the Dictionary dialogs. +This is implemented by the wx.ListCtrl class. The C++ implementation of +wxListCtrl::OnPaint hardcodes penColour, and there is no way to override it. +See https://github.com/wxWidgets/wxWidgets/blob/master/src/msw/listctrl.cpp + +4) Tab controls are not themed. An example can be seen at the top of the Add-In Store. +This is implemented by the wx.Notebook class. I have not been able to figure out how +to influence the colors it uses. + Note: Config settings must be in a non-profile-specific config section (e.g. "general"). Profile-specific config sections (e.g. "vision") aren't available to read until after the main app window is created. But _SetPreferredAppMode must be called BEFORE the main window is created in order for popup context menus to be properly styled. - -TODO: dictionary dialogs and the add-on store look bad because column titles aren't styled. -These are wx.Notebook controls. """ import ctypes.wintypes @@ -134,20 +149,20 @@ def handleEvent(window: wx.Window, eventType): else: window.SetBackgroundColour("White") window.SetForegroundColour(fgColor) - elif isinstance(window, wx.TextCtrl): + elif isinstance(window, wx.TextCtrl) or isinstance(window, wx.ListCtrl): window.SetBackgroundColour(bgColor) # Foreground colors for TextCtrls are surprisingly tricky, because their behavior is # inconsistent. In particular, the Add-On Store Details pane behaves differently than # the Debug Log, Python Console, etc. Here is a table of what happens with different # possibilites: # - # Color Add-on Store Everything else Usable? - # ----- ------------ --------------- ------- - # white white black no - # light grey black white no - # yellow yellow white no - # 0xFEFEFE white white YES - # black black black YES + # Color Add-on Store Debug Log Usable? + # ----- ------------ --------- ------- + # white white black no + # light grey black white no + # yellow yellow white no + # 0xFEFEFE white white YES + # black black black YES if isDark: window.SetForegroundColour(wx.Colour(254, 254, 254)) else: From 900f9b55e5264aa7fcca95257b8cd1b47a09b4db Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 25 Aug 2024 18:31:53 -0400 Subject: [PATCH 24/29] Add (probably unnecessary) error handling, to make CodeRabbit happy --- source/gui/darkMode.py | 81 +++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index e835d309b38..9e771cdf602 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -7,7 +7,9 @@ This is a best-effort attempt to implement dark mode. There are some remaining known issues: -1) MessageBox'es are not themed. An example is the NVDA About dialog. +1) MessageBox'es are not themed. An example is the NVDA About dialog. These dialogs +are extremely modal, and there is no way to gain control until after the user dismisses +the message box. 2) Menu bars are not themed. An example can be seen in the Debug Log. Supporting themed menu bars would require intercepting several undocumented events and drawing the menu items @@ -61,22 +63,25 @@ def initialize(): global _initialized _initialized = True - global _SetWindowTheme - uxtheme = ctypes.cdll.LoadLibrary("uxtheme") - _SetWindowTheme = uxtheme.SetWindowTheme - _SetWindowTheme.restype = ctypes.HRESULT - _SetWindowTheme.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR] - - global _DwmSetWindowAttribute - dwmapi = ctypes.cdll.LoadLibrary("dwmapi") - _DwmSetWindowAttribute = dwmapi.DwmSetWindowAttribute - _DwmSetWindowAttribute.restype = ctypes.HRESULT - _DwmSetWindowAttribute.argtypes = [wintypes.HWND, wintypes.DWORD, wintypes.LPCVOID, wintypes.DWORD] - - global _SendMessageW - user32 = ctypes.cdll.LoadLibrary("user32") - _SendMessageW = user32.SendMessageW - _SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] + try: + global _SetWindowTheme + uxtheme = ctypes.cdll.LoadLibrary("uxtheme") + _SetWindowTheme = uxtheme.SetWindowTheme + _SetWindowTheme.restype = ctypes.HRESULT + _SetWindowTheme.argtypes = [wintypes.HWND, wintypes.LPCWSTR, wintypes.LPCWSTR] + + global _DwmSetWindowAttribute + dwmapi = ctypes.cdll.LoadLibrary("dwmapi") + _DwmSetWindowAttribute = dwmapi.DwmSetWindowAttribute + _DwmSetWindowAttribute.restype = ctypes.HRESULT + _DwmSetWindowAttribute.argtypes = [wintypes.HWND, wintypes.DWORD, wintypes.LPCVOID, wintypes.DWORD] + + global _SendMessageW + user32 = ctypes.cdll.LoadLibrary("user32") + _SendMessageW = user32.SendMessageW + _SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] + except Exception as err: + logging.debug("Error initializing dark mode: " + str(err)) try: global _SetPreferredAppMode @@ -89,29 +94,39 @@ def initialize(): def DwmSetWindowAttribute_ImmersiveDarkMode(window: wx.Window, isDark: bool): """This makes title bars dark""" - useDarkMode = ctypes.wintypes.BOOL(isDark) - _DwmSetWindowAttribute( - window.Handle, - DWMWA_USE_IMMERSIVE_DARK_MODE, - ctypes.byref(useDarkMode), - ctypes.sizeof(ctypes.c_int32), - ) + if _DwmSetWindowAttribute: + try: + useDarkMode = ctypes.wintypes.BOOL(isDark) + _DwmSetWindowAttribute( + window.Handle, + DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(useDarkMode), + ctypes.sizeof(ctypes.c_int32), + ) + except Exception as err: + logging.debug("Error calling DwmSetWindowAttribute: " + str(err)) def SetPreferredAppMode(curTheme: ColorTheme): """This makes popup context menus dark""" if _SetPreferredAppMode and config.conf["general"]["darkModeCanUseUndocumentedAPIs"]: - if curTheme == ColorTheme.AUTO: - _SetPreferredAppMode(1) - elif curTheme == ColorTheme.DARK: - _SetPreferredAppMode(2) - else: - _SetPreferredAppMode(0) - + try: + if curTheme == ColorTheme.AUTO: + _SetPreferredAppMode(1) + elif curTheme == ColorTheme.DARK: + _SetPreferredAppMode(2) + else: + _SetPreferredAppMode(0) + except Exception as err: + logging.debug("Error calling SetPreferredAppMode: " + str(err)) def SetWindowTheme(window: wx.Window, theme: str): - _SetWindowTheme(window.Handle, theme, None) - _SendMessageW(window.Handle, WM_THEMECHANGED, 0, 0) + if _SetWindowTheme and _SendMessageW: + try: + _SetWindowTheme(window.Handle, theme, None) + _SendMessageW(window.Handle, WM_THEMECHANGED, 0, 0) + except Exception as err: + logging.debug("Error calling SetWindowTheme: " + str(err)) def _getDescendants(window: wx.Window) -> Generator[wx.Window, None, None]: From 3fb781aa1d953d8169d2c2656301bcb1e8d1bef9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 22:32:48 +0000 Subject: [PATCH 25/29] Pre-commit auto-fix --- source/gui/darkMode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/gui/darkMode.py b/source/gui/darkMode.py index 9e771cdf602..eff81bd0d89 100644 --- a/source/gui/darkMode.py +++ b/source/gui/darkMode.py @@ -120,6 +120,7 @@ def SetPreferredAppMode(curTheme: ColorTheme): except Exception as err: logging.debug("Error calling SetPreferredAppMode: " + str(err)) + def SetWindowTheme(window: wx.Window, theme: str): if _SetWindowTheme and _SendMessageW: try: From d0efee764fa106a9216340866f906bc9e68dc745 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Sun, 25 Aug 2024 18:40:41 -0400 Subject: [PATCH 26/29] more exception handling, to make CodeRabbit happy --- source/core.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/source/core.py b/source/core.py index bd9a14f91c2..c841cce9626 100644 --- a/source/core.py +++ b/source/core.py @@ -615,10 +615,13 @@ def InitLocale(self): def FilterEvent(self, event: wx.Event): """FilterEvent is called for every UI event in the entire application. Keep it quick to avoid slowing everything down.""" - if isinstance(event, wx.WindowCreateEvent): - gui.darkMode.handleEvent(event.EventObject, event.EventType) - elif isinstance(event, wx.ShowEvent) and event.IsShown: - gui.darkMode.handleEvent(event.EventObject, event.EventType) + try: + if isinstance(event, wx.WindowCreateEvent): + gui.darkMode.handleEvent(event.EventObject, event.EventType) + elif isinstance(event, wx.ShowEvent) and event.IsShown: + gui.darkMode.handleEvent(event.EventObject, event.EventType) + except Exception: + log.exception("Error applying dark mode") return -1 app = App(redirect=False) From b083b5c8ba7805d6e9159d10a84537ad3eabe896 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Tue, 12 Nov 2024 21:08:07 -0500 Subject: [PATCH 27/29] revert changes to speechDict.py --- source/gui/speechDict.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index 4968277894d..b5aa176c78d 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -28,11 +28,11 @@ class DictionaryEntryDialog( TYPE_LABELS = { # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_ANYWHERE: _("Anywhere"), + speechDictHandler.ENTRY_TYPE_ANYWHERE: _("&Anywhere"), # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_WORD: _("Whole word"), + speechDictHandler.ENTRY_TYPE_WORD: _("Whole &word"), # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. - speechDictHandler.ENTRY_TYPE_REGEXP: _("Regular expression"), + speechDictHandler.ENTRY_TYPE_REGEXP: _("Regular &expression"), } TYPE_LABELS_ORDERING = ( speechDictHandler.ENTRY_TYPE_ANYWHERE, @@ -63,13 +63,12 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): caseSensitiveText = _("Case &sensitive") self.caseSensitiveCheckBox = sHelper.addItem(wx.CheckBox(self, label=caseSensitiveText)) - # Translators: This is a label for a dropdown list in add dictionary entry dialog. + # Translators: This is a label for a set of radio buttons in add dictionary entry dialog. typeText = _("&Type") typeChoices = [ DictionaryEntryDialog.TYPE_LABELS[i] for i in DictionaryEntryDialog.TYPE_LABELS_ORDERING ] - self.typeList = sHelper.addLabeledControl(typeText, wx.Choice, choices=typeChoices) - self.typeList.SetSelection(0) + self.typeRadioBox = sHelper.addItem(wx.RadioBox(self, label=typeText, choices=typeChoices)) sHelper.addDialogDismissButtons(wx.OK | wx.CANCEL, separated=True) @@ -81,10 +80,10 @@ def __init__(self, parent, title=_("Edit Dictionary Entry")): self.Bind(wx.EVT_BUTTON, self.onOk, id=wx.ID_OK) def getType(self): - selection = self.typeList.GetSelection() - if selection == wx.NOT_FOUND: + typeRadioValue = self.typeRadioBox.GetSelection() + if typeRadioValue == wx.NOT_FOUND: return speechDictHandler.ENTRY_TYPE_ANYWHERE - return DictionaryEntryDialog.TYPE_LABELS_ORDERING[selection] + return DictionaryEntryDialog.TYPE_LABELS_ORDERING[typeRadioValue] def onOk(self, evt): if not self.patternTextCtrl.GetValue(): @@ -143,7 +142,7 @@ def onOk(self, evt): evt.Skip() def setType(self, type): - self.typeList.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type)) + self.typeRadioBox.SetSelection(DictionaryEntryDialog.TYPE_LABELS_ORDERING.index(type)) class DictionaryDialog( From 245f058b633c658344dd0bc20e9128e66dc71ff9 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Tue, 12 Nov 2024 21:10:36 -0500 Subject: [PATCH 28/29] missed a spot in previous reversion of speechDict.py --- source/gui/speechDict.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/gui/speechDict.py b/source/gui/speechDict.py index b5aa176c78d..090ce5192cd 100644 --- a/source/gui/speechDict.py +++ b/source/gui/speechDict.py @@ -27,11 +27,11 @@ class DictionaryEntryDialog( helpId = "SpeechDictionaries" TYPE_LABELS = { - # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. speechDictHandler.ENTRY_TYPE_ANYWHERE: _("&Anywhere"), - # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. speechDictHandler.ENTRY_TYPE_WORD: _("Whole &word"), - # Translators: This is a label for an Entry Type choice in add dictionary entry dialog. + # Translators: This is a label for an Entry Type radio button in add dictionary entry dialog. speechDictHandler.ENTRY_TYPE_REGEXP: _("Regular &expression"), } TYPE_LABELS_ORDERING = ( From d3ad7cd99549259d1e5e2acbcd058da7a05e8e45 Mon Sep 17 00:00:00 2001 From: Tristan Burchett Date: Tue, 12 Nov 2024 21:13:48 -0500 Subject: [PATCH 29/29] revert changes to configProfiles.py --- source/gui/configProfiles.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/source/gui/configProfiles.py b/source/gui/configProfiles.py index d6358324fae..7fcc16421a2 100644 --- a/source/gui/configProfiles.py +++ b/source/gui/configProfiles.py @@ -460,13 +460,14 @@ def __init__(self, parent): # in the new configuration profile dialog. self.triggers = triggers = [(None, _("Manual activation"), True)] triggers.extend(parent.getSimpleTriggers()) - self.triggerChoice = sHelper.addLabeledControl( - _("Use this profile for:"), - wx.Choice, - choices=[trig[1] for trig in triggers], + self.triggerChoice = sHelper.addItem( + wx.RadioBox( + self, + label=_("Use this profile for:"), + choices=[trig[1] for trig in triggers], + ), ) - self.triggerChoice.Bind(wx.EVT_CHOICE, self.onTriggerChoice) - self.triggerChoice.SetSelection(0) + self.triggerChoice.Bind(wx.EVT_RADIOBOX, self.onTriggerChoice) self.autoProfileName = "" self.onTriggerChoice(None)