From 3a83cf8b9e981b4973bc15c83f032dcec2dd1d63 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 13 Dec 2018 10:22:21 +0100 Subject: [PATCH 001/116] Add a graphical user interface for the vision framework --- source/gui/settingsDialogs.py | 168 +++++++++++++++++++++++++++++++++- source/vision/providerBase.py | 9 ++ tests/checkPot.py | 2 +- 3 files changed, 173 insertions(+), 6 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b72dca1fb59..7529e1a8774 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -8,7 +8,7 @@ # See the file COPYING for more details. import logging -from abc import abstractmethod +from abc import abstractmethod, abstractproperty, ABCMeta import os import copy import re @@ -33,6 +33,8 @@ import braille import brailleTables import brailleInput +import vision +from typing import Callable import core import keyboardHandler import characterProcessing @@ -264,10 +266,9 @@ class SettingsPanel(wx.Panel, DpiScalingHelperMixin, metaclass=guiHelper.SIPABCM title="" panelDescription=u"" - def __init__(self, parent): + def __init__(self, parent: wx.Window): """ @param parent: The parent for this panel; C{None} for no parent. - @type parent: wx.Window """ if gui._isDebug(): startTime = time.time() @@ -1023,7 +1024,8 @@ def __call__(self,evt): getattr(self.container,"_%ss"%self.setting.id)[evt.GetSelection()].id ) -class DriverSettingsMixin(object): + +class DriverSettingsMixin(metaclass=ABCMeta): """ Mixin class that provides support for driver specific gui settings. Derived classes should implement L{driver}. @@ -1035,7 +1037,7 @@ def __init__(self, *args, **kwargs): super(DriverSettingsMixin,self).__init__(*args,**kwargs) self._curDriverRef = weakref.ref(self.driver) - @property + @abstractproperty def driver(self): raise NotImplementedError @@ -2663,6 +2665,9 @@ def onOk(self, evt): port = self.possiblePorts[self.portsList.GetSelection()][0] config.conf["braille"][display]["port"] = port if not braille.handler.setDisplayByName(display): + # Translators: This message is presented when + # NVDA is unable to load the selected + # braille display. gui.messageBox(_("Could not load the %s display.")%display, _("Braille Display Error"), wx.OK|wx.ICON_WARNING, self) return @@ -2858,6 +2863,158 @@ def onBlinkCursorChange(self, evt): def onNoMessageTimeoutChange(self, evt): self.messageTimeoutEdit.Enable(not evt.IsChecked()) + +class VisionSettingsPanel(SettingsPanel): + # Translators: This is the label for the vision panel + title = _("Vision") + + def makeSettings(self, settingsSizer): + self.initialProviders = list(vision.handler.providers) + self.providerPanelInstances = [] + + settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + + # Translators: The label for a setting in vision settings to choose a vision enhancement provider. + providerLabelText = _("&Vision enhancement providers") + + providersBox = wx.StaticBox(self, label=providerLabelText) + providersGroupDescription = _( + "Providers are activated and deactivated as soon as you check or uncheck check boxes.") + providersGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(providersBox, wx.HORIZONTAL)) + providersGroup.addItem(wx.StaticText(self, label=providersGroupDescription)) + self.providerList = providersGroup.addLabeledControl( + "{}:".format(providerLabelText), + nvdaControls.CustomCheckListBox, + choices=[] + ) + self.providerList.Bind(wx.EVT_CHECKLISTBOX, self.onProviderToggled) + + self.initializeProviderList() + + settingsSizerHelper.addItem(providersGroup) + + self.providerPanelsSizer = wx.BoxSizer(wx.VERTICAL) + settingsSizerHelper.addItem(self.providerPanelsSizer) + + def initializeProviderList(self): + providerList = vision.getProviderList() + self.providerNames = [provider[0] for provider in providerList] + providerChoices = [provider[1] for provider in providerList] + self.providerList.Clear() + self.providerList.Items = providerChoices + self.providerList.Select(0) + + def syncProviderCheckboxes(self): + self.providerList.CheckedItems = [self.providerNames.index(name) for name in vision.handler.providers] + + def onProviderToggled(self, evt): + index = evt.Int + if index != self.providerList.Selection: + # Toggled an unselected provider + self.providerList.Select(index) + providerName = self.providerNames[index] + isChecked = self.providerList.IsChecked(index) + if not isChecked: + vision.handler.terminateProvider(providerName) + elif not vision.handler.initializeProvider(providerName): + gui.messageBox( + # Translators: This message is presented when + # NVDA is unable to load just checked + # vision enhancement provider. + _(f"Could not load the {providerName} vision enhancement provider."), + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + self + ) + self.providerList.Check(index, False) + return + evt.Skip() + self.refreshPanel() + + def refreshPanel(self): + self.Freeze() + # trigger a refresh of the settings + self.onPanelActivated() + self._sendLayoutUpdatedEvent() + self.Thaw() + + def updateCurrentProviderPanels(self): + self.providerPanelsSizer.Clear(delete_windows=True) + self.providerPanelInstances[:] = [] + for name, providerInst in sorted(vision.handler.providers.items()): + if providerInst.guiPanelClass and ( + providerInst.guiPanelClass is not VisionProviderSubPanel or providerInst.supportedSettings + ): + if len(self.providerPanelInstances) > 0: + self.providerPanelsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + panelSizer = wx.StaticBoxSizer(wx.StaticBox(self, label=providerInst.description), wx.VERTICAL) + panel = providerInst.guiPanelClass(self, providerCallable=weakref.ref(providerInst)) + panelSizer.Add(panel) + self.providerPanelsSizer.Add(panelSizer) + self.providerPanelInstances.append(panel) + + def onPanelActivated(self): + self.syncProviderCheckboxes() + self.updateCurrentProviderPanels() + super().onPanelActivated() + + def onDiscard(self): + initErrors = [] + for panel in self.providerPanelInstances: + panel.onDiscard() + + providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] + for provider in providersToInitialize: + if not vision.handler.initializeProvider(provider): + initErrors.append(provider) + + providersToTerminate = [name for name in vision.handler.providers if name not in self.initialProviders] + for provider in providersToTerminate: + vision.handler.terminateProvider(provider) + + if initErrors: + # Translators: This message is presented when + # NVDA is unable to load certain + # vision enhancement providers. + initErrorsList = ", ".join(initErrors) + gui.messageBox( + # Translators: This message is presented when + # NVDA is unable to load vision enhancement providers after discarding settings. + _(f"Could not load the following vision enhancement providers:\n{initErrorsList}"), + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + self) + + def onSave(self): + for panel in self.providerPanelInstances: + panel.onSave() + self.initialProviders = list(vision.handler.providers) + + +class VisionProviderSubPanel(DriverSettingsMixin, SettingsPanel): + + def __init__( + self, + parent: wx.Window, + *, # Make next argument keyword only + providerCallable: Callable[[], vision.providerBase.VisionEnhancementProvider] + ): + """ + @param providerCallable: A callable that returns an instance to a VisionEnhancementProvider. + This will usually be a weakref, but could be any callable taking no arguments. + """ + self._providerCallable = providerCallable + super().__init__(parent=parent) + + @property + def driver(self): + return self._providerCallable() + + def makeSettings(self, settingsSizer): + # Construct vision enhancement provider settings + self.updateDriverSettings() + + """ The name of the config profile currently being edited, if any. This is set when the currently edited configuration profile is determined and returned to None when the dialog is destroyed. This can be used by an AppModule for NVDA to identify and announce @@ -2871,6 +3028,7 @@ class NVDASettingsDialog(MultiCategorySettingsDialog): GeneralSettingsPanel, SpeechSettingsPanel, BrailleSettingsPanel, + VisionSettingsPanel, KeyboardSettingsPanel, MouseSettingsPanel, ReviewCursorPanel, diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index fb7937be195..97c5482364d 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -27,6 +27,15 @@ class VisionEnhancementProvider(driverHandler.Driver): #: but might be later for presentational purposes. supportedRoles: FrozenSet[Role] = frozenset() + @classmethod + def _get_guiPanelClass(cls): + """Returns the class to be used in order to construct a settings panel for the provider. + The class constructor should take the required providerCallable keyword argument. + """ + # Import late to avoid circular import + from gui.settingsDialogs import VisionProviderSubPanel + return VisionProviderSubPanel + def reinitialize(self): """Reinitializes a vision enhancement provider, reusing the same instance. This base implementation simply calls terminate and __init__ consecutively. diff --git a/tests/checkPot.py b/tests/checkPot.py index a84dd41a1d2..bb95f439589 100644 --- a/tests/checkPot.py +++ b/tests/checkPot.py @@ -81,8 +81,8 @@ 'Insufficient Privileges', 'Synthesizer Error', 'Dictionary Entry Error', - 'Could not load the %s display.', 'Braille Display Error', + 'Vision Enhancement Provider Error', 'word', 'Taskbar', '%s items', From f76da24e94ed1ddec77c614d1423ee0a5e11de4e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 22 Aug 2019 14:43:43 +0200 Subject: [PATCH 002/116] Layout adjustments to vision framework --- source/gui/settingsDialogs.py | 38 +++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7529e1a8774..7a10a667d2a 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -278,7 +278,7 @@ def __init__(self, parent: wx.Window): self.mainSizer=wx.BoxSizer(wx.VERTICAL) self.settingsSizer=wx.BoxSizer(wx.VERTICAL) self.makeSettings(self.settingsSizer) - self.mainSizer.Add(self.settingsSizer, flag=wx.ALL) + self.mainSizer.Add(self.settingsSizer, flag=wx.ALL | wx.EXPAND) self.mainSizer.Fit(self) self.SetSizer(self.mainSizer) if gui._isDebug(): @@ -497,7 +497,10 @@ def _getCategoryPanel(self, catId): raise ValueError("Unable to create panel for unknown category ID: {}".format(catId)) panel = cls(parent=self.container) panel.Hide() - self.containerSizer.Add(panel, flag=wx.ALL, border=guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL) + self.containerSizer.Add( + panel, flag=wx.ALL | wx.EXPAND, + border=guiHelper.SPACE_BETWEEN_ASSOCIATED_CONTROL_HORIZONTAL + ) self.catIdToInstanceMap[catId] = panel panelWidth = panel.Size[0] availableWidth = self.containerSizer.GetSize()[0] @@ -2877,13 +2880,18 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in vision settings to choose a vision enhancement provider. providerLabelText = _("&Vision enhancement providers") - providersBox = wx.StaticBox(self, label=providerLabelText) - providersGroupDescription = _( - "Providers are activated and deactivated as soon as you check or uncheck check boxes.") - providersGroup = guiHelper.BoxSizerHelper(self, sizer=wx.StaticBoxSizer(providersBox, wx.HORIZONTAL)) - providersGroup.addItem(wx.StaticText(self, label=providersGroupDescription)) - self.providerList = providersGroup.addLabeledControl( - "{}:".format(providerLabelText), + # Translators: Description in vision settings to choose a vision enhancement provider. + providersListDescriptionText = _( + "Providers are activated and deactivated as soon as you check or uncheck check boxes." + ) + providersSizer = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) + providerListDescTextCtrl = providersSizer.addItem(wx.StaticText(self, label=providersListDescriptionText)) + # The width that is available to the text is 544. + # Using providerSizer.Size.Width would be prefered, but it is not correct during construction. + # The value is dependent on the nesting level of the text, and has to be checked manually. + providerListDescTextCtrl.Wrap(self.scaleSize(544)) + self.providerList = providersSizer.addLabeledControl( + f"{providerLabelText}:", nvdaControls.CustomCheckListBox, choices=[] ) @@ -2891,10 +2899,14 @@ def makeSettings(self, settingsSizer): self.initializeProviderList() - settingsSizerHelper.addItem(providersGroup) + settingsSizerHelper.addItem(providersSizer, flag=wx.EXPAND) + settingsSizerHelper.addItem( + wx.StaticLine(self), + flag=wx.EXPAND + ) self.providerPanelsSizer = wx.BoxSizer(wx.VERTICAL) - settingsSizerHelper.addItem(self.providerPanelsSizer) + settingsSizerHelper.addItem(self.providerPanelsSizer, flag=wx.EXPAND) def initializeProviderList(self): providerList = vision.getProviderList() @@ -2949,8 +2961,8 @@ def updateCurrentProviderPanels(self): self.providerPanelsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) panelSizer = wx.StaticBoxSizer(wx.StaticBox(self, label=providerInst.description), wx.VERTICAL) panel = providerInst.guiPanelClass(self, providerCallable=weakref.ref(providerInst)) - panelSizer.Add(panel) - self.providerPanelsSizer.Add(panelSizer) + panelSizer.Add(panel, flag=wx.EXPAND) + self.providerPanelsSizer.Add(panelSizer, flag=wx.EXPAND) self.providerPanelInstances.append(panel) def onPanelActivated(self): From 641ccdcc3591e4d3ca6325c0a3bd2f439bf3422c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 22 Aug 2019 16:16:17 +0200 Subject: [PATCH 003/116] Update user guide Typo: vvision > vision BNrowse mode caret is shown using a rectangle, not a single line --- user_docs/en/userGuide.t2t | 64 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 7dd6be1e7d7..e87a62c7119 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -29,8 +29,9 @@ Major highlights include: - Support for modern Windows Operating Systems including both 32 and 64 bit variants - Ability to run on Windows logon and other secure screens - Announcing controls and text while using touch gestures -- Support for common accessibility interfaces such as Microsoft Active Accessibility, Java Access Bridge, IAccessible2 and UI Automation (UI Automation only supported in Windows 7 and later) +- Support for common accessibility interfaces such as Microsoft Active Accessibility, Java Access Bridge, IAccessible2 and UI Automation - Support for Windows Command Prompt and console applications +- The ability to highlight the system focus - ++ Internationalization ++[Internationalization] @@ -290,6 +291,7 @@ For example, if you are typing into an editable text field, the editable text fi The most common way of navigating around Windows with NVDA is to simply move the system focus using standard Windows keyboard commands, such as pressing tab and shift+tab to move forward and back between controls, pressing alt to get to the menu bar and then using the arrows to navigate menus, and using alt+tab to move between running applications. As you do this, NVDA will report information about the object with focus, such as its name, type, value, state, description, keyboard shortcut and positional information. +When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the current system focus is also exposed visually. There are some key commands that are useful when moving with the System focus: %kc:beginInclude @@ -345,6 +347,7 @@ Similarly, a toolbar contains controls, so you must move inside the toolbar to a The object currently being reviewed is called the navigator object. Once you navigate to an object, you can review its content using the [text review commands #ReviewingText] while in [Object review mode #ObjectReview]. +When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the current navigator object is also exposed visually. By default, the navigator object moves along with the System focus, though this behavior can be toggled on and off. Note that braille follows both the [focus #SystemFocus] and [caret #SystemCaret] as well as object navigation and text review by default. @@ -485,10 +488,12 @@ This includes documents in the following applications: - Adobe Flash - Supported books in Amazon Kindle for PC - + Browse mode is also optionally available for Microsoft Word documents. In browse mode, the content of the document is made available in a flat representation that can be navigated with the cursor keys as if it were a normal text document. All of NVDA's [system caret #SystemCaret] key commands will work in this mode; e.g. say all, report formatting, table navigation commands, etc. +When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the virtual browse mode caret is also exposed visually. Information such as whether text is a link, heading, etc. is reported along with the text as you move. Sometimes, you will need to interact directly with controls in these documents. @@ -739,6 +744,39 @@ Dot 8 translates any braille input and presses the enter key. Pressing dot 7 + dot 8 translates any braille input, but without adding a space or pressing enter. %kc:endInclude ++ Vision +[Vision] +While NVDA is primairly aimed at blind or vision impaired people how primarily use speech and/or braille to operate a computer, it also provides built-in facilities to change the contents of the screen. +Within NVDA, such a facility is called a vision enhancement provider. + +NVDA offers several built-in vision enhaancement providers which are described below. +Additional vision enhancement providers can be provided in [NVDA add-ons #AddonsManager]. + +NVDA's vision settings can be changed in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. + +++ NVDA Highlighter ++[VisionNVDAHighlighter] +When the NVDA Highlighter is enabled, it draws lines on the computer screen around the [system focus #SystemFocus], [navigator object #ObjectNavigation] and/or [browse mode virtual caret #BrowseMode]. +This visual highlights make it much easier for sighted or low vision people to see the objects they are interacting with. +It can also aid in accessibility testing by application developers. + +By default, the NVDA Highlighter highlights the focus, navigator object and browse mode position. +- If the navigator object overlaps the system focus (e.g. because the navigator object follows the system focus), a solid blue line is drawn around the object. +- If the navigator object does not overlap the system focus, the focus object is surrounded by a dashed blue rectangle. In this case, a solid pink line is drawn around the navigator object. +- When browse mode is enabled in cases where there is no physical caret (such as in web browsers), the character at the virtual browse mode caret is surrounded by a solid yellow line. +- + +When the NVDA Highlighter is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsVisionEnhancementProviderSettings] + +++ Screen Curtain ++[VisionScreenCurtain] +As a blind or vision impaired user, it is often not possible or necessary to see the contents of the screen. +Furthermore, it might be hard to ensure that there isn't someone looking over your shoulder. +For this situation, NVDA contains a screen curtain that, when enabled, makes the screen black. + +When enabling the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, a warning will be displayed telling you that your screen will be black after activating the curtain. +If you are sure, you can choose the Yes button to enable the screen curtain. +If not, choosing No will disable the screen curtain again. +If you no longer want to see this warning message every time, you can change this behavior in the dialog that displays the message. +When the screen curtain is enabled, you can always restore the warning message from [the vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. + + Content Recognition +[ContentRecognition] When authors don't provide sufficient information for a screen reader user to determine the content of something, various tools can be used to attempt to recognize the content from an image. NVDA supports the optical character recognition (OCR) functionality built into Windows 10 to recognize text from images. @@ -1054,7 +1092,7 @@ If this is enabled, NVDA will inform you when there is a pending update on start You can also manually install the pending update from the Exit NVDA dialog (if enabled), from the NVDA menu, or when you perform a new check from the Help menu. +++ Speech Settings (NVDA+control+v) +++[SpeechSettings] -The speech category in the NVDA Settings dialog contains options that lets you change the speech synthesizer as well as voice characteristics for the chosen synthesizer. +The Speech category in the NVDA Settings dialog contains options that lets you change the speech synthesizer as well as voice characteristics for the chosen synthesizer. For a quicker alternative way of controlling speech parameters from anywhere, please see the [Synth Settings Ring #SynthSettingsRing] section. The Speech Settings category contains the following options: @@ -1316,6 +1354,28 @@ This option won't be available if your braille display only supports automatic p You may consult the documentation for your braille display in the section [Supported Braille Displays #SupportedBrailleDisplays] to check for more details on the supported types of communication and available ports. ++++ Vision +++[VisionSettings] +The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [vision enhancement providers #Vision]. +This settings category contains the following options: + +==== Vision enhancement providers ====[VisionSettingsVisionEnhancementProviders] +The checkboxes in this list control allow you to enable vision enhancement providers that are either built into NVDA or provided using add-ons. + +NVDA has the following providers built in: +- [NVDA Highlighter #VisionNVDAHighlighter] +- [Screen Curtain #VisionScreenCurtain] +- + +When you enable a disabled provider, the provider will be automatically enabled as soon as you check the check box. +When you disable an enabled provider, the provider will be automatically disabled as soon as you uncheck the check box. +If you accidentally enable or disable a provider, you can always restore the previous situation by choosing the Cancel button. + +==== Vision enhancement provider settings ====[VisionSettingsVisionEnhancementProviderSettings] +When you enable a provider in the list control and this provider has adjustable settings, these settings are automatically shown in the vision category. +For example, when you enable the NVDA Highlighter, navigating the dialog with tab brings you to several settings that allow you to enable highlighting the focus or navigator object. +For the supported settings per provider, please refer to de documentation for that provider. +For the built in providers, supported settings are listed in the [vision #Vision] section of this user guide. + +++ Keyboard (NVDA+control+k) +++[KeyboardSettings] The Keyboard category in the NVDA Settings dialog contains options that sets how NVDA behaves as you use and type on your keyboard. This settings category contains the following options: From 1816b284d01d8c73f6c68b03c35981adbf27121e Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 23 Aug 2019 07:13:13 +0200 Subject: [PATCH 004/116] Many gui and framework updates and improvements, including a load warning for the screen curtain --- source/driverHandler.py | 125 +++++++++------- source/globalCommands.py | 20 ++- source/gui/nvdaControls.py | 14 +- source/gui/settingsDialogs.py | 135 +++++++++++++----- source/vision/providerBase.py | 14 +- source/vision/visionHandler.py | 108 ++++++++------ .../NVDAHighlighter.py | 4 +- .../screenCurtain.py | 89 +++++++++++- 8 files changed, 366 insertions(+), 143 deletions(-) diff --git a/source/driverHandler.py b/source/driverHandler.py index b12580507fc..3060f5113a1 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -11,6 +11,8 @@ import config from copy import deepcopy from logHandler import log +from typing import List, Dict + class Driver(AutoPropertyObject): """ @@ -44,41 +46,58 @@ def __init__(self): super(Driver, self).__init__() config.pre_configSave.register(self.saveSettings) - def initSettings(self): - """ - Initializes the configuration for this driver. - This method is called when initializing the driver. - """ - firstLoad = not config.conf[self._configSection].isSet(self.name) + @classmethod + def _initSpecificSettings(cls, clsOrInst, settings): + firstLoad = not config.conf[cls._configSection].isSet(cls.name) if firstLoad: # Create the new section. - config.conf[self._configSection][self.name] = {} + config.conf[cls._configSection][cls.name] = {} # Make sure the config spec is up to date, so the config validator does its work. - config.conf[self._configSection][self.name].spec.update(self.getConfigSpec()) - # Make sure the instance has attributes for every setting - for setting in self.supportedSettings: - if not hasattr(self, setting.id): - setattr(self, setting.id, setting.defaultVal) + config.conf[cls._configSection][cls.name].spec.update( + cls._getConfigSPecForSettings(settings) + ) + # Make sure the clsOrInst has attributes for every setting + for setting in settings: + if not hasattr(clsOrInst, setting.id): + setattr(clsOrInst, setting.id, setting.defaultVal) if firstLoad: - self.saveSettings() #save defaults + cls._saveSpecificSettings(clsOrInst, settings) #save defaults else: - self.loadSettings() + cls._loadSpecificSettings(clsOrInst, settings) + + def initSettings(self): + """ + Initializes the configuration for this driver. + This method is called when initializing the driver. + """ + self._initSpecificSettings(self, self.supportedSettings) - def terminate(self): + def terminate(self, saveSettings: bool = True): """Terminate this driver. This should be used for any required clean up. + @param saveSettings: Whether settings should be saved on termination. @precondition: L{initialize} has been called. @postcondition: This driver can no longer be used. """ - self.saveSettings() + if saveSettings: + self.saveSettings() config.pre_configSave.unregister(self.saveSettings) + @classmethod + def _get_preInitSettings(self): + """The settings supported by the driver at pre initialisation time. + @rtype: list or tuple of L{DriverSetting} + """ + return () + _abstract_supportedSettings = True def _get_supportedSettings(self): """The settings supported by the driver. + When overriding this property, subclasses are encouraged to extend the getter method + to ensure that L{preInitSettings} is part of the list of supported settings. @rtype: list or tuple of L{DriverSetting} """ - return () + return self.preInitSettings @classmethod def check(cls): @@ -98,60 +117,70 @@ def isSupported(self,settingID): if s.id == settingID: return True return False - def getConfigSpec(self): - spec=deepcopy(config.confspec[self._configSection]["__many__"]) - for setting in self.supportedSettings: + @classmethod + def _getConfigSPecForSettings(cls, settings) -> Dict: + spec=deepcopy(config.confspec[cls._configSection]["__many__"]) + for setting in settings: if not setting.useConfig: continue spec[setting.id]=setting.configSpec return spec + def getConfigSpec(self): + return self._getConfigSPecForSettings(self.supportedSettings) + + @classmethod + def _saveSpecificSettings(cls, clsOrInst, settings): + conf=config.conf[cls._configSection][cls.name] + for setting in settings: + if not setting.useConfig: + continue + try: + conf[setting.id] = getattr(clsOrInst, setting.id) + except UnsupportedConfigParameterError: + log.debugWarning(f"Unsupported setting {s.id!r}; ignoring", exc_info=True) + continue + if settings: + log.debug(f"Saved settings for {cls.__qualname__}") + def saveSettings(self): """ Saves the current settings for the driver to the configuration. This method is also executed when the driver is loaded for the first time, in order to populate the configuration with the initial settings.. """ - conf=config.conf[self._configSection][self.name] - for setting in self.supportedSettings: - if not setting.useConfig: + self._saveSpecificSettings(self, self.supportedSettings) + + @classmethod + def _loadSpecificSettings(cls, clsOrInst, settings, onlyChanged=False): + conf = config.conf[cls._configSection][cls.name] + for setting in settings: + if not setting.useConfig or conf.get(setting.id) is None: + continue + val = conf[setting.id] + if onlyChanged and getattr(clsOrInst, setting.id) == val: continue try: - conf[setting.id] = getattr(self,setting.id) + setattr(clsOrInst, setting.id, val) except UnsupportedConfigParameterError: - log.debugWarning("Unsupported setting %s; ignoring"%s.id, exc_info=True) + log.debugWarning(f"Unsupported setting {setting.name!r}; ignoring", exc_info=True) continue - if self.supportedSettings: - log.debug("Saved settings for {} {}".format(self.__class__.__name__, self.name)) + if settings: + log.debug( + f"Loaded changed settings for {cls.__qualname__}" + if onlyChanged else + f"Loaded settings for {cls.__qualname__}" + ) - def loadSettings(self, onlyChanged=False): + def loadSettings(self, onlyChanged: bool = False): """ Loads settings for this driver from the configuration. This method assumes that the instance has attributes o/properties corresponding with the name of every setting in L{supportedSettings}. @param onlyChanged: When loading settings, only apply those for which the value in the configuration differs from the current value. - @type onlyChanged: bool """ - conf=config.conf[self._configSection][self.name] - for setting in self.supportedSettings: - if not setting.useConfig or conf.get(setting.id) is None: - continue - val=conf[setting.id] - if onlyChanged and getattr(self,setting.id) == val: - continue - try: - setattr(self,setting.id, val) - except UnsupportedConfigParameterError: - log.debugWarning("Unsupported setting %s; ignoring"%setting.name, exc_info=True) - continue - if self.supportedSettings: - log.debug( - ( - "Loaded changed settings for {} {}" - if onlyChanged else - "Loaded settings for {} {}" - ).format(self.__class__.__name__, self.name)) + self._loadSpecificSettings(self, self.supportedSettings, onlyChanged) @classmethod def _paramToPercent(cls, current, min, max): diff --git a/source/globalCommands.py b/source/globalCommands.py index ac49fe69023..7a8b62e83e1 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2302,15 +2302,25 @@ def script_toggleScreenCurtain(self, gesture): return scriptCount = scriptHandler.getLastScriptRepeatCount() if scriptCount == 0 and screenCurtainName in vision.handler.providers: - vision.handler.terminateProvider(screenCurtainName) + try: + vision.handler.terminateProvider(screenCurtainName) + except Exception: + # If the screen curtain was enabled, we do not expect exceptions. + log.error("Screen curtain termination error", exc_info=True) + # Translators: Reported when the screen curtain could not be enabled. + message = _("Could not disable screen curtain") + return # Translators: Reported when the screen curtain is disabled. message = _("Screen curtain disabled") elif scriptCount in (0, 2): temporary = scriptCount == 0 - if not vision.handler.initializeProvider( - screenCurtainName, - temporary=temporary, - ): + try: + vision.handler.initializeProvider( + screenCurtainName, + temporary=temporary, + ) + except Exception: + log.error("Screen curtain initialization error", exc_info=True) # Translators: Reported when the screen curtain could not be enabled. message = _("Could not enable screen curtain") return diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index ca9aeb8dec4..070342a3a81 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -3,14 +3,15 @@ #Copyright (C) 2016-2018 NV Access Limited, Derek Riemer #This file is covered by the GNU General Public License. #See the file COPYING for more details. + from ctypes.wintypes import BOOL from typing import Any, Tuple, Optional - import wx from comtypes import GUID from wx.lib.mixins import listctrl as listmix -from gui import accPropServer -from gui.dpiScalingHelper import DpiScalingHelperMixin +from . import accPropServer +from .dpiScalingHelper import DpiScalingHelperMixin +from . import guiHelper import oleacc import winUser import winsound @@ -305,6 +306,11 @@ def _addButtons(self, buttonHelper): ) cancel.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.CANCEL)) + def _addContents(self, contentsSizer: guiHelper.BoxSizerHelper): + """Adds additional contents to the dialog, before the buttons. + Subclasses may implement this method. + """ + def _setIcon(self, type): try: iconID = self._DIALOG_TYPE_ICON_ID_MAP[type] @@ -333,12 +339,12 @@ def __init__(self, parent, title, message, dialogType=DIALOG_TYPE_STANDARD): self.Bind(wx.EVT_SHOW, self._onShowEvt, source=self) mainSizer = wx.BoxSizer(wx.VERTICAL) - from . import guiHelper contentsSizer = guiHelper.BoxSizerHelper(parent=self, orientation=wx.VERTICAL) text = wx.StaticText(self, label=message) text.Wrap(self.scaleSize(self.GetSize().Width)) contentsSizer.addItem(text) + self._addContents(contentsSizer) buttonHelper = guiHelper.ButtonHelper(wx.HORIZONTAL) self._addButtons(buttonHelper) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7a10a667d2a..5d068bc20a4 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -34,7 +34,7 @@ import brailleTables import brailleInput import vision -from typing import Callable +from typing import Callable, List import core import keyboardHandler import characterProcessing @@ -2897,7 +2897,7 @@ def makeSettings(self, settingsSizer): ) self.providerList.Bind(wx.EVT_CHECKLISTBOX, self.onProviderToggled) - self.initializeProviderList() + self.populateProviderList() settingsSizerHelper.addItem(providersSizer, flag=wx.EXPAND) settingsSizerHelper.addItem( @@ -2908,7 +2908,7 @@ def makeSettings(self, settingsSizer): self.providerPanelsSizer = wx.BoxSizer(wx.VERTICAL) settingsSizerHelper.addItem(self.providerPanelsSizer, flag=wx.EXPAND) - def initializeProviderList(self): + def populateProviderList(self): providerList = vision.getProviderList() self.providerNames = [provider[0] for provider in providerList] providerChoices = [provider[1] for provider in providerList] @@ -2916,10 +2916,101 @@ def initializeProviderList(self): self.providerList.Items = providerChoices self.providerList.Select(0) + def safeInitProviders( + self, + providerNames: List[str], + confirmInitWithUser: bool = True + ) -> bool: + """Initializes one or more providers in a way that is gui friendly, + showing an error if appropriate. + @returns: Whether initialization succeeded for all providers. + """ + success = True + initErrors = [] + for providerName in providerNames: + try: + if confirmInitWithUser and not vision.handler.confirmInitWithUser(providerName): + success = False + continue + vision.handler.initializeProvider(providerName) + except Exception as e: + initErrors.append(providerName) + log.error( + f"Could not initialize the {providerName} vision enhancement provider", + exc_info=True + ) + success = False + if not success and initErrors: + if len(initErrors) == 1: + # Translators: This message is presented when + # NVDA is unable to load a single vision enhancement provider. + message = _( + "Could not load the {provider} vision enhancement provider" + ).format(provider=initErrors[0]) + else: + # Translators: This message is presented when + # NVDA is unable to load multiple vision enhancement providers. + initErrorsList = ", ".join(initErrors) + message = _(f"Could not load the following vision enhancement providers:\n{initErrorsList}") + gui.messageBox( + message, + # Translators: The title of the vision enhancement provider error message box. + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + self + ) + return success + + def safeTerminateProviders( + self, + providerNames: List[str], + verbose: bool = False + ): + """Terminates one or more providers in a way that is gui friendly, + @verbose: Whether to show a termination error. + @returns: Whether initialization succeeded for all providers. + """ + terminateErrors = [] + for providerName in providerNames: + try: + # Terminating a provider from the gui should never save the settings. + # This is because termination happens on the fly when unchecking check boxes. + # Saving settings would be harmful if a user opens the vision panel, + # then changes some settings and disables the provider. + vision.handler.terminateProvider(providerName, saveSettings=False) + except Exception: + terminateErrors.append(providerName) + log.error( + f"Could not terminate the {providerName} vision enhancement provider", + exc_info=True + ) + + if terminateErrors: + if verbose: + if len(terminateErrors) == 1: + # Translators: This message is presented when + # NVDA is unable to gracefully terminate a single vision enhancement provider. + message = _( + "Could not gracefully terminate the {provider} vision enhancement provider" + ).format(provider=list(terminateErrors)[0]) + else: + terminateErrorsList = ", ".join(terminateErrors) + # Translators: This message is presented when + # NVDA is unable to termiante multiple vision enhancement providers. + message = _(f"Could not gracefully terminate the following vision enhancement providers:\n{initErrorsList}") + gui.messageBox( + message, + # Translators: The title of the vision enhancement provider error message box. + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + self + ) + def syncProviderCheckboxes(self): self.providerList.CheckedItems = [self.providerNames.index(name) for name in vision.handler.providers] def onProviderToggled(self, evt): + evt.Skip() index = evt.Int if index != self.providerList.Selection: # Toggled an unselected provider @@ -2927,20 +3018,10 @@ def onProviderToggled(self, evt): providerName = self.providerNames[index] isChecked = self.providerList.IsChecked(index) if not isChecked: - vision.handler.terminateProvider(providerName) - elif not vision.handler.initializeProvider(providerName): - gui.messageBox( - # Translators: This message is presented when - # NVDA is unable to load just checked - # vision enhancement provider. - _(f"Could not load the {providerName} vision enhancement provider."), - _("Vision Enhancement Provider Error"), - wx.OK | wx.ICON_WARNING, - self - ) + self.safeTerminateProviders([providerName], verbose=True) + elif not self.safeInitProviders([providerName]): self.providerList.Check(index, False) return - evt.Skip() self.refreshPanel() def refreshPanel(self): @@ -2971,31 +3052,15 @@ def onPanelActivated(self): super().onPanelActivated() def onDiscard(self): - initErrors = [] for panel in self.providerPanelInstances: panel.onDiscard() providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] - for provider in providersToInitialize: - if not vision.handler.initializeProvider(provider): - initErrors.append(provider) - + # These providers were already initialized. + # Therefore, we do not display user confirmation messages, if any. + self.safeInitProviders(providersToInitialize, confirmInitWithUser=False) providersToTerminate = [name for name in vision.handler.providers if name not in self.initialProviders] - for provider in providersToTerminate: - vision.handler.terminateProvider(provider) - - if initErrors: - # Translators: This message is presented when - # NVDA is unable to load certain - # vision enhancement providers. - initErrorsList = ", ".join(initErrors) - gui.messageBox( - # Translators: This message is presented when - # NVDA is unable to load vision enhancement providers after discarding settings. - _(f"Could not load the following vision enhancement providers:\n{initErrorsList}"), - _("Vision Enhancement Provider Error"), - wx.OK | wx.ICON_WARNING, - self) + self.safeTerminateProviders(providersToTerminate) def onSave(self): for panel in self.providerPanelInstances: diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 97c5482364d..c9299974d9f 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -20,13 +20,14 @@ class VisionEnhancementProvider(driverHandler.Driver): _configSection = "vision" cachePropertiesByDefault = True - #: Override of supportedSettings to be a class property. - supportedSettings = () #: The roles supported by this provider. #: This attribute is currently not used, #: but might be later for presentational purposes. supportedRoles: FrozenSet[Role] = frozenset() + def _get_supportedSettings(self): + return super().supportedSettings + @classmethod def _get_guiPanelClass(cls): """Returns the class to be used in order to construct a settings panel for the provider. @@ -64,3 +65,12 @@ def canStart(cls) -> bool: @classmethod def check(cls) -> bool: return cls.canStart() + + @classmethod + def confirmInitWithUser(cls) -> bool: + """Before initialisation of the provider, + confirm with the user that the provider should start. + This method should be executed on the main thread. + @returns: C{True} if initialisation should continue, C{False} otherwise. + """ + return True diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index b565cecaa6c..6aec6ffa31b 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -22,6 +22,7 @@ import visionEnhancementProviders import queueHandler from typing import Type, Dict, List +from . import exceptions def getProviderClass( @@ -71,28 +72,29 @@ def postGuiInit(self) -> None: self.handleConfigProfileSwitch() config.post_configProfileSwitch.register(self.handleConfigProfileSwitch) - def terminateProvider(self, providerName: str) -> bool: + def terminateProvider(self, providerName: str, saveSettings: bool = True): """Terminates a currently active provider. + When termnation fails, an exception is raised. + Yet, the provider wil lbe removed from the providers dictionary, + so its instance goes out of scope and wil lbe garbage collected. @param providerName: The provider to terminate. - @returns: Whether termination succeeded or failed. - When termnation fails, return False so the caller knows that something failed. - Yet, the provider wil lbe removed from the providers dictionary, - so its instance goes out of scope and wil lbe garbage collected. + @param saveSettings: Whether settings should be saved on termionation. """ success = True # Remove the provider from the providers dictionary. providerInstance = self.providers.pop(providerName, None) if not providerInstance: - log.warning("Tried to terminate uninitialized provider %s" % providerName) - return False + raise exceptions.ProviderTerminateException( + f"Tried to terminate uninitialized provider {providerName!r}" + ) + exception = None try: - providerInstance.terminate() - except Exception: + providerInstance.terminate(saveSettings=saveSettings) + except Exception as e: # Purposely catch everything. # A provider can raise whatever exception, # therefore it is unknown what to expect. - log.error("Error while terminating vision provider %s" % providerName, exc_info=True) - success = False + exception = e # Copy the configured providers before mutating the list. # If we don't, configobj won't be aware of changes the list. configuredProviders: List = config.conf['vision']['providers'][:] @@ -109,53 +111,58 @@ def terminateProvider(self, providerName: str) -> bool: providerInst.registerEventExtensionPoints(self.extensionPoints) except Exception: log.error("Error while registering to extension points for provider %s" % providerName, exc_info=True) - return success + if exception: + raise exception + + @staticmethod + def confirmInitWithUser(providerName: str) -> bool: + """Before initialisation of a provider, + confirm with the user that the provider should start. + This method calls confirmInitWithUser on the provider, + and should be executed on the main thread. + @returns: C{True} if initialisation should continue, C{False} otherwise. + """ + providerCls = getProviderClass(providerName) + return providerCls.confirmInitWithUser() - def initializeProvider(self, providerName: str, temporary: bool = False) -> bool: + def initializeProvider(self, providerName: str, temporary: bool = False): """ Enables and activates the supplied provider. @param providerName: The name of the registered provider. @param temporary: Whether the selected provider is enabled temporarily (e.g. as a fallback). This defaults to C{False}. If C{True}, no changes will be performed to the configuration. - @returns: Whether initializing the requested provider succeeded. """ providerCls = None providerInst = self.providers.pop(providerName, None) if providerInst is not None: providerCls = type(providerInst) - try: - providerInst.reinitialize() - except Exception: - # Purposely catch everything. - # A provider can raise whatever exception, - # therefore it is unknown what to expect. - log.error("Error while reinitializing provider %s" % providerName, exc_info=True) - return False + providerInst.reinitialize() else: try: providerCls = getProviderClass(providerName) + except ModuleNotFoundError: + raise exceptions.ProviderInitException(f"No provider named {providerName!r}") + else: if not providerCls.canStart(): - log.error("Trying to initialize provider %s which reported being unable to start" % providerName) - return False - # Initialize the provider. - providerInst = providerCls() - # Register extension points. + raise exceptions.ProviderInitException( + f"Trying to initialize provider {providerName!r} which reported being unable to start" + ) + # Initialize the provider. + providerInst = providerCls() + # Register extension points. + try: + providerInst.registerEventExtensionPoints(self.extensionPoints) + except Exception as registerEventExtensionPointsException: + log.error( + f"Error while registering to extension points for provider {providerName}", + ) try: - providerInst.registerEventExtensionPoints(self.extensionPoints) - except Exception as registerEventExtensionPointsException: - log.error(f"Error while registering to extension points for provider {providerName}", exc_info=True) - try: - providerInst.terminate() - except Exception: - log.error("Error while registering to extension points for provider %s" % providerName, exc_info=True) - raise registerEventExtensionPointsException - except Exception: - # Purposely catch everything. - # A provider can raise whatever exception, - # therefore it is unknown what to expect. - log.error("Error while initializing provider %s" % providerName, exc_info=True) - return False + providerInst.terminate() + except Exception: + log.error( + f"Error terminating provider {providerName} after registering to extension points", exc_info=True) + raise registerEventExtensionPointsException providerInst.initSettings() if not temporary and providerCls.name not in config.conf['vision']['providers']: config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerCls.name] @@ -168,7 +175,6 @@ def initializeProvider(self, providerName: str, temporary: bool = False) -> bool # We should handle this more gracefully, since this is no reason # to stop a provider from loading successfully. log.debugWarning("Error in initial focus after provider load", exc_info=True) - return True def terminate(self) -> None: self.extensionPoints = None @@ -209,9 +215,21 @@ def handleConfigProfileSwitch(self) -> None: providersToInitialize = configuredProviders - curProviders providersToTerminate = curProviders - configuredProviders for provider in providersToTerminate: - self.terminateProvider(provider) - for provider in providersToInitialize: - self.initializeProvider(provider) + try: + self.terminateProvider(provider) + except Exception: + log.error( + f"Could not terminate the {provider} vision enhancement provider", + exc_info=True + ) + for provider in rovidersToInitialize: + try: + self.initializeProvider(provider) + except Exception: + log.error( + f"Could not initialize the {provider} vision enhancement provider", + exc_info=True + ) def initialFocus(self) -> None: if not api.getDesktopObject(): diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 7c75b6ca4df..668652ad1db 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -245,7 +245,7 @@ def __init__(self): self._highlighterThread.daemon = True self._highlighterThread.start() - def terminate(self): + def terminate(self, *args, **kwargs): if self._highlighterThread: if not winUser.user32.PostThreadMessageW(self._highlighterThread.ident, winUser.WM_QUIT, 0, 0): raise WinError() @@ -253,7 +253,7 @@ def terminate(self): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() - super(VisionEnhancementProvider, self).terminate() + super().terminate(*args, **kwargs) def _run(self): if vision._isDebug(): diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 6ab656924cb..4dd038c67d7 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -11,6 +11,10 @@ import winVersion from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError from ctypes.wintypes import BOOL +import driverHandler +import wx +import gui +import config class MAGCOLOREFFECT(Structure): @@ -66,6 +70,18 @@ class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): description = _("Screen Curtain") supportedRoles = frozenset([vision.constants.Role.COLORENHANCER]) + # Translators: Description for a screen curtain setting that shows a warning when loading + # the screen curtain. + warnOnLoadCheckBoxText = _(f"Always &show a warning when loading {description}") + + preInitSettings = [ + driverHandler.BooleanDriverSetting( + "warnOnLoad", + warnOnLoadCheckBoxText, + defaultVal=True + ), + ] + @classmethod def canStart(cls): return winVersion.isFullScreenMagnificationAvailable() @@ -75,10 +91,79 @@ def __init__(self): Magnification.MagInitialize() Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) - def terminate(self): - super(VisionEnhancementProvider, self).terminate() + def terminate(self, *args, **kwargs): + super().terminate(*args, **kwargs) Magnification.MagUninitialize() def registerEventExtensionPoints(self, extensionPoints): # The screen curtain isn't interested in any events pass + + # Translators: A warning shown when activating the screen curtain. + # {description} is replaced by the translation of "screen curtain" + warnOnLoadText = _( + f"You are about to enable {description}.\n" + f"When {description} is enabled, the screen of your computer will go completely black.\n" + f"Do you really want to enable {description}?" + ) + + @classmethod + def confirmInitWithUser(cls) -> bool: + cls._initSpecificSettings(cls, cls.preInitSettings) + if cls.warnOnLoad: + parent = next( + ( + dlg for dlg, state in gui.settingsDialogs.NVDASettingsDialog._instances.items() + if isinstance(dlg, gui.settingsDialogs.NVDASettingsDialog) + and state == gui.settingsDialogs.SettingsDialog._DIALOG_CREATED_STATE + ), + gui.mainFrame + ) + with WarnOnLoadDialog( + parent=parent, + # Translators: Title for the screen curtain warning dialog. + title=_("Warning"), + message=cls.warnOnLoadText, + dialogType=WarnOnLoadDialog.DIALOG_TYPE_WARNING + ) as dlg: + res = dlg.ShowModal() + if res == wx.NO: + return False + else: + cls.warnOnLoad = dlg.showWarningOnLoadCheckBox.IsChecked() + cls._saveSpecificSettings(cls, cls.preInitSettings) + return True + +class WarnOnLoadDialog(gui.nvdaControls.MessageDialog): + + def _addContents(self, contentsSizer): + self.showWarningOnLoadCheckBox = contentsSizer.addItem(wx.CheckBox( + self, + label=VisionEnhancementProvider.warnOnLoadCheckBoxText + )) + self.showWarningOnLoadCheckBox.SetValue( + config.conf[VisionEnhancementProvider._configSection][VisionEnhancementProvider.name][ + "warnOnLoad" + ] + ) + + def _addButtons(self, buttonHelper): + yesButton = buttonHelper.addButton( + self, + id=wx.ID_YES, + # Translators: A button in the screen curtain warning dialog which allows the user to + # agree to enabling the curtain. + label=_("&Yes") + ) + yesButton.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.YES)) + + noButton = buttonHelper.addButton( + self, + id=wx.ID_NO, + # Translators: A button in the screen curtain warning dialog which allows the user to + # disagree to enabling the curtain. + label=_("&No") + ) + noButton.SetDefault() + noButton.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.NO)) + noButton.SetFocus() From 5a8de044d17fe3bcdf92d23e78dc2b5317be2524 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Fri, 6 Sep 2019 17:10:38 +0200 Subject: [PATCH 005/116] Fix last (linting) issues --- include/liblouis | 2 +- source/driverHandler.py | 43 +++++++++++++------ source/gui/settingsDialogs.py | 12 ++++-- source/vision/exceptions.py | 15 +++++++ source/vision/visionHandler.py | 2 +- .../screenCurtain.py | 1 + 6 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 source/vision/exceptions.py diff --git a/include/liblouis b/include/liblouis index cc834a39b28..58d67e632aa 160000 --- a/include/liblouis +++ b/include/liblouis @@ -1 +1 @@ -Subproject commit cc834a39b28b22242d8546417afa1fda5a39440b +Subproject commit 58d67e632aa01de9e1c80fbf6b456701039f475d diff --git a/source/driverHandler.py b/source/driverHandler.py index 3060f5113a1..b098621e121 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -11,7 +11,7 @@ import config from copy import deepcopy from logHandler import log -from typing import List, Dict +from typing import List, Tuple, Dict, Union class Driver(AutoPropertyObject): @@ -47,7 +47,7 @@ def __init__(self): config.pre_configSave.register(self.saveSettings) @classmethod - def _initSpecificSettings(cls, clsOrInst, settings): + def _initSpecificSettings(cls, clsOrInst, settings: List): firstLoad = not config.conf[cls._configSection].isSet(cls.name) if firstLoad: # Create the new section. @@ -61,7 +61,7 @@ def _initSpecificSettings(cls, clsOrInst, settings): if not hasattr(clsOrInst, setting.id): setattr(clsOrInst, setting.id, setting.defaultVal) if firstLoad: - cls._saveSpecificSettings(clsOrInst, settings) #save defaults + cls._saveSpecificSettings(clsOrInst, settings) # save defaults else: cls._loadSpecificSettings(clsOrInst, settings) @@ -84,14 +84,15 @@ def terminate(self, saveSettings: bool = True): config.pre_configSave.unregister(self.saveSettings) @classmethod - def _get_preInitSettings(self): + def _get_preInitSettings(self) -> Union[List, Tuple]: """The settings supported by the driver at pre initialisation time. @rtype: list or tuple of L{DriverSetting} """ return () _abstract_supportedSettings = True - def _get_supportedSettings(self): + + def _get_supportedSettings(self) -> Union[List, Tuple]: """The settings supported by the driver. When overriding this property, subclasses are encouraged to extend the getter method to ensure that L{preInitSettings} is part of the list of supported settings. @@ -118,8 +119,11 @@ def isSupported(self,settingID): return False @classmethod - def _getConfigSPecForSettings(cls, settings) -> Dict: - spec=deepcopy(config.confspec[cls._configSection]["__many__"]) + def _getConfigSPecForSettings( + cls, + settings: Union[List, Tuple] + ) -> Dict: + spec = deepcopy(config.confspec[cls._configSection]["__many__"]) for setting in settings: if not setting.useConfig: continue @@ -130,15 +134,22 @@ def getConfigSpec(self): return self._getConfigSPecForSettings(self.supportedSettings) @classmethod - def _saveSpecificSettings(cls, clsOrInst, settings): - conf=config.conf[cls._configSection][cls.name] + def _saveSpecificSettings( + cls, + clsOrInst, + settings: Union[List, Tuple] + ): + conf = config.conf[cls._configSection][cls.name] for setting in settings: if not setting.useConfig: continue try: conf[setting.id] = getattr(clsOrInst, setting.id) except UnsupportedConfigParameterError: - log.debugWarning(f"Unsupported setting {s.id!r}; ignoring", exc_info=True) + log.debugWarning( + f"Unsupported setting {setting.id!r}; ignoring", + exc_info=True + ) continue if settings: log.debug(f"Saved settings for {cls.__qualname__}") @@ -152,7 +163,12 @@ def saveSettings(self): self._saveSpecificSettings(self, self.supportedSettings) @classmethod - def _loadSpecificSettings(cls, clsOrInst, settings, onlyChanged=False): + def _loadSpecificSettings( + cls, + clsOrInst, + settings: Union[List, Tuple], + onlyChanged: bool = False + ): conf = config.conf[cls._configSection][cls.name] for setting in settings: if not setting.useConfig or conf.get(setting.id) is None: @@ -163,7 +179,10 @@ def _loadSpecificSettings(cls, clsOrInst, settings, onlyChanged=False): try: setattr(clsOrInst, setting.id, val) except UnsupportedConfigParameterError: - log.debugWarning(f"Unsupported setting {setting.name!r}; ignoring", exc_info=True) + log.debugWarning( + f"Unsupported setting {setting.name!r}; ignoring", + exc_info=True + ) continue if settings: log.debug( diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 5d068bc20a4..a2977941a7a 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2933,7 +2933,7 @@ def safeInitProviders( success = False continue vision.handler.initializeProvider(providerName) - except Exception as e: + except Exception: initErrors.append(providerName) log.error( f"Could not initialize the {providerName} vision enhancement provider", @@ -2951,7 +2951,10 @@ def safeInitProviders( # Translators: This message is presented when # NVDA is unable to load multiple vision enhancement providers. initErrorsList = ", ".join(initErrors) - message = _(f"Could not load the following vision enhancement providers:\n{initErrorsList}") + message = _( + "Could not load the following vision enhancement providers:\n" + f"{initErrorsList}" + ) gui.messageBox( message, # Translators: The title of the vision enhancement provider error message box. @@ -2997,7 +3000,10 @@ def safeTerminateProviders( terminateErrorsList = ", ".join(terminateErrors) # Translators: This message is presented when # NVDA is unable to termiante multiple vision enhancement providers. - message = _(f"Could not gracefully terminate the following vision enhancement providers:\n{initErrorsList}") + message = _( + "Could not gracefully terminate the following vision enhancement providers:\n" + f"{terminateErrorsList}" + ) gui.messageBox( message, # Translators: The title of the vision enhancement provider error message box. diff --git a/source/vision/exceptions.py b/source/vision/exceptions.py new file mode 100644 index 00000000000..544e5a6ecf4 --- /dev/null +++ b/source/vision/exceptions.py @@ -0,0 +1,15 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited, Babbage B.V. + +"""Module containing exceptions for the vision framework. +""" + + +class ProviderTerminateException(RuntimeError): + ... + + +class ProviderInitException(RuntimeError): + ... diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 6aec6ffa31b..7f41b35eff6 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -222,7 +222,7 @@ def handleConfigProfileSwitch(self) -> None: f"Could not terminate the {provider} vision enhancement provider", exc_info=True ) - for provider in rovidersToInitialize: + for provider in providersToInitialize: try: self.initializeProvider(provider) except Exception: diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 4dd038c67d7..6c415c7e5ab 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -134,6 +134,7 @@ def confirmInitWithUser(cls) -> bool: cls._saveSpecificSettings(cls, cls.preInitSettings) return True + class WarnOnLoadDialog(gui.nvdaControls.MessageDialog): def _addContents(self, contentsSizer): From 99ea075fe97eb507e6bd01832bd829d5f19bc48d Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 9 Sep 2019 08:48:55 +0200 Subject: [PATCH 006/116] Apply user guide suggestions from code review Co-Authored-By: Reef Turner --- user_docs/en/userGuide.t2t | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index e87a62c7119..8b2d52438d7 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -748,7 +748,7 @@ Pressing dot 7 + dot 8 translates any braille input, but without adding a space While NVDA is primairly aimed at blind or vision impaired people how primarily use speech and/or braille to operate a computer, it also provides built-in facilities to change the contents of the screen. Within NVDA, such a facility is called a vision enhancement provider. -NVDA offers several built-in vision enhaancement providers which are described below. +NVDA offers several built-in vision enhancement providers which are described below. Additional vision enhancement providers can be provided in [NVDA add-ons #AddonsManager]. NVDA's vision settings can be changed in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. @@ -769,9 +769,12 @@ When the NVDA Highlighter is enabled in the [vision category #VisionSettings] of ++ Screen Curtain ++[VisionScreenCurtain] As a blind or vision impaired user, it is often not possible or necessary to see the contents of the screen. Furthermore, it might be hard to ensure that there isn't someone looking over your shoulder. -For this situation, NVDA contains a screen curtain that, when enabled, makes the screen black. +For this situation, NVDA contains a feature called "screen curtain" which can be enabled to make the screen black. -When enabling the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, a warning will be displayed telling you that your screen will be black after activating the curtain. +Enable the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. +A warning that your screen will become black after activation will be displayed. +Before continuing (selecting "Yes"), ensure you have enabled speech / braille and will be able to control your computer without the use of the screen. +Select "No" if you no longer wish to enable the Screen Curtain. If you are sure, you can choose the Yes button to enable the screen curtain. If not, choosing No will disable the screen curtain again. If you no longer want to see this warning message every time, you can change this behavior in the dialog that displays the message. From 985dc51da622fc82726f8dfaf8fa4ee3558e9ab7 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 9 Sep 2019 08:09:41 +0200 Subject: [PATCH 007/116] Revert accidental change to liblouis submodule --- include/liblouis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/liblouis b/include/liblouis index 58d67e632aa..cc834a39b28 160000 --- a/include/liblouis +++ b/include/liblouis @@ -1 +1 @@ -Subproject commit 58d67e632aa01de9e1c80fbf6b456701039f475d +Subproject commit cc834a39b28b22242d8546417afa1fda5a39440b From a419254ecddf5835286790f62aeb252ec9ca30fb Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 9 Sep 2019 08:32:03 +0200 Subject: [PATCH 008/116] Fix translator comments --- source/gui/settingsDialogs.py | 13 +++++-------- source/visionEnhancementProviders/screenCurtain.py | 4 ++-- tests/checkPot.py | 1 - 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index a2977941a7a..29702d08779 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2944,17 +2944,14 @@ def safeInitProviders( if len(initErrors) == 1: # Translators: This message is presented when # NVDA is unable to load a single vision enhancement provider. - message = _( - "Could not load the {provider} vision enhancement provider" - ).format(provider=initErrors[0]) + message = _("Could not load the {provider} vision enhancement provider").format( + provider=initErrors[0] + ) else: + initErrorsList = ", ".join(initErrors) # Translators: This message is presented when # NVDA is unable to load multiple vision enhancement providers. - initErrorsList = ", ".join(initErrors) - message = _( - "Could not load the following vision enhancement providers:\n" - f"{initErrorsList}" - ) + message = _(f"Could not load the following vision enhancement providers:\n{initErrorsList}") gui.messageBox( message, # Translators: The title of the vision enhancement provider error message box. diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 6c415c7e5ab..39be774ef24 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -99,9 +99,9 @@ def registerEventExtensionPoints(self, extensionPoints): # The screen curtain isn't interested in any events pass - # Translators: A warning shown when activating the screen curtain. - # {description} is replaced by the translation of "screen curtain" warnOnLoadText = _( + # Translators: A warning shown when activating the screen curtain. + # {description} is replaced by the translation of "screen curtain" f"You are about to enable {description}.\n" f"When {description} is enabled, the screen of your computer will go completely black.\n" f"Do you really want to enable {description}?" diff --git a/tests/checkPot.py b/tests/checkPot.py index bb95f439589..10a03c3ff91 100644 --- a/tests/checkPot.py +++ b/tests/checkPot.py @@ -82,7 +82,6 @@ 'Synthesizer Error', 'Dictionary Entry Error', 'Braille Display Error', - 'Vision Enhancement Provider Error', 'word', 'Taskbar', '%s items', From 3fdc434093377d1f4142c70ce82f3684077870b1 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 9 Sep 2019 08:55:37 +0200 Subject: [PATCH 009/116] Apply additional review action for the highlighter user guide description --- user_docs/en/userGuide.t2t | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 8b2d52438d7..2efc14563c2 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -754,14 +754,12 @@ Additional vision enhancement providers can be provided in [NVDA add-ons #Addons NVDA's vision settings can be changed in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. ++ NVDA Highlighter ++[VisionNVDAHighlighter] -When the NVDA Highlighter is enabled, it draws lines on the computer screen around the [system focus #SystemFocus], [navigator object #ObjectNavigation] and/or [browse mode virtual caret #BrowseMode]. -This visual highlights make it much easier for sighted or low vision people to see the objects they are interacting with. -It can also aid in accessibility testing by application developers. - -By default, the NVDA Highlighter highlights the focus, navigator object and browse mode position. -- If the navigator object overlaps the system focus (e.g. because the navigator object follows the system focus), a solid blue line is drawn around the object. -- If the navigator object does not overlap the system focus, the focus object is surrounded by a dashed blue rectangle. In this case, a solid pink line is drawn around the navigator object. -- When browse mode is enabled in cases where there is no physical caret (such as in web browsers), the character at the virtual browse mode caret is surrounded by a solid yellow line. +NVDA Highlighter can help to identify the [system focus #SystemFocus], [navigator object #ObjectNavigation] and [browse mode #BrowseMode] positions. +These positions are highlighted with a colored rectangle outline. +- Solid blue highlights a combined navigator object and system focus location (e.g. because [the navigator object follows the system focus #ReviewCursorFollowFocus]). +- Dashed blue highlights just the system focus object. +- Solid pink highlights just the navigator object. +- Solid yellow highlights the virtual caret used in browse mode (where there is no physical caret such as in web browsers). - When the NVDA Highlighter is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsVisionEnhancementProviderSettings] From f76ee191d65668c824d0064022f2d0dc9b99c4dc Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 21 Sep 2019 19:02:28 +0200 Subject: [PATCH 010/116] Tidy ctypes declarations for screen curtain --- .../screenCurtain.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 39be774ef24..8757098f071 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -21,6 +21,8 @@ class MAGCOLOREFFECT(Structure): _fields_ = (("transform", c_float * 5 * 5),) +# homogeneous matrix for a 4-space transformation (red, green, blue, opacity). +# https://docs.microsoft.com/en-gb/windows/win32/gdiplus/-gdiplus-using-a-color-matrix-to-transform-a-single-color-use TRANSFORM_BLACK = MAGCOLOREFFECT() TRANSFORM_BLACK.transform[4][4] = 1.0 @@ -36,17 +38,28 @@ class Magnification: _magnification = windll.Magnification - _MagInitializeFuncType = WINFUNCTYPE(BOOL) - _MagUninitializeFuncType = WINFUNCTYPE(BOOL) + # Set full screen color effect _MagSetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) _MagSetFullscreenColorEffectArgTypes = ((1, "effect"),) + + # Get full screen color effect _MagGetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) _MagGetFullscreenColorEffectArgTypes = ((2, "effect"),) + # show system cursor + _MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL) + _MagShowSystemCursorArgTypes = ((1, "showCursor"),) + + # initialise + _MagInitializeFuncType = WINFUNCTYPE(BOOL) MagInitialize = _MagInitializeFuncType(("MagInitialize", _magnification)) MagInitialize.errcheck = _errCheck + + # uninitialize + _MagUninitializeFuncType = WINFUNCTYPE(BOOL) MagUninitialize = _MagUninitializeFuncType(("MagUninitialize", _magnification)) MagUninitialize.errcheck = _errCheck + try: MagSetFullscreenColorEffect = _MagSetFullscreenColorEffectFuncType( ("MagSetFullscreenColorEffect", _magnification), @@ -61,6 +74,11 @@ class Magnification: except AttributeError: MagSetFullscreenColorEffect = None MagGetFullscreenColorEffect = None + MagShowSystemCursor = _MagShowSystemCursorFuncType( + ("MagShowSystemCursor", _magnification), + _MagShowSystemCursorArgTypes + ) + MagShowSystemCursor.errcheck = _errCheck class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): @@ -89,10 +107,12 @@ def canStart(cls): def __init__(self): super(VisionEnhancementProvider, self).__init__() Magnification.MagInitialize() + Magnification.MagShowSystemCursor(False) Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) def terminate(self, *args, **kwargs): super().terminate(*args, **kwargs) + Magnification.MagShowSystemCursor(True) Magnification.MagUninitialize() def registerEventExtensionPoints(self, extensionPoints): From 8a6fac2fea2589243c0b723e3583eee9413bc4fa Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 21 Sep 2019 19:13:47 +0200 Subject: [PATCH 011/116] List each vision provider in it's own group. Distinguish between runtime and static settings for providers. --- source/gui/settingsDialogs.py | 177 ++++++++++++++++++---------------- source/vision/__init__.py | 10 +- source/vision/providerBase.py | 14 +-- 3 files changed, 106 insertions(+), 95 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 29702d08779..4cd8e4a5627 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -34,7 +34,7 @@ import brailleTables import brailleInput import vision -from typing import Callable, List +from typing import Callable, List, Type, Optional import core import keyboardHandler import characterProcessing @@ -2871,55 +2871,37 @@ class VisionSettingsPanel(SettingsPanel): # Translators: This is the label for the vision panel title = _("Vision") - def makeSettings(self, settingsSizer): + def makeSettings(self, settingsSizer: wx.BoxSizer): self.initialProviders = list(vision.handler.providers) self.providerPanelInstances = [] - settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - - # Translators: The label for a setting in vision settings to choose a vision enhancement provider. - providerLabelText = _("&Vision enhancement providers") + self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - # Translators: Description in vision settings to choose a vision enhancement provider. - providersListDescriptionText = _( - "Providers are activated and deactivated as soon as you check or uncheck check boxes." - ) - providersSizer = guiHelper.BoxSizerHelper(self, orientation=wx.VERTICAL) - providerListDescTextCtrl = providersSizer.addItem(wx.StaticText(self, label=providersListDescriptionText)) - # The width that is available to the text is 544. - # Using providerSizer.Size.Width would be prefered, but it is not correct during construction. - # The value is dependent on the nesting level of the text, and has to be checked manually. - providerListDescTextCtrl.Wrap(self.scaleSize(544)) - self.providerList = providersSizer.addLabeledControl( - f"{providerLabelText}:", - nvdaControls.CustomCheckListBox, - choices=[] - ) - self.providerList.Bind(wx.EVT_CHECKLISTBOX, self.onProviderToggled) - - self.populateProviderList() - - settingsSizerHelper.addItem(providersSizer, flag=wx.EXPAND) - settingsSizerHelper.addItem( - wx.StaticLine(self), - flag=wx.EXPAND - ) - - self.providerPanelsSizer = wx.BoxSizer(wx.VERTICAL) - settingsSizerHelper.addItem(self.providerPanelsSizer, flag=wx.EXPAND) + for providerName, providerDesc, _providerRole, providerClass in vision.getProviderList(): + providerSizer = self.settingsSizerHelper.addItem( + wx.StaticBoxSizer(wx.StaticBox(self, label=providerDesc), wx.VERTICAL), + flag=wx.EXPAND + ) + settingsPanel = providerClass.getSettingsPanel() + if not settingsPanel: + settingsPanel = VisionProviderSubPanel_Default( + self, + providerClass=providerClass, + getProvider=self._getProvider, + initProvider=lambda providerName: self.safeInitProviders([providerName]), + terminateProvider=lambda providerName: self.safeTerminateProviders([providerName], verbose=True) + ) + if len(self.providerPanelInstances) > 0: + settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + providerSizer.Add(settingsPanel, flag=wx.EXPAND) + self.providerPanelInstances.append(settingsPanel) - def populateProviderList(self): - providerList = vision.getProviderList() - self.providerNames = [provider[0] for provider in providerList] - providerChoices = [provider[1] for provider in providerList] - self.providerList.Clear() - self.providerList.Items = providerChoices - self.providerList.Select(0) + def _getProvider(self, providerName: str) -> Optional[vision.VisionEnhancementProvider]: + return vision.handler.providers.get(providerName, None) def safeInitProviders( self, - providerNames: List[str], - confirmInitWithUser: bool = True + providerNames: List[str] ) -> bool: """Initializes one or more providers in a way that is gui friendly, showing an error if appropriate. @@ -2929,9 +2911,6 @@ def safeInitProviders( initErrors = [] for providerName in providerNames: try: - if confirmInitWithUser and not vision.handler.confirmInitWithUser(providerName): - success = False - continue vision.handler.initializeProvider(providerName) except Exception: initErrors.append(providerName) @@ -3009,24 +2988,6 @@ def safeTerminateProviders( self ) - def syncProviderCheckboxes(self): - self.providerList.CheckedItems = [self.providerNames.index(name) for name in vision.handler.providers] - - def onProviderToggled(self, evt): - evt.Skip() - index = evt.Int - if index != self.providerList.Selection: - # Toggled an unselected provider - self.providerList.Select(index) - providerName = self.providerNames[index] - isChecked = self.providerList.IsChecked(index) - if not isChecked: - self.safeTerminateProviders([providerName], verbose=True) - elif not self.safeInitProviders([providerName]): - self.providerList.Check(index, False) - return - self.refreshPanel() - def refreshPanel(self): self.Freeze() # trigger a refresh of the settings @@ -3034,24 +2995,7 @@ def refreshPanel(self): self._sendLayoutUpdatedEvent() self.Thaw() - def updateCurrentProviderPanels(self): - self.providerPanelsSizer.Clear(delete_windows=True) - self.providerPanelInstances[:] = [] - for name, providerInst in sorted(vision.handler.providers.items()): - if providerInst.guiPanelClass and ( - providerInst.guiPanelClass is not VisionProviderSubPanel or providerInst.supportedSettings - ): - if len(self.providerPanelInstances) > 0: - self.providerPanelsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - panelSizer = wx.StaticBoxSizer(wx.StaticBox(self, label=providerInst.description), wx.VERTICAL) - panel = providerInst.guiPanelClass(self, providerCallable=weakref.ref(providerInst)) - panelSizer.Add(panel, flag=wx.EXPAND) - self.providerPanelsSizer.Add(panelSizer, flag=wx.EXPAND) - self.providerPanelInstances.append(panel) - def onPanelActivated(self): - self.syncProviderCheckboxes() - self.updateCurrentProviderPanels() super().onPanelActivated() def onDiscard(self): @@ -3059,9 +3003,7 @@ def onDiscard(self): panel.onDiscard() providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] - # These providers were already initialized. - # Therefore, we do not display user confirmation messages, if any. - self.safeInitProviders(providersToInitialize, confirmInitWithUser=False) + self.safeInitProviders(providersToInitialize) providersToTerminate = [name for name in vision.handler.providers if name not in self.initialProviders] self.safeTerminateProviders(providersToTerminate) @@ -3071,8 +3013,10 @@ def onSave(self): self.initialProviders = list(vision.handler.providers) -class VisionProviderSubPanel(DriverSettingsMixin, SettingsPanel): - +class VisionProviderSubPanel_Runtime( + DriverSettingsMixin, + SettingsPanel +): def __init__( self, parent: wx.Window, @@ -3095,6 +3039,69 @@ def makeSettings(self, settingsSizer): self.updateDriverSettings() +class VisionProviderSubPanel_Default( + SettingsPanel +): + + def __init__( + self, + parent: wx.Window, + *, # Make next argument keyword only + providerClass: Type[vision.VisionEnhancementProvider], + getProvider: Callable[[str], vision.VisionEnhancementProvider], + initProvider, + terminateProvider + ): + """ + @param getProvider: A callable that returns an instance to a VisionEnhancementProvider. + This will usually be a weakref, but could be any callable taking no arguments. + """ + self.providerClass = providerClass + self._getProvider = lambda: getProvider(providerClass.name) + self._initProvider = lambda: initProvider(providerClass.name) + self._terminateProvider = lambda: terminateProvider(providerClass.name) + self._runtimeSettings: Optional[VisionProviderSubPanel_Runtime] = None + self._runtimeSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) + super().__init__(parent=parent) + + def makeSettings(self, settingsSizer): + checkBox = wx.CheckBox(self, label=_("Enable")) + settingsSizer.Add(checkBox) + settingsSizer.Add(self._runtimeSettingsSizer, flag=wx.EXPAND, proportion=1.0) + self._checkBox: wx.CheckBox = checkBox + if self._getProvider(): + self._createRuntimeSettings() + checkBox.SetValue(True) + checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) + + def _createRuntimeSettings(self): + self._runtimeSettings = VisionProviderSubPanel_Runtime( + self, + providerCallable=weakref.ref(self._getProvider()) + ) + self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) + + def _enableToggle(self, evt): + if not evt.IsChecked(): + self._runtimeSettings.onPanelDeactivated() + self._runtimeSettingsSizer.Clear(delete_windows=True) + self._runtimeSettings: Optional[VisionProviderSubPanel_Runtime] = None + self._terminateProvider() + else: + self._initProvider() + self._createRuntimeSettings() + self._runtimeSettings.onPanelActivated() + self._sendLayoutUpdatedEvent() + + def onDiscard(self): + if self._runtimeSettings: + self._runtimeSettings.onDiscard() + + def onSave(self): + if self._runtimeSettings: + self._runtimeSettings.onSave() + + """ The name of the config profile currently being edited, if any. This is set when the currently edited configuration profile is determined and returned to None when the dialog is destroyed. This can be used by an AppModule for NVDA to identify and announce diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 64dccc68ab6..afa2c56175a 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -10,15 +10,16 @@ Add-ons can provide their own provider using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ - +from vision.providerBase import VisionEnhancementProvider from .constants import Role from .visionHandler import VisionHandler, getProviderClass import pkgutil import visionEnhancementProviders import config from logHandler import log -from typing import List, Tuple +from typing import List, Tuple, Type, Optional +handler: Optional[VisionHandler] = None def initialize(): global handler @@ -40,7 +41,7 @@ def terminate(): def getProviderList( onlyStartable: bool = True -) -> List[Tuple[str, str, List[Role]]]: +) -> List[Tuple[str, str, List[Role], Type[VisionEnhancementProvider]]]: """Gets a list of available vision enhancement names with their descriptions as well as supported roles. @param onlyStartable: excludes all providers for which the check method returns C{False}. @return: list of tuples with provider names, provider descriptions, and supported roles. @@ -66,7 +67,8 @@ def getProviderList( providerList.append(( provider.name, provider.description, - list(provider.supportedRoles) + list(provider.supportedRoles), + provider )) else: log.debugWarning("Vision enhancement provider %s reports as unable to start, excluding" % provider.name) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index c9299974d9f..96a9cac143f 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -14,6 +14,9 @@ from typing import FrozenSet +class VisionEnhancementProviderStaticSettings(driverHandler.Driver): + _configSection = "vision" + class VisionEnhancementProvider(driverHandler.Driver): """A class for vision enhancement providers. """ @@ -29,13 +32,12 @@ def _get_supportedSettings(self): return super().supportedSettings @classmethod - def _get_guiPanelClass(cls): - """Returns the class to be used in order to construct a settings panel for the provider. - The class constructor should take the required providerCallable keyword argument. + def getSettingsPanel(cls): + """Returns the instance to be used in order to construct a settings panel for the provider. + @return: Optional[SettingsPanel] + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. """ - # Import late to avoid circular import - from gui.settingsDialogs import VisionProviderSubPanel - return VisionProviderSubPanel + return None def reinitialize(self): """Reinitializes a vision enhancement provider, reusing the same instance. From a39fca98afe8ddcab7ffac94230dc160420691cc Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 4 Oct 2019 17:09:50 +0200 Subject: [PATCH 012/116] Use a custom GUI for highlighter --- source/driverHandler.py | 5 + source/gui/settingsDialogs.py | 76 ++++--- source/vision/providerBase.py | 5 +- .../NVDAHighlighter.py | 193 +++++++++++++++--- .../screenCurtain.py | 2 + 5 files changed, 222 insertions(+), 59 deletions(-) diff --git a/source/driverHandler.py b/source/driverHandler.py index b098621e121..dc5af7e4a54 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -44,6 +44,10 @@ def __init__(self): @postcondition: This driver can be used. """ super(Driver, self).__init__() + self._registerConfigSaveAction() + + def _registerConfigSaveAction(self): + log.debug(f"registering: {self.__class__!r}") config.pre_configSave.register(self.saveSettings) @classmethod @@ -169,6 +173,7 @@ def _loadSpecificSettings( settings: Union[List, Tuple], onlyChanged: bool = False ): + log.debug(f"loading {cls._configSection} {cls.name}") conf = config.conf[cls._configSection][cls.name] for setting in settings: if not setting.useConfig or conf.get(setting.id) is None: diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 4cd8e4a5627..e20bfc4f130 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1109,8 +1109,12 @@ def makeBooleanSettingControl(self,setting): """Same as L{makeSliderSettingControl} but for boolean settings. Returns checkbox.""" checkbox=wx.CheckBox(self,wx.ID_ANY,label=setting.displayNameWithAccelerator) setattr(self,"%sCheckbox"%setting.id,checkbox) - checkbox.Bind(wx.EVT_CHECKBOX, - lambda evt: setattr(self.driver,setting.id,evt.IsChecked())) + + def onCheckChanged(evt: wx.CommandEvent): + evt.Skip() # allow other handlers to also process this event. + setattr(self.driver, setting.id, evt.IsChecked()) + + checkbox.Bind(wx.EVT_CHECKBOX, onCheckChanged) checkbox.SetValue(getattr(self.driver,setting.id)) if self.lastControl: checkbox.MoveAfterInTabOrder(self.lastControl) @@ -2882,21 +2886,32 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): wx.StaticBoxSizer(wx.StaticBox(self, label=providerDesc), wx.VERTICAL), flag=wx.EXPAND ) - settingsPanel = providerClass.getSettingsPanel() - if not settingsPanel: - settingsPanel = VisionProviderSubPanel_Default( + settingsPanelCls = providerClass.getSettingsPanelClass() + if not settingsPanelCls: + log.debug(f"Using default panel for providerName: {providerName}") + settingsPanelCls = VisionProviderSubPanel_Default + else: + log.debug(f"Using custom panel for providerName: {providerName}") + try: + settingsPanel = settingsPanelCls( self, - providerClass=providerClass, - getProvider=self._getProvider, - initProvider=lambda providerName: self.safeInitProviders([providerName]), - terminateProvider=lambda providerName: self.safeTerminateProviders([providerName], verbose=True) + # default value for name parameter to lambda, recommended by python3 FAQ: + # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result + getProvider=lambda name=providerName: self._getProvider(name), + initProvider=lambda name=providerName: self.safeInitProviders([name]), + terminateProvider=lambda name=providerName: self.safeTerminateProviders([name], verbose=True) ) + except: + log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) + continue + if len(self.providerPanelInstances) > 0: settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) providerSizer.Add(settingsPanel, flag=wx.EXPAND) self.providerPanelInstances.append(settingsPanel) def _getProvider(self, providerName: str) -> Optional[vision.VisionEnhancementProvider]: + log.debug(f"providerName: {providerName}") return vision.handler.providers.get(providerName, None) def safeInitProviders( @@ -3000,7 +3015,10 @@ def onPanelActivated(self): def onDiscard(self): for panel in self.providerPanelInstances: - panel.onDiscard() + try: + panel.onDiscard() + except: + log.debug(f"Error discarding providerPanel: {panel.__class__!r}", exc_info=True) providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] self.safeInitProviders(providersToInitialize) @@ -3009,7 +3027,10 @@ def onDiscard(self): def onSave(self): for panel in self.providerPanelInstances: - panel.onSave() + try: + panel.onSave() + except: + log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = list(vision.handler.providers) @@ -3047,19 +3068,17 @@ def __init__( self, parent: wx.Window, *, # Make next argument keyword only - providerClass: Type[vision.VisionEnhancementProvider], - getProvider: Callable[[str], vision.VisionEnhancementProvider], - initProvider, - terminateProvider + getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], + initProvider: Callable[[], bool], + terminateProvider: Callable[[], None] ): """ @param getProvider: A callable that returns an instance to a VisionEnhancementProvider. This will usually be a weakref, but could be any callable taking no arguments. """ - self.providerClass = providerClass - self._getProvider = lambda: getProvider(providerClass.name) - self._initProvider = lambda: initProvider(providerClass.name) - self._terminateProvider = lambda: terminateProvider(providerClass.name) + self._getProvider = getProvider + self._initProvider = initProvider + self._terminateProvider = terminateProvider self._runtimeSettings: Optional[VisionProviderSubPanel_Runtime] = None self._runtimeSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) super().__init__(parent=parent) @@ -3075,11 +3094,15 @@ def makeSettings(self, settingsSizer): checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) def _createRuntimeSettings(self): - self._runtimeSettings = VisionProviderSubPanel_Runtime( - self, - providerCallable=weakref.ref(self._getProvider()) - ) - self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) + try: + self._runtimeSettings = VisionProviderSubPanel_Runtime( + self, + providerCallable=weakref.ref(self._getProvider()) + ) + self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) + except: + return False + return True def _enableToggle(self, evt): if not evt.IsChecked(): @@ -3089,7 +3112,9 @@ def _enableToggle(self, evt): self._terminateProvider() else: self._initProvider() - self._createRuntimeSettings() + if not self._createRuntimeSettings(): + self._checkBox.SetValue(False) + return self._runtimeSettings.onPanelActivated() self._sendLayoutUpdatedEvent() @@ -3098,6 +3123,7 @@ def onDiscard(self): self._runtimeSettings.onDiscard() def onSave(self): + log.debug(f"calling VisionProviderSubPanel_Default") if self._runtimeSettings: self._runtimeSettings.onSave() diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 96a9cac143f..72c49e5c7db 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -11,11 +11,12 @@ from abc import abstractmethod from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import FrozenSet +from typing import FrozenSet, Type, Optional class VisionEnhancementProviderStaticSettings(driverHandler.Driver): _configSection = "vision" + cachePropertiesByDefault = True class VisionEnhancementProvider(driverHandler.Driver): """A class for vision enhancement providers. @@ -32,7 +33,7 @@ def _get_supportedSettings(self): return super().supportedSettings @classmethod - def getSettingsPanel(cls): + def getSettingsPanelClass(cls) -> Optional[Type]: """Returns the instance to be used in order to construct a settings panel for the provider. @return: Optional[SettingsPanel] @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 668652ad1db..43fd762f0d5 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -5,8 +5,10 @@ # Copyright (C) 2018-2019 NV Access Limited, Babbage B.V., Takuya Nishimoto """Default highlighter based on GDI Plus.""" +from typing import Callable, Optional, List, Tuple import vision +from baseObject import AutoPropertyObject from vision.constants import Role, Context from vision.util import getContextRect from windowUtils import CustomWindow @@ -184,53 +186,179 @@ def refresh(self): winUser.user32.InvalidateRect(self.handle, None, True) +_contextOptionLabelsWithAccelerators = { + # Translators: shown for a highlighter setting that toggles + # highlighting the system focus. + Context.FOCUS: _("Highlight system fo&cus"), + # Translators: shown for a highlighter setting that toggles + # highlighting the browse mode cursor. + Context.BROWSEMODE: _("Highlight browse &mode cursor"), + # Translators: shown for a highlighter setting that toggles + # highlighting the navigator object. + Context.NAVIGATOR: _("Highlight navigator &object"), +} + +_supportedContexts = (Context.FOCUS, Context.NAVIGATOR, Context.BROWSEMODE) + + +class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderStaticSettings): + name = "NVDAHighlighter" + # Default settings for parameters + highlightFocus = False + highlightNavigator = False + highlightBrowseMode = False + + supportedSettings: List[driverHandler.DriverSetting] + + def __init__(self): + super().__init__() + self.initSettings() + + def _get_supportedSettings(cls): + return [ + driverHandler.BooleanDriverSetting( + 'highlight%s' % (context[0].upper() + context[1:]), + _contextOptionLabelsWithAccelerators[context], + defaultVal=True + ) + for context in _supportedContexts + ] + + def check(cls): + return True + + def loadSettings(self, onlyChanged: bool = False): + super().loadSettings(onlyChanged) + + def saveSettings(self): + super().saveSettings() + + +class NVDAHighlighterGuiPanel( + gui.DriverSettingsMixin, + gui.SettingsPanel +): + def __init__( + self, + parent, + getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], + initProvider: Callable[[], bool], + terminateProvider: Callable[[], None] + ): + self._getProvider = getProvider + self._initProvider = initProvider + self._terminateProvider = terminateProvider + super().__init__(parent) + + + @property + def driver(self) -> driverHandler.Driver: + # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. + # We want them set on this class. + return VisionEnhancementProvider.settings + + def makeSettings(self, sizer): + self._enabledCheckbox = wx.CheckBox(self, label="Highlight focus", style=wx.CHK_3STATE) + self.lastControl = self._enabledCheckbox + sizer.Add(self._enabledCheckbox) + self._enableCheckSizer = sizer + self.mainSizer.Add(self._enableCheckSizer, flag=wx.ALL | wx.EXPAND) + optionsSizer = wx.StaticBoxSizer( + wx.StaticBox( + self, + # Translators: The label for a group box containing the NVDA welcome dialog options. + label=_("Options") + ), + wx.VERTICAL + ) + self.settingsSizer = optionsSizer + self.updateDriverSettings() + self.Bind(wx.EVT_CHECKBOX, self._onCheckEvent) + self._updateEnabledState() + + def onPanelActivated(self): + self.lastControl = self._enabledCheckbox + + def _updateEnabledState(self): + settings = VisionEnhancementProvider.settings + settingsToTriggerActivation = [ + settings.highlightBrowseMode, + settings.highlightFocus, + settings.highlightNavigator, + ] + if any(settingsToTriggerActivation): + if all(settingsToTriggerActivation): + self._enabledCheckbox.Set3StateValue(wx.CHK_CHECKED) + log.debug("all") + else: + self._enabledCheckbox.Set3StateValue(wx.CHK_UNDETERMINED) + log.debug("some") + self._ensureEnableState(True) + else: + self._enabledCheckbox.Set3StateValue(wx.CHK_UNCHECKED) + self._ensureEnableState(False) + + def _ensureEnableState(self, shouldBeEnabled: bool): + currentlyEnabled = bool(self._getProvider()) + if shouldBeEnabled and not currentlyEnabled: + log.debug("init provider") + self._initProvider() + elif not shouldBeEnabled and currentlyEnabled: + log.debug("terminate provider") + self._terminateProvider() + + def _onCheckEvent(self, evt: wx.CommandEvent): + settings = VisionEnhancementProvider.settings + if evt.GetEventObject() is self._enabledCheckbox: + settings.highlightBrowseMode = evt.IsChecked() + settings.highlightFocus = evt.IsChecked() + settings.highlightNavigator = evt.IsChecked() + self._ensureEnableState(evt.IsChecked()) + self.updateDriverSettings() + else: + self._updateEnabledState() + if self._getProvider(): + self._getProvider().refresh() + + class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): name = "NVDAHighlighter" # Translators: Description for NVDA's built-in screen highlighter. description = _("NVDA Highlighter") supportedRoles = frozenset([Role.HIGHLIGHTER]) - supportedContexts = (Context.FOCUS, Context.NAVIGATOR, Context.BROWSEMODE) _ContextStyles = { Context.FOCUS: DASH_BLUE, Context.NAVIGATOR: SOLID_PINK, Context.FOCUS_NAVIGATOR: SOLID_BLUE, Context.BROWSEMODE: SOLID_YELLOW, } - refreshInterval = 100 + _refreshInterval = 100 customWindowClass = HighlightWindow + settings = NVDAHighlighterSettings() - # Default settings for parameters - highlightFocus = True - highlightNavigator = True - highlightBrowseMode = True - - _contextOptionLabelsWithAccelerators = { - # Translators: shown for a highlighter setting that toggles - # highlighting the system focus. - Context.FOCUS: _("Highlight system fo&cus"), - # Translators: shown for a highlighter setting that toggles - # highlighting the browse mode cursor. - Context.BROWSEMODE: _("Highlight browse &mode cursor"), - # Translators: shown for a highlighter setting that toggles - # highlighting the navigator object. - Context.NAVIGATOR: _("Highlight navigator &object"), - } + @classmethod # impl required by vision.providerBase.VisionEnhancementProvider + def getSettingsPanelClass(cls): + """Returns the instance to be used in order to construct a settings panel for the provider. + @return: Optional[SettingsPanel] + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. + """ + return NVDAHighlighterGuiPanel - @classmethod + @classmethod # impl required by driverHandler.Driver def _get_supportedSettings(cls): - return [ - driverHandler.BooleanDriverSetting( - 'highlight%s' % (context[0].upper() + context[1:]), - cls._contextOptionLabelsWithAccelerators[context], - defaultVal=True - ) - for context in cls.supportedContexts - ] + return cls.settings.supportedSettings - @classmethod + @classmethod # impl required by driverHandler.Driver def canStart(cls) -> bool: return True + def _registerConfigSaveAction(self): + # we don't want to register + pass + + def initSettings(self): + pass + def registerEventExtensionPoints(self, extensionPoints): extensionPoints.post_focusChange.register(self.handleFocusChange) extensionPoints.post_reviewMove.register(self.handleReviewMove) @@ -253,13 +381,13 @@ def terminate(self, *args, **kwargs): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() - super().terminate(*args, **kwargs) + #self.settings.terminate(*args, **kwargs) def _run(self): if vision._isDebug(): log.debug("Starting NVDAHighlighter thread") window = self.window = self.customWindowClass(self) - self.timer = winUser.WinTimer(window.handle, 0, self.refreshInterval, None) + self.timer = winUser.WinTimer(window.handle, 0, self._refreshInterval, None) msg = MSG() while winUser.getMessage(byref(msg), None, 0, 0): winUser.user32.TranslateMessage(byref(msg)) @@ -306,10 +434,11 @@ def refresh(self): if self.window: self.window.refresh() + enabledContexts: Tuple[Context] def _get_enabledContexts(self): """Gets the contexts for which the highlighter is enabled. """ return tuple( - context for context in self.supportedContexts - if getattr(self, 'highlight%s' % (context[0].upper() + context[1:])) + context for context in _supportedContexts + if getattr(self.settings, 'highlight%s' % (context[0].upper() + context[1:])) ) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 8757098f071..f315e31f70f 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -15,6 +15,7 @@ import wx import gui import config +from logHandler import log class MAGCOLOREFFECT(Structure): @@ -106,6 +107,7 @@ def canStart(cls): def __init__(self): super(VisionEnhancementProvider, self).__init__() + log.debug(f"ScreenCurtain", stack_info=True) Magnification.MagInitialize() Magnification.MagShowSystemCursor(False) Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) From 5a226b13437d2835c3a57321a5e9783dd3633974 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 4 Oct 2019 19:15:54 +0200 Subject: [PATCH 013/116] Highligher custom gui works --- source/vision/__init__.py | 9 ++- source/vision/providerBase.py | 72 ++++++++++++++++--- source/vision/visionHandler.py | 10 +-- .../NVDAHighlighter.py | 63 ++++++---------- .../screenCurtain.py | 3 +- 5 files changed, 95 insertions(+), 62 deletions(-) diff --git a/source/vision/__init__.py b/source/vision/__init__.py index afa2c56175a..43038e56519 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -64,14 +64,17 @@ def getProviderList( continue try: if not onlyStartable or provider.canStart(): + providerSettings = provider.getSettings() providerList.append(( - provider.name, - provider.description, + providerSettings.name, + providerSettings.description, list(provider.supportedRoles), provider )) else: - log.debugWarning("Vision enhancement provider %s reports as unable to start, excluding" % provider.name) + log.debugWarning( + f"Excluding Vision enhancement provider {providerSettings.name} which is unable to start" + ) # todo: accessed before init? except Exception: # Purposely catch everything else as we don't want one failing provider # make it impossible to list all the others. diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 72c49e5c7db..6b132b67292 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -9,28 +9,72 @@ import driverHandler from abc import abstractmethod + +from baseObject import AutoPropertyObject from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import FrozenSet, Type, Optional +from typing import FrozenSet, Type, Optional, List, Union, Tuple + +SupportedSettingType = Union[ + List[driverHandler.DriverSetting], + Tuple[driverHandler.DriverSetting] +] class VisionEnhancementProviderStaticSettings(driverHandler.Driver): _configSection = "vision" cachePropertiesByDefault = True -class VisionEnhancementProvider(driverHandler.Driver): + supportedSettings: SupportedSettingType # Typing for autoprop L{_get_supportedSettings} + + def __init__(self): + super().__init__() + self.initSettings() + + @property + @abstractmethod + def name(self): # todo: rename this? "providerID" + """Application Friendly name, should be unique!""" + + @property + @abstractmethod + def description(self): # todo: rename this? "translated Name" + """Translated name""" + + def _get_supportedSettings(self) -> SupportedSettingType: + raise NotImplementedError( + f"_get_supportedSettings must be implemented in Class {self.__class__.__qualname__}" + ) + + @classmethod + def check(cls): # todo: remove, comes from Driver + return True + + def loadSettings(self, onlyChanged: bool = False): + super().loadSettings(onlyChanged) + + def saveSettings(self): + super().saveSettings() + + +class VisionEnhancementProvider(AutoPropertyObject): """A class for vision enhancement providers. """ - - _configSection = "vision" cachePropertiesByDefault = True #: The roles supported by this provider. #: This attribute is currently not used, #: but might be later for presentational purposes. supportedRoles: FrozenSet[Role] = frozenset() - def _get_supportedSettings(self): - return super().supportedSettings + @classmethod + def getSettings(cls) -> VisionEnhancementProviderStaticSettings: + """ + @remarks: The L{VisionEnhancementProviderStaticSettings} class should be implemented to define the settings + for your provider + """ + raise NotImplementedError( + f"getSettings must be implemented in Class {cls.__qualname__}" + ) @classmethod def getSettingsPanelClass(cls) -> Optional[Type]: @@ -41,12 +85,21 @@ def getSettingsPanelClass(cls) -> Optional[Type]: return None def reinitialize(self): - """Reinitializes a vision enhancement provider, reusing the same instance. + """Reinitialize a vision enhancement provider, reusing the same instance. This base implementation simply calls terminate and __init__ consecutively. """ self.terminate() self.__init__() + # todo: remove saveSettings param + def terminate(self, saveSettings: bool = True): + """Terminate this driver. + This should be used for any required clean up. + @param saveSettings: Whether settings should be saved on termination. + @precondition: L{initialize} has been called. + @postcondition: This driver can no longer be used. + """ + @abstractmethod def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints): """ @@ -65,10 +118,7 @@ def canStart(cls) -> bool: """Returns whether this provider is able to start.""" return False - @classmethod - def check(cls) -> bool: - return cls.canStart() - + # todo: remove this, providers should do this themselves @classmethod def confirmInitWithUser(cls) -> bool: """Before initialisation of the provider, diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 7f41b35eff6..c28af8161fb 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -80,7 +80,7 @@ def terminateProvider(self, providerName: str, saveSettings: bool = True): @param providerName: The provider to terminate. @param saveSettings: Whether settings should be saved on termionation. """ - success = True + success = True # todo: can we remove this? seems unused, what is the history? # Remove the provider from the providers dictionary. providerInstance = self.providers.pop(providerName, None) if not providerInstance: @@ -133,7 +133,6 @@ def initializeProvider(self, providerName: str, temporary: bool = False): This defaults to C{False}. If C{True}, no changes will be performed to the configuration. """ - providerCls = None providerInst = self.providers.pop(providerName, None) if providerInst is not None: providerCls = type(providerInst) @@ -163,9 +162,10 @@ def initializeProvider(self, providerName: str, temporary: bool = False): log.error( f"Error terminating provider {providerName} after registering to extension points", exc_info=True) raise registerEventExtensionPointsException - providerInst.initSettings() - if not temporary and providerCls.name not in config.conf['vision']['providers']: - config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerCls.name] + providerSettings = providerCls.getSettings() + providerSettings.initSettings() # todo: do we actually have to do this here? It might actually cause a bug, reloading settings and overwriting current static settings. + if not temporary and providerName not in config.conf['vision']['providers']: + config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerName] self.providers[providerName] = providerInst try: self.initialFocus() diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 43fd762f0d5..c0fd514b735 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -5,10 +5,9 @@ # Copyright (C) 2018-2019 NV Access Limited, Babbage B.V., Takuya Nishimoto """Default highlighter based on GDI Plus.""" -from typing import Callable, Optional, List, Tuple +from typing import Callable, Optional, Tuple import vision -from baseObject import AutoPropertyObject from vision.constants import Role, Context from vision.util import getContextRect from windowUtils import CustomWindow @@ -203,17 +202,14 @@ def refresh(self): class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderStaticSettings): name = "NVDAHighlighter" + # Translators: Description for NVDA's built-in screen highlighter. + description = _("NVDA Highlighter") + supportedRoles = frozenset([Role.HIGHLIGHTER]) # Default settings for parameters highlightFocus = False highlightNavigator = False highlightBrowseMode = False - supportedSettings: List[driverHandler.DriverSetting] - - def __init__(self): - super().__init__() - self.initSettings() - def _get_supportedSettings(cls): return [ driverHandler.BooleanDriverSetting( @@ -224,15 +220,6 @@ def _get_supportedSettings(cls): for context in _supportedContexts ] - def check(cls): - return True - - def loadSettings(self, onlyChanged: bool = False): - super().loadSettings(onlyChanged) - - def saveSettings(self): - super().saveSettings() - class NVDAHighlighterGuiPanel( gui.DriverSettingsMixin, @@ -252,10 +239,10 @@ def __init__( @property - def driver(self) -> driverHandler.Driver: + def driver(self) -> driverHandler.Driver: # todo: call this something other than driver # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. # We want them set on this class. - return VisionEnhancementProvider.settings + return VisionEnhancementProvider.getSettings() def makeSettings(self, sizer): self._enabledCheckbox = wx.CheckBox(self, label="Highlight focus", style=wx.CHK_3STATE) @@ -280,7 +267,7 @@ def onPanelActivated(self): self.lastControl = self._enabledCheckbox def _updateEnabledState(self): - settings = VisionEnhancementProvider.settings + settings = VisionEnhancementProvider._settings settingsToTriggerActivation = [ settings.highlightBrowseMode, settings.highlightFocus, @@ -308,7 +295,7 @@ def _ensureEnableState(self, shouldBeEnabled: bool): self._terminateProvider() def _onCheckEvent(self, evt: wx.CommandEvent): - settings = VisionEnhancementProvider.settings + settings = VisionEnhancementProvider._settings if evt.GetEventObject() is self._enabledCheckbox: settings.highlightBrowseMode = evt.IsChecked() settings.highlightFocus = evt.IsChecked() @@ -321,11 +308,7 @@ def _onCheckEvent(self, evt: wx.CommandEvent): self._getProvider().refresh() -class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): - name = "NVDAHighlighter" - # Translators: Description for NVDA's built-in screen highlighter. - description = _("NVDA Highlighter") - supportedRoles = frozenset([Role.HIGHLIGHTER]) +class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): _ContextStyles = { Context.FOCUS: DASH_BLUE, Context.NAVIGATOR: SOLID_PINK, @@ -334,7 +317,13 @@ class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): } _refreshInterval = 100 customWindowClass = HighlightWindow - settings = NVDAHighlighterSettings() + _settings = NVDAHighlighterSettings() + + enabledContexts: Tuple[Context] # type info for autoprop: L{_get_enableContexts} + + @classmethod + def getSettings(cls): + return cls._settings @classmethod # impl required by vision.providerBase.VisionEnhancementProvider def getSettingsPanelClass(cls): @@ -344,21 +333,10 @@ def getSettingsPanelClass(cls): """ return NVDAHighlighterGuiPanel - @classmethod # impl required by driverHandler.Driver - def _get_supportedSettings(cls): - return cls.settings.supportedSettings - - @classmethod # impl required by driverHandler.Driver + @classmethod # impl required by proivderBase.VisionEnhancementProvider def canStart(cls) -> bool: return True - def _registerConfigSaveAction(self): - # we don't want to register - pass - - def initSettings(self): - pass - def registerEventExtensionPoints(self, extensionPoints): extensionPoints.post_focusChange.register(self.handleFocusChange) extensionPoints.post_reviewMove.register(self.handleReviewMove) @@ -381,7 +359,6 @@ def terminate(self, *args, **kwargs): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() - #self.settings.terminate(*args, **kwargs) def _run(self): if vision._isDebug(): @@ -434,11 +411,13 @@ def refresh(self): if self.window: self.window.refresh() - enabledContexts: Tuple[Context] def _get_enabledContexts(self): """Gets the contexts for which the highlighter is enabled. """ return tuple( context for context in _supportedContexts - if getattr(self.settings, 'highlight%s' % (context[0].upper() + context[1:])) + if getattr(self.getSettings(), 'highlight%s' % (context[0].upper() + context[1:])) ) + + +VisionEnhancementProvider = NVDAHightlighter diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index f315e31f70f..6818ceddfd5 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -103,7 +103,8 @@ class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): @classmethod def canStart(cls): - return winVersion.isFullScreenMagnificationAvailable() + # return winVersion.isFullScreenMagnificationAvailable() + return False def __init__(self): super(VisionEnhancementProvider, self).__init__() From 2b8701c99b40bd22ac2a388bb2134181bf22ea22 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 4 Oct 2019 19:37:49 +0200 Subject: [PATCH 014/116] Highlighter works without custom gui --- source/gui/settingsDialogs.py | 3 ++- source/visionEnhancementProviders/NVDAHighlighter.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index e20bfc4f130..751134bdcd5 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3053,7 +3053,7 @@ def __init__( @property def driver(self): - return self._providerCallable() + return self._providerCallable().getSettings() def makeSettings(self, settingsSizer): # Construct vision enhancement provider settings @@ -3101,6 +3101,7 @@ def _createRuntimeSettings(self): ) self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) except: + log.error("unable to create runtime settings", exc_info=True) return False return True diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index c0fd514b735..62035310555 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -331,7 +331,7 @@ def getSettingsPanelClass(cls): @return: Optional[SettingsPanel] @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. """ - return NVDAHighlighterGuiPanel + return None @classmethod # impl required by proivderBase.VisionEnhancementProvider def canStart(cls) -> bool: From 82ea4282cf69f3dde9ef52010cfa275fb58f48f6 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 7 Oct 2019 12:38:44 +0200 Subject: [PATCH 015/116] dynamic addition of settings at runtime is possible with auto (not custom) settings GUI --- source/gui/settingsDialogs.py | 92 +++++++++++++------ source/vision/__init__.py | 4 +- source/vision/providerBase.py | 4 +- .../NVDAHighlighter.py | 33 ++++++- 4 files changed, 98 insertions(+), 35 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 751134bdcd5..160049bfa8e 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1040,8 +1040,11 @@ def __init__(self, *args, **kwargs): super(DriverSettingsMixin,self).__init__(*args,**kwargs) self._curDriverRef = weakref.ref(self.driver) - @abstractproperty def driver(self): + return self.getSettings() + + @abstractmethod + def getSettings(self): raise NotImplementedError @classmethod @@ -1131,6 +1134,9 @@ def updateDriverSettings(self, changedSetting=None): if not self.driver.isSupported(name): self.settingsSizer.Hide(sizer) #Create new controls, update already existing + log.debug(f"Current sizerDict: {self.sizerDict!r}") + + log.debug(f"Current supportedSettings: {self.driver.supportedSettings!r}") for setting in self.driver.supportedSettings: if setting.id == changedSetting: # Changing a setting shouldn't cause that setting's own values to change. @@ -1154,6 +1160,7 @@ def updateDriverSettings(self, changedSetting=None): if isinstance(setting,NumericDriverSetting): settingMaker=self.makeSliderSettingControl elif isinstance(setting,BooleanDriverSetting): + log.debug(f"creating a new bool driver setting: {setting.id}") settingMaker=self.makeBooleanSettingControl else: settingMaker=self.makeStringSettingControl @@ -2886,20 +2893,25 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): wx.StaticBoxSizer(wx.StaticBox(self, label=providerDesc), wx.VERTICAL), flag=wx.EXPAND ) + kwargs = { + # default value for name parameter to lambda, recommended by python3 FAQ: + # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result + "getProvider": lambda name=providerName: self._getProvider(name), + "initProvider": lambda name=providerName: self.safeInitProviders([name]), + "terminateProvider": lambda name=providerName: self.safeTerminateProviders([name], verbose=True) + } + settingsPanelCls = providerClass.getSettingsPanelClass() if not settingsPanelCls: log.debug(f"Using default panel for providerName: {providerName}") - settingsPanelCls = VisionProviderSubPanel_Default + settingsPanelCls = VisionProviderSubPanel_Wrapper + kwargs["providerType"] = providerClass else: log.debug(f"Using custom panel for providerName: {providerName}") try: settingsPanel = settingsPanelCls( self, - # default value for name parameter to lambda, recommended by python3 FAQ: - # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result - getProvider=lambda name=providerName: self._getProvider(name), - initProvider=lambda name=providerName: self.safeInitProviders([name]), - terminateProvider=lambda name=providerName: self.safeTerminateProviders([name], verbose=True) + **kwargs ) except: log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) @@ -3034,33 +3046,39 @@ def onSave(self): self.initialProviders = list(vision.handler.providers) -class VisionProviderSubPanel_Runtime( +class VisionProviderSubPanel_Settings( DriverSettingsMixin, SettingsPanel ): + cachePropertiesByDefault = False def __init__( self, parent: wx.Window, *, # Make next argument keyword only - providerCallable: Callable[[], vision.providerBase.VisionEnhancementProvider] + settingsCallable: Callable[[], vision.providerBase.VisionEnhancementProviderStaticSettings] ): """ @param providerCallable: A callable that returns an instance to a VisionEnhancementProvider. This will usually be a weakref, but could be any callable taking no arguments. """ - self._providerCallable = providerCallable + self._settingsCallable = settingsCallable super().__init__(parent=parent) @property def driver(self): - return self._providerCallable().getSettings() + return self.getSettings() + + def getSettings(self): + settings = self._settingsCallable() + log.debug(f"getting settings: {settings.__class__!r}") + return settings def makeSettings(self, settingsSizer): # Construct vision enhancement provider settings self.updateDriverSettings() -class VisionProviderSubPanel_Default( +class VisionProviderSubPanel_Wrapper( SettingsPanel ): @@ -3068,18 +3086,30 @@ def __init__( self, parent: wx.Window, *, # Make next argument keyword only - getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], - initProvider: Callable[[], bool], - terminateProvider: Callable[[], None] + # todo: make these part of a class: + providerType: Type[vision.VisionEnhancementProvider], + getProvider: Callable[ + [], + Optional[vision.VisionEnhancementProvider] + ],# mostly used to see if the provider is initialised or not. + initProvider: Callable[ + [], + bool + ], + terminateProvider: Callable[ + [], + None + ] ): """ @param getProvider: A callable that returns an instance to a VisionEnhancementProvider. This will usually be a weakref, but could be any callable taking no arguments. """ + self._providerType = providerType self._getProvider = getProvider self._initProvider = initProvider self._terminateProvider = terminateProvider - self._runtimeSettings: Optional[VisionProviderSubPanel_Runtime] = None + self._runtimeSettings: Optional[VisionProviderSubPanel_Settings] = None self._runtimeSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) super().__init__(parent=parent) @@ -3089,15 +3119,17 @@ def makeSettings(self, settingsSizer): settingsSizer.Add(self._runtimeSettingsSizer, flag=wx.EXPAND, proportion=1.0) self._checkBox: wx.CheckBox = checkBox if self._getProvider(): - self._createRuntimeSettings() checkBox.SetValue(True) - checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) + if self._createRuntimeSettings(): + checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) + else: + checkBox.Bind(wx.EVT_CHECKBOX, self._nonEnableableGUI) def _createRuntimeSettings(self): try: - self._runtimeSettings = VisionProviderSubPanel_Runtime( + self._runtimeSettings = VisionProviderSubPanel_Settings( self, - providerCallable=weakref.ref(self._getProvider()) + settingsCallable=self._providerType.getSettings ) self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) except: @@ -3105,17 +3137,23 @@ def _createRuntimeSettings(self): return False return True + def _nonEnableableGUI(self, evt): + wx.MessageBox( + # Translators: Shown when there is an error showing the GUI for a vision enhancement provider + _("Unable to configure GUI for Vision Enhancement Provider, it can not be enabled."), + parent=self, + ) + self._checkBox.SetValue(False) + + def _enableToggle(self, evt): if not evt.IsChecked(): - self._runtimeSettings.onPanelDeactivated() - self._runtimeSettingsSizer.Clear(delete_windows=True) - self._runtimeSettings: Optional[VisionProviderSubPanel_Runtime] = None self._terminateProvider() + self._runtimeSettings.updateDriverSettings() + self._runtimeSettings.onPanelActivated() else: self._initProvider() - if not self._createRuntimeSettings(): - self._checkBox.SetValue(False) - return + self._runtimeSettings.updateDriverSettings() self._runtimeSettings.onPanelActivated() self._sendLayoutUpdatedEvent() @@ -3124,7 +3162,7 @@ def onDiscard(self): self._runtimeSettings.onDiscard() def onSave(self): - log.debug(f"calling VisionProviderSubPanel_Default") + log.debug(f"calling VisionProviderSubPanel_Wrapper") if self._runtimeSettings: self._runtimeSettings.onSave() diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 43038e56519..bc2e179b7d9 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -73,8 +73,8 @@ def getProviderList( )) else: log.debugWarning( - f"Excluding Vision enhancement provider {providerSettings.name} which is unable to start" - ) # todo: accessed before init? + f"Excluding Vision enhancement provider {name} which is unable to start" + ) except Exception: # Purposely catch everything else as we don't want one failing provider # make it impossible to list all the others. diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 6b132b67292..6bd001aebc1 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -23,7 +23,7 @@ class VisionEnhancementProviderStaticSettings(driverHandler.Driver): _configSection = "vision" - cachePropertiesByDefault = True + cachePropertiesByDefault = False supportedSettings: SupportedSettingType # Typing for autoprop L{_get_supportedSettings} @@ -80,7 +80,7 @@ def getSettings(cls) -> VisionEnhancementProviderStaticSettings: def getSettingsPanelClass(cls) -> Optional[Type]: """Returns the instance to be used in order to construct a settings panel for the provider. @return: Optional[SettingsPanel] - @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. """ return None diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 62035310555..c2ee68f4446 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -210,7 +210,7 @@ class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderStati highlightNavigator = False highlightBrowseMode = False - def _get_supportedSettings(cls): + def _get_supportedSettings(self): return [ driverHandler.BooleanDriverSetting( 'highlight%s' % (context[0].upper() + context[1:]), @@ -221,6 +221,18 @@ def _get_supportedSettings(cls): ] +class NVDAHighlighterSettings_Runtime(NVDAHighlighterSettings): + someRuntimeOnlySetting = True + + def _get_supportedSettings(self): + settings = super()._get_supportedSettings() + settings.append(driverHandler.BooleanDriverSetting( + "someRuntimeOnlySetting", "Some runtime only setting", + defaultVal=True + )) + log.info("Runtime settings!") + return settings + class NVDAHighlighterGuiPanel( gui.DriverSettingsMixin, gui.SettingsPanel @@ -237,9 +249,11 @@ def __init__( self._terminateProvider = terminateProvider super().__init__(parent) - @property def driver(self) -> driverHandler.Driver: # todo: call this something other than driver + return self.getSettings() + + def getSettings(self) -> driverHandler.Driver: # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. # We want them set on this class. return VisionEnhancementProvider.getSettings() @@ -323,15 +337,19 @@ class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): @classmethod def getSettings(cls): + log.debug(f"getting settings: {cls._settings.__class__!r}") return cls._settings @classmethod # impl required by vision.providerBase.VisionEnhancementProvider def getSettingsPanelClass(cls): """Returns the instance to be used in order to construct a settings panel for the provider. @return: Optional[SettingsPanel] - @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Default} is used. + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. """ - return None + # When using custom panel, dont change settings dynamically + # see comment in __init__ + return NVDAHighlighterGuiPanel + # return None @classmethod # impl required by proivderBase.VisionEnhancementProvider def canStart(cls) -> bool: @@ -351,6 +369,11 @@ def __init__(self): self._highlighterThread.daemon = True self._highlighterThread.start() + # Demonstrate adding runtime settings, to test this: + # - make getSettingsPanelClass return None + # - un-comment equivelent line in terminate (restoring settings to non-runtime version) + # self.__class__._settings = NVDAHighlighterSettings_Runtime() + def terminate(self, *args, **kwargs): if self._highlighterThread: if not winUser.user32.PostThreadMessageW(self._highlighterThread.ident, winUser.WM_QUIT, 0, 0): @@ -359,6 +382,8 @@ def terminate(self, *args, **kwargs): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() + # see comment in __init__ + # self.__class__._settings = NVDAHighlighterSettings() def _run(self): if vision._isDebug(): From e40f864f240a03ad5d4ef314efa88982623223d2 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 17 Oct 2019 10:18:28 +0200 Subject: [PATCH 016/116] move driver settings and utils --- source/autoSettingsUtils/__init__.py | 0 source/autoSettingsUtils/driverSetting.py | 87 +++++++++++++++++ source/autoSettingsUtils/utils.py | 23 +++++ source/driverHandler.py | 110 ---------------------- 4 files changed, 110 insertions(+), 110 deletions(-) create mode 100644 source/autoSettingsUtils/__init__.py create mode 100644 source/autoSettingsUtils/driverSetting.py create mode 100644 source/autoSettingsUtils/utils.py diff --git a/source/autoSettingsUtils/__init__.py b/source/autoSettingsUtils/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py new file mode 100644 index 00000000000..97c4f425300 --- /dev/null +++ b/source/autoSettingsUtils/driverSetting.py @@ -0,0 +1,87 @@ + +class DriverSetting(AutoPropertyObject): + """Represents a synthesizer or braille display setting such as voice, variant or dot firmness. + """ + + def _get_configSpec(self): + """Returns the configuration specification of this particular setting for config file validator. + @rtype: str + """ + return "string(default={defaultVal})".format(defaultVal=self.defaultVal) + + def __init__(self, id, displayNameWithAccelerator, + availableInSettingsRing=False, defaultVal=None, displayName=None, useConfig=True): + """ + @param id: internal identifier of the setting + @type id: str + @param displayNameWithAccelerator: the localized string shown in voice or braille settings dialog + @type displayNameWithAccelerator: str + @param availableInSettingsRing: Will this option be available in a settings ring? + @type availableInSettingsRing: bool + @param defaultVal: Specifies the default value for a driver setting. + @type param defaultVal: str or C{None} + @param displayName: the localized string used in synth settings ring or None to use displayNameWithAccelerator + @type displayName: str + @param useConfig: Whether the value of this option is loaded from and saved to NVDA's configuration. + Set this to C{False} if the driver deals with loading and saving. + @type useConfig: bool + """ + self.id = id + self.displayNameWithAccelerator = displayNameWithAccelerator + if not displayName: + # Strip accelerator from displayNameWithAccelerator. + displayName = displayNameWithAccelerator.replace("&","") + self.displayName = displayName + self.availableInSettingsRing = availableInSettingsRing + self.defaultVal = defaultVal + self.useConfig = useConfig + +class NumericDriverSetting(DriverSetting): + """Represents a numeric driver setting such as rate, volume, pitch or dot firmness.""" + + def _get_configSpec(self): + return "integer(default={defaultVal},min={minVal},max={maxVal})".format( + defaultVal=self.defaultVal,minVal=self.minVal,maxVal=self.maxVal) + + def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, + defaultVal=50, minVal=0, maxVal=100, minStep=1, normalStep=5, largeStep=10, + displayName=None, useConfig=True): + """ + @param defaultVal: Specifies the default value for a numeric driver setting. + @type defaultVal: int + @param minVal: Specifies the minimum valid value for a numeric driver setting. + @type minVal: int + @param maxVal: Specifies the maximum valid value for a numeric driver setting. + @type maxVal: int + @param minStep: Specifies the minimum step between valid values for each numeric setting. For example, if L{minStep} is set to 10, setting values can only be multiples of 10; 10, 20, 30, etc. + @type minStep: int + @param normalStep: Specifies the step between values that a user will normally prefer. This is used in the settings ring. + @type normalStep: int + @param largeStep: Specifies the step between values if a large adjustment is desired. This is used for pageUp/pageDown on sliders in the Voice Settings dialog. + @type largeStep: int + @note: If necessary, the step values will be normalised so that L{minStep} <= L{normalStep} <= L{largeStep}. + """ + super(NumericDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, + defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) + self.minVal=minVal + self.maxVal=max(maxVal,self.defaultVal) + self.minStep=minStep + self.normalStep=max(normalStep,minStep) + self.largeStep=max(largeStep,self.normalStep) + +class BooleanDriverSetting(DriverSetting): + """Represents a boolean driver setting such as rate boost or automatic time sync. + """ + + def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, + displayName=None, defaultVal=False, useConfig=True): + """ + @param defaultVal: Specifies the default value for a boolean driver setting. + @type defaultVal: bool + """ + super(BooleanDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, + defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) + + def _get_configSpec(self): + defaultVal = repr(self.defaultVal) if self.defaultVal is not None else self.defaultVal + return "boolean(default={defaultVal})".format(defaultVal=defaultVal) diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py new file mode 100644 index 00000000000..5a2ee834d59 --- /dev/null +++ b/source/autoSettingsUtils/utils.py @@ -0,0 +1,23 @@ + +class UnsupportedConfigParameterError(NotImplementedError): + """ + Raised when changing or retrieving a driver setting that is unsupported for the connected device. + """ + +class StringParameterInfo(object): + """ + The base class used to represent a value of a string driver setting. + """ + + def __init__(self, id, displayName): + """ + @param id: The unique identifier of the value. + @type id: str + @param displayName: The name of the value, visible to the user. + @type displayName: str + """ + self.id = id + self.displayName = displayName + # Keep backwards compatibility + self.ID = id + self.name = displayName diff --git a/source/driverHandler.py b/source/driverHandler.py index dc5af7e4a54..73908663478 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -229,113 +229,3 @@ def _percentToParam(cls, percent, min, max): @type max: int """ return int(round(float(percent) / 100 * (max - min) + min)) - -class DriverSetting(AutoPropertyObject): - """Represents a synthesizer or braille display setting such as voice, variant or dot firmness. - """ - - def _get_configSpec(self): - """Returns the configuration specification of this particular setting for config file validator. - @rtype: str - """ - return "string(default={defaultVal})".format(defaultVal=self.defaultVal) - - def __init__(self, id, displayNameWithAccelerator, - availableInSettingsRing=False, defaultVal=None, displayName=None, useConfig=True): - """ - @param id: internal identifier of the setting - @type id: str - @param displayNameWithAccelerator: the localized string shown in voice or braille settings dialog - @type displayNameWithAccelerator: str - @param availableInSettingsRing: Will this option be available in a settings ring? - @type availableInSettingsRing: bool - @param defaultVal: Specifies the default value for a driver setting. - @type param defaultVal: str or C{None} - @param displayName: the localized string used in synth settings ring or None to use displayNameWithAccelerator - @type displayName: str - @param useConfig: Whether the value of this option is loaded from and saved to NVDA's configuration. - Set this to C{False} if the driver deals with loading and saving. - @type useConfig: bool - """ - self.id = id - self.displayNameWithAccelerator = displayNameWithAccelerator - if not displayName: - # Strip accelerator from displayNameWithAccelerator. - displayName = displayNameWithAccelerator.replace("&","") - self.displayName = displayName - self.availableInSettingsRing = availableInSettingsRing - self.defaultVal = defaultVal - self.useConfig = useConfig - -class NumericDriverSetting(DriverSetting): - """Represents a numeric driver setting such as rate, volume, pitch or dot firmness.""" - - def _get_configSpec(self): - return "integer(default={defaultVal},min={minVal},max={maxVal})".format( - defaultVal=self.defaultVal,minVal=self.minVal,maxVal=self.maxVal) - - def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, - defaultVal=50, minVal=0, maxVal=100, minStep=1, normalStep=5, largeStep=10, - displayName=None, useConfig=True): - """ - @param defaultVal: Specifies the default value for a numeric driver setting. - @type defaultVal: int - @param minVal: Specifies the minimum valid value for a numeric driver setting. - @type minVal: int - @param maxVal: Specifies the maximum valid value for a numeric driver setting. - @type maxVal: int - @param minStep: Specifies the minimum step between valid values for each numeric setting. For example, if L{minStep} is set to 10, setting values can only be multiples of 10; 10, 20, 30, etc. - @type minStep: int - @param normalStep: Specifies the step between values that a user will normally prefer. This is used in the settings ring. - @type normalStep: int - @param largeStep: Specifies the step between values if a large adjustment is desired. This is used for pageUp/pageDown on sliders in the Voice Settings dialog. - @type largeStep: int - @note: If necessary, the step values will be normalised so that L{minStep} <= L{normalStep} <= L{largeStep}. - """ - super(NumericDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, - defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) - self.minVal=minVal - self.maxVal=max(maxVal,self.defaultVal) - self.minStep=minStep - self.normalStep=max(normalStep,minStep) - self.largeStep=max(largeStep,self.normalStep) - -class BooleanDriverSetting(DriverSetting): - """Represents a boolean driver setting such as rate boost or automatic time sync. - """ - - def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, - displayName=None, defaultVal=False, useConfig=True): - """ - @param defaultVal: Specifies the default value for a boolean driver setting. - @type defaultVal: bool - """ - super(BooleanDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, - defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) - - def _get_configSpec(self): - defaultVal = repr(self.defaultVal) if self.defaultVal is not None else self.defaultVal - return "boolean(default={defaultVal})".format(defaultVal=defaultVal) - -class UnsupportedConfigParameterError(NotImplementedError): - """ - Raised when changing or retrieving a driver setting that is unsupported for the connected device. - """ - -class StringParameterInfo(object): - """ - The base class used to represent a value of a string driver setting. - """ - - def __init__(self, id, displayName): - """ - @param id: The unique identifier of the value. - @type id: str - @param displayName: The name of the value, visible to the user. - @type displayName: str - """ - self.id = id - self.displayName = displayName - # Keep backwards compatibility - self.ID = id - self.name = displayName From 920ca6d1eec563379b88874cfff08e7ba6f94124 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 17 Oct 2019 11:36:42 +0200 Subject: [PATCH 017/116] Move percent/param conversion helpers --- source/autoSettingsUtils/driverSetting.py | 2 ++ source/autoSettingsUtils/utils.py | 24 ++++++++++++++++++ source/driverHandler.py | 31 +++++++++++++++++------ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py index 97c4f425300..4cc39a434be 100644 --- a/source/autoSettingsUtils/driverSetting.py +++ b/source/autoSettingsUtils/driverSetting.py @@ -1,3 +1,5 @@ +from baseObject import AutoPropertyObject + class DriverSetting(AutoPropertyObject): """Represents a synthesizer or braille display setting such as voice, variant or dot firmness. diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py index 5a2ee834d59..183603fd46a 100644 --- a/source/autoSettingsUtils/utils.py +++ b/source/autoSettingsUtils/utils.py @@ -1,4 +1,28 @@ +def paramToPercent(current, min, max): + """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. + @param current: The current value. + @type current: int + @param min: The minimum value. + @type current: int + @param max: The maximum value. + @type max: int + """ + return int(round(float(current - min) / (max - min) * 100)) + + +def percentToParam(percent, min, max): + """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum raw parameter values. + @param percent: The current percentage. + @type percent: int + @param min: The minimum raw parameter value. + @type min: int + @param max: The maximum raw parameter value. + @type max: int + """ + return int(round(float(percent) / 100 * (max - min) + min)) + + class UnsupportedConfigParameterError(NotImplementedError): """ Raised when changing or retrieving a driver setting that is unsupported for the connected device. diff --git a/source/driverHandler.py b/source/driverHandler.py index 73908663478..e7f2d9aed22 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -1,12 +1,27 @@ # -*- coding: UTF-8 -*- -#driverHandler.py -#A part of NonVisual Desktop Access (NVDA) -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. -#Copyright (C) 2006-2018 NV Access Limited, Leonard de Ruijter +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2006-2019 NV Access Limited, Leonard de Ruijter """Handler for driver functionality that is global to synthesizers and braille displays.""" - +from autoSettingsUtils.autoSettings import AutoSettings +from autoSettingsUtils.utils import ( + paramToPercent, + percentToParam +) + +# F401: the following imports, while unused in this file, are provided for backwards compatibility. +from autoSettingsUtils.driverSetting import ( # noqa: F401 + DriverSetting, + BooleanDriverSetting, + NumericDriverSetting, + AutoPropertyObject, +) +from autoSettingsUtils.utils import ( # noqa: F401 + UnsupportedConfigParameterError, + StringParameterInfo, +) from baseObject import AutoPropertyObject import config from copy import deepcopy @@ -216,7 +231,7 @@ def _paramToPercent(cls, current, min, max): @param max: The maximum value. @type max: int """ - return int(round(float(current - min) / (max - min) * 100)) + return paramToPercent(current, min, max) @classmethod def _percentToParam(cls, percent, min, max): @@ -228,4 +243,4 @@ def _percentToParam(cls, percent, min, max): @param max: The maximum raw parameter value. @type max: int """ - return int(round(float(percent) / 100 * (max - min) + min)) + return percentToParam(percent, min, max) From 5f7df99fe55bb07fe34d79d85760d1c15e3fe5b4 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 17 Oct 2019 12:16:22 +0200 Subject: [PATCH 018/116] Move settings related content to AutoSettings class --- source/autoSettingsUtils/autoSettings.py | 180 +++++++++++++++++++++++ source/driverHandler.py | 140 +----------------- 2 files changed, 181 insertions(+), 139 deletions(-) create mode 100644 source/autoSettingsUtils/autoSettings.py diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py new file mode 100644 index 00000000000..ddc7907f5b0 --- /dev/null +++ b/source/autoSettingsUtils/autoSettings.py @@ -0,0 +1,180 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + +"""autoSettings for add-ons""" +from abc import abstractmethod +from copy import deepcopy +from typing import List, Tuple, Union, Dict, Type + +import config +from autoSettingsUtils.utils import paramToPercent, percentToParam, UnsupportedConfigParameterError +from baseObject import AutoPropertyObject +from logHandler import log +from .driverSetting import DriverSetting + +SupportedSettingType: Type = Union[ + List[DriverSetting], + Tuple[DriverSetting] +] + + +class AutoSettings(AutoPropertyObject): + """ + """ + + def __init__(self): + """ + """ + super().__init__() + self._registerConfigSaveAction() + + def _registerConfigSaveAction(self): + log.debug(f"registering: {self.__class__!r}") + config.pre_configSave.register(self.saveSettings) + + @classmethod + def _initSpecificSettings(cls, clsOrInst, settings: List): + firstLoad = not config.conf[cls._configSection].isSet(cls.name) + if firstLoad: + # Create the new section. + config.conf[cls._configSection][cls.name] = {} + # Make sure the config spec is up to date, so the config validator does its work. + config.conf[cls._configSection][cls.name].spec.update( + cls._getConfigSPecForSettings(settings) + ) + # Make sure the clsOrInst has attributes for every setting + for setting in settings: + if not hasattr(clsOrInst, setting.id): + setattr(clsOrInst, setting.id, setting.defaultVal) + if firstLoad: + cls._saveSpecificSettings(clsOrInst, settings) # save defaults + else: + cls._loadSpecificSettings(clsOrInst, settings) + + def initSettings(self): + """ + Initializes the configuration for this driver. + This method is called when initializing the driver. + """ + self._initSpecificSettings(self, self.supportedSettings) + + + @classmethod + def _get_preInitSettings(self) -> Union[List, Tuple]: + """The settings supported by the driver at pre initialisation time. + @rtype: list or tuple of L{DriverSetting} + """ + return () + + _abstract_supportedSettings = True + + def _get_supportedSettings(self) -> Union[List, Tuple]: + """The settings supported by the driver. + When overriding this property, subclasses are encouraged to extend the getter method + to ensure that L{preInitSettings} is part of the list of supported settings. + @rtype: list or tuple of L{DriverSetting} + """ + return self.preInitSettings + + def isSupported(self,settingID): + """Checks whether given setting is supported by the driver. + @rtype: l{bool} + """ + for s in self.supportedSettings: + if s.id == settingID: return True + return False + + @classmethod + def _getConfigSPecForSettings( + cls, + settings: Union[List, Tuple] + ) -> Dict: + spec = deepcopy(config.confspec[cls._configSection]["__many__"]) + for setting in settings: + if not setting.useConfig: + continue + spec[setting.id]=setting.configSpec + return spec + + def getConfigSpec(self): + return self._getConfigSPecForSettings(self.supportedSettings) + + @classmethod + def _saveSpecificSettings( + cls, + clsOrInst, + settings: Union[List, Tuple] + ): + conf = config.conf[cls._configSection][cls.name] + for setting in settings: + if not setting.useConfig: + continue + try: + conf[setting.id] = getattr(clsOrInst, setting.id) + except UnsupportedConfigParameterError: + log.debugWarning( + f"Unsupported setting {setting.id!r}; ignoring", + exc_info=True + ) + continue + if settings: + log.debug(f"Saved settings for {cls.__qualname__}") + + def saveSettings(self): + """ + Saves the current settings for the driver to the configuration. + This method is also executed when the driver is loaded for the first time, + in order to populate the configuration with the initial settings.. + """ + self._saveSpecificSettings(self, self.supportedSettings) + + @classmethod + def _loadSpecificSettings( + cls, + clsOrInst, + settings: Union[List, Tuple], + onlyChanged: bool = False + ): + log.debug(f"loading {cls._configSection} {cls.name}") + conf = config.conf[cls._configSection][cls.name] + for setting in settings: + if not setting.useConfig or conf.get(setting.id) is None: + continue + val = conf[setting.id] + if onlyChanged and getattr(clsOrInst, setting.id) == val: + continue + try: + setattr(clsOrInst, setting.id, val) + except UnsupportedConfigParameterError: + log.debugWarning( + f"Unsupported setting {setting.name!r}; ignoring", + exc_info=True + ) + continue + if settings: + log.debug( + f"Loaded changed settings for {cls.__qualname__}" + if onlyChanged else + f"Loaded settings for {cls.__qualname__}" + ) + + def loadSettings(self, onlyChanged: bool = False): + """ + Loads settings for this driver from the configuration. + This method assumes that the instance has attributes o/properties + corresponding with the name of every setting in L{supportedSettings}. + @param onlyChanged: When loading settings, only apply those for which + the value in the configuration differs from the current value. + """ + self._loadSpecificSettings(self, self.supportedSettings, onlyChanged) + + @classmethod + def _paramToPercent(cls, current, min, max): + return paramToPercent(current, min, max) + + @classmethod + def _percentToParam(cls, percent, min, max): + return percentToParam(percent, min, max) diff --git a/source/driverHandler.py b/source/driverHandler.py index e7f2d9aed22..a4d976736c9 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -29,7 +29,7 @@ from typing import List, Tuple, Dict, Union -class Driver(AutoPropertyObject): +class Driver(AutoSettings): """ Abstract base class for drivers, such as speech synthesizer and braille display drivers. Abstract subclasses such as L{braille.BrailleDisplayDriver} should set L{_configSection}. @@ -61,36 +61,6 @@ def __init__(self): super(Driver, self).__init__() self._registerConfigSaveAction() - def _registerConfigSaveAction(self): - log.debug(f"registering: {self.__class__!r}") - config.pre_configSave.register(self.saveSettings) - - @classmethod - def _initSpecificSettings(cls, clsOrInst, settings: List): - firstLoad = not config.conf[cls._configSection].isSet(cls.name) - if firstLoad: - # Create the new section. - config.conf[cls._configSection][cls.name] = {} - # Make sure the config spec is up to date, so the config validator does its work. - config.conf[cls._configSection][cls.name].spec.update( - cls._getConfigSPecForSettings(settings) - ) - # Make sure the clsOrInst has attributes for every setting - for setting in settings: - if not hasattr(clsOrInst, setting.id): - setattr(clsOrInst, setting.id, setting.defaultVal) - if firstLoad: - cls._saveSpecificSettings(clsOrInst, settings) # save defaults - else: - cls._loadSpecificSettings(clsOrInst, settings) - - def initSettings(self): - """ - Initializes the configuration for this driver. - This method is called when initializing the driver. - """ - self._initSpecificSettings(self, self.supportedSettings) - def terminate(self, saveSettings: bool = True): """Terminate this driver. This should be used for any required clean up. @@ -102,23 +72,6 @@ def terminate(self, saveSettings: bool = True): self.saveSettings() config.pre_configSave.unregister(self.saveSettings) - @classmethod - def _get_preInitSettings(self) -> Union[List, Tuple]: - """The settings supported by the driver at pre initialisation time. - @rtype: list or tuple of L{DriverSetting} - """ - return () - - _abstract_supportedSettings = True - - def _get_supportedSettings(self) -> Union[List, Tuple]: - """The settings supported by the driver. - When overriding this property, subclasses are encouraged to extend the getter method - to ensure that L{preInitSettings} is part of the list of supported settings. - @rtype: list or tuple of L{DriverSetting} - """ - return self.preInitSettings - @classmethod def check(cls): """Determine whether this driver is available. @@ -129,97 +82,6 @@ def check(cls): """ return False - def isSupported(self,settingID): - """Checks whether given setting is supported by the driver. - @rtype: l{bool} - """ - for s in self.supportedSettings: - if s.id == settingID: return True - return False - - @classmethod - def _getConfigSPecForSettings( - cls, - settings: Union[List, Tuple] - ) -> Dict: - spec = deepcopy(config.confspec[cls._configSection]["__many__"]) - for setting in settings: - if not setting.useConfig: - continue - spec[setting.id]=setting.configSpec - return spec - - def getConfigSpec(self): - return self._getConfigSPecForSettings(self.supportedSettings) - - @classmethod - def _saveSpecificSettings( - cls, - clsOrInst, - settings: Union[List, Tuple] - ): - conf = config.conf[cls._configSection][cls.name] - for setting in settings: - if not setting.useConfig: - continue - try: - conf[setting.id] = getattr(clsOrInst, setting.id) - except UnsupportedConfigParameterError: - log.debugWarning( - f"Unsupported setting {setting.id!r}; ignoring", - exc_info=True - ) - continue - if settings: - log.debug(f"Saved settings for {cls.__qualname__}") - - def saveSettings(self): - """ - Saves the current settings for the driver to the configuration. - This method is also executed when the driver is loaded for the first time, - in order to populate the configuration with the initial settings.. - """ - self._saveSpecificSettings(self, self.supportedSettings) - - @classmethod - def _loadSpecificSettings( - cls, - clsOrInst, - settings: Union[List, Tuple], - onlyChanged: bool = False - ): - log.debug(f"loading {cls._configSection} {cls.name}") - conf = config.conf[cls._configSection][cls.name] - for setting in settings: - if not setting.useConfig or conf.get(setting.id) is None: - continue - val = conf[setting.id] - if onlyChanged and getattr(clsOrInst, setting.id) == val: - continue - try: - setattr(clsOrInst, setting.id, val) - except UnsupportedConfigParameterError: - log.debugWarning( - f"Unsupported setting {setting.name!r}; ignoring", - exc_info=True - ) - continue - if settings: - log.debug( - f"Loaded changed settings for {cls.__qualname__}" - if onlyChanged else - f"Loaded settings for {cls.__qualname__}" - ) - - def loadSettings(self, onlyChanged: bool = False): - """ - Loads settings for this driver from the configuration. - This method assumes that the instance has attributes o/properties - corresponding with the name of every setting in L{supportedSettings}. - @param onlyChanged: When loading settings, only apply those for which - the value in the configuration differs from the current value. - """ - self._loadSpecificSettings(self, self.supportedSettings, onlyChanged) @classmethod def _paramToPercent(cls, current, min, max): From e78d37646a65639f8c510e4eeb2771167492947a Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 17 Oct 2019 14:19:42 +0200 Subject: [PATCH 019/116] Tidy up config save reg / unreg --- source/autoSettingsUtils/autoSettings.py | 11 ++++++++++- source/driverHandler.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index ddc7907f5b0..c8e04d01c5c 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -31,10 +31,19 @@ def __init__(self): super().__init__() self._registerConfigSaveAction() + def __del__(self): + self._unregisterConfigSaveAction() + def _registerConfigSaveAction(self): - log.debug(f"registering: {self.__class__!r}") + """Overrideable pre_configSave registration""" + log.debug(f"registering pre_configSave action: {self.__class__!r}") config.pre_configSave.register(self.saveSettings) + def _unregisterConfigSaveAction(self): + """Overrideable pre_configSave de-registration""" + log.debug(f"de-registering pre_configSave action: {self.__class__!r}") + config.pre_configSave.unregister(self.saveSettings) + @classmethod def _initSpecificSettings(cls, clsOrInst, settings: List): firstLoad = not config.conf[cls._configSection].isSet(cls.name) diff --git a/source/driverHandler.py b/source/driverHandler.py index a4d976736c9..638a81ab05d 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -70,7 +70,7 @@ def terminate(self, saveSettings: bool = True): """ if saveSettings: self.saveSettings() - config.pre_configSave.unregister(self.saveSettings) + self._unregisterConfigSaveAction() @classmethod def check(cls): From bdc2c7292f098813b951d2b0ed972bc359f059c5 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 17 Oct 2019 14:29:30 +0200 Subject: [PATCH 020/116] Adapt identifier, section, and productName to AutoSettings class These can now be overriden, Driver maps them to name, section and description. --- source/autoSettingsUtils/autoSettings.py | 36 +++++++++++++++++++----- source/driverHandler.py | 13 +++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index c8e04d01c5c..a70036d8c8d 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -44,14 +44,31 @@ def _unregisterConfigSaveAction(self): log.debug(f"de-registering pre_configSave action: {self.__class__!r}") config.pre_configSave.unregister(self.saveSettings) + @classmethod + @abstractmethod + def getId(cls) -> str: + ... + + @classmethod + @abstractmethod + def getProductName(cls) -> str: + ... + + @classmethod + @abstractmethod + def _getConfigSection(cls) -> str: + ... + @classmethod def _initSpecificSettings(cls, clsOrInst, settings: List): - firstLoad = not config.conf[cls._configSection].isSet(cls.name) + section = cls._getConfigSection() + id = cls.getId() + firstLoad = not config.conf[section].isSet(id) if firstLoad: # Create the new section. - config.conf[cls._configSection][cls.name] = {} + config.conf[section][id] = {} # Make sure the config spec is up to date, so the config validator does its work. - config.conf[cls._configSection][cls.name].spec.update( + config.conf[section][id].spec.update( cls._getConfigSPecForSettings(settings) ) # Make sure the clsOrInst has attributes for every setting @@ -101,7 +118,8 @@ def _getConfigSPecForSettings( cls, settings: Union[List, Tuple] ) -> Dict: - spec = deepcopy(config.confspec[cls._configSection]["__many__"]) + section = cls._getConfigSection() + spec = deepcopy(config.confspec[section]["__many__"]) for setting in settings: if not setting.useConfig: continue @@ -117,7 +135,9 @@ def _saveSpecificSettings( clsOrInst, settings: Union[List, Tuple] ): - conf = config.conf[cls._configSection][cls.name] + section = cls._getConfigSection() + id = cls.getId() + conf = config.conf[section][id] for setting in settings: if not setting.useConfig: continue @@ -147,8 +167,10 @@ def _loadSpecificSettings( settings: Union[List, Tuple], onlyChanged: bool = False ): - log.debug(f"loading {cls._configSection} {cls.name}") - conf = config.conf[cls._configSection][cls.name] + section = cls._getConfigSection() + id = cls.getId() + log.debug(f"loading {section} {id}") + conf = config.conf[section][id] for setting in settings: if not setting.useConfig or conf.get(setting.id) is None: continue diff --git a/source/driverHandler.py b/source/driverHandler.py index 638a81ab05d..b8963c7659f 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -106,3 +106,16 @@ def _percentToParam(cls, percent, min, max): @type max: int """ return percentToParam(percent, min, max) + +# Impl for abstract methods in AutoSettings class + @classmethod + def getId(cls) -> str: + return cls.name + + @classmethod + def getProductName(cls) -> str: + return cls.description + + @classmethod + def _getConfigSection(cls) -> str: + return cls._configSection From a57b6b2c39688ac39b009d50bf0b34656cb48768 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 19 Oct 2019 18:22:16 +0200 Subject: [PATCH 021/116] Add type hinting and comments --- source/autoSettingsUtils/autoSettings.py | 61 ++++++++---- source/autoSettingsUtils/driverSetting.py | 114 +++++++++++++++------- 2 files changed, 120 insertions(+), 55 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index a70036d8c8d..28766c35b96 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -7,7 +7,7 @@ """autoSettings for add-ons""" from abc import abstractmethod from copy import deepcopy -from typing import List, Tuple, Union, Dict, Type +from typing import Union, Dict, Type, Any, Iterable import config from autoSettingsUtils.utils import paramToPercent, percentToParam, UnsupportedConfigParameterError @@ -16,8 +16,7 @@ from .driverSetting import DriverSetting SupportedSettingType: Type = Union[ - List[DriverSetting], - Tuple[DriverSetting] + Iterable[DriverSetting] ] @@ -60,7 +59,11 @@ def _getConfigSection(cls) -> str: ... @classmethod - def _initSpecificSettings(cls, clsOrInst, settings: List): + def _initSpecificSettings( + cls, + clsOrInst: Any, + settings: SupportedSettingType + ) -> None: section = cls._getConfigSection() id = cls.getId() firstLoad = not config.conf[section].isSet(id) @@ -87,43 +90,47 @@ def initSettings(self): """ self._initSpecificSettings(self, self.supportedSettings) + #: type hinting for _get_preInitSettings + preInitSettings: SupportedSettingType @classmethod - def _get_preInitSettings(self) -> Union[List, Tuple]: + def _get_preInitSettings(cls) -> SupportedSettingType: """The settings supported by the driver at pre initialisation time. - @rtype: list or tuple of L{DriverSetting} """ - return () + return [] + + #: Typing for auto property L{_get_supportedSettings} + supportedSettings: SupportedSettingType + # make supportedSettings an abstract property _abstract_supportedSettings = True - def _get_supportedSettings(self) -> Union[List, Tuple]: + def _get_supportedSettings(self) -> SupportedSettingType: """The settings supported by the driver. When overriding this property, subclasses are encouraged to extend the getter method to ensure that L{preInitSettings} is part of the list of supported settings. - @rtype: list or tuple of L{DriverSetting} """ return self.preInitSettings - def isSupported(self,settingID): + def isSupported(self, settingID) -> bool: """Checks whether given setting is supported by the driver. - @rtype: l{bool} """ for s in self.supportedSettings: - if s.id == settingID: return True + if s.id == settingID: + return True return False @classmethod def _getConfigSPecForSettings( cls, - settings: Union[List, Tuple] + settings: SupportedSettingType ) -> Dict: section = cls._getConfigSection() spec = deepcopy(config.confspec[section]["__many__"]) for setting in settings: if not setting.useConfig: continue - spec[setting.id]=setting.configSpec + spec[setting.id] = setting.configSpec return spec def getConfigSpec(self): @@ -132,9 +139,15 @@ def getConfigSpec(self): @classmethod def _saveSpecificSettings( cls, - clsOrInst, - settings: Union[List, Tuple] - ): + clsOrInst: Any, + settings: SupportedSettingType + ) -> None: + """ + Save values for settings to config. + The values from the attributes of `clsOrInst` that match the `id` of each setting are saved to config. + @param clsOrInst: Destination for the values. + @param settings: The settings to load. + """ section = cls._getConfigSection() id = cls.getId() conf = config.conf[section][id] @@ -163,10 +176,18 @@ def saveSettings(self): @classmethod def _loadSpecificSettings( cls, - clsOrInst, - settings: Union[List, Tuple], + clsOrInst: Any, + settings: SupportedSettingType, onlyChanged: bool = False - ): + ) -> None: + """ + Load settings from config, set them on `clsOrInst`. + @param clsOrInst: Destination for the values. + @param settings: The settings to load. + @param onlyChanged: When True, only settings that no longer match the config are set. + @note: attributes are set on clsOrInst using setattr. + The id of each setting in `settings` is used as the attribute name. + """ section = cls._getConfigSection() id = cls.getId() log.debug(f"loading {section} {id}") diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py index 4cc39a434be..19273efbe56 100644 --- a/source/autoSettingsUtils/driverSetting.py +++ b/source/autoSettingsUtils/driverSetting.py @@ -1,9 +1,21 @@ +from numbers import Number +from typing import Optional + from baseObject import AutoPropertyObject class DriverSetting(AutoPropertyObject): """Represents a synthesizer or braille display setting such as voice, variant or dot firmness. """ + id: str + displayName: str + displayNameWithAccelerator: str + availableInSettingsRing: bool + defaultVal: object + useConfig: bool + + #: Type information for _get_configSpec + configSpec: str def _get_configSpec(self): """Returns the configuration specification of this particular setting for config file validator. @@ -11,78 +23,110 @@ def _get_configSpec(self): """ return "string(default={defaultVal})".format(defaultVal=self.defaultVal) - def __init__(self, id, displayNameWithAccelerator, - availableInSettingsRing=False, defaultVal=None, displayName=None, useConfig=True): + def __init__( + self, + id: str, + displayNameWithAccelerator: str, + availableInSettingsRing: bool = False, + defaultVal: object = None, + displayName: Optional[str] = None, + useConfig: bool = True + ): """ @param id: internal identifier of the setting - @type id: str @param displayNameWithAccelerator: the localized string shown in voice or braille settings dialog - @type displayNameWithAccelerator: str @param availableInSettingsRing: Will this option be available in a settings ring? - @type availableInSettingsRing: bool @param defaultVal: Specifies the default value for a driver setting. - @type param defaultVal: str or C{None} - @param displayName: the localized string used in synth settings ring or None to use displayNameWithAccelerator - @type displayName: str + @param displayName: the localized string used in synth settings ring or + None to use displayNameWithAccelerator @param useConfig: Whether the value of this option is loaded from and saved to NVDA's configuration. Set this to C{False} if the driver deals with loading and saving. - @type useConfig: bool """ self.id = id self.displayNameWithAccelerator = displayNameWithAccelerator if not displayName: # Strip accelerator from displayNameWithAccelerator. - displayName = displayNameWithAccelerator.replace("&","") + displayName = displayNameWithAccelerator.replace("&", "") self.displayName = displayName self.availableInSettingsRing = availableInSettingsRing self.defaultVal = defaultVal self.useConfig = useConfig + class NumericDriverSetting(DriverSetting): """Represents a numeric driver setting such as rate, volume, pitch or dot firmness.""" + defaultVal: int + def _get_configSpec(self): return "integer(default={defaultVal},min={minVal},max={maxVal})".format( - defaultVal=self.defaultVal,minVal=self.minVal,maxVal=self.maxVal) + defaultVal=self.defaultVal, minVal=self.minVal, maxVal=self.maxVal) - def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, - defaultVal=50, minVal=0, maxVal=100, minStep=1, normalStep=5, largeStep=10, - displayName=None, useConfig=True): + def __init__( + self, + id, + displayNameWithAccelerator, + availableInSettingsRing=False, + defaultVal: int = 50, + minVal: int = 0, + maxVal: int = 100, + minStep: int = 1, + normalStep: int = 5, + largeStep: int = 10, + displayName: Optional[str] = None, + useConfig: bool = True): """ @param defaultVal: Specifies the default value for a numeric driver setting. - @type defaultVal: int @param minVal: Specifies the minimum valid value for a numeric driver setting. - @type minVal: int @param maxVal: Specifies the maximum valid value for a numeric driver setting. - @type maxVal: int - @param minStep: Specifies the minimum step between valid values for each numeric setting. For example, if L{minStep} is set to 10, setting values can only be multiples of 10; 10, 20, 30, etc. - @type minStep: int - @param normalStep: Specifies the step between values that a user will normally prefer. This is used in the settings ring. - @type normalStep: int - @param largeStep: Specifies the step between values if a large adjustment is desired. This is used for pageUp/pageDown on sliders in the Voice Settings dialog. - @type largeStep: int + @param minStep: Specifies the minimum step between valid values for each numeric setting. + For example, if L{minStep} is set to 10, setting values can only be multiples of 10; 10, 20, 30, etc. + @param normalStep: Specifies the step between values that a user will normally prefer. + This is used in the settings ring. + @param largeStep: Specifies the step between values if a large adjustment is desired. + This is used for pageUp/pageDown on sliders in the Voice Settings dialog. @note: If necessary, the step values will be normalised so that L{minStep} <= L{normalStep} <= L{largeStep}. """ - super(NumericDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, - defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) - self.minVal=minVal - self.maxVal=max(maxVal,self.defaultVal) - self.minStep=minStep - self.normalStep=max(normalStep,minStep) - self.largeStep=max(largeStep,self.normalStep) + super(NumericDriverSetting, self).__init__( + id, + displayNameWithAccelerator, + availableInSettingsRing=availableInSettingsRing, + defaultVal=defaultVal, + displayName=displayName, + useConfig=useConfig + ) + self.minVal = minVal + self.maxVal = max(maxVal, self.defaultVal) + self.minStep = minStep + self.normalStep = max(normalStep, minStep) + self.largeStep = max(largeStep, self.normalStep) + class BooleanDriverSetting(DriverSetting): """Represents a boolean driver setting such as rate boost or automatic time sync. """ + defaultVal: bool - def __init__(self, id, displayNameWithAccelerator, availableInSettingsRing=False, - displayName=None, defaultVal=False, useConfig=True): + def __init__( + self, + id: str, + displayNameWithAccelerator: str, + availableInSettingsRing: bool = False, + displayName: Optional[str] = None, + defaultVal: bool = False, + useConfig: bool = True + ): """ @param defaultVal: Specifies the default value for a boolean driver setting. - @type defaultVal: bool """ - super(BooleanDriverSetting,self).__init__(id, displayNameWithAccelerator, availableInSettingsRing=availableInSettingsRing, - defaultVal=defaultVal, displayName=displayName, useConfig=useConfig) + super(BooleanDriverSetting, self).__init__( + id, + displayNameWithAccelerator, + availableInSettingsRing=availableInSettingsRing, + defaultVal=defaultVal, + displayName=displayName, + useConfig=useConfig + ) def _get_configSpec(self): defaultVal = repr(self.defaultVal) if self.defaultVal is not None else self.defaultVal From 8c1043daee4f1f1c970d0b0e0d986f8451c86fac Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 19 Oct 2019 18:23:10 +0200 Subject: [PATCH 022/116] Fix: don't hide 'id' --- source/autoSettingsUtils/autoSettings.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 28766c35b96..480f7eb081b 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -65,13 +65,13 @@ def _initSpecificSettings( settings: SupportedSettingType ) -> None: section = cls._getConfigSection() - id = cls.getId() - firstLoad = not config.conf[section].isSet(id) + settingsId = cls.getId() + firstLoad = not config.conf[section].isSet(settingsId) if firstLoad: # Create the new section. - config.conf[section][id] = {} + config.conf[section][settingsId] = {} # Make sure the config spec is up to date, so the config validator does its work. - config.conf[section][id].spec.update( + config.conf[section][settingsId].spec.update( cls._getConfigSPecForSettings(settings) ) # Make sure the clsOrInst has attributes for every setting @@ -149,8 +149,8 @@ def _saveSpecificSettings( @param settings: The settings to load. """ section = cls._getConfigSection() - id = cls.getId() - conf = config.conf[section][id] + setingsId = cls.getId() + conf = config.conf[section][setingsId] for setting in settings: if not setting.useConfig: continue @@ -189,9 +189,9 @@ def _loadSpecificSettings( The id of each setting in `settings` is used as the attribute name. """ section = cls._getConfigSection() - id = cls.getId() - log.debug(f"loading {section} {id}") - conf = config.conf[section][id] + settingsID = cls.getId() + log.debug(f"loading {section} {settingsID}") + conf = config.conf[section][settingsID] for setting in settings: if not setting.useConfig or conf.get(setting.id) is None: continue From 5526dd116063e1151251fdd4b331557d9b94e388 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 19 Oct 2019 18:24:11 +0200 Subject: [PATCH 023/116] Fix non-existent id attribute on DriverSetting instance. --- source/autoSettingsUtils/autoSettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 480f7eb081b..6f9a02be002 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -202,7 +202,7 @@ def _loadSpecificSettings( setattr(clsOrInst, setting.id, val) except UnsupportedConfigParameterError: log.debugWarning( - f"Unsupported setting {setting.name!r}; ignoring", + f"Unsupported setting {setting.id!r}; ignoring", exc_info=True ) continue From b4df54b6bfb71fc8ff6882893134d2aa19005686 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sat, 26 Oct 2019 17:12:10 +0200 Subject: [PATCH 024/116] Update DriverSettingsMixin and derived Class DriverSettingsMixin - rename self._curDriverRef -> self._currentSettingsRef - replace driver method with getSettings and _getSettingsStorage - getSettings to get settings definitions - _getSettingsStorage to load / save configuration values. - renamed various internal only methods: - makeSliderSettingControl -> _makeSliderSettingControl - makeBooleanSettingControl -> _makeBooleanSettingControl - makeStringSettingControl -> _makeStringSettingControl - settingsStorage is passed into the following: - _makeSliderSettingControl - _makeBooleanSettingControl - _makeStringSettingControl Also update derived classes: - VoiceSettingsPanel - BrailleSettingsSubPanel - VisionProviderSubPanel_Settings --- source/gui/settingsDialogs.py | 150 ++++++++++++------ .../NVDAHighlighter.py | 9 +- 2 files changed, 103 insertions(+), 56 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 160049bfa8e..8d233216850 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -34,7 +34,7 @@ import brailleTables import brailleInput import vision -from typing import Callable, List, Type, Optional +from typing import Callable, List, Type, Optional, Any import core import keyboardHandler import characterProcessing @@ -1031,33 +1031,46 @@ def __call__(self,evt): class DriverSettingsMixin(metaclass=ABCMeta): """ Mixin class that provides support for driver specific gui settings. - Derived classes should implement L{driver}. + Derived classes should implement: + - L{getSettings} + - L{settingsSizer} """ def __init__(self, *args, **kwargs): self.sizerDict={} self.lastControl=None super(DriverSettingsMixin,self).__init__(*args,**kwargs) - self._curDriverRef = weakref.ref(self.driver) + # because settings instances can be of type L{Driver} as well, we have to handle + # showing settings for non-instances. Because of this, we must reacquire a reference + # to the settings class whenever we wish to use it (via L{getSettings}) in case the instance changes. + self._currentSettingsRef = weakref.ref(self.getSettings()) - def driver(self): - return self.getSettings() + settingsSizer: wx.BoxSizer @abstractmethod - def getSettings(self): - raise NotImplementedError + def getSettings(self) -> AutoSettings: + ... + + def _getSettingsStorage(self) -> Any: + """ Override to change storage object for settings.""" + return self.getSettings() @classmethod def _setSliderStepSizes(cls, slider, setting): slider.SetLineSize(setting.minStep) slider.SetPageSize(setting.largeStep) - def makeSliderSettingControl(self,setting): + def _makeSliderSettingControl( + self, + setting: NumericDriverSetting, + settingsStorage: Any + ) -> wx.BoxSizer: """Constructs appropriate GUI controls for given L{DriverSetting} such as label and slider. @param setting: Setting to construct controls for - @type setting: L{DriverSetting} - @returns: WXSizer containing newly created controls. - @rtype: L{wx.BoxSizer} + @param settingsStorage: where to get initial values / set values. + This param must have an attribute with a name matching setting.id. + In most cases it will be of type L{AutoSettings} + @returns: wx.BoxSizer containing newly created controls. """ labeledControl = guiHelper.LabeledControlHelper( self, @@ -1068,24 +1081,36 @@ def makeSliderSettingControl(self,setting): ) lSlider=labeledControl.control setattr(self,"%sSlider"%setting.id,lSlider) - lSlider.Bind(wx.EVT_SLIDER,DriverSettingChanger(self.driver,setting)) self._setSliderStepSizes(lSlider,setting) - lSlider.SetValue(getattr(self.driver,setting.id)) + lSlider.Bind(wx.EVT_SLIDER, DriverSettingChanger( + settingsStorage, setting + )) + lSlider.SetValue(getattr(settingsStorage, setting.id)) if self.lastControl: lSlider.MoveAfterInTabOrder(self.lastControl) self.lastControl=lSlider return labeledControl.sizer - def makeStringSettingControl(self,setting): - """Same as L{makeSliderSettingControl} but for string settings. Returns sizer with label and combobox.""" + def _makeStringSettingControl( + self, + setting: DriverSetting, + settingsStorage: Any + ): + """ + Same as L{_makeSliderSettingControl} but for string settings. Returns sizer with label and combobox. + """ labelText="%s:"%setting.displayNameWithAccelerator + settingsInst = self.getSettings() setattr( self, "_%ss"%setting.id, # Settings are stored as an ordered dict. # Therefore wrap this inside a list call. - list(getattr(self.driver,"available%ss"%setting.id.capitalize()).values()) + list(getattr( + settingsStorage, + f"available{setting.id.capitalize()}s" + ).values()) ) l=getattr(self,"_%ss"%setting.id) labeledControl=guiHelper.LabeledControlHelper( @@ -1097,28 +1122,40 @@ def makeStringSettingControl(self,setting): lCombo = labeledControl.control setattr(self,"%sList"%setting.id,lCombo) try: - cur=getattr(self.driver,setting.id) + cur = getattr(settingsStorage, setting.id) i=[x.id for x in l].index(cur) lCombo.SetSelection(i) except ValueError: pass - lCombo.Bind(wx.EVT_CHOICE,StringDriverSettingChanger(self.driver,setting,self)) + lCombo.Bind( + wx.EVT_CHOICE, + StringDriverSettingChanger(settingsStorage, setting, self) + ) if self.lastControl: lCombo.MoveAfterInTabOrder(self.lastControl) self.lastControl=lCombo return labeledControl.sizer - def makeBooleanSettingControl(self,setting): - """Same as L{makeSliderSettingControl} but for boolean settings. Returns checkbox.""" + def _makeBooleanSettingControl( + self, + setting: BooleanDriverSetting, + settingsStorage: Any + ): + """ + Same as L{_makeSliderSettingControl} but for boolean settings. Returns checkbox. + """ checkbox=wx.CheckBox(self,wx.ID_ANY,label=setting.displayNameWithAccelerator) setattr(self,"%sCheckbox"%setting.id,checkbox) - def onCheckChanged(evt: wx.CommandEvent): + def _onCheckChanged(evt: wx.CommandEvent): evt.Skip() # allow other handlers to also process this event. - setattr(self.driver, setting.id, evt.IsChecked()) + setattr(settingsStorage, setting.id, evt.IsChecked()) - checkbox.Bind(wx.EVT_CHECKBOX, onCheckChanged) - checkbox.SetValue(getattr(self.driver,setting.id)) + checkbox.Bind(wx.EVT_CHECKBOX, _onCheckChanged) + checkbox.SetValue(getattr( + settingsStorage, + setting.id + )) if self.lastControl: checkbox.MoveAfterInTabOrder(self.lastControl) self.lastControl=checkbox @@ -1126,32 +1163,38 @@ def onCheckChanged(evt: wx.CommandEvent): def updateDriverSettings(self, changedSetting=None): """Creates, hides or updates existing GUI controls for all of supported settings.""" - #firstly check already created options - for name,sizer in self.sizerDict.items(): + settingsInst = self.getSettings() + settingsStorage = self._getSettingsStorage() + # firstly check already created options + for name, sizer in self.sizerDict.items(): if name == changedSetting: # Changing a setting shouldn't cause that setting itself to disappear. continue - if not self.driver.isSupported(name): + if not settingsInst.isSupported(name): self.settingsSizer.Hide(sizer) #Create new controls, update already existing log.debug(f"Current sizerDict: {self.sizerDict!r}") - log.debug(f"Current supportedSettings: {self.driver.supportedSettings!r}") - for setting in self.driver.supportedSettings: + log.debug(f"Current supportedSettings: {self.getSettings().supportedSettings!r}") + for setting in settingsInst.supportedSettings: if setting.id == changedSetting: # Changing a setting shouldn't cause that setting's own values to change. continue if setting.id in self.sizerDict: #update a value self.settingsSizer.Show(self.sizerDict[setting.id]) if isinstance(setting,NumericDriverSetting): - getattr(self,"%sSlider"%setting.id).SetValue(getattr(self.driver,setting.id)) + getattr(self, f"{setting.id}Slider").SetValue( + getattr(settingsStorage, setting.id) + ) elif isinstance(setting,BooleanDriverSetting): - getattr(self,"%sCheckbox"%setting.id).SetValue(getattr(self.driver,setting.id)) + getattr(self, f"{setting.id}Checkbox").SetValue( + getattr(settingsStorage, setting.id) + ) else: l=getattr(self,"_%ss"%setting.id) lCombo=getattr(self,"%sList"%setting.id) try: - cur=getattr(self.driver,setting.id) + cur = getattr(settingsStorage, setting.id) i=[x.id for x in l].index(cur) lCombo.SetSelection(i) except ValueError: @@ -1161,11 +1204,11 @@ def updateDriverSettings(self, changedSetting=None): settingMaker=self.makeSliderSettingControl elif isinstance(setting,BooleanDriverSetting): log.debug(f"creating a new bool driver setting: {setting.id}") - settingMaker=self.makeBooleanSettingControl + settingMaker = self._makeBooleanSettingControl else: - settingMaker=self.makeStringSettingControl + settingMaker = self._makeStringSettingControl try: - s=settingMaker(setting) + s = settingMaker(setting, settingsStorage) except UnsupportedConfigParameterError: log.debugWarning("Unsupported setting %s; ignoring"%setting.id, exc_info=True) continue @@ -1175,23 +1218,25 @@ def updateDriverSettings(self, changedSetting=None): self.settingsSizer.Layout() def onDiscard(self): - #unbind change events for string settings as wx closes combo boxes on cancel - for setting in self.driver.supportedSettings: - if isinstance(setting,(NumericDriverSetting,BooleanDriverSetting)): continue - getattr(self,"%sList"%setting.id).Unbind(wx.EVT_CHOICE) - #restore settings - self.driver.loadSettings() + # unbind change events for string settings as wx closes combo boxes on cancel + settingsInst = self.getSettings() + for setting in settingsInst.supportedSettings: + if isinstance(setting, (NumericDriverSetting, BooleanDriverSetting)): + continue + getattr(self, f"{setting.id}List").Unbind(wx.EVT_CHOICE) + # restore settings + settingsInst.loadSettings() def onSave(self): - self.driver.saveSettings() + self.getSettings().saveSettings() def onPanelActivated(self): - if not self._curDriverRef(): + if not self._currentSettingsRef(): if gui._isDebug(): log.debug("refreshing panel") self.sizerDict.clear() self.settingsSizer.Clear(delete_windows=True) - self._curDriverRef = weakref.ref(self.driver) + self._currentSettingsRef = weakref.ref(self.getSettings()) self.makeSettings(self.settingsSizer) super(DriverSettingsMixin,self).onPanelActivated() @@ -1201,7 +1246,11 @@ class VoiceSettingsPanel(DriverSettingsMixin, SettingsPanel): @property def driver(self): - return getSynth() + synth: SynthDriver = getSynth() + return synth + + def getSettings(self) -> AutoSettings: + return self.driver def makeSettings(self, settingsSizer): # Construct synthesizer settings @@ -2697,6 +2746,9 @@ class BrailleSettingsSubPanel(DriverSettingsMixin, SettingsPanel): def driver(self): return braille.handler.display + def getSettings(self) -> AutoSettings: + return self.driver + def makeSettings(self, settingsSizer): if gui._isDebug(): startTime = time.time() @@ -3064,11 +3116,7 @@ def __init__( self._settingsCallable = settingsCallable super().__init__(parent=parent) - @property - def driver(self): - return self.getSettings() - - def getSettings(self): + def getSettings(self) -> AutoSettings: settings = self._settingsCallable() log.debug(f"getting settings: {settings.__class__!r}") return settings @@ -3091,7 +3139,7 @@ def __init__( getProvider: Callable[ [], Optional[vision.VisionEnhancementProvider] - ],# mostly used to see if the provider is initialised or not. + ], # mostly used to see if the provider is initialised or not. initProvider: Callable[ [], bool diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index c2ee68f4446..a14b2cb2100 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -5,7 +5,7 @@ # Copyright (C) 2018-2019 NV Access Limited, Babbage B.V., Takuya Nishimoto """Default highlighter based on GDI Plus.""" -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Any import vision from vision.constants import Role, Context @@ -249,15 +249,14 @@ def __init__( self._terminateProvider = terminateProvider super().__init__(parent) - @property - def driver(self) -> driverHandler.Driver: # todo: call this something other than driver - return self.getSettings() - def getSettings(self) -> driverHandler.Driver: # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. # We want them set on this class. return VisionEnhancementProvider.getSettings() + def _getSettingsStorage(self) -> Any: + return self.getSettings() + def makeSettings(self, sizer): self._enabledCheckbox = wx.CheckBox(self, label="Highlight focus", style=wx.CHK_3STATE) self.lastControl = self._enabledCheckbox From e2803abef7f3a2b582754d855a927f30da352f6d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 28 Oct 2019 20:33:43 +0100 Subject: [PATCH 025/116] Code cleanup Fix linter errors / add documentation --- source/autoSettingsUtils/driverSetting.py | 1 - source/autoSettingsUtils/utils.py | 4 +- source/driverHandler.py | 8 +- source/gui/settingsDialogs.py | 268 +++++++++++++++------- source/vision/visionHandler.py | 5 +- 5 files changed, 191 insertions(+), 95 deletions(-) diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py index 19273efbe56..6ddb617275a 100644 --- a/source/autoSettingsUtils/driverSetting.py +++ b/source/autoSettingsUtils/driverSetting.py @@ -1,4 +1,3 @@ -from numbers import Number from typing import Optional from baseObject import AutoPropertyObject diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py index 183603fd46a..18d3551deae 100644 --- a/source/autoSettingsUtils/utils.py +++ b/source/autoSettingsUtils/utils.py @@ -12,7 +12,8 @@ def paramToPercent(current, min, max): def percentToParam(percent, min, max): - """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum raw parameter values. + """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum + raw parameter values. @param percent: The current percentage. @type percent: int @param min: The minimum raw parameter value. @@ -28,6 +29,7 @@ class UnsupportedConfigParameterError(NotImplementedError): Raised when changing or retrieving a driver setting that is unsupported for the connected device. """ + class StringParameterInfo(object): """ The base class used to represent a value of a string driver setting. diff --git a/source/driverHandler.py b/source/driverHandler.py index b8963c7659f..27d46bfb850 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -12,6 +12,9 @@ ) # F401: the following imports, while unused in this file, are provided for backwards compatibility. +from autoSettingsUtils.autoSettings import ( # noqa: F401 + SupportedSettingType, +) from autoSettingsUtils.driverSetting import ( # noqa: F401 DriverSetting, BooleanDriverSetting, @@ -22,11 +25,6 @@ UnsupportedConfigParameterError, StringParameterInfo, ) -from baseObject import AutoPropertyObject -import config -from copy import deepcopy -from logHandler import log -from typing import List, Tuple, Dict, Union class Driver(AutoSettings): diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 8d233216850..e7dd3c6bbaa 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -8,8 +8,7 @@ # See the file COPYING for more details. import logging -from abc import abstractmethod, abstractproperty, ABCMeta -import os +from abc import abstractmethod, ABCMeta import copy import re import wx @@ -20,6 +19,7 @@ import logHandler import installer from synthDriverHandler import * +from synthDriverHandler import SynthDriver, getSynth import config import languageHandler import speech @@ -46,6 +46,8 @@ import inputCore from . import nvdaControls from driverHandler import * +from autoSettingsUtils.autoSettings import AutoSettings +from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting, DriverSetting from UIAUtils import shouldUseUIAConsole import touchHandler import winVersion @@ -1037,9 +1039,15 @@ class DriverSettingsMixin(metaclass=ABCMeta): """ def __init__(self, *args, **kwargs): - self.sizerDict={} - self.lastControl=None - super(DriverSettingsMixin,self).__init__(*args,**kwargs) + """ + Mixin init, forwards args to other base class. + The other base class is likely L{gui.SettingsPanel}. + @param args: Positional args to passed to other base class. + @param kwargs: Keyword args to passed to other base class. + """ + self.sizerDict = {} + self.lastControl = None + super(DriverSettingsMixin, self).__init__(*args, **kwargs) # because settings instances can be of type L{Driver} as well, we have to handle # showing settings for non-instances. Because of this, we must reacquire a reference # to the settings class whenever we wish to use it (via L{getSettings}) in case the instance changes. @@ -1074,17 +1082,17 @@ def _makeSliderSettingControl( """ labeledControl = guiHelper.LabeledControlHelper( self, - "%s:"%setting.displayNameWithAccelerator, + f"{setting.displayNameWithAccelerator}:", nvdaControls.EnhancedInputSlider, minValue=setting.minVal, maxValue=setting.maxVal ) lSlider=labeledControl.control - setattr(self,"%sSlider"%setting.id,lSlider) - self._setSliderStepSizes(lSlider,setting) + setattr(self, f"{setting.id}Slider", lSlider) lSlider.Bind(wx.EVT_SLIDER, DriverSettingChanger( settingsStorage, setting )) + self._setSliderStepSizes(lSlider, setting) lSlider.SetValue(getattr(settingsStorage, setting.id)) if self.lastControl: lSlider.MoveAfterInTabOrder(self.lastControl) @@ -1099,12 +1107,11 @@ def _makeStringSettingControl( """ Same as L{_makeSliderSettingControl} but for string settings. Returns sizer with label and combobox. """ - - labelText="%s:"%setting.displayNameWithAccelerator - settingsInst = self.getSettings() + labelText = f"{setting.displayNameWithAccelerator}:" + stringSettingAttribName = f"_{setting.id}s" setattr( self, - "_%ss"%setting.id, + stringSettingAttribName, # Settings are stored as an ordered dict. # Therefore wrap this inside a list call. list(getattr( @@ -1112,19 +1119,21 @@ def _makeStringSettingControl( f"available{setting.id.capitalize()}s" ).values()) ) - l=getattr(self,"_%ss"%setting.id) - labeledControl=guiHelper.LabeledControlHelper( + stringSettings = getattr(self, stringSettingAttribName) + labeledControl = guiHelper.LabeledControlHelper( self, labelText, wx.Choice, - choices=[x.displayName for x in l] + choices=[x.displayName for x in stringSettings] ) lCombo = labeledControl.control - setattr(self,"%sList"%setting.id,lCombo) + setattr(self, f"{setting.id}List", lCombo) try: cur = getattr(settingsStorage, setting.id) - i=[x.id for x in l].index(cur) - lCombo.SetSelection(i) + selectionIndex = [ + x.id for x in stringSettings + ].index(cur) + lCombo.SetSelection(selectionIndex) except ValueError: pass lCombo.Bind( @@ -1133,7 +1142,7 @@ def _makeStringSettingControl( ) if self.lastControl: lCombo.MoveAfterInTabOrder(self.lastControl) - self.lastControl=lCombo + self.lastControl = lCombo return labeledControl.sizer def _makeBooleanSettingControl( @@ -1144,8 +1153,8 @@ def _makeBooleanSettingControl( """ Same as L{_makeSliderSettingControl} but for boolean settings. Returns checkbox. """ - checkbox=wx.CheckBox(self,wx.ID_ANY,label=setting.displayNameWithAccelerator) - setattr(self,"%sCheckbox"%setting.id,checkbox) + checkbox = wx.CheckBox(self, label=setting.displayNameWithAccelerator) + setattr(self, f"{setting.id}Checkbox", checkbox) def _onCheckChanged(evt: wx.CommandEvent): evt.Skip() # allow other handlers to also process this event. @@ -1162,7 +1171,9 @@ def _onCheckChanged(evt: wx.CommandEvent): return checkbox def updateDriverSettings(self, changedSetting=None): - """Creates, hides or updates existing GUI controls for all of supported settings.""" + """ + Creates, hides or updates existing GUI controls for all of supported settings. + """ settingsInst = self.getSettings() settingsStorage = self._getSettingsStorage() # firstly check already created options @@ -1172,7 +1183,7 @@ def updateDriverSettings(self, changedSetting=None): continue if not settingsInst.isSupported(name): self.settingsSizer.Hide(sizer) - #Create new controls, update already existing + # Create new controls, update already existing log.debug(f"Current sizerDict: {self.sizerDict!r}") log.debug(f"Current supportedSettings: {self.getSettings().supportedSettings!r}") @@ -1180,29 +1191,29 @@ def updateDriverSettings(self, changedSetting=None): if setting.id == changedSetting: # Changing a setting shouldn't cause that setting's own values to change. continue - if setting.id in self.sizerDict: #update a value + if setting.id in self.sizerDict: # update a value self.settingsSizer.Show(self.sizerDict[setting.id]) - if isinstance(setting,NumericDriverSetting): + if isinstance(setting, NumericDriverSetting): getattr(self, f"{setting.id}Slider").SetValue( getattr(settingsStorage, setting.id) ) - elif isinstance(setting,BooleanDriverSetting): + elif isinstance(setting, BooleanDriverSetting): getattr(self, f"{setting.id}Checkbox").SetValue( getattr(settingsStorage, setting.id) ) else: - l=getattr(self,"_%ss"%setting.id) - lCombo=getattr(self,"%sList"%setting.id) + options = getattr(self, f"_{setting.id}s") + lCombo = getattr(self, f"{setting.id}List") try: cur = getattr(settingsStorage, setting.id) - i=[x.id for x in l].index(cur) - lCombo.SetSelection(i) + indexOfItem = [x.id for x in options].index(cur) + lCombo.SetSelection(indexOfItem) except ValueError: pass - else: #create a new control - if isinstance(setting,NumericDriverSetting): - settingMaker=self.makeSliderSettingControl - elif isinstance(setting,BooleanDriverSetting): + else: # create a new control + if isinstance(setting, NumericDriverSetting): + settingMaker = self._makeSliderSettingControl + elif isinstance(setting, BooleanDriverSetting): log.debug(f"creating a new bool driver setting: {setting.id}") settingMaker = self._makeBooleanSettingControl else: @@ -1210,11 +1221,16 @@ def updateDriverSettings(self, changedSetting=None): try: s = settingMaker(setting, settingsStorage) except UnsupportedConfigParameterError: - log.debugWarning("Unsupported setting %s; ignoring"%setting.id, exc_info=True) + log.debugWarning(f"Unsupported setting {setting.id}; ignoring", exc_info=True) continue - self.sizerDict[setting.id]=s - self.settingsSizer.Insert(len(self.sizerDict)-1,s,border=10,flag=wx.BOTTOM) - #Update graphical layout of the dialog + self.sizerDict[setting.id] = s + self.settingsSizer.Insert( + len(self.sizerDict) - 1, + s, + border=10, + flag=wx.BOTTOM + ) + # Update graphical layout of the dialog self.settingsSizer.Layout() def onDiscard(self): @@ -1238,7 +1254,7 @@ def onPanelActivated(self): self.settingsSizer.Clear(delete_windows=True) self._currentSettingsRef = weakref.ref(self.getSettings()) self.makeSettings(self.settingsSizer) - super(DriverSettingsMixin,self).onPanelActivated() + super(DriverSettingsMixin, self).onPanelActivated() class VoiceSettingsPanel(DriverSettingsMixin, SettingsPanel): # Translators: This is the label for the voice settings panel. @@ -1260,67 +1276,113 @@ def makeSettings(self, settingsSizer): # Translators: This is the label for a checkbox in the # voice settings panel (if checked, text will be read using the voice for the language of the text). autoLanguageSwitchingText = _("Automatic language switching (when supported)") - self.autoLanguageSwitchingCheckbox = settingsSizerHelper.addItem(wx.CheckBox(self,label=autoLanguageSwitchingText)) - self.autoLanguageSwitchingCheckbox.SetValue(config.conf["speech"]["autoLanguageSwitching"]) + self.autoLanguageSwitchingCheckbox = settingsSizerHelper.addItem( + wx.CheckBox( + self, + label=autoLanguageSwitchingText + )) + self.autoLanguageSwitchingCheckbox.SetValue( + config.conf["speech"]["autoLanguageSwitching"] + ) # Translators: This is the label for a checkbox in the - # voice settings panel (if checked, different voices for dialects will be used to read text in that dialect). - autoDialectSwitchingText =_("Automatic dialect switching (when supported)") - self.autoDialectSwitchingCheckbox=settingsSizerHelper.addItem(wx.CheckBox(self,label=autoDialectSwitchingText)) - self.autoDialectSwitchingCheckbox.SetValue(config.conf["speech"]["autoDialectSwitching"]) + # voice settings panel (if checked, different voices for dialects will be used to + # read text in that dialect). + autoDialectSwitchingText = _("Automatic dialect switching (when supported)") + self.autoDialectSwitchingCheckbox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=autoDialectSwitchingText + )) + self.autoDialectSwitchingCheckbox.SetValue( + config.conf["speech"]["autoDialectSwitching"] + ) # Translators: This is the label for a combobox in the # voice settings panel (possible choices are none, some, most and all). punctuationLabelText = _("Punctuation/symbol &level:") - symbolLevelLabels=characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS - symbolLevelChoices =[symbolLevelLabels[level] for level in characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS] - self.symbolLevelList = settingsSizerHelper.addLabeledControl(punctuationLabelText, wx.Choice, choices=symbolLevelChoices) + symbolLevelLabels = characterProcessing.SPEECH_SYMBOL_LEVEL_LABELS + symbolLevelChoices = [ + symbolLevelLabels[level] for level in characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS + ] + self.symbolLevelList = settingsSizerHelper.addLabeledControl( + punctuationLabelText, wx.Choice, choices=symbolLevelChoices + ) curLevel = config.conf["speech"]["symbolLevel"] - self.symbolLevelList.SetSelection(characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS.index(curLevel)) + self.symbolLevelList.SetSelection( + characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS.index(curLevel) + ) # Translators: This is the label for a checkbox in the # voice settings panel (if checked, text will be read using the voice for the language of the text). trustVoiceLanguageText = _("Trust voice's language when processing characters and symbols") - self.trustVoiceLanguageCheckbox = settingsSizerHelper.addItem(wx.CheckBox(self,label=trustVoiceLanguageText)) + self.trustVoiceLanguageCheckbox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=trustVoiceLanguageText) + ) self.trustVoiceLanguageCheckbox.SetValue(config.conf["speech"]["trustVoiceLanguage"]) # Translators: This is the label for a checkbox in the # voice settings panel (if checked, data from the unicode CLDR will be used # to speak emoji descriptions). - includeCLDRText = _("Include Unicode Consortium data (including emoji) when processing characters and symbols") - self.includeCLDRCheckbox = settingsSizerHelper.addItem(wx.CheckBox(self,label=includeCLDRText)) + includeCLDRText = _( + "Include Unicode Consortium data (including emoji) when processing characters and symbols" + ) + self.includeCLDRCheckbox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=includeCLDRText) + ) self.includeCLDRCheckbox.SetValue(config.conf["speech"]["includeCLDR"]) - # Translators: This is a label for a setting in voice settings (an edit box to change voice pitch for capital letters; the higher the value, the pitch will be higher). - capPitchChangeLabelText=_("Capital pitch change percentage") - self.capPitchChangeEdit=settingsSizerHelper.addLabeledControl(capPitchChangeLabelText, nvdaControls.SelectOnFocusSpinCtrl, - min=int(config.conf.getConfigValidation(("speech", self.driver.name, "capPitchChange")).kwargs["min"]), - max=int(config.conf.getConfigValidation(("speech", self.driver.name, "capPitchChange")).kwargs["max"]), + minPitchChange = int(config.conf.getConfigValidation( + ("speech", self.driver.name, "capPitchChange") + ).kwargs["min"]) + + maxPitchChange = int(config.conf.getConfigValidation( + ("speech", self.driver.name, "capPitchChange") + ).kwargs["max"]) + + # Translators: This is a label for a setting in voice settings (an edit box to change + # voice pitch for capital letters; the higher the value, the pitch will be higher). + capPitchChangeLabelText = _("Capital pitch change percentage") + self.capPitchChangeEdit = settingsSizerHelper.addLabeledControl( + capPitchChangeLabelText, + nvdaControls.SelectOnFocusSpinCtrl, + min=minPitchChange, + max=maxPitchChange, initial=config.conf["speech"][self.driver.name]["capPitchChange"]) # Translators: This is the label for a checkbox in the # voice settings panel. sayCapForCapsText = _("Say &cap before capitals") - self.sayCapForCapsCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self,label=sayCapForCapsText)) - self.sayCapForCapsCheckBox.SetValue(config.conf["speech"][self.driver.name]["sayCapForCapitals"]) + self.sayCapForCapsCheckBox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=sayCapForCapsText) + ) + self.sayCapForCapsCheckBox.SetValue( + config.conf["speech"][self.driver.name]["sayCapForCapitals"] + ) # Translators: This is the label for a checkbox in the # voice settings panel. beepForCapsText =_("&Beep for capitals") - self.beepForCapsCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = beepForCapsText)) - self.beepForCapsCheckBox.SetValue(config.conf["speech"][self.driver.name]["beepForCapitals"]) + self.beepForCapsCheckBox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=beepForCapsText) + ) + self.beepForCapsCheckBox.SetValue( + config.conf["speech"][self.driver.name]["beepForCapitals"] + ) # Translators: This is the label for a checkbox in the # voice settings panel. useSpellingFunctionalityText = _("Use &spelling functionality if supported") - self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem(wx.CheckBox(self, label = useSpellingFunctionalityText)) - self.useSpellingFunctionalityCheckBox.SetValue(config.conf["speech"][self.driver.name]["useSpellingFunctionality"]) + self.useSpellingFunctionalityCheckBox = settingsSizerHelper.addItem( + wx.CheckBox(self, label=useSpellingFunctionalityText) + ) + self.useSpellingFunctionalityCheckBox.SetValue( + config.conf["speech"][self.driver.name]["useSpellingFunctionality"] + ) def onSave(self): DriverSettingsMixin.onSave(self) - config.conf["speech"]["autoLanguageSwitching"]=self.autoLanguageSwitchingCheckbox.IsChecked() - config.conf["speech"]["autoDialectSwitching"]=self.autoDialectSwitchingCheckbox.IsChecked() + config.conf["speech"]["autoLanguageSwitching"] = self.autoLanguageSwitchingCheckbox.IsChecked() + config.conf["speech"]["autoDialectSwitching"] = self.autoDialectSwitchingCheckbox.IsChecked() config.conf["speech"]["symbolLevel"]=characterProcessing.CONFIGURABLE_SPEECH_SYMBOL_LEVELS[self.symbolLevelList.GetSelection()] config.conf["speech"]["trustVoiceLanguage"]=self.trustVoiceLanguageCheckbox.IsChecked() currentIncludeCLDR = config.conf["speech"]["includeCLDR"] @@ -2750,8 +2812,8 @@ def getSettings(self) -> AutoSettings: return self.driver def makeSettings(self, settingsSizer): - if gui._isDebug(): - startTime = time.time() + shouldDebugGui = gui._isDebug() + startTime = 0 if not shouldDebugGui else time.time() # Construct braille display specific settings self.updateDriverSettings() @@ -2769,8 +2831,11 @@ def makeSettings(self, settingsSizer): self.outTableList.SetSelection(selection) except: pass - if gui._isDebug(): - log.debug("Loading output tables completed, now at %.2f seconds from start"%(time.time() - startTime)) + if shouldDebugGui: + timePassed = time.time() - startTime + log.debug( + f"Loading output tables completed, now at {timePassed:.2f} seconds from start" + ) # Translators: The label for a setting in braille settings to select the input table (the braille table used to type braille characters on a braille keyboard). inputLabelText = _("&Input table:") @@ -2782,12 +2847,17 @@ def makeSettings(self, settingsSizer): self.inTableList.SetSelection(selection) except: pass - if gui._isDebug(): - log.debug("Loading input tables completed, now at %.2f seconds from start"%(time.time() - startTime)) + if shouldDebugGui: + timePassed = time.time() - startTime + log.debug( + f"Loading input tables completed, now at {timePassed:.2f} seconds from start" + ) # Translators: The label for a setting in braille settings to expand the current word under cursor to computer braille. expandAtCursorText = _("E&xpand to computer braille for the word at the cursor") - self.expandAtCursorCheckBox = sHelper.addItem(wx.CheckBox(self, wx.ID_ANY, label=expandAtCursorText)) + self.expandAtCursorCheckBox = sHelper.addItem( + wx.CheckBox(self, wx.ID_ANY, label=expandAtCursorText) + ) self.expandAtCursorCheckBox.SetValue(config.conf["braille"]["expandAtCursor"]) # Translators: The label for a setting in braille settings to show the cursor. @@ -2798,7 +2868,9 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to enable cursor blinking. cursorBlinkLabelText = _("Blink cursor") - self.cursorBlinkCheckBox = sHelper.addItem(wx.CheckBox(self, label=cursorBlinkLabelText)) + self.cursorBlinkCheckBox = sHelper.addItem( + wx.CheckBox(self, label=cursorBlinkLabelText) + ) self.cursorBlinkCheckBox.Bind(wx.EVT_CHECKBOX, self.onBlinkCursorChange) self.cursorBlinkCheckBox.SetValue(config.conf["braille"]["cursorBlink"]) if not self.showCursorCheckBox.GetValue(): @@ -2806,10 +2878,17 @@ def makeSettings(self, settingsSizer): # Translators: The label for a setting in braille settings to change cursor blink rate in milliseconds (1 second is 1000 milliseconds). cursorBlinkRateLabelText = _("Cursor blink rate (ms)") - minBlinkRate = int(config.conf.getConfigValidation(("braille", "cursorBlinkRate")).kwargs["min"]) + minBlinkRate = int(config.conf.getConfigValidation( + ("braille", "cursorBlinkRate") + ).kwargs["min"]) maxBlinkRate = int(config.conf.getConfigValidation(("braille", "cursorBlinkRate")).kwargs["max"]) - self.cursorBlinkRateEdit = sHelper.addLabeledControl(cursorBlinkRateLabelText, nvdaControls.SelectOnFocusSpinCtrl, - min=minBlinkRate, max=maxBlinkRate, initial=config.conf["braille"]["cursorBlinkRate"]) + self.cursorBlinkRateEdit = sHelper.addLabeledControl( + cursorBlinkRateLabelText, + nvdaControls.SelectOnFocusSpinCtrl, + min=minBlinkRate, + max=maxBlinkRate, + initial=config.conf["braille"]["cursorBlinkRate"] + ) if not self.showCursorCheckBox.GetValue() or not self.cursorBlinkCheckBox.GetValue() : self.cursorBlinkRateEdit.Disable() @@ -2840,12 +2919,22 @@ def makeSettings(self, settingsSizer): if gui._isDebug(): log.debug("Loading cursor settings completed, now at %.2f seconds from start"%(time.time() - startTime)) + minTimeout = int(config.conf.getConfigValidation( + ("braille", "messageTimeout") + ).kwargs["min"]) + maxTimeOut = int(config.conf.getConfigValidation( + ("braille", "messageTimeout") + ).kwargs["max"]) + # Translators: The label for a setting in braille settings to change how long a message stays on the braille display (in seconds). messageTimeoutText = _("Message &timeout (sec)") - self.messageTimeoutEdit = sHelper.addLabeledControl(messageTimeoutText, nvdaControls.SelectOnFocusSpinCtrl, - min=int(config.conf.getConfigValidation(("braille", "messageTimeout")).kwargs["min"]), - max=int(config.conf.getConfigValidation(("braille", "messageTimeout")).kwargs["max"]), - initial=config.conf["braille"]["messageTimeout"]) + self.messageTimeoutEdit = sHelper.addLabeledControl( + messageTimeoutText, + nvdaControls.SelectOnFocusSpinCtrl, + min=minTimeout, + max=maxTimeOut, + initial=config.conf["braille"]["messageTimeout"] + ) # Translators: The label for a setting in braille settings to display a message on the braille display indefinitely. noMessageTimeoutLabelText = _("Show &messages indefinitely") @@ -2965,7 +3054,9 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): self, **kwargs ) - except: + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) continue @@ -3081,7 +3172,9 @@ def onDiscard(self): for panel in self.providerPanelInstances: try: panel.onDiscard() - except: + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 log.debug(f"Error discarding providerPanel: {panel.__class__!r}", exc_info=True) providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] @@ -3093,7 +3186,9 @@ def onSave(self): for panel in self.providerPanelInstances: try: panel.onSave() - except: + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = list(vision.handler.providers) @@ -3102,7 +3197,7 @@ class VisionProviderSubPanel_Settings( DriverSettingsMixin, SettingsPanel ): - cachePropertiesByDefault = False + def __init__( self, parent: wx.Window, @@ -3180,7 +3275,9 @@ def _createRuntimeSettings(self): settingsCallable=self._providerType.getSettings ) self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) - except: + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 log.error("unable to create runtime settings", exc_info=True) return False return True @@ -3193,7 +3290,6 @@ def _nonEnableableGUI(self, evt): ) self._checkBox.SetValue(False) - def _enableToggle(self, evt): if not evt.IsChecked(): self._terminateProvider() diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index c28af8161fb..39ad07f018a 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -80,7 +80,6 @@ def terminateProvider(self, providerName: str, saveSettings: bool = True): @param providerName: The provider to terminate. @param saveSettings: Whether settings should be saved on termionation. """ - success = True # todo: can we remove this? seems unused, what is the history? # Remove the provider from the providers dictionary. providerInstance = self.providers.pop(providerName, None) if not providerInstance: @@ -163,7 +162,9 @@ def initializeProvider(self, providerName: str, temporary: bool = False): f"Error terminating provider {providerName} after registering to extension points", exc_info=True) raise registerEventExtensionPointsException providerSettings = providerCls.getSettings() - providerSettings.initSettings() # todo: do we actually have to do this here? It might actually cause a bug, reloading settings and overwriting current static settings. + # todo: do we actually have to do initSettings here? + # It might actually cause a bug, reloading settings and overwriting current static settings. + providerSettings.initSettings() if not temporary and providerName not in config.conf['vision']['providers']: config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerName] self.providers[providerName] = providerInst From 42aede5aef571a7bba061d37b522101227da04cb Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 28 Oct 2019 20:33:55 +0100 Subject: [PATCH 026/116] Fix error on exit --- source/autoSettingsUtils/autoSettings.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 6f9a02be002..625726736f3 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -40,7 +40,6 @@ def _registerConfigSaveAction(self): def _unregisterConfigSaveAction(self): """Overrideable pre_configSave de-registration""" - log.debug(f"de-registering pre_configSave action: {self.__class__!r}") config.pre_configSave.unregister(self.saveSettings) @classmethod From ed6f3f7baca0c705d62b8b8a8b576d0bd95e4c33 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 13:44:18 +0100 Subject: [PATCH 027/116] Better docs in autoSettings class --- source/autoSettingsUtils/autoSettings.py | 32 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 625726736f3..9db42a7fa3f 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -21,12 +21,18 @@ class AutoSettings(AutoPropertyObject): - """ + """ An AutoSettings instance is used to simplify the load/save of user config for NVDA extensions + (Synth drivers, braille drivers, vision providers) and make it possible to automatically provide a + standard GUI for these settings. + Derived classes must implement: + - getId + - getTranslatedName + Although technically optional, derived classes probably need to implement: + - _get_preInitSettings + - _get_supportedSettings """ def __init__(self): - """ - """ super().__init__() self._registerConfigSaveAction() @@ -34,27 +40,40 @@ def __del__(self): self._unregisterConfigSaveAction() def _registerConfigSaveAction(self): - """Overrideable pre_configSave registration""" + """ Overrideable pre_configSave registration + """ log.debug(f"registering pre_configSave action: {self.__class__!r}") config.pre_configSave.register(self.saveSettings) def _unregisterConfigSaveAction(self): - """Overrideable pre_configSave de-registration""" + """ Overrideable pre_configSave de-registration + """ config.pre_configSave.unregister(self.saveSettings) @classmethod @abstractmethod def getId(cls) -> str: + """ + @return: Application friendly name, should be globally unique, however since this is used in the config file + human readable is also beneficial. + """ ... @classmethod @abstractmethod def getProductName(cls) -> str: + """ + @return: The translated name for this collection of settings. This is for use in the GUI to represent the + group of these settings. + """ ... @classmethod @abstractmethod def _getConfigSection(cls) -> str: + """ + @return: The section of the config that these settings belong in. + """ ... @classmethod @@ -83,8 +102,7 @@ def _initSpecificSettings( cls._loadSpecificSettings(clsOrInst, settings) def initSettings(self): - """ - Initializes the configuration for this driver. + """Initializes the configuration for this driver. This method is called when initializing the driver. """ self._initSpecificSettings(self, self.supportedSettings) From 57495437557cafa54356bda2dfdb4a1f3c05e966 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 14:05:42 +0100 Subject: [PATCH 028/116] Rename VisionEnhancementProviderStaticSettings Static is misleading, this class contains the class method _get_preInitSettings for settings that don't need an instance (or to query an external dependency). It contains the instance method _get_supportedSettings for settings that do need an instance (or to query an external dependency). --- source/gui/settingsDialogs.py | 2 +- source/vision/providerBase.py | 6 +++--- source/visionEnhancementProviders/NVDAHighlighter.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index e7dd3c6bbaa..7148466e736 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3202,7 +3202,7 @@ def __init__( self, parent: wx.Window, *, # Make next argument keyword only - settingsCallable: Callable[[], vision.providerBase.VisionEnhancementProviderStaticSettings] + settingsCallable: Callable[[], vision.providerBase.VisionEnhancementProviderSettings] ): """ @param providerCallable: A callable that returns an instance to a VisionEnhancementProvider. diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 6bd001aebc1..8a54476a785 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -21,7 +21,7 @@ ] -class VisionEnhancementProviderStaticSettings(driverHandler.Driver): +class VisionEnhancementProviderSettings(driverHandler.Driver): _configSection = "vision" cachePropertiesByDefault = False @@ -67,9 +67,9 @@ class VisionEnhancementProvider(AutoPropertyObject): supportedRoles: FrozenSet[Role] = frozenset() @classmethod - def getSettings(cls) -> VisionEnhancementProviderStaticSettings: + def getSettings(cls) -> VisionEnhancementProviderSettings: """ - @remarks: The L{VisionEnhancementProviderStaticSettings} class should be implemented to define the settings + @remarks: The L{VisionEnhancementProviderSettings} class should be implemented to define the settings for your provider """ raise NotImplementedError( diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index a14b2cb2100..08fe258a5a1 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -200,7 +200,7 @@ def refresh(self): _supportedContexts = (Context.FOCUS, Context.NAVIGATOR, Context.BROWSEMODE) -class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderStaticSettings): +class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderSettings): name = "NVDAHighlighter" # Translators: Description for NVDA's built-in screen highlighter. description = _("NVDA Highlighter") From 9cc72ce63e9aaddfa390efa8a12ab8e4ce8044ad Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 14:24:49 +0100 Subject: [PATCH 029/116] Remove saveSettings param from terminate method on visionEnhancementProviderBase --- source/vision/providerBase.py | 4 +--- source/vision/visionHandler.py | 7 ++++++- source/visionEnhancementProviders/NVDAHighlighter.py | 2 +- source/visionEnhancementProviders/screenCurtain.py | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 8a54476a785..0814888394d 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -91,11 +91,9 @@ def reinitialize(self): self.terminate() self.__init__() - # todo: remove saveSettings param - def terminate(self, saveSettings: bool = True): + def terminate(self): """Terminate this driver. This should be used for any required clean up. - @param saveSettings: Whether settings should be saved on termination. @precondition: L{initialize} has been called. @postcondition: This driver can no longer be used. """ diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 39ad07f018a..ed335faea14 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -87,8 +87,13 @@ def terminateProvider(self, providerName: str, saveSettings: bool = True): f"Tried to terminate uninitialized provider {providerName!r}" ) exception = None + if saveSettings: + try: + providerInstance.getSettings().saveSettings() + except Exception: + log.error(f"Error while saving settings during termination of {providerName}") try: - providerInstance.terminate(saveSettings=saveSettings) + providerInstance.terminate() except Exception as e: # Purposely catch everything. # A provider can raise whatever exception, diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 08fe258a5a1..c04a93a624a 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -373,7 +373,7 @@ def __init__(self): # - un-comment equivelent line in terminate (restoring settings to non-runtime version) # self.__class__._settings = NVDAHighlighterSettings_Runtime() - def terminate(self, *args, **kwargs): + def terminate(self): if self._highlighterThread: if not winUser.user32.PostThreadMessageW(self._highlighterThread.ident, winUser.WM_QUIT, 0, 0): raise WinError() diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 6818ceddfd5..36d1c6d7b7c 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -113,8 +113,8 @@ def __init__(self): Magnification.MagShowSystemCursor(False) Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) - def terminate(self, *args, **kwargs): - super().terminate(*args, **kwargs) + def terminate(self): + super().terminate() Magnification.MagShowSystemCursor(True) Magnification.MagUninitialize() From 4730e47b211e4ec9424f32fe13170850ff0515b3 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 15:29:02 +0100 Subject: [PATCH 030/116] Clarify naming, consolidate classes Use getTranslatedName for AutoSettings classes - Driver - providerBase.VisionEnhancementProvider Improve docs --- source/autoSettingsUtils/autoSettings.py | 2 +- source/driverHandler.py | 2 +- source/gui/settingsDialogs.py | 52 +++++++++------- source/vision/__init__.py | 4 +- source/vision/providerBase.py | 61 +++++++++---------- source/vision/visionHandler.py | 36 +++++------ .../NVDAHighlighter.py | 53 ++++++++-------- 7 files changed, 107 insertions(+), 103 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 9db42a7fa3f..5da1fcbe6a7 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -61,7 +61,7 @@ def getId(cls) -> str: @classmethod @abstractmethod - def getProductName(cls) -> str: + def getTranslatedName(cls) -> str: """ @return: The translated name for this collection of settings. This is for use in the GUI to represent the group of these settings. diff --git a/source/driverHandler.py b/source/driverHandler.py index 27d46bfb850..407b4a80bfb 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -111,7 +111,7 @@ def getId(cls) -> str: return cls.name @classmethod - def getProductName(cls) -> str: + def getTranslatedName(cls) -> str: # todo rename to getTranslatedName return cls.description @classmethod diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7148466e736..11dd09d70f9 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -291,7 +291,7 @@ def makeSettings(self, sizer): """Populate the panel with settings controls. Subclasses must override this method. @param sizer: The sizer to which to add the settings controls. - @type sizer: wx.Sizer + @type sizer: wx.BoxSizer """ raise NotImplementedError @@ -3029,26 +3029,26 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - for providerName, providerDesc, _providerRole, providerClass in vision.getProviderList(): + for providerId, provTransName, _providerRole, providerClass in vision.getProviderList(): providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=providerDesc), wx.VERTICAL), + wx.StaticBoxSizer(wx.StaticBox(self, label=provTransName), wx.VERTICAL), flag=wx.EXPAND ) kwargs = { # default value for name parameter to lambda, recommended by python3 FAQ: # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result - "getProvider": lambda name=providerName: self._getProvider(name), - "initProvider": lambda name=providerName: self.safeInitProviders([name]), - "terminateProvider": lambda name=providerName: self.safeTerminateProviders([name], verbose=True) + "getProvider": lambda id=providerId: self._getProvider(id), + "initProvider": lambda id=providerId: self.safeInitProviders([id]), + "terminateProvider": lambda id=providerId: self.safeTerminateProviders([id], verbose=True) } settingsPanelCls = providerClass.getSettingsPanelClass() if not settingsPanelCls: - log.debug(f"Using default panel for providerName: {providerName}") + log.debug(f"Using default panel for providerId: {providerId}") settingsPanelCls = VisionProviderSubPanel_Wrapper kwargs["providerType"] = providerClass else: - log.debug(f"Using custom panel for providerName: {providerName}") + log.debug(f"Using custom panel for providerId: {providerId}") try: settingsPanel = settingsPanelCls( self, @@ -3065,13 +3065,13 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): providerSizer.Add(settingsPanel, flag=wx.EXPAND) self.providerPanelInstances.append(settingsPanel) - def _getProvider(self, providerName: str) -> Optional[vision.VisionEnhancementProvider]: - log.debug(f"providerName: {providerName}") - return vision.handler.providers.get(providerName, None) + def _getProvider(self, providerId: str) -> Optional[vision.VisionEnhancementProvider]: + log.debug(f"providerId: {providerId}") + return vision.handler.providers.get(providerId, None) def safeInitProviders( self, - providerNames: List[str] + providerIds: List[str] ) -> bool: """Initializes one or more providers in a way that is gui friendly, showing an error if appropriate. @@ -3079,13 +3079,13 @@ def safeInitProviders( """ success = True initErrors = [] - for providerName in providerNames: + for providerId in providerIds: try: - vision.handler.initializeProvider(providerName) + vision.handler.initializeProvider(providerId) except Exception: - initErrors.append(providerName) + initErrors.append(providerId) log.error( - f"Could not initialize the {providerName} vision enhancement provider", + f"Could not initialize the {providerId} vision enhancement provider", exc_info=True ) success = False @@ -3112,7 +3112,7 @@ def safeInitProviders( def safeTerminateProviders( self, - providerNames: List[str], + providerIds: List[str], verbose: bool = False ): """Terminates one or more providers in a way that is gui friendly, @@ -3120,17 +3120,17 @@ def safeTerminateProviders( @returns: Whether initialization succeeded for all providers. """ terminateErrors = [] - for providerName in providerNames: + for providerId in providerIds: try: # Terminating a provider from the gui should never save the settings. # This is because termination happens on the fly when unchecking check boxes. # Saving settings would be harmful if a user opens the vision panel, # then changes some settings and disables the provider. - vision.handler.terminateProvider(providerName, saveSettings=False) + vision.handler.terminateProvider(providerId, saveSettings=False) except Exception: - terminateErrors.append(providerName) + terminateErrors.append(providerId) log.error( - f"Could not terminate the {providerName} vision enhancement provider", + f"Could not terminate the {providerId} vision enhancement provider", exc_info=True ) @@ -3177,9 +3177,15 @@ def onDiscard(self): except: # noqa: E722 log.debug(f"Error discarding providerPanel: {panel.__class__!r}", exc_info=True) - providersToInitialize = [name for name in self.initialProviders if name not in vision.handler.providers] + providersToInitialize = [ + providerId for providerId in self.initialProviders + if providerId not in vision.handler.providers + ] self.safeInitProviders(providersToInitialize) - providersToTerminate = [name for name in vision.handler.providers if name not in self.initialProviders] + providersToTerminate = [ + providerId for providerId in vision.handler.providers + if providerId not in self.initialProviders + ] self.safeTerminateProviders(providersToTerminate) def onSave(self): diff --git a/source/vision/__init__.py b/source/vision/__init__.py index bc2e179b7d9..eafd342cf2c 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -66,8 +66,8 @@ def getProviderList( if not onlyStartable or provider.canStart(): providerSettings = provider.getSettings() providerList.append(( - providerSettings.name, - providerSettings.description, + providerSettings.getId(), + providerSettings.getTranslatedName(), list(provider.supportedRoles), provider )) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 0814888394d..0a91be5c815 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -8,8 +8,9 @@ """ import driverHandler -from abc import abstractmethod +from abc import abstractmethod, ABC +from autoSettingsUtils.autoSettings import AutoSettings from baseObject import AutoPropertyObject from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints @@ -21,44 +22,38 @@ ] -class VisionEnhancementProviderSettings(driverHandler.Driver): - _configSection = "vision" - cachePropertiesByDefault = False - +class VisionEnhancementProviderSettings(AutoSettings, ABC): + """ + Base class for settings for a vision enhancement provider. + Ensure that the following are implemented: + - AutoSettings.getId + - AutoSettings.getTranslatedName + Although technically optional, derived classes probably need to implement: + - AutoSettings._get_preInitSettings + - AutoSettings._get_supportedSettings + """ supportedSettings: SupportedSettingType # Typing for autoprop L{_get_supportedSettings} def __init__(self): super().__init__() + # ensure that settings are loaded at construction time. self.initSettings() - @property - @abstractmethod - def name(self): # todo: rename this? "providerID" - """Application Friendly name, should be unique!""" - - @property - @abstractmethod - def description(self): # todo: rename this? "translated Name" - """Translated name""" - - def _get_supportedSettings(self) -> SupportedSettingType: - raise NotImplementedError( - f"_get_supportedSettings must be implemented in Class {self.__class__.__qualname__}" - ) - @classmethod - def check(cls): # todo: remove, comes from Driver - return True - - def loadSettings(self, onlyChanged: bool = False): - super().loadSettings(onlyChanged) - - def saveSettings(self): - super().saveSettings() + def _getConfigSection(cls) -> str: + # all providers should be in the "vision" section. + return "vision" class VisionEnhancementProvider(AutoPropertyObject): """A class for vision enhancement providers. + Derived classes should implement: + - terminate + - registerEventExtensionPoints + - canStart + - getSettings + To provide a custom GUI, return a SettingsPanel class type from: + - getSettingsPanelClass """ cachePropertiesByDefault = True #: The roles supported by this provider. @@ -72,9 +67,7 @@ def getSettings(cls) -> VisionEnhancementProviderSettings: @remarks: The L{VisionEnhancementProviderSettings} class should be implemented to define the settings for your provider """ - raise NotImplementedError( - f"getSettings must be implemented in Class {cls.__qualname__}" - ) + ... @classmethod def getSettingsPanelClass(cls) -> Optional[Type]: @@ -91,12 +84,14 @@ def reinitialize(self): self.terminate() self.__init__() + @abstractmethod def terminate(self): """Terminate this driver. This should be used for any required clean up. @precondition: L{initialize} has been called. - @postcondition: This driver can no longer be used. + @postcondition: This provider can no longer be used. """ + ... @abstractmethod def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints): @@ -108,7 +103,7 @@ def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints): as it might be called again several times between initialization and termination. @param extensionPoints: An object containing available extension points as attributes. """ - pass + ... @classmethod @abstractmethod diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index ed335faea14..d7e4584119b 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -72,26 +72,26 @@ def postGuiInit(self) -> None: self.handleConfigProfileSwitch() config.post_configProfileSwitch.register(self.handleConfigProfileSwitch) - def terminateProvider(self, providerName: str, saveSettings: bool = True): + def terminateProvider(self, providerId: str, saveSettings: bool = True): """Terminates a currently active provider. When termnation fails, an exception is raised. Yet, the provider wil lbe removed from the providers dictionary, so its instance goes out of scope and wil lbe garbage collected. - @param providerName: The provider to terminate. + @param providerId: The provider to terminate. @param saveSettings: Whether settings should be saved on termionation. """ # Remove the provider from the providers dictionary. - providerInstance = self.providers.pop(providerName, None) + providerInstance = self.providers.pop(providerId, None) if not providerInstance: raise exceptions.ProviderTerminateException( - f"Tried to terminate uninitialized provider {providerName!r}" + f"Tried to terminate uninitialized provider {providerId!r}" ) exception = None if saveSettings: try: providerInstance.getSettings().saveSettings() except Exception: - log.error(f"Error while saving settings during termination of {providerName}") + log.error(f"Error while saving settings during termination of {providerId}") try: providerInstance.terminate() except Exception as e: @@ -103,7 +103,7 @@ def terminateProvider(self, providerName: str, saveSettings: bool = True): # If we don't, configobj won't be aware of changes the list. configuredProviders: List = config.conf['vision']['providers'][:] try: - configuredProviders.remove(providerName) + configuredProviders.remove(providerId) config.conf['vision']['providers'] = configuredProviders except ValueError: pass @@ -114,7 +114,7 @@ def terminateProvider(self, providerName: str, saveSettings: bool = True): try: providerInst.registerEventExtensionPoints(self.extensionPoints) except Exception: - log.error("Error while registering to extension points for provider %s" % providerName, exc_info=True) + log.error(f"Error while registering to extension points for provider {providerId}", exc_info=True) if exception: raise exception @@ -129,27 +129,27 @@ def confirmInitWithUser(providerName: str) -> bool: providerCls = getProviderClass(providerName) return providerCls.confirmInitWithUser() - def initializeProvider(self, providerName: str, temporary: bool = False): + def initializeProvider(self, providerId: str, temporary: bool = False): """ Enables and activates the supplied provider. - @param providerName: The name of the registered provider. + @param providerId: The id of the registered provider. @param temporary: Whether the selected provider is enabled temporarily (e.g. as a fallback). This defaults to C{False}. If C{True}, no changes will be performed to the configuration. """ - providerInst = self.providers.pop(providerName, None) + providerInst = self.providers.pop(providerId, None) if providerInst is not None: providerCls = type(providerInst) providerInst.reinitialize() else: try: - providerCls = getProviderClass(providerName) + providerCls = getProviderClass(providerId) except ModuleNotFoundError: - raise exceptions.ProviderInitException(f"No provider named {providerName!r}") + raise exceptions.ProviderInitException(f"No provider: {providerId!r}") else: if not providerCls.canStart(): raise exceptions.ProviderInitException( - f"Trying to initialize provider {providerName!r} which reported being unable to start" + f"Trying to initialize provider {providerId!r} which reported being unable to start" ) # Initialize the provider. providerInst = providerCls() @@ -158,21 +158,21 @@ def initializeProvider(self, providerName: str, temporary: bool = False): providerInst.registerEventExtensionPoints(self.extensionPoints) except Exception as registerEventExtensionPointsException: log.error( - f"Error while registering to extension points for provider {providerName}", + f"Error while registering to extension points for provider: {providerId}", ) try: providerInst.terminate() except Exception: log.error( - f"Error terminating provider {providerName} after registering to extension points", exc_info=True) + f"Error terminating provider {providerId} after registering to extension points", exc_info=True) raise registerEventExtensionPointsException providerSettings = providerCls.getSettings() # todo: do we actually have to do initSettings here? # It might actually cause a bug, reloading settings and overwriting current static settings. providerSettings.initSettings() - if not temporary and providerName not in config.conf['vision']['providers']: - config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerName] - self.providers[providerName] = providerInst + if not temporary and providerId not in config.conf['vision']['providers']: + config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerId] + self.providers[providerId] = providerInst try: self.initialFocus() except Exception: diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index c04a93a624a..4401076c692 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -5,7 +5,7 @@ # Copyright (C) 2018-2019 NV Access Limited, Babbage B.V., Takuya Nishimoto """Default highlighter based on GDI Plus.""" -from typing import Callable, Optional, Tuple, Any +from typing import Callable, Optional, Tuple import vision from vision.constants import Role, Context @@ -210,6 +210,15 @@ class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderSetti highlightNavigator = False highlightBrowseMode = False + @classmethod + def getId(cls) -> str: + return "NVDAHighlighter" + + @classmethod + def getTranslatedName(cls) -> str: + # Translators: Description for NVDA's built-in screen highlighter. + return _("NVDA Highlighter") + def _get_supportedSettings(self): return [ driverHandler.BooleanDriverSetting( @@ -221,22 +230,13 @@ def _get_supportedSettings(self): ] -class NVDAHighlighterSettings_Runtime(NVDAHighlighterSettings): - someRuntimeOnlySetting = True - - def _get_supportedSettings(self): - settings = super()._get_supportedSettings() - settings.append(driverHandler.BooleanDriverSetting( - "someRuntimeOnlySetting", "Some runtime only setting", - defaultVal=True - )) - log.info("Runtime settings!") - return settings - class NVDAHighlighterGuiPanel( gui.DriverSettingsMixin, gui.SettingsPanel ): + _enableCheckSizer: wx.BoxSizer + _enabledCheckbox: wx.CheckBox + def __init__( self, parent, @@ -249,16 +249,19 @@ def __init__( self._terminateProvider = terminateProvider super().__init__(parent) - def getSettings(self) -> driverHandler.Driver: + def getSettings(self) -> NVDAHighlighterSettings: # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. # We want them set on this class. return VisionEnhancementProvider.getSettings() - def _getSettingsStorage(self) -> Any: - return self.getSettings() - - def makeSettings(self, sizer): - self._enabledCheckbox = wx.CheckBox(self, label="Highlight focus", style=wx.CHK_3STATE) + def makeSettings(self, sizer: wx.BoxSizer): + self._enabledCheckbox = wx.CheckBox( + self, + # Translators: The label for a checkbox that enables / disables focus highlighting + # in the NVDA Highlighter vision settings panel. + label=_("Highlight focus"), + style=wx.CHK_3STATE + ) self.lastControl = self._enabledCheckbox sizer.Add(self._enabledCheckbox) self._enableCheckSizer = sizer @@ -280,7 +283,7 @@ def onPanelActivated(self): self.lastControl = self._enabledCheckbox def _updateEnabledState(self): - settings = VisionEnhancementProvider._settings + settings = self._getSettingsStorage() settingsToTriggerActivation = [ settings.highlightBrowseMode, settings.highlightFocus, @@ -308,11 +311,11 @@ def _ensureEnableState(self, shouldBeEnabled: bool): self._terminateProvider() def _onCheckEvent(self, evt: wx.CommandEvent): - settings = VisionEnhancementProvider._settings + settingsStorage = self._getSettingsStorage() if evt.GetEventObject() is self._enabledCheckbox: - settings.highlightBrowseMode = evt.IsChecked() - settings.highlightFocus = evt.IsChecked() - settings.highlightNavigator = evt.IsChecked() + settingsStorage.highlightBrowseMode = evt.IsChecked() + settingsStorage.highlightFocus = evt.IsChecked() + settingsStorage.highlightNavigator = evt.IsChecked() self._ensureEnableState(evt.IsChecked()) self.updateDriverSettings() else: @@ -335,7 +338,7 @@ class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): enabledContexts: Tuple[Context] # type info for autoprop: L{_get_enableContexts} @classmethod - def getSettings(cls): + def getSettings(cls) -> NVDAHighlighterSettings: log.debug(f"getting settings: {cls._settings.__class__!r}") return cls._settings From 15a285c588b88c05ad01f3db7d0f91ca153ffc69 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 19:38:46 +0100 Subject: [PATCH 031/116] Make screen curtain work --- source/vision/providerBase.py | 10 - source/vision/visionHandler.py | 11 - .../screenCurtain.py | 254 +++++++++++++----- 3 files changed, 182 insertions(+), 93 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 0a91be5c815..a5ef339e527 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -110,13 +110,3 @@ def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints): def canStart(cls) -> bool: """Returns whether this provider is able to start.""" return False - - # todo: remove this, providers should do this themselves - @classmethod - def confirmInitWithUser(cls) -> bool: - """Before initialisation of the provider, - confirm with the user that the provider should start. - This method should be executed on the main thread. - @returns: C{True} if initialisation should continue, C{False} otherwise. - """ - return True diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index d7e4584119b..4423bdc0b8f 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -118,17 +118,6 @@ def terminateProvider(self, providerId: str, saveSettings: bool = True): if exception: raise exception - @staticmethod - def confirmInitWithUser(providerName: str) -> bool: - """Before initialisation of a provider, - confirm with the user that the provider should start. - This method calls confirmInitWithUser on the provider, - and should be executed on the main thread. - @returns: C{True} if initialisation should continue, C{False} otherwise. - """ - providerCls = getProviderClass(providerName) - return providerCls.confirmInitWithUser() - def initializeProvider(self, providerId: str, temporary: bool = False): """ Enables and activates the supplied provider. diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 36d1c6d7b7c..21ef9fcd0e3 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -14,8 +14,9 @@ import driverHandler import wx import gui -import config from logHandler import log +from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType +from typing import Optional, Type, Callable class MAGCOLOREFFECT(Structure): @@ -82,93 +83,73 @@ class Magnification: MagShowSystemCursor.errcheck = _errCheck -class VisionEnhancementProvider(vision.providerBase.VisionEnhancementProvider): - name = "screenCurtain" - # Translators: Description of a vision enhancement provider that disables output to the screen, - # making it black. - description = _("Screen Curtain") - supportedRoles = frozenset([vision.constants.Role.COLORENHANCER]) +# Translators: Description of a vision enhancement provider that disables output to the screen, +# making it black. +screenCurtainTranslatedName = _("Screen Curtain") +warnOnLoadCheckBoxText = ( # Translators: Description for a screen curtain setting that shows a warning when loading # the screen curtain. - warnOnLoadCheckBoxText = _(f"Always &show a warning when loading {description}") + _(f"Always &show a warning when loading {screenCurtainTranslatedName}") +) - preInitSettings = [ - driverHandler.BooleanDriverSetting( - "warnOnLoad", - warnOnLoadCheckBoxText, - defaultVal=True - ), - ] + +class ScreenCurtainSettings(VisionEnhancementProviderSettings): + + warnOnLoad: bool @classmethod - def canStart(cls): - # return winVersion.isFullScreenMagnificationAvailable() - return False + def getId(cls) -> str: + return "screenCurtain" - def __init__(self): - super(VisionEnhancementProvider, self).__init__() - log.debug(f"ScreenCurtain", stack_info=True) - Magnification.MagInitialize() - Magnification.MagShowSystemCursor(False) - Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) + @classmethod + def getTranslatedName(cls) -> str: + return screenCurtainTranslatedName - def terminate(self): - super().terminate() - Magnification.MagShowSystemCursor(True) - Magnification.MagUninitialize() + @classmethod + def _get_preInitSettings(cls) -> SupportedSettingType: + return [ + driverHandler.BooleanDriverSetting( + "warnOnLoad", + warnOnLoadCheckBoxText, + defaultVal=True + ), + ] - def registerEventExtensionPoints(self, extensionPoints): - # The screen curtain isn't interested in any events - pass + def _get_supportedSettings(self) -> SupportedSettingType: + return super().supportedSettings - warnOnLoadText = _( - # Translators: A warning shown when activating the screen curtain. - # {description} is replaced by the translation of "screen curtain" - f"You are about to enable {description}.\n" - f"When {description} is enabled, the screen of your computer will go completely black.\n" - f"Do you really want to enable {description}?" - ) - @classmethod - def confirmInitWithUser(cls) -> bool: - cls._initSpecificSettings(cls, cls.preInitSettings) - if cls.warnOnLoad: - parent = next( - ( - dlg for dlg, state in gui.settingsDialogs.NVDASettingsDialog._instances.items() - if isinstance(dlg, gui.settingsDialogs.NVDASettingsDialog) - and state == gui.settingsDialogs.SettingsDialog._DIALOG_CREATED_STATE - ), - gui.mainFrame - ) - with WarnOnLoadDialog( - parent=parent, - # Translators: Title for the screen curtain warning dialog. - title=_("Warning"), - message=cls.warnOnLoadText, - dialogType=WarnOnLoadDialog.DIALOG_TYPE_WARNING - ) as dlg: - res = dlg.ShowModal() - if res == wx.NO: - return False - else: - cls.warnOnLoad = dlg.showWarningOnLoadCheckBox.IsChecked() - cls._saveSpecificSettings(cls, cls.preInitSettings) - return True +warnOnLoadText = _( + # Translators: A warning shown when activating the screen curtain. + # {description} is replaced by the translation of "screen curtain" + f"You are about to enable {screenCurtainTranslatedName}.\n" + f"When {screenCurtainTranslatedName} is enabled, the screen of your computer will go completely black.\n" + f"Do you really want to enable {screenCurtainTranslatedName}?" +) class WarnOnLoadDialog(gui.nvdaControls.MessageDialog): + def __init__( + self, + screenCurtainSettingsStorage: ScreenCurtainSettings, + parent, + title=_("Warning"), + message=warnOnLoadText, + dialogType=gui.nvdaControls.MessageDialog.DIALOG_TYPE_WARNING + ): + self._settingsStorage = screenCurtainSettingsStorage + super().__init__(parent, title, message, dialogType) + def _addContents(self, contentsSizer): - self.showWarningOnLoadCheckBox = contentsSizer.addItem(wx.CheckBox( + self.showWarningOnLoadCheckBox: wx.CheckBox = wx.CheckBox( self, - label=VisionEnhancementProvider.warnOnLoadCheckBoxText - )) + label=warnOnLoadCheckBoxText + ) + contentsSizer.addItem(self.showWarningOnLoadCheckBox) self.showWarningOnLoadCheckBox.SetValue( - config.conf[VisionEnhancementProvider._configSection][VisionEnhancementProvider.name][ - "warnOnLoad" - ] + self._settingsStorage.warnOnLoad ) def _addButtons(self, buttonHelper): @@ -179,7 +160,7 @@ def _addButtons(self, buttonHelper): # agree to enabling the curtain. label=_("&Yes") ) - yesButton.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.YES)) + yesButton.Bind(wx.EVT_BUTTON, lambda evt: self._exitDialog(wx.YES)) noButton = buttonHelper.addButton( self, @@ -189,5 +170,134 @@ def _addButtons(self, buttonHelper): label=_("&No") ) noButton.SetDefault() - noButton.Bind(wx.EVT_BUTTON, lambda evt: self.EndModal(wx.NO)) + noButton.Bind(wx.EVT_BUTTON, lambda evt: self._exitDialog(wx.NO)) noButton.SetFocus() + + def _exitDialog(self, result: int): + """ + @param result: either wx.YES or wx.No + """ + if result == wx.YES: + settingsStorage = self._settingsStorage + settingsStorage.warnOnLoad = self.showWarningOnLoadCheckBox.IsChecked() + settingsStorage._saveSpecificSettings(settingsStorage, settingsStorage.preInitSettings) + self.EndModal(result) + + +class ScreenCurtainGuiPanel( + gui.DriverSettingsMixin, + gui.SettingsPanel, +): + + _enabledCheckbox: wx.CheckBox + _enableCheckSizer: wx.BoxSizer + + def __init__( + self, + parent, + getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], + initProvider: Callable[[], bool], + terminateProvider: Callable[[], None] + ): + self._getProvider = getProvider + self._initProvider = initProvider + self._terminateProvider = terminateProvider + super().__init__(parent) + + def getSettings(self) -> ScreenCurtainSettings: + return ScreenCurtainProvider.getSettings() + + def makeSettings(self, sizer: wx.BoxSizer): + self._enabledCheckbox = wx.CheckBox( + self, + # Translators: option to enable screen curtain in the vision settings panel + label=_("Make screen black (immediate effect)") + ) + self.lastControl = self._enabledCheckbox + sizer.Add(self._enabledCheckbox) + self._enableCheckSizer = sizer + self.mainSizer.Add(self._enableCheckSizer, flag=wx.ALL | wx.EXPAND) + optionsSizer = wx.StaticBoxSizer( + wx.StaticBox( + self, + # Translators: The label for a group box containing the NVDA welcome dialog options. + label=_("Options") + ), + wx.VERTICAL + ) + self.settingsSizer = optionsSizer + self.updateDriverSettings() + self.Bind(wx.EVT_CHECKBOX, self._onCheckEvent) + + def onPanelActivated(self): + self.lastControl = self._enabledCheckbox + + def _onCheckEvent(self, evt: wx.CommandEvent): + if evt.GetEventObject() is self._enabledCheckbox: + self._ensureEnableState(evt.IsChecked()) + + def _ensureEnableState(self, shouldBeEnabled: bool): + currentlyEnabled = bool(self._getProvider()) + if shouldBeEnabled and not currentlyEnabled: + log.debug("init provider") + confirmed = self.confirmInitWithUser() + if confirmed: + self._initProvider() + else: + self._enabledCheckbox.SetValue(False) + elif not shouldBeEnabled and currentlyEnabled: + log.debug("terminate provider") + self._terminateProvider() + + def confirmInitWithUser(self) -> bool: + settingsStorage = self._getSettingsStorage() + if not settingsStorage.warnOnLoad: + return True + parent = self + dlg = WarnOnLoadDialog( + screenCurtainSettingsStorage=settingsStorage, + parent=parent + ) + res = dlg.ShowModal() + # WarnOnLoadDialog can change settings, reload them + self.updateDriverSettings() + return res == wx.YES + + +class ScreenCurtainProvider(vision.providerBase.VisionEnhancementProvider): + _settings = ScreenCurtainSettings() + + @classmethod + def canStart(cls): + return winVersion.isFullScreenMagnificationAvailable() + + @classmethod + def getSettingsPanelClass(cls) -> Optional[Type]: + """Returns the instance to be used in order to construct a settings panel for the provider. + @return: Optional[SettingsPanel] + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. + """ + return ScreenCurtainGuiPanel + + @classmethod + def getSettings(cls) -> ScreenCurtainSettings: + return cls._settings + + def __init__(self): + super(VisionEnhancementProvider, self).__init__() + log.debug(f"ScreenCurtain", stack_info=True) + Magnification.MagInitialize() + Magnification.MagShowSystemCursor(False) + Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) + + def terminate(self): + super().terminate() + Magnification.MagShowSystemCursor(True) + Magnification.MagUninitialize() + + def registerEventExtensionPoints(self, extensionPoints): + # The screen curtain isn't interested in any events + pass + + +VisionEnhancementProvider = ScreenCurtainProvider From 94ed26e7b019c6a5ebad77451fe3a3dbd9e3646a Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 19:40:29 +0100 Subject: [PATCH 032/116] Use preInitSettings for NVDAHighlighter There are no external dependencies change the settings for the highlighter. --- source/visionEnhancementProviders/NVDAHighlighter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 4401076c692..224db5c758d 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -9,6 +9,7 @@ import vision from vision.constants import Role, Context +from vision.providerBase import SupportedSettingType from vision.util import getContextRect from windowUtils import CustomWindow import wx @@ -219,7 +220,8 @@ def getTranslatedName(cls) -> str: # Translators: Description for NVDA's built-in screen highlighter. return _("NVDA Highlighter") - def _get_supportedSettings(self): + @classmethod + def _get_preInitSettings(cls) -> SupportedSettingType: return [ driverHandler.BooleanDriverSetting( 'highlight%s' % (context[0].upper() + context[1:]), @@ -229,6 +231,9 @@ def _get_supportedSettings(self): for context in _supportedContexts ] + def _get_supportedSettings(self) -> SupportedSettingType: + return super().supportedSettings + class NVDAHighlighterGuiPanel( gui.DriverSettingsMixin, From 42ee9d0319d3f2474735514e276d8d1bb89523fe Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 19:41:22 +0100 Subject: [PATCH 033/116] Clarify abstract methods Provide better docs about what derived classes must implement. --- source/autoSettingsUtils/autoSettings.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 5da1fcbe6a7..77d410abad3 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -27,9 +27,10 @@ class AutoSettings(AutoPropertyObject): Derived classes must implement: - getId - getTranslatedName - Although technically optional, derived classes probably need to implement: + - _get_supportedSettings - you may just call super from this implementation, + it will return _get_preInitSettings + Derived classes should use the following to return settings if possible: - _get_preInitSettings - - _get_supportedSettings """ def __init__(self): @@ -123,7 +124,7 @@ def _get_preInitSettings(cls) -> SupportedSettingType: _abstract_supportedSettings = True def _get_supportedSettings(self) -> SupportedSettingType: - """The settings supported by the driver. + """The settings supported by the driver. Abstract. When overriding this property, subclasses are encouraged to extend the getter method to ensure that L{preInitSettings} is part of the list of supported settings. """ From 3338ef9a82c5911df2c3a34aad1f90174305401b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 30 Oct 2019 20:32:01 +0100 Subject: [PATCH 034/116] Prettify screen curtain and highlighter settings --- source/gui/settingsDialogs.py | 10 +++- .../NVDAHighlighter.py | 46 +++++++++++------- .../screenCurtain.py | 47 +++++++++++-------- 3 files changed, 64 insertions(+), 39 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 11dd09d70f9..dd8e317f039 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -277,14 +277,20 @@ def __init__(self, parent: wx.Window): wx.Panel.__init__(self, parent, wx.ID_ANY) DpiScalingHelperMixin.__init__(self, self.GetHandle()) + self._buildGui() + + if gui._isDebug(): + elapsedSeconds = time.time() - startTime + panelName = self.__class__.__qualname__ + log.debug(f"Loading {panelName} took {elapsedSeconds:.2f} seconds") + + def _buildGui(self): self.mainSizer=wx.BoxSizer(wx.VERTICAL) self.settingsSizer=wx.BoxSizer(wx.VERTICAL) self.makeSettings(self.settingsSizer) self.mainSizer.Add(self.settingsSizer, flag=wx.ALL | wx.EXPAND) self.mainSizer.Fit(self) self.SetSizer(self.mainSizer) - if gui._isDebug(): - log.debug("Loading %s took %.2f seconds"%(self.__class__.__name__, time.time() - startTime)) @abstractmethod def makeSettings(self, sizer): diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 224db5c758d..fc77a474a60 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -254,12 +254,9 @@ def __init__( self._terminateProvider = terminateProvider super().__init__(parent) - def getSettings(self) -> NVDAHighlighterSettings: - # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. - # We want them set on this class. - return VisionEnhancementProvider.getSettings() + def _buildGui(self): + self.mainSizer = wx.BoxSizer(wx.VERTICAL) - def makeSettings(self, sizer: wx.BoxSizer): self._enabledCheckbox = wx.CheckBox( self, # Translators: The label for a checkbox that enables / disables focus highlighting @@ -267,25 +264,38 @@ def makeSettings(self, sizer: wx.BoxSizer): label=_("Highlight focus"), style=wx.CHK_3STATE ) - self.lastControl = self._enabledCheckbox - sizer.Add(self._enabledCheckbox) - self._enableCheckSizer = sizer - self.mainSizer.Add(self._enableCheckSizer, flag=wx.ALL | wx.EXPAND) - optionsSizer = wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA welcome dialog options. - label=_("Options") - ), - wx.VERTICAL + + self.mainSizer.Add(self._enabledCheckbox) + self.mainSizer.AddSpacer(size=self.scaleSize(10)) + # this options separator is done with text rather than a group box because a groupbox is too verbose, + # but visually some separation is helpful, since the rest of the options are really sub-settings. + self.optionsText = wx.StaticText( + self, + # Translators: The label for a group box containing the NVDA highlighter options. + label=_("Options:") ) - self.settingsSizer = optionsSizer + self.mainSizer.Add(self.optionsText) + + self.lastControl = self.optionsText + self.settingsSizer = wx.BoxSizer(wx.VERTICAL) + self.makeSettings(self.settingsSizer) + self.mainSizer.Add(self.settingsSizer, border=self.scaleSize(15), flag=wx.LEFT | wx.EXPAND) + self.mainSizer.Fit(self) + self.SetSizer(self.mainSizer) + + def getSettings(self) -> NVDAHighlighterSettings: + # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. + # We want them set on this class. + return VisionEnhancementProvider.getSettings() + + def makeSettings(self, sizer: wx.BoxSizer): self.updateDriverSettings() + # bind to all check box events self.Bind(wx.EVT_CHECKBOX, self._onCheckEvent) self._updateEnabledState() def onPanelActivated(self): - self.lastControl = self._enabledCheckbox + self.lastControl = self.optionsText def _updateEnabledState(self): settings = self._getSettingsStorage() diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 21ef9fcd0e3..b80769dd41e 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -122,10 +122,11 @@ def _get_supportedSettings(self) -> SupportedSettingType: warnOnLoadText = _( # Translators: A warning shown when activating the screen curtain. - # {description} is replaced by the translation of "screen curtain" - f"You are about to enable {screenCurtainTranslatedName}.\n" - f"When {screenCurtainTranslatedName} is enabled, the screen of your computer will go completely black.\n" - f"Do you really want to enable {screenCurtainTranslatedName}?" + # the translation of "Screen Curtain" should match the "translated name" + "Enabling Screen Curtain will make the screen of your computer completely black. " + "Ensure you will be able to navigate without any use of your screen before continuing. " + "\n\n" + "Do you wish to continue?" ) @@ -204,28 +205,36 @@ def __init__( self._terminateProvider = terminateProvider super().__init__(parent) - def getSettings(self) -> ScreenCurtainSettings: - return ScreenCurtainProvider.getSettings() + def _buildGui(self): + self.mainSizer = wx.BoxSizer(wx.VERTICAL) - def makeSettings(self, sizer: wx.BoxSizer): self._enabledCheckbox = wx.CheckBox( self, # Translators: option to enable screen curtain in the vision settings panel label=_("Make screen black (immediate effect)") ) - self.lastControl = self._enabledCheckbox - sizer.Add(self._enabledCheckbox) - self._enableCheckSizer = sizer - self.mainSizer.Add(self._enableCheckSizer, flag=wx.ALL | wx.EXPAND) - optionsSizer = wx.StaticBoxSizer( - wx.StaticBox( - self, - # Translators: The label for a group box containing the NVDA welcome dialog options. - label=_("Options") - ), - wx.VERTICAL + + self.mainSizer.Add(self._enabledCheckbox) + self.mainSizer.AddSpacer(size=self.scaleSize(10)) + # this options separator is done with text rather than a group box because a groupbox is too verbose, + # but visually some separation is helpful, since the rest of the options are really sub-settings. + self.optionsText = wx.StaticText( + self, + # Translators: The label for a group box containing the NVDA highlighter options. + label=_("Options:") ) - self.settingsSizer = optionsSizer + self.mainSizer.Add(self.optionsText) + self.lastControl = self.optionsText + self.settingsSizer = wx.BoxSizer(wx.VERTICAL) + self.makeSettings(self.settingsSizer) + self.mainSizer.Add(self.settingsSizer, border=self.scaleSize(15), flag=wx.LEFT | wx.EXPAND) + self.mainSizer.Fit(self) + self.SetSizer(self.mainSizer) + + def getSettings(self) -> ScreenCurtainSettings: + return ScreenCurtainProvider.getSettings() + + def makeSettings(self, sizer: wx.BoxSizer): self.updateDriverSettings() self.Bind(wx.EVT_CHECKBOX, self._onCheckEvent) From 3a32607990aba7d733886a6082a12f7cd7e18b54 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 31 Oct 2019 17:52:47 +0100 Subject: [PATCH 035/116] Fix up toggle screen curtain command Use 1 press for temp, and 2 presses for permanent. With warning dialog: - enabling acts in "deferred mode" - dialog is refocused if already open and command is triggered again, the dialog is re-read in this case. - whether screen curtain is run in temp mode or not is updated in the background Without warning dialog: - subsequent script calls repeat previous outcome. Otherwise three taps presses results in no speech --- source/globalCommands.py | 145 +++++++++++++----- source/gui/nvdaControls.py | 4 + .../screenCurtain.py | 22 ++- 3 files changed, 134 insertions(+), 37 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index 7a8b62e83e1..26bca735850 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -7,6 +7,8 @@ import time import itertools +from typing import Optional + import tones import audioDucking import touchHandler @@ -2282,58 +2284,131 @@ def script_recognizeWithUwpOcr(self, gesture): # Translators: Describes a command. script_recognizeWithUwpOcr.__doc__ = _("Recognizes the content of the current navigator object with Windows 10 OCR") + _tempEnableScreenCurtain = True + _waitingOnScreenCurtainWarningDialog: Optional[wx.Dialog] = None + _toggleScreenCurtainMessage: Optional[str] = None @script( # Translators: Describes a command. description=_( "Toggles the state of the screen curtain, " - "either by making the screen black or SHOWING the contents of the screen. " - "If pressed to enable once, the screen curtain is enabled until you restart NVDA. " - "If pressed tree times, it is enabled until you disable it" + "enable to make the screen black or disable to show the contents of the screen. " + "Pressed once, screen curtain is enabled until you restart NVDA. " + "Pressed twice, screen curtain is enabled until you disable it" ), category=SCRCAT_VISION ) def script_toggleScreenCurtain(self, gesture): - message = None - try: - screenCurtainName = "screenCurtain" - if not vision.getProviderClass(screenCurtainName).canStart(): + scriptCount = scriptHandler.getLastScriptRepeatCount() + if scriptCount == 0: # first call should reset last message + self._toggleScreenCurtainMessage = None + + from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider + screenCurtainId = ScreenCurtainProvider.getSettings().getId() + alreadyRunning = screenCurtainId in vision.handler.providers + + GlobalCommands._tempEnableScreenCurtain = scriptCount == 0 + + if self._waitingOnScreenCurtainWarningDialog: + # Already in the process of enabling the screen curtain, exit early. + # Ensure that the dialog is in the foreground, and read it again. + self._waitingOnScreenCurtainWarningDialog.Raise() + speech.cancelSpeech() + speech.speakObject( + api.getForegroundObject(), + reason=controlTypes.REASON_FOCUS + ) + speech.speakObject( + api.getFocusObject(), + reason=controlTypes.REASON_FOCUS + ) + return + + if scriptCount >= 2 and self._toggleScreenCurtainMessage: + # Only the first two presses have actions, all subsequent presses should just repeat the last outcome. + # This is important when not showing warning dialog, otherwise the script completion message can be + # suppressed. + # Must happen after the code to raise / read the warning dialog, since if there is a warning dialog + # it takes preference, and there shouldn't be a valid completion message in this case anyway. + ui.message( + self._toggleScreenCurtainMessage, + speechPriority=speech.priorities.SPRI_NOW + ) + return + + # Disable if running + if ( + alreadyRunning + and scriptCount == 0 # a second press might be trying to achieve non temp enable + ): + # Translators: Reported when the screen curtain is disabled. + message = _("Screen curtain disabled") + try: + vision.handler.terminateProvider(screenCurtainId) + except Exception: + # If the screen curtain was enabled, we do not expect exceptions. + log.error("Screen curtain termination error", exc_info=True) + # Translators: Reported when the screen curtain could not be enabled. + message = _("Could not disable screen curtain") + finally: + self._toggleScreenCurtainMessage = message + ui.message(message, speechPriority=speech.priorities.SPRI_NOW) + return + elif ( # enable it + scriptCount in (0, 1) # 1 press (temp enable) or 2 presses (enable) + ): + # Check if screen curtain is available, exit early if not. + if not vision.getProviderClass(screenCurtainId).canStart(): # Translators: Reported when the screen curtain is not available. message = _("Screen curtain not available") + self._toggleScreenCurtainMessage = message + ui.message(message, speechPriority=speech.priorities.SPRI_NOW) return - scriptCount = scriptHandler.getLastScriptRepeatCount() - if scriptCount == 0 and screenCurtainName in vision.handler.providers: - try: - vision.handler.terminateProvider(screenCurtainName) - except Exception: - # If the screen curtain was enabled, we do not expect exceptions. - log.error("Screen curtain termination error", exc_info=True) - # Translators: Reported when the screen curtain could not be enabled. - message = _("Could not disable screen curtain") - return - # Translators: Reported when the screen curtain is disabled. - message = _("Screen curtain disabled") - elif scriptCount in (0, 2): - temporary = scriptCount == 0 + + def _enableScreenCurtain(doEnable: bool = True): + self._waitingOnScreenCurtainWarningDialog = None + if not doEnable: + return # exit early with no ui.message because the user has decided to abort. + + tempEnable = GlobalCommands._tempEnableScreenCurtain + # Translators: Reported when the screen curtain is enabled. + enableMessage = _("Screen curtain enabled") + if tempEnable: + # Translators: Reported when the screen curtain is temporarily enabled. + enableMessage = _("Temporary Screen curtain, enabled until next restart") + try: vision.handler.initializeProvider( - screenCurtainName, - temporary=temporary, + screenCurtainId, + temporary=tempEnable, ) except Exception: log.error("Screen curtain initialization error", exc_info=True) # Translators: Reported when the screen curtain could not be enabled. - message = _("Could not enable screen curtain") - return - else: - if temporary: - # Translators: Reported when the screen curtain is temporarily enabled. - message = _("Temporary Screen curtain, enabled until next restart") - else: - # Translators: Reported when the screen curtain is enabled. - message = _("Screen curtain enabled") - finally: - if message is not None: - ui.message(message, speechPriority=speech.priorities.SPRI_NOW) + enableMessage = _("Could not enable screen curtain") + finally: + self._toggleScreenCurtainMessage = enableMessage + ui.message(enableMessage, speechPriority=speech.priorities.SPRI_NOW) + + # Show warning if necessary and do enable. + settingsStorage = ScreenCurtainProvider.getSettings() + if settingsStorage.warnOnLoad: + from visionEnhancementProviders.screenCurtain import WarnOnLoadDialog + parent = gui.mainFrame + dlg = WarnOnLoadDialog( + screenCurtainSettingsStorage=settingsStorage, + parent=parent + ) + self._waitingOnScreenCurtainWarningDialog = dlg + gui.runScriptModalDialog( + dlg, + lambda res: wx.CallLater( + millis=100, + callableObj=_enableScreenCurtain, + doEnable=res == wx.YES + ) + ) + else: + _enableScreenCurtain() __gestures = { # Basic diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index 070342a3a81..a0a08dcf326 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -337,6 +337,7 @@ def __init__(self, parent, title, message, dialogType=DIALOG_TYPE_STANDARD): self._setIcon(dialogType) self._setSound(dialogType) self.Bind(wx.EVT_SHOW, self._onShowEvt, source=self) + self.Bind(wx.EVT_ACTIVATE, self._onDialogActivated, source=self) mainSizer = wx.BoxSizer(wx.VERTICAL) contentsSizer = guiHelper.BoxSizerHelper(parent=self, orientation=wx.VERTICAL) @@ -359,6 +360,9 @@ def __init__(self, parent, title, message, dialogType=DIALOG_TYPE_STANDARD): self.SetSizer(mainSizer) self.CentreOnScreen() + def _onDialogActivated(self, evt): + evt.Skip() + def _onShowEvt(self, evt): """ :type evt: wx.ShowEvent diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index b80769dd41e..2c0b57d4279 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -132,6 +132,9 @@ def _get_supportedSettings(self) -> SupportedSettingType: class WarnOnLoadDialog(gui.nvdaControls.MessageDialog): + showWarningOnLoadCheckBox: wx.CheckBox + noButton: wx.Button + def __init__( self, screenCurtainSettingsStorage: ScreenCurtainSettings, @@ -142,6 +145,7 @@ def __init__( ): self._settingsStorage = screenCurtainSettingsStorage super().__init__(parent, title, message, dialogType) + self.noButton.SetFocus() def _addContents(self, contentsSizer): self.showWarningOnLoadCheckBox: wx.CheckBox = wx.CheckBox( @@ -163,7 +167,7 @@ def _addButtons(self, buttonHelper): ) yesButton.Bind(wx.EVT_BUTTON, lambda evt: self._exitDialog(wx.YES)) - noButton = buttonHelper.addButton( + noButton: wx.Button = buttonHelper.addButton( self, id=wx.ID_NO, # Translators: A button in the screen curtain warning dialog which allows the user to @@ -172,7 +176,7 @@ def _addButtons(self, buttonHelper): ) noButton.SetDefault() noButton.Bind(wx.EVT_BUTTON, lambda evt: self._exitDialog(wx.NO)) - noButton.SetFocus() + self.noButton = noButton # so we can manually set the focus. def _exitDialog(self, result: int): """ @@ -184,6 +188,20 @@ def _exitDialog(self, result: int): settingsStorage._saveSpecificSettings(settingsStorage, settingsStorage.preInitSettings) self.EndModal(result) + def _onDialogActivated(self, evt): + # focus is normally set to the first child, however, we want people to easily be able to cancel this + # dialog + super()._onDialogActivated(evt) + self.noButton.SetFocus() + + def _onShowEvt(self, evt): + """When no other dialogs have been opened first, focus lands in the wrong place (on the checkbox), + so we correct it after the dialog is opened. + """ + if evt.IsShown(): + self.noButton.SetFocus() + super()._onShowEvt(evt) + class ScreenCurtainGuiPanel( gui.DriverSettingsMixin, From 19ab2654bf0f7be073f5eafcb4d609b1b76cc000 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 1 Nov 2019 18:28:08 +0100 Subject: [PATCH 036/116] Add Example auto gui provider This visionEnhancementProvider exercises the GUI auto construction for vision provider panel. --- source/gui/settingsDialogs.py | 58 +++++--- .../NVDAHighlighter.py | 2 +- source/visionEnhancementProviders/autoGui.py | 132 ++++++++++++++++++ .../screenCurtain.py | 2 +- 4 files changed, 173 insertions(+), 21 deletions(-) create mode 100644 source/visionEnhancementProviders/autoGui.py diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index dd8e317f039..27c583866e4 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1066,7 +1066,7 @@ def getSettings(self) -> AutoSettings: ... def _getSettingsStorage(self) -> Any: - """ Override to change storage object for settings.""" + """ Override to change storage object for setting values.""" return self.getSettings() @classmethod @@ -1111,7 +1111,11 @@ def _makeStringSettingControl( settingsStorage: Any ): """ - Same as L{_makeSliderSettingControl} but for string settings. Returns sizer with label and combobox. + Same as L{_makeSliderSettingControl} but for string settings displayed in a wx.Choice control + Options for the choice control come from the availableXstringvalues property + (Dict[id, StringParameterInfo]) on the instance returned by self.getSettings() + The id of the value is stored on settingsStorage. + Returns sizer with label and combobox. """ labelText = f"{setting.displayNameWithAccelerator}:" stringSettingAttribName = f"_{setting.id}s" @@ -1121,7 +1125,7 @@ def _makeStringSettingControl( # Settings are stored as an ordered dict. # Therefore wrap this inside a list call. list(getattr( - settingsStorage, + self.getSettings(), f"available{setting.id.capitalize()}s" ).values()) ) @@ -3029,12 +3033,17 @@ class VisionSettingsPanel(SettingsPanel): # Translators: This is the label for the vision panel title = _("Vision") + # Translators: This is a label appearing on the vision settings panel. + panelDescription = _("Configure visual aides.") + def makeSettings(self, settingsSizer: wx.BoxSizer): self.initialProviders = list(vision.handler.providers) self.providerPanelInstances = [] self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) + for providerId, provTransName, _providerRole, providerClass in vision.getProviderList(): providerSizer = self.settingsSizerHelper.addItem( wx.StaticBoxSizer(wx.StaticBox(self, label=provTransName), wx.VERTICAL), @@ -3237,6 +3246,8 @@ class VisionProviderSubPanel_Wrapper( SettingsPanel ): + _checkBox: wx.CheckBox + def __init__( self, parent: wx.Window, @@ -3264,33 +3275,42 @@ def __init__( self._getProvider = getProvider self._initProvider = initProvider self._terminateProvider = terminateProvider - self._runtimeSettings: Optional[VisionProviderSubPanel_Settings] = None - self._runtimeSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) + self._providerSettings: Optional[VisionProviderSubPanel_Settings] = None + self._providerSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) super().__init__(parent=parent) def makeSettings(self, settingsSizer): + # Translators: Enable checkbox on a vision enhancement provider on the vision settings category panel checkBox = wx.CheckBox(self, label=_("Enable")) settingsSizer.Add(checkBox) - settingsSizer.Add(self._runtimeSettingsSizer, flag=wx.EXPAND, proportion=1.0) + settingsSizer.AddSpacer(size=self.scaleSize(10)) + # Translators: Options label on a vision enhancement provider on the vision settings category panel + settingsSizer.Add(wx.StaticText(self, label=_("Options:"))) + settingsSizer.Add( + self._providerSettingsSizer, + border=self.scaleSize(15), + flag=wx.LEFT | wx.EXPAND, + proportion=1.0 + ) self._checkBox: wx.CheckBox = checkBox if self._getProvider(): checkBox.SetValue(True) - if self._createRuntimeSettings(): + if self._createProviderSettings(): checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) else: checkBox.Bind(wx.EVT_CHECKBOX, self._nonEnableableGUI) - def _createRuntimeSettings(self): + def _createProviderSettings(self): try: - self._runtimeSettings = VisionProviderSubPanel_Settings( + self._providerSettings = VisionProviderSubPanel_Settings( self, settingsCallable=self._providerType.getSettings ) - self._runtimeSettingsSizer.Add(self._runtimeSettings, flag=wx.EXPAND, proportion=1.0) + self._providerSettingsSizer.Add(self._providerSettings, flag=wx.EXPAND, proportion=1.0) # E722: bare except used since we can not know what exceptions a provider might throw. # We should be able to continue despite a buggy provider. except: # noqa: E722 - log.error("unable to create runtime settings", exc_info=True) + log.error("unable to create provider settings", exc_info=True) return False return True @@ -3305,22 +3325,22 @@ def _nonEnableableGUI(self, evt): def _enableToggle(self, evt): if not evt.IsChecked(): self._terminateProvider() - self._runtimeSettings.updateDriverSettings() - self._runtimeSettings.onPanelActivated() + self._providerSettings.updateDriverSettings() + self._providerSettings.onPanelActivated() else: self._initProvider() - self._runtimeSettings.updateDriverSettings() - self._runtimeSettings.onPanelActivated() + self._providerSettings.updateDriverSettings() + self._providerSettings.onPanelActivated() self._sendLayoutUpdatedEvent() def onDiscard(self): - if self._runtimeSettings: - self._runtimeSettings.onDiscard() + if self._providerSettings: + self._providerSettings.onDiscard() def onSave(self): log.debug(f"calling VisionProviderSubPanel_Wrapper") - if self._runtimeSettings: - self._runtimeSettings.onSave() + if self._providerSettings: + self._providerSettings.onSave() """ The name of the config profile currently being edited, if any. diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index fc77a474a60..cc484dc4d58 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -378,7 +378,7 @@ def registerEventExtensionPoints(self, extensionPoints): extensionPoints.post_browseModeMove.register(self.handleBrowseModeMove) def __init__(self): - super(VisionEnhancementProvider, self).__init__() + super().__init__() self.contextToRectMap = {} winGDI.gdiPlusInitialize() self.window = None diff --git a/source/visionEnhancementProviders/autoGui.py b/source/visionEnhancementProviders/autoGui.py new file mode 100644 index 00000000000..ccd9e4f8218 --- /dev/null +++ b/source/visionEnhancementProviders/autoGui.py @@ -0,0 +1,132 @@ +import vision +import driverHandler +import wx +from autoSettingsUtils.utils import StringParameterInfo +from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType +from typing import Optional, Type + + +class AutoGuiTestSettings(VisionEnhancementProviderSettings): + + #: dictionary of the setting id's available when provider is running. + _availableRuntimeSettings = [ + ] + + shouldDoX: bool + shouldDoY: bool + amountOfZ: int + nameOfSomething: str + + availableNameofsomethings = { + "n1": StringParameterInfo(id="n1", displayName="name one"), + "n2": StringParameterInfo(id="n2", displayName="name two"), + "n3": StringParameterInfo(id="n3", displayName="name three"), + "n4": StringParameterInfo(id="n4", displayName="name four"), + } + + runtimeOnlySetting: int + + @classmethod + def getId(cls) -> str: + return "autoGui" + + @classmethod + def getTranslatedName(cls) -> str: + return "Auto Gui" # Should actually be translated with _() method. + + @classmethod + def _get_preInitSettings(cls) -> SupportedSettingType: + return [ + driverHandler.BooleanDriverSetting( + "shouldDoX", # value stored in matching property name on class + "Should Do X", + defaultVal=True + ), + driverHandler.BooleanDriverSetting( + "shouldDoY", # value stored in matching property name on class + "Should Do Y", + defaultVal=False + ), + driverHandler.NumericDriverSetting( + "amountOfZ", # value stored in matching property name on class + "Amount of Z", + defaultVal=11 + ), + driverHandler.DriverSetting( + # options for this come from a property with name generated by + # f"available{settingID.capitalize()}s" + # Note: + # First letter of Id becomes capital, the rest lowercase. + # the 's' character on the end. + # result: 'availableNameofsomethings' + "nameOfSomething", # value stored in matching property name on class + "Name of something", + ) + ] + + @classmethod + def clearRuntimeSettings(cls): + cls._availableRuntimeSettings = [] + + @classmethod + def addRuntimeSettingAvailibility(cls, settingID: str): + cls._availableRuntimeSettings.append(settingID) + + def _hasFeature(self, settingID: str) -> bool: + return settingID in AutoGuiTestSettings._availableRuntimeSettings + + def _get_supportedSettings(self) -> SupportedSettingType: + settings = [] + settings.extend(self.preInitSettings) + if self._hasFeature("runtimeOnlySetting"): + settings.extend([ + driverHandler.NumericDriverSetting( + "runtimeOnlySetting", # value stored in matching property name on class + "Runtime Only amount", + defaultVal=50, + ), + ]) + return settings + + +class AutoGuiTestProvider(vision.providerBase.VisionEnhancementProvider): + _settings = AutoGuiTestSettings() + + @classmethod + def canStart(cls): + return True # Check any dependencies (Windows version, Hardware access, Installed applications) + + @classmethod + def getSettingsPanelClass(cls) -> Optional[Type]: + """Returns the instance to be used in order to construct a settings panel for the provider. + @return: Optional[SettingsPanel] + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. + """ + return None # No custom GUI + + @classmethod + def getSettings(cls) -> AutoGuiTestSettings: + return cls._settings + + def __init__(self): + super().__init__() + result = ( + f"AutoGuiTestProvider:\n" + f"x: {self._settings.shouldDoX}\n" + f"y: {self._settings.shouldDoY}\n" + f"z: {self._settings.amountOfZ}\n" + f"name: {self._settings.nameOfSomething}\n" + f"runtimeOnlySetting: {getattr(self, 'runtimeOnlySetting', None)}" + ) + wx.MessageBox(result, caption="started") + AutoGuiTestSettings.addRuntimeSettingAvailibility("runtimeOnlySetting") + + def terminate(self): + AutoGuiTestSettings.clearRuntimeSettings() + super().terminate() + + def registerEventExtensionPoints(self, extensionPoints): + pass + + +VisionEnhancementProvider = AutoGuiTestProvider diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 2c0b57d4279..e98316849e3 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -311,7 +311,7 @@ def getSettings(cls) -> ScreenCurtainSettings: return cls._settings def __init__(self): - super(VisionEnhancementProvider, self).__init__() + super().__init__() log.debug(f"ScreenCurtain", stack_info=True) Magnification.MagInitialize() Magnification.MagShowSystemCursor(False) From 4bb5cbf4cd325c06df930d27cd5695ff4f6c7170 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 1 Nov 2019 18:28:25 +0100 Subject: [PATCH 037/116] Add visionEnhancementProvider Docs --- source/visionEnhancementProviders/readme.md | 177 ++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 source/visionEnhancementProviders/readme.md diff --git a/source/visionEnhancementProviders/readme.md b/source/visionEnhancementProviders/readme.md new file mode 100644 index 00000000000..ef7ac4b67ee --- /dev/null +++ b/source/visionEnhancementProviders/readme.md @@ -0,0 +1,177 @@ +## Vision Enhancement Providers + +These modules use the "vision framework" (see source/vision/) to augment visual information presented to the user. +Two of the built-in examples are: +- NVDA Highlighter which will react to changes in focus and draw a rectangle outline around the focused object. +- Screen Curtain which when enabled makes the screen black for privacy reasons. + +A vision enhancement provider module should have a class called `VisionEnhancementProvider`. +To make identifying a provider that is causing errors easier, name your provider class something descriptive and set +`VisionEnhancementProvider = MyProviderClass` at the bottom of your module. +See the NVDAHighlighter module as an example. +EG: + +``` +class MyProviderClass: + ... + +VisionEnhancementProvider = MyProviderClass +print(VisionEnhancementProvider.__qualname__) # prints: MyProviderClass +``` + +### Provider settings + +Providers must provide an VisionEnhancementProviderSettings object (via VisionEnhancementProvider.getSettings). +This VisionEnhancementProviderSettings instance then provides the DriverSettings objects via the supportedSettings +property. +These are used to save / load settings for the provider. + +### Providing a GUI + +A GUI can be built automatically from the DriverSettings objects accessed via the VisionEnhancementProviderSettings. +Alternatively the provider can supply a custom settings panel implementation via the getSettingsPanelClass class method. +A custom settings panel must return a class type derived from gui.SettingsPanel which will take responsibility for +building the GUI. +For an exmaple see NVDAHighlighter or ScreenCurtain. + +#### Automatic GUI building + +The provider settings (described above) are also used to automatically construct a GUI for the provider when +getSettingsPanelClass returns None. + +Example: +``` Python +import vision +import driverHandler +import wx +from autoSettingsUtils.utils import StringParameterInfo +from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType +from typing import Optional, Type + + +class AutoGuiTestSettings(VisionEnhancementProviderSettings): + + #: dictionary of the setting id's available when provider is running. + _availableRuntimeSettings = [ + ] + + shouldDoX: bool + shouldDoY: bool + amountOfZ: int + nameOfSomething: str + + availableNameofsomethings = { + "n1": StringParameterInfo(id="n1", displayName="name one"), + "n2": StringParameterInfo(id="n2", displayName="name two"), + "n3": StringParameterInfo(id="n3", displayName="name three"), + "n4": StringParameterInfo(id="n4", displayName="name four"), + } + + runtimeOnlySetting: int + + @classmethod + def getId(cls) -> str: + return "autoGui" + + @classmethod + def getTranslatedName(cls) -> str: + return "Auto Gui" # Should actually be translated with _() method. + + @classmethod + def _get_preInitSettings(cls) -> SupportedSettingType: + return [ + driverHandler.BooleanDriverSetting( + "shouldDoX", # value stored in matching property name on class + "Should Do X", + defaultVal=True + ), + driverHandler.BooleanDriverSetting( + "shouldDoY", # value stored in matching property name on class + "Should Do Y", + defaultVal=False + ), + driverHandler.NumericDriverSetting( + "amountOfZ", # value stored in matching property name on class + "Amount of Z", + defaultVal=11 + ), + driverHandler.DriverSetting( + # options for this come from a property with name generated by + # f"available{settingID.capitalize()}s" + # Note: + # First letter of Id becomes capital, the rest lowercase. + # the 's' character on the end. + # result: 'availableNameofsomethings' + "nameOfSomething", # value stored in matching property name on class + "Name of something", + ) + ] + + @classmethod + def clearRuntimeSettings(cls): + cls._availableRuntimeSettings = [] + + @classmethod + def addRuntimeSettingAvailibility(cls, settingID: str): + cls._availableRuntimeSettings.append(settingID) + + def _hasFeature(self, settingID: str) -> bool: + return settingID in AutoGuiTestSettings._availableRuntimeSettings + + def _get_supportedSettings(self) -> SupportedSettingType: + settings = [] + settings.extend(self.preInitSettings) + if self._hasFeature("runtimeOnlySetting"): + settings.extend([ + driverHandler.NumericDriverSetting( + "runtimeOnlySetting", # value stored in matching property name on class + "Runtime Only amount", + defaultVal=50, + ), + ]) + return settings + + +class AutoGuiTestProvider(vision.providerBase.VisionEnhancementProvider): + _settings = AutoGuiTestSettings() + + @classmethod + def canStart(cls): + return True # Check any dependencies (Windows version, Hardware access, Installed applications) + + @classmethod + def getSettingsPanelClass(cls) -> Optional[Type]: + """Returns the instance to be used in order to construct a settings panel for the provider. + @return: Optional[SettingsPanel] + @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. + """ + return None # No custom GUI + + @classmethod + def getSettings(cls) -> AutoGuiTestSettings: + return cls._settings + + def __init__(self): + super().__init__() + result = ( + f"AutoGuiTestProvider:\n" + f"x: {self._settings.shouldDoX}\n" + f"y: {self._settings.shouldDoY}\n" + f"z: {self._settings.amountOfZ}\n" + f"name: {self._settings.nameOfSomething}\n" + f"runtimeOnlySetting: {getattr(self, 'runtimeOnlySetting', None)}" + ) + wx.MessageBox(result, caption="started") + AutoGuiTestSettings.addRuntimeSettingAvailibility("runtimeOnlySetting") + + def terminate(self): + AutoGuiTestSettings.clearRuntimeSettings() + super().terminate() + + def registerEventExtensionPoints(self, extensionPoints): + pass + + +VisionEnhancementProvider = AutoGuiTestProvider + +``` \ No newline at end of file From 76e93ceff140d490b79f4820dd6f015cbc6c8468 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:09:11 +0100 Subject: [PATCH 038/116] Fix highlighter checkboxes being overwritten --- source/vision/visionHandler.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 4423bdc0b8f..026f58855be 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -155,10 +155,6 @@ def initializeProvider(self, providerId: str, temporary: bool = False): log.error( f"Error terminating provider {providerId} after registering to extension points", exc_info=True) raise registerEventExtensionPointsException - providerSettings = providerCls.getSettings() - # todo: do we actually have to do initSettings here? - # It might actually cause a bug, reloading settings and overwriting current static settings. - providerSettings.initSettings() if not temporary and providerId not in config.conf['vision']['providers']: config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerId] self.providers[providerId] = providerInst From 8a85637686e542a7b742f62959895110a25a3721 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:13:03 +0100 Subject: [PATCH 039/116] Fix auto gui example Differentiate between runtime settings and pre-runtime settings. --- source/visionEnhancementProviders/autoGui.py | 49 ++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/source/visionEnhancementProviders/autoGui.py b/source/visionEnhancementProviders/autoGui.py index ccd9e4f8218..bbcb36b342c 100644 --- a/source/visionEnhancementProviders/autoGui.py +++ b/source/visionEnhancementProviders/autoGui.py @@ -3,7 +3,7 @@ import wx from autoSettingsUtils.utils import StringParameterInfo from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType -from typing import Optional, Type +from typing import Optional, Type, Any, List class AutoGuiTestSettings(VisionEnhancementProviderSettings): @@ -64,20 +64,19 @@ def _get_preInitSettings(cls) -> SupportedSettingType: ) ] - @classmethod - def clearRuntimeSettings(cls): - cls._availableRuntimeSettings = [] + def clearRuntimeSettings(self): + self._availableRuntimeSettings = [] - @classmethod - def addRuntimeSettingAvailibility(cls, settingID: str): - cls._availableRuntimeSettings.append(settingID) + def addRuntimeSettingsAvailibility(self, settingIDs: List[str]): + self._availableRuntimeSettings.extend(settingIDs) + # ensure any previously saved settings are loaded from config file: + self._initSpecificSettings(self, self._getAvailableRuntimeSettings()) def _hasFeature(self, settingID: str) -> bool: - return settingID in AutoGuiTestSettings._availableRuntimeSettings + return settingID in self._availableRuntimeSettings - def _get_supportedSettings(self) -> SupportedSettingType: + def _getAvailableRuntimeSettings(self) -> SupportedSettingType: settings = [] - settings.extend(self.preInitSettings) if self._hasFeature("runtimeOnlySetting"): settings.extend([ driverHandler.NumericDriverSetting( @@ -88,6 +87,12 @@ def _get_supportedSettings(self) -> SupportedSettingType: ]) return settings + def _get_supportedSettings(self) -> SupportedSettingType: + settings = [] + settings.extend(self.preInitSettings) + settings.extend(self._getAvailableRuntimeSettings()) + return settings + class AutoGuiTestProvider(vision.providerBase.VisionEnhancementProvider): _settings = AutoGuiTestSettings() @@ -110,19 +115,37 @@ def getSettings(cls) -> AutoGuiTestSettings: def __init__(self): super().__init__() + self._initRuntimeOnlySettings() + self._showCurrentConfig() + + def _initRuntimeOnlySettings(self): + """ This method might query another application for its capabilities and initialise these configuration + options. + """ + settings = self.getSettings() + settings.addRuntimeSettingsAvailibility(["runtimeOnlySetting"]) + if not hasattr(settings, "runtimeOnlySetting"): + # Set the default + settings.runtimeOnlySetting = self._getValueFromDeviceOrOtherApplication("runtimeOnlySetting") + + def _getValueFromDeviceOrOtherApplication(self, settingId: str) -> Any: + """ This method might connect to another application / device and fetch default values.""" + return 75 + + def _showCurrentConfig(self): + """Simple mechanism to test updating values.""" result = ( f"AutoGuiTestProvider:\n" f"x: {self._settings.shouldDoX}\n" f"y: {self._settings.shouldDoY}\n" f"z: {self._settings.amountOfZ}\n" f"name: {self._settings.nameOfSomething}\n" - f"runtimeOnlySetting: {getattr(self, 'runtimeOnlySetting', None)}" + f"runtimeOnlySetting: {self._settings.runtimeOnlySetting}" ) wx.MessageBox(result, caption="started") - AutoGuiTestSettings.addRuntimeSettingAvailibility("runtimeOnlySetting") def terminate(self): - AutoGuiTestSettings.clearRuntimeSettings() + self._settings.clearRuntimeSettings() super().terminate() def registerEventExtensionPoints(self, extensionPoints): From 66a0ca6fc06f64e412357a44fc4fd34ba58d5914 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:16:05 +0100 Subject: [PATCH 040/116] Rename autoGui.py To make it clear this is just an example implementation. --- ...{autoGui.py => exampleProvider_autoGui.py} | 0 source/visionEnhancementProviders/readme.md | 139 +----------------- 2 files changed, 2 insertions(+), 137 deletions(-) rename source/visionEnhancementProviders/{autoGui.py => exampleProvider_autoGui.py} (100%) diff --git a/source/visionEnhancementProviders/autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py similarity index 100% rename from source/visionEnhancementProviders/autoGui.py rename to source/visionEnhancementProviders/exampleProvider_autoGui.py diff --git a/source/visionEnhancementProviders/readme.md b/source/visionEnhancementProviders/readme.md index ef7ac4b67ee..2e5a12f05dd 100644 --- a/source/visionEnhancementProviders/readme.md +++ b/source/visionEnhancementProviders/readme.md @@ -32,146 +32,11 @@ A GUI can be built automatically from the DriverSettings objects accessed via th Alternatively the provider can supply a custom settings panel implementation via the getSettingsPanelClass class method. A custom settings panel must return a class type derived from gui.SettingsPanel which will take responsibility for building the GUI. -For an exmaple see NVDAHighlighter or ScreenCurtain. +For an example see NVDAHighlighter or ScreenCurtain. #### Automatic GUI building The provider settings (described above) are also used to automatically construct a GUI for the provider when getSettingsPanelClass returns None. -Example: -``` Python -import vision -import driverHandler -import wx -from autoSettingsUtils.utils import StringParameterInfo -from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType -from typing import Optional, Type - - -class AutoGuiTestSettings(VisionEnhancementProviderSettings): - - #: dictionary of the setting id's available when provider is running. - _availableRuntimeSettings = [ - ] - - shouldDoX: bool - shouldDoY: bool - amountOfZ: int - nameOfSomething: str - - availableNameofsomethings = { - "n1": StringParameterInfo(id="n1", displayName="name one"), - "n2": StringParameterInfo(id="n2", displayName="name two"), - "n3": StringParameterInfo(id="n3", displayName="name three"), - "n4": StringParameterInfo(id="n4", displayName="name four"), - } - - runtimeOnlySetting: int - - @classmethod - def getId(cls) -> str: - return "autoGui" - - @classmethod - def getTranslatedName(cls) -> str: - return "Auto Gui" # Should actually be translated with _() method. - - @classmethod - def _get_preInitSettings(cls) -> SupportedSettingType: - return [ - driverHandler.BooleanDriverSetting( - "shouldDoX", # value stored in matching property name on class - "Should Do X", - defaultVal=True - ), - driverHandler.BooleanDriverSetting( - "shouldDoY", # value stored in matching property name on class - "Should Do Y", - defaultVal=False - ), - driverHandler.NumericDriverSetting( - "amountOfZ", # value stored in matching property name on class - "Amount of Z", - defaultVal=11 - ), - driverHandler.DriverSetting( - # options for this come from a property with name generated by - # f"available{settingID.capitalize()}s" - # Note: - # First letter of Id becomes capital, the rest lowercase. - # the 's' character on the end. - # result: 'availableNameofsomethings' - "nameOfSomething", # value stored in matching property name on class - "Name of something", - ) - ] - - @classmethod - def clearRuntimeSettings(cls): - cls._availableRuntimeSettings = [] - - @classmethod - def addRuntimeSettingAvailibility(cls, settingID: str): - cls._availableRuntimeSettings.append(settingID) - - def _hasFeature(self, settingID: str) -> bool: - return settingID in AutoGuiTestSettings._availableRuntimeSettings - - def _get_supportedSettings(self) -> SupportedSettingType: - settings = [] - settings.extend(self.preInitSettings) - if self._hasFeature("runtimeOnlySetting"): - settings.extend([ - driverHandler.NumericDriverSetting( - "runtimeOnlySetting", # value stored in matching property name on class - "Runtime Only amount", - defaultVal=50, - ), - ]) - return settings - - -class AutoGuiTestProvider(vision.providerBase.VisionEnhancementProvider): - _settings = AutoGuiTestSettings() - - @classmethod - def canStart(cls): - return True # Check any dependencies (Windows version, Hardware access, Installed applications) - - @classmethod - def getSettingsPanelClass(cls) -> Optional[Type]: - """Returns the instance to be used in order to construct a settings panel for the provider. - @return: Optional[SettingsPanel] - @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. - """ - return None # No custom GUI - - @classmethod - def getSettings(cls) -> AutoGuiTestSettings: - return cls._settings - - def __init__(self): - super().__init__() - result = ( - f"AutoGuiTestProvider:\n" - f"x: {self._settings.shouldDoX}\n" - f"y: {self._settings.shouldDoY}\n" - f"z: {self._settings.amountOfZ}\n" - f"name: {self._settings.nameOfSomething}\n" - f"runtimeOnlySetting: {getattr(self, 'runtimeOnlySetting', None)}" - ) - wx.MessageBox(result, caption="started") - AutoGuiTestSettings.addRuntimeSettingAvailibility("runtimeOnlySetting") - - def terminate(self): - AutoGuiTestSettings.clearRuntimeSettings() - super().terminate() - - def registerEventExtensionPoints(self, extensionPoints): - pass - - -VisionEnhancementProvider = AutoGuiTestProvider - -``` \ No newline at end of file +See exampleProvider_autoGui.py \ No newline at end of file From 6fe7ba6c81019f5f3230dccb7d1b716ca909302d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:30:18 +0100 Subject: [PATCH 041/116] Match name with filename --- source/visionEnhancementProviders/exampleProvider_autoGui.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index bbcb36b342c..e2eed8ce995 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -28,11 +28,11 @@ class AutoGuiTestSettings(VisionEnhancementProviderSettings): @classmethod def getId(cls) -> str: - return "autoGui" + return "exampleProvider_autoGui" @classmethod def getTranslatedName(cls) -> str: - return "Auto Gui" # Should actually be translated with _() method. + return "Example Provider with Auto Gui" # Should normally be translated with _() method. @classmethod def _get_preInitSettings(cls) -> SupportedSettingType: From 357b6fb620bc9b6aca3779dfe9d98e95cdfc385b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:39:20 +0100 Subject: [PATCH 042/116] Demo different sources for GUI values Values may come from: - an external device/application. - the config defaults. - the last saved config. --- .../exampleProvider_autoGui.py | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index e2eed8ce995..46d5a55052f 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -12,6 +12,7 @@ class AutoGuiTestSettings(VisionEnhancementProviderSettings): _availableRuntimeSettings = [ ] + # The following settings can be configured prior to runtime in this example shouldDoX: bool shouldDoY: bool amountOfZ: int @@ -24,7 +25,9 @@ class AutoGuiTestSettings(VisionEnhancementProviderSettings): "n4": StringParameterInfo(id="n4", displayName="name four"), } - runtimeOnlySetting: int + # The following settings are runtime only in this example + runtimeOnlySetting_externalValueLoad: int + runtimeOnlySetting_localDefault: int @classmethod def getId(cls) -> str: @@ -64,7 +67,7 @@ def _get_preInitSettings(cls) -> SupportedSettingType: ) ] - def clearRuntimeSettings(self): + def clearRuntimeSettingAvailability(self): self._availableRuntimeSettings = [] def addRuntimeSettingsAvailibility(self, settingIDs: List[str]): @@ -77,11 +80,19 @@ def _hasFeature(self, settingID: str) -> bool: def _getAvailableRuntimeSettings(self) -> SupportedSettingType: settings = [] - if self._hasFeature("runtimeOnlySetting"): + if self._hasFeature("runtimeOnlySetting_externalValueLoad"): settings.extend([ driverHandler.NumericDriverSetting( - "runtimeOnlySetting", # value stored in matching property name on class - "Runtime Only amount", + "runtimeOnlySetting_externalValueLoad", # value stored in matching property name on class + "Runtime Only amount, external value load", + # no GUI default + ), + ]) + if self._hasFeature("runtimeOnlySetting_localDefault"): + settings.extend([ + driverHandler.NumericDriverSetting( + "runtimeOnlySetting_localDefault", # value stored in matching property name on class + "Runtime Only amount, local default", defaultVal=50, ), ]) @@ -123,14 +134,21 @@ def _initRuntimeOnlySettings(self): options. """ settings = self.getSettings() - settings.addRuntimeSettingsAvailibility(["runtimeOnlySetting"]) - if not hasattr(settings, "runtimeOnlySetting"): - # Set the default - settings.runtimeOnlySetting = self._getValueFromDeviceOrOtherApplication("runtimeOnlySetting") + settings.addRuntimeSettingsAvailibility([ + "runtimeOnlySetting_localDefault", + "runtimeOnlySetting_externalValueLoad" + ]) + + # load and set values from the external source, this will override values loaded from config. + settings.runtimeOnlySetting_externalValueLoad = self._getValueFromDeviceOrOtherApplication( + "runtimeOnlySetting_externalValueLoad" + ) def _getValueFromDeviceOrOtherApplication(self, settingId: str) -> Any: """ This method might connect to another application / device and fetch default values.""" - return 75 + if settingId == "runtimeOnlySetting_externalValueLoad": + return 75 + return None def _showCurrentConfig(self): """Simple mechanism to test updating values.""" @@ -140,12 +158,13 @@ def _showCurrentConfig(self): f"y: {self._settings.shouldDoY}\n" f"z: {self._settings.amountOfZ}\n" f"name: {self._settings.nameOfSomething}\n" - f"runtimeOnlySetting: {self._settings.runtimeOnlySetting}" + f"runtimeOnlySetting_externalValueLoad: {self._settings.runtimeOnlySetting_externalValueLoad}\n" + f"runtimeOnlySetting_localDefault: {self._settings.runtimeOnlySetting_localDefault}\n" ) wx.MessageBox(result, caption="started") def terminate(self): - self._settings.clearRuntimeSettings() + self._settings.clearRuntimeSettingAvailability() super().terminate() def registerEventExtensionPoints(self, extensionPoints): From 574887bca1709cd384ce362a15a32411c2d2f461 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 16:50:20 +0100 Subject: [PATCH 043/116] Add copyright header and module doc. --- .../exampleProvider_autoGui.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index 46d5a55052f..f0c40f26d62 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -1,3 +1,8 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + import vision import driverHandler import wx @@ -5,6 +10,15 @@ from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType from typing import Optional, Type, Any, List +"""Example provider, which demonstrates using the automatically constructed GUI. + +For examples of overriding the GUI and using a custom implementation, see NVDAHighlighter or ScreenCurtain. + +This example imagines that some settings are "always available", while the availability of others is unknown +until "runtime". +This might be because the provider must interface with an external application or device. +""" + class AutoGuiTestSettings(VisionEnhancementProviderSettings): From b2662db6d65ae2082f1cdf0af18741763b4e98a2 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Sun, 3 Nov 2019 17:08:20 +0100 Subject: [PATCH 044/116] Use dataclass for "provider info" Rather than having to remember the order of these values, use names. Currently the id of a provider must match it's module name. Bundling these together (rather than just relying on them matching) allows for more flexibility. --- source/gui/settingsDialogs.py | 13 +++++++------ source/vision/__init__.py | 29 ++++++++++++++++------------- source/vision/providerInfo.py | 20 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 source/vision/providerInfo.py diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 27c583866e4..8b3cba88ae6 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3044,11 +3044,12 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) - for providerId, provTransName, _providerRole, providerClass in vision.getProviderList(): + for providerInfo in vision.getProviderList(): providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=provTransName), wx.VERTICAL), + wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), flag=wx.EXPAND ) + providerId = providerInfo.providerId kwargs = { # default value for name parameter to lambda, recommended by python3 FAQ: # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result @@ -3057,13 +3058,13 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): "terminateProvider": lambda id=providerId: self.safeTerminateProviders([id], verbose=True) } - settingsPanelCls = providerClass.getSettingsPanelClass() + settingsPanelCls = providerInfo.providerClass.getSettingsPanelClass() if not settingsPanelCls: - log.debug(f"Using default panel for providerId: {providerId}") + log.debug(f"Using default panel for providerId: {providerInfo.providerId}") settingsPanelCls = VisionProviderSubPanel_Wrapper - kwargs["providerType"] = providerClass + kwargs["providerType"] = providerInfo.providerClass else: - log.debug(f"Using custom panel for providerId: {providerId}") + log.debug(f"Using custom panel for providerId: {providerInfo.providerId}") try: settingsPanel = settingsPanelCls( self, diff --git a/source/vision/__init__.py b/source/vision/__init__.py index eafd342cf2c..681d783c590 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -11,6 +11,7 @@ using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ from vision.providerBase import VisionEnhancementProvider +from vision.providerInfo import ProviderInfo from .constants import Role from .visionHandler import VisionHandler, getProviderClass import pkgutil @@ -41,46 +42,48 @@ def terminate(): def getProviderList( onlyStartable: bool = True -) -> List[Tuple[str, str, List[Role], Type[VisionEnhancementProvider]]]: +) -> List[ProviderInfo]: """Gets a list of available vision enhancement names with their descriptions as well as supported roles. @param onlyStartable: excludes all providers for which the check method returns C{False}. @return: list of tuples with provider names, provider descriptions, and supported roles. See L{constants.Role} for the available roles. """ providerList = [] - for loader, name, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): - if name.startswith('_'): + for loader, moduleName, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): + if moduleName.startswith('_'): continue try: - provider = getProviderClass(name) + provider = getProviderClass(moduleName) except Exception: # Purposely catch everything. # A provider can raise whatever exception it likes, # therefore it is unknown what to expect. log.error( - "Error while importing vision enhancement provider %s" % name, + f"Error while importing vision enhancement provider module {moduleName}", exc_info=True ) continue try: if not onlyStartable or provider.canStart(): providerSettings = provider.getSettings() - providerList.append(( - providerSettings.getId(), - providerSettings.getTranslatedName(), - list(provider.supportedRoles), - provider - )) + providerList.append( + ProviderInfo( + providerId=providerSettings.getId(), + moduleName=moduleName, + translatedName=providerSettings.getTranslatedName(), + providerClass=provider + ) + ) else: log.debugWarning( - f"Excluding Vision enhancement provider {name} which is unable to start" + f"Excluding Vision enhancement provider module {moduleName} which is unable to start" ) except Exception: # Purposely catch everything else as we don't want one failing provider # make it impossible to list all the others. log.error("", exc_info=True) # Sort the providers alphabetically by name. - providerList.sort(key=lambda d: d[1].lower()) + providerList.sort(key=lambda info: info.translatedName.lower()) return providerList diff --git a/source/vision/providerInfo.py b/source/vision/providerInfo.py new file mode 100644 index 00000000000..dee816bcca4 --- /dev/null +++ b/source/vision/providerInfo.py @@ -0,0 +1,20 @@ +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited +from dataclasses import dataclass +from typing import Type +from .providerBase import VisionEnhancementProvider + +ProviderIdT = str +ModuleNameT = str +TranslatedNameT = str +#RolesT = List[Role] + + +@dataclass +class ProviderInfo: + providerId: ProviderIdT + moduleName: ModuleNameT + translatedName: TranslatedNameT + providerClass: Type[VisionEnhancementProvider] From 365f7ecd122ca9145f11e3ec3a178e2f4b032959 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:16:37 +0100 Subject: [PATCH 045/116] Use providerInfo rather than providerId: str to control providers The providerInfo allows for differentiating providerId and moduleName. This will be helpful when a provider wants a single implementation to work with several versions of an app which have incompatible configurations. In this case, they will need different config sections, hence different providerId's. --- source/globalCommands.py | 5 +- source/gui/settingsDialogs.py | 7 ++- source/vision/__init__.py | 47 ++------------ source/vision/providerInfo.py | 4 +- source/vision/visionHandler.py | 111 ++++++++++++++++++++++++++------- 5 files changed, 101 insertions(+), 73 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index 26bca735850..3c1590f3c4f 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2304,6 +2304,7 @@ def script_toggleScreenCurtain(self, gesture): from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider screenCurtainId = ScreenCurtainProvider.getSettings().getId() + screenCurtainProviderInfo = vision.visionHandler.getProviderInfo(screenCurtainId) alreadyRunning = screenCurtainId in vision.handler.providers GlobalCommands._tempEnableScreenCurtain = scriptCount == 0 @@ -2343,7 +2344,7 @@ def script_toggleScreenCurtain(self, gesture): # Translators: Reported when the screen curtain is disabled. message = _("Screen curtain disabled") try: - vision.handler.terminateProvider(screenCurtainId) + vision.handler.terminateProvider(screenCurtainProviderInfo) except Exception: # If the screen curtain was enabled, we do not expect exceptions. log.error("Screen curtain termination error", exc_info=True) @@ -2378,7 +2379,7 @@ def _enableScreenCurtain(doEnable: bool = True): try: vision.handler.initializeProvider( - screenCurtainId, + screenCurtainProviderInfo, temporary=tempEnable, ) except Exception: diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 8b3cba88ae6..8f9b6ae146f 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3030,6 +3030,7 @@ def onNoMessageTimeoutChange(self, evt): class VisionSettingsPanel(SettingsPanel): + initialProviders: List[str] # Translators: This is the label for the vision panel title = _("Vision") @@ -3096,8 +3097,9 @@ def safeInitProviders( success = True initErrors = [] for providerId in providerIds: + providerInfo = vision.visionHandler.getProviderInfo(providerId) try: - vision.handler.initializeProvider(providerId) + vision.handler.initializeProvider(providerInfo) except Exception: initErrors.append(providerId) log.error( @@ -3138,11 +3140,12 @@ def safeTerminateProviders( terminateErrors = [] for providerId in providerIds: try: + providerInfo = vision.visionHandler.getProviderInfo(providerId) # Terminating a provider from the gui should never save the settings. # This is because termination happens on the fly when unchecking check boxes. # Saving settings would be harmful if a user opens the vision panel, # then changes some settings and disables the provider. - vision.handler.terminateProvider(providerId, saveSettings=False) + vision.handler.terminateProvider(providerInfo, saveSettings=False) except Exception: terminateErrors.append(providerId) log.error( diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 681d783c590..ec6689e3500 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -14,11 +14,9 @@ from vision.providerInfo import ProviderInfo from .constants import Role from .visionHandler import VisionHandler, getProviderClass -import pkgutil import visionEnhancementProviders import config -from logHandler import log -from typing import List, Tuple, Type, Optional +from typing import List, Optional handler: Optional[VisionHandler] = None @@ -43,48 +41,11 @@ def terminate(): def getProviderList( onlyStartable: bool = True ) -> List[ProviderInfo]: - """Gets a list of available vision enhancement names with their descriptions as well as supported roles. + """Gets a list of available vision enhancement providers @param onlyStartable: excludes all providers for which the check method returns C{False}. - @return: list of tuples with provider names, provider descriptions, and supported roles. - See L{constants.Role} for the available roles. + @return: Details of providers available """ - providerList = [] - for loader, moduleName, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): - if moduleName.startswith('_'): - continue - try: - provider = getProviderClass(moduleName) - except Exception: - # Purposely catch everything. - # A provider can raise whatever exception it likes, - # therefore it is unknown what to expect. - log.error( - f"Error while importing vision enhancement provider module {moduleName}", - exc_info=True - ) - continue - try: - if not onlyStartable or provider.canStart(): - providerSettings = provider.getSettings() - providerList.append( - ProviderInfo( - providerId=providerSettings.getId(), - moduleName=moduleName, - translatedName=providerSettings.getTranslatedName(), - providerClass=provider - ) - ) - else: - log.debugWarning( - f"Excluding Vision enhancement provider module {moduleName} which is unable to start" - ) - except Exception: - # Purposely catch everything else as we don't want one failing provider - # make it impossible to list all the others. - log.error("", exc_info=True) - # Sort the providers alphabetically by name. - providerList.sort(key=lambda info: info.translatedName.lower()) - return providerList + return visionHandler.getProviderList(onlyStartable) def _isDebug() -> bool: diff --git a/source/vision/providerInfo.py b/source/vision/providerInfo.py index dee816bcca4..e5bc67aadc8 100644 --- a/source/vision/providerInfo.py +++ b/source/vision/providerInfo.py @@ -4,7 +4,7 @@ # Copyright (C) 2019 NV Access Limited from dataclasses import dataclass from typing import Type -from .providerBase import VisionEnhancementProvider +from vision import providerBase ProviderIdT = str ModuleNameT = str @@ -17,4 +17,4 @@ class ProviderInfo: providerId: ProviderIdT moduleName: ModuleNameT translatedName: TranslatedNameT - providerClass: Type[VisionEnhancementProvider] + providerClass: Type[providerBase.VisionEnhancementProvider] diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 026f58855be..a21c8e938a8 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -9,7 +9,7 @@ The vision handler is the core of the vision framework. See the documentation of L{VisionHandler} for more details about what it does. """ - +from . import providerInfo from .constants import Context from .providerBase import VisionEnhancementProvider from .visionHandlerExtensionPoints import EventExtensionPoints @@ -21,7 +21,7 @@ from logHandler import log import visionEnhancementProviders import queueHandler -from typing import Type, Dict, List +from typing import Type, Dict, List, Optional from . import exceptions @@ -49,6 +49,62 @@ def getProviderClass( raise initialException +def getProviderList( + onlyStartable: bool = True + ) -> List[providerInfo.ProviderInfo]: + """Gets a list of available vision enhancement names with their descriptions as well as supported roles. + @param onlyStartable: excludes all providers for which the check method returns C{False}. + @return: list of tuples with provider names, provider descriptions, and supported roles. + See L{constants.Role} for the available roles. + """ + providerList = [] + for loader, moduleName, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): + if moduleName.startswith('_'): + continue + try: + provider = getProviderClass(moduleName) + except Exception: + # Purposely catch everything. + # A provider can raise whatever exception it likes, + # therefore it is unknown what to expect. + log.error( + f"Error while importing vision enhancement provider module {moduleName}", + exc_info=True + ) + continue + try: + if not onlyStartable or provider.canStart(): + providerSettings = provider.getSettings() + providerList.append( + providerInfo.ProviderInfo( + providerId=providerSettings.getId(), + moduleName=moduleName, + translatedName=providerSettings.getTranslatedName(), + providerClass=provider + ) + ) + else: + log.debugWarning( + f"Excluding Vision enhancement provider module {moduleName} which is unable to start" + ) + except Exception: + # Purposely catch everything else as we don't want one failing provider + # make it impossible to list all the others. + log.error("", exc_info=True) + # Sort the providers alphabetically by name. + providerList.sort(key=lambda info: info.translatedName.lower()) + return providerList + + +def getProviderInfo(providerId: providerInfo.ProviderIdT) -> Optional[providerInfo.ProviderInfo]: + # This mechanism of getting the provider list and looking it up is particularly inefficient, but, before + # refactoring, confirm that getProviderList is / isn't cached. + for p in getProviderList(onlyStartable=False): + if p.providerId == providerId: + return p + raise LookupError(f"Provider with id ({providerId}) does not exist.") + + class VisionHandler(AutoPropertyObject): """The singleton vision handler is the core of the vision framework. It performs the following tasks: @@ -60,7 +116,7 @@ class VisionHandler(AutoPropertyObject): """ def __init__(self): - self.providers: Dict[str, VisionEnhancementProvider] = dict() + self.providers: Dict[providerInfo.ProviderIdT, VisionEnhancementProvider] = dict() self.extensionPoints: EventExtensionPoints = EventExtensionPoints() queueHandler.queueFunction(queueHandler.eventQueue, self.postGuiInit) @@ -72,14 +128,19 @@ def postGuiInit(self) -> None: self.handleConfigProfileSwitch() config.post_configProfileSwitch.register(self.handleConfigProfileSwitch) - def terminateProvider(self, providerId: str, saveSettings: bool = True): + def terminateProvider( + self, + provider: providerInfo.ProviderInfo, + saveSettings: bool = True + ): """Terminates a currently active provider. When termnation fails, an exception is raised. Yet, the provider wil lbe removed from the providers dictionary, so its instance goes out of scope and wil lbe garbage collected. - @param providerId: The provider to terminate. - @param saveSettings: Whether settings should be saved on termionation. + @param provider: The provider to terminate. + @param saveSettings: Whether settings should be saved on termination. """ + providerId = provider.providerId # Remove the provider from the providers dictionary. providerInstance = self.providers.pop(providerId, None) if not providerInstance: @@ -118,28 +179,28 @@ def terminateProvider(self, providerId: str, saveSettings: bool = True): if exception: raise exception - def initializeProvider(self, providerId: str, temporary: bool = False): + def initializeProvider( + self, + provider: providerInfo.ProviderInfo, + temporary: bool = False + ): """ Enables and activates the supplied provider. - @param providerId: The id of the registered provider. + @param provider: The provider to initialise. @param temporary: Whether the selected provider is enabled temporarily (e.g. as a fallback). This defaults to C{False}. If C{True}, no changes will be performed to the configuration. """ + providerId = provider.providerId providerInst = self.providers.pop(providerId, None) if providerInst is not None: - providerCls = type(providerInst) providerInst.reinitialize() else: - try: - providerCls = getProviderClass(providerId) - except ModuleNotFoundError: - raise exceptions.ProviderInitException(f"No provider: {providerId!r}") - else: - if not providerCls.canStart(): - raise exceptions.ProviderInitException( - f"Trying to initialize provider {providerId!r} which reported being unable to start" - ) + providerCls = provider.providerClass + if not providerCls.canStart(): + raise exceptions.ProviderInitException( + f"Trying to initialize provider {providerId!r} which reported being unable to start" + ) # Initialize the provider. providerInst = providerCls() # Register extension points. @@ -205,20 +266,22 @@ def handleConfigProfileSwitch(self) -> None: curProviders = set(self.providers) providersToInitialize = configuredProviders - curProviders providersToTerminate = curProviders - configuredProviders - for provider in providersToTerminate: + for providerId in providersToTerminate: try: - self.terminateProvider(provider) + providerInfo = getProviderInfo(providerId) + self.terminateProvider(providerInfo) except Exception: log.error( - f"Could not terminate the {provider} vision enhancement provider", + f"Could not terminate the {providerId} vision enhancement providerId", exc_info=True ) - for provider in providersToInitialize: + for providerId in providersToInitialize: try: - self.initializeProvider(provider) + providerInfo = getProviderInfo(providerId) + self.initializeProvider(providerInfo) except Exception: log.error( - f"Could not initialize the {provider} vision enhancement provider", + f"Could not initialize the {providerId} vision enhancement providerId", exc_info=True ) From dcef2dbd233e72fd84061b82c3efc6b4d54ee04e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:21:24 +0100 Subject: [PATCH 046/116] Use a class to control start/stop of providers This bundles together the several callables that were being passed around. --- source/gui/settingsDialogs.py | 259 ++++++++++-------- source/vision/providerBase.py | 32 +++ .../NVDAHighlighter.py | 25 +- .../screenCurtain.py | 23 +- 4 files changed, 192 insertions(+), 147 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 8f9b6ae146f..9ff485dbb20 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3029,84 +3029,33 @@ def onNoMessageTimeoutChange(self, evt): self.messageTimeoutEdit.Enable(not evt.IsChecked()) -class VisionSettingsPanel(SettingsPanel): - initialProviders: List[str] - # Translators: This is the label for the vision panel - title = _("Vision") - - # Translators: This is a label appearing on the vision settings panel. - panelDescription = _("Configure visual aides.") - - def makeSettings(self, settingsSizer: wx.BoxSizer): - self.initialProviders = list(vision.handler.providers) - self.providerPanelInstances = [] - - self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - - self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) - - for providerInfo in vision.getProviderList(): - providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), - flag=wx.EXPAND - ) - providerId = providerInfo.providerId - kwargs = { - # default value for name parameter to lambda, recommended by python3 FAQ: - # https://docs.python.org/3/faq/programming.html#why-do-lambdas-defined-in-a-loop-with-different-values-all-return-the-same-result - "getProvider": lambda id=providerId: self._getProvider(id), - "initProvider": lambda id=providerId: self.safeInitProviders([id]), - "terminateProvider": lambda id=providerId: self.safeTerminateProviders([id], verbose=True) - } - - settingsPanelCls = providerInfo.providerClass.getSettingsPanelClass() - if not settingsPanelCls: - log.debug(f"Using default panel for providerId: {providerInfo.providerId}") - settingsPanelCls = VisionProviderSubPanel_Wrapper - kwargs["providerType"] = providerInfo.providerClass - else: - log.debug(f"Using custom panel for providerId: {providerInfo.providerId}") - try: - settingsPanel = settingsPanelCls( - self, - **kwargs - ) - # E722: bare except used since we can not know what exceptions a provider might throw. - # We should be able to continue despite a buggy provider. - except: # noqa: E722 - log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) - continue - - if len(self.providerPanelInstances) > 0: - settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - providerSizer.Add(settingsPanel, flag=wx.EXPAND) - self.providerPanelInstances.append(settingsPanel) - - def _getProvider(self, providerId: str) -> Optional[vision.VisionEnhancementProvider]: - log.debug(f"providerId: {providerId}") - return vision.handler.providers.get(providerId, None) +class VisionProviderStateControl(vision.providerBase.VisionProviderStateControl): + def __init__( + self, + parent: wx.Window, + providerInfo: vision.providerInfo.ProviderInfo + ): + self._providerInfo = providerInfo + self._parent = parent - def safeInitProviders( + def startProvider( self, - providerIds: List[str] ) -> bool: - """Initializes one or more providers in a way that is gui friendly, + """Initializes the provider in a way that is gui friendly, showing an error if appropriate. - @returns: Whether initialization succeeded for all providers. + @returns: Whether initialization succeeded. """ success = True initErrors = [] - for providerId in providerIds: - providerInfo = vision.visionHandler.getProviderInfo(providerId) - try: - vision.handler.initializeProvider(providerInfo) - except Exception: - initErrors.append(providerId) - log.error( - f"Could not initialize the {providerId} vision enhancement provider", - exc_info=True - ) - success = False + try: + vision.handler.initializeProvider(self._providerInfo) + except Exception: + initErrors.append(self._providerInfo.providerId) + log.error( + f"Could not initialize the {self._providerInfo.providerId} vision enhancement provider", + exc_info=True + ) + success = False if not success and initErrors: if len(initErrors) == 1: # Translators: This message is presented when @@ -3124,13 +3073,12 @@ def safeInitProviders( # Translators: The title of the vision enhancement provider error message box. _("Vision Enhancement Provider Error"), wx.OK | wx.ICON_WARNING, - self + self._parent ) return success - def safeTerminateProviders( + def terminateProvider( self, - providerIds: List[str], verbose: bool = False ): """Terminates one or more providers in a way that is gui friendly, @@ -3138,20 +3086,18 @@ def safeTerminateProviders( @returns: Whether initialization succeeded for all providers. """ terminateErrors = [] - for providerId in providerIds: - try: - providerInfo = vision.visionHandler.getProviderInfo(providerId) - # Terminating a provider from the gui should never save the settings. - # This is because termination happens on the fly when unchecking check boxes. - # Saving settings would be harmful if a user opens the vision panel, - # then changes some settings and disables the provider. - vision.handler.terminateProvider(providerInfo, saveSettings=False) - except Exception: - terminateErrors.append(providerId) - log.error( - f"Could not terminate the {providerId} vision enhancement provider", - exc_info=True - ) + try: + # Terminating a provider from the gui should never save the settings. + # This is because termination happens on the fly when unchecking check boxes. + # Saving settings would be harmful if a user opens the vision panel, + # then changes some settings and disables the provider. + vision.handler.terminateProvider(self._providerInfo, saveSettings=False) + except Exception: + terminateErrors.append(self._providerInfo.providerId) + log.error( + f"Could not terminate the {self._providerInfo.providerId} vision enhancement provider", + exc_info=True + ) if terminateErrors: if verbose: @@ -3174,9 +3120,94 @@ def safeTerminateProviders( # Translators: The title of the vision enhancement provider error message box. _("Vision Enhancement Provider Error"), wx.OK | wx.ICON_WARNING, - self + self._parent ) + def getProviderInstance(self) -> Optional[vision.VisionEnhancementProvider]: + return vision.handler.providers.get(self._providerInfo.providerId, None) + + def getProviderInfo(self) -> vision.providerInfo.ProviderInfo: + return self._providerInfo + + +class VisionSettingsPanel(SettingsPanel): + initialProviders: List[vision.providerInfo.ProviderInfo] + # Translators: This is the label for the vision panel + title = _("Vision") + + # Translators: This is a label appearing on the vision settings panel. + panelDescription = _("Configure visual aides.") + + def _getProviderInfos(self) -> List[vision.providerInfo.ProviderInfo]: + return list( + vision.visionHandler.getProviderInfo(providerId) for providerId in vision.handler.providers + ) + + def makeSettings(self, settingsSizer: wx.BoxSizer): + self.initialProviders = self._getProviderInfos() + self.providerPanelInstances = [] + + self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + + self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) + + for providerInfo in vision.getProviderList(): + providerSizer = self.settingsSizerHelper.addItem( + wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), + flag=wx.EXPAND + ) + providerControl = VisionProviderStateControl(parent=self, providerInfo=providerInfo) + + settingsPanelCls = providerInfo.providerClass.getSettingsPanelClass() + if not settingsPanelCls: + log.debug(f"Using default panel for providerId: {providerInfo.providerId}") + settingsPanelCls = VisionProviderSubPanel_Wrapper + else: + log.debug(f"Using custom panel for providerId: {providerInfo.providerId}") + try: + settingsPanel = settingsPanelCls(parent=self, providerControl=providerControl) + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 + log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) + continue + + if len(self.providerPanelInstances) > 0: + settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) + providerSizer.Add(settingsPanel, flag=wx.EXPAND) + self.providerPanelInstances.append(settingsPanel) + + def safeInitProviders( + self, + providers: List[vision.providerInfo.ProviderInfo] + ) -> bool: + """Initializes one or more providers in a way that is gui friendly, + showing an error if appropriate. + @returns: Whether initialization succeeded for all providers. + """ + results = [True] + for provider in providers: + results.append( + VisionProviderStateControl(self, provider).startProvider() + ) + return all(results) + + def safeTerminateProviders( + self, + providers: List[vision.providerInfo.ProviderInfo], + verbose: bool = False + ): + """Terminates one or more providers in a way that is gui friendly, + @verbose: Whether to show a termination error. + @returns: Whether termination succeeded for all providers. + """ + results = [True] + for provider in providers: + results.append( + VisionProviderStateControl(self, provider).terminateProvider() + ) + return all(results) + def refreshPanel(self): self.Freeze() # trigger a refresh of the settings @@ -3197,13 +3228,16 @@ def onDiscard(self): log.debug(f"Error discarding providerPanel: {panel.__class__!r}", exc_info=True) providersToInitialize = [ - providerId for providerId in self.initialProviders - if providerId not in vision.handler.providers + provider for provider in self.initialProviders + if provider.providerId not in vision.handler.providers ] self.safeInitProviders(providersToInitialize) + initialProviderIds = [ + providerInfo.providerId for providerInfo in self.initialProviders + ] providersToTerminate = [ - providerId for providerId in vision.handler.providers - if providerId not in self.initialProviders + vision.visionHandler.getProviderInfo(providerId) for providerId in vision.handler.providers + if providerId not in initialProviderIds ] self.safeTerminateProviders(providersToTerminate) @@ -3215,7 +3249,10 @@ def onSave(self): # We should be able to continue despite a buggy provider. except: # noqa: E722 log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) - self.initialProviders = list(vision.handler.providers) + self.initialProviders = list( + vision.visionHandler.getProviderInfo(providerId) + for providerId in vision.handler.providers + ) class VisionProviderSubPanel_Settings( @@ -3255,30 +3292,9 @@ class VisionProviderSubPanel_Wrapper( def __init__( self, parent: wx.Window, - *, # Make next argument keyword only - # todo: make these part of a class: - providerType: Type[vision.VisionEnhancementProvider], - getProvider: Callable[ - [], - Optional[vision.VisionEnhancementProvider] - ], # mostly used to see if the provider is initialised or not. - initProvider: Callable[ - [], - bool - ], - terminateProvider: Callable[ - [], - None - ] + providerControl: VisionProviderStateControl ): - """ - @param getProvider: A callable that returns an instance to a VisionEnhancementProvider. - This will usually be a weakref, but could be any callable taking no arguments. - """ - self._providerType = providerType - self._getProvider = getProvider - self._initProvider = initProvider - self._terminateProvider = terminateProvider + self._providerControl = providerControl self._providerSettings: Optional[VisionProviderSubPanel_Settings] = None self._providerSettingsSizer = wx.BoxSizer(orient=wx.VERTICAL) super().__init__(parent=parent) @@ -3297,7 +3313,7 @@ def makeSettings(self, settingsSizer): proportion=1.0 ) self._checkBox: wx.CheckBox = checkBox - if self._getProvider(): + if self._providerControl.getProviderInstance(): checkBox.SetValue(True) if self._createProviderSettings(): checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) @@ -3306,9 +3322,10 @@ def makeSettings(self, settingsSizer): def _createProviderSettings(self): try: + getSettingsCallable = self._providerControl.getProviderInfo().providerClass.getSettings self._providerSettings = VisionProviderSubPanel_Settings( self, - settingsCallable=self._providerType.getSettings + settingsCallable=getSettingsCallable ) self._providerSettingsSizer.Add(self._providerSettings, flag=wx.EXPAND, proportion=1.0) # E722: bare except used since we can not know what exceptions a provider might throw. @@ -3328,11 +3345,11 @@ def _nonEnableableGUI(self, evt): def _enableToggle(self, evt): if not evt.IsChecked(): - self._terminateProvider() + self._providerControl.terminateProvider() self._providerSettings.updateDriverSettings() self._providerSettings.onPanelActivated() else: - self._initProvider() + self._providerControl.startProvider() self._providerSettings.updateDriverSettings() self._providerSettings.onPanelActivated() self._sendLayoutUpdatedEvent() diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index a5ef339e527..3fb0a2e5834 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -44,6 +44,38 @@ def _getConfigSection(cls) -> str: # all providers should be in the "vision" section. return "vision" +class VisionProviderStateControl: + """ Stub showing the interface for controling the start/termination of a single provider. + Implementors of this class should handle the outcome when things go wrong. + """ + + @abstractmethod + def startProvider(self) -> bool: + """Initializes the provider in a way that is gui friendly, + showing an error if appropriate. + @returns: True on initialization success. + """ + + @abstractmethod + def terminateProvider(self, verbose: bool = False): + """Terminates one or more providers in a way that is gui friendly, + @verbose: Whether to show a termination error. + @returns: Whether initialization succeeded for all providers. + """ + + @abstractmethod + def getProviderInstance(self): + """Gets an instance of the provider if it already exists + @rtype: Optional[VisionEnhancementProvider] + """ + + @abstractmethod + def getProviderInfo(self): + """ + @return: The provider info + @rtype: providerInfo.ProviderInfo + """ + class VisionEnhancementProvider(AutoPropertyObject): """A class for vision enhancement providers. diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index cc484dc4d58..7c50ba1cb49 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -242,16 +242,14 @@ class NVDAHighlighterGuiPanel( _enableCheckSizer: wx.BoxSizer _enabledCheckbox: wx.CheckBox + from gui.settingsDialogs import VisionProviderStateControl + def __init__( self, - parent, - getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], - initProvider: Callable[[], bool], - terminateProvider: Callable[[], None] + parent: wx.Window, + providerControl: VisionProviderStateControl ): - self._getProvider = getProvider - self._initProvider = initProvider - self._terminateProvider = terminateProvider + self._providerControl = providerControl super().__init__(parent) def _buildGui(self): @@ -317,13 +315,11 @@ def _updateEnabledState(self): self._ensureEnableState(False) def _ensureEnableState(self, shouldBeEnabled: bool): - currentlyEnabled = bool(self._getProvider()) + currentlyEnabled = bool(self._providerControl.getProviderInstance()) if shouldBeEnabled and not currentlyEnabled: - log.debug("init provider") - self._initProvider() + self._providerControl.startProvider() elif not shouldBeEnabled and currentlyEnabled: - log.debug("terminate provider") - self._terminateProvider() + self._providerControl.terminateProvider() def _onCheckEvent(self, evt: wx.CommandEvent): settingsStorage = self._getSettingsStorage() @@ -335,8 +331,9 @@ def _onCheckEvent(self, evt: wx.CommandEvent): self.updateDriverSettings() else: self._updateEnabledState() - if self._getProvider(): - self._getProvider().refresh() + providerInst: Optional[NVDAHightlighter] = self._providerControl.getProviderInstance() + if providerInst: + providerInst.refresh() class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index e98316849e3..911b506dc86 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -15,7 +15,10 @@ import wx import gui from logHandler import log -from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType +from vision.providerBase import ( + VisionEnhancementProviderSettings, + SupportedSettingType, +) from typing import Optional, Type, Callable @@ -211,16 +214,14 @@ class ScreenCurtainGuiPanel( _enabledCheckbox: wx.CheckBox _enableCheckSizer: wx.BoxSizer + from gui.settingsDialogs import VisionProviderStateControl + def __init__( self, parent, - getProvider: Callable[[], Optional[vision.VisionEnhancementProvider]], - initProvider: Callable[[], bool], - terminateProvider: Callable[[], None] + providerControl: VisionProviderStateControl ): - self._getProvider = getProvider - self._initProvider = initProvider - self._terminateProvider = terminateProvider + self._providerControl = providerControl super().__init__(parent) def _buildGui(self): @@ -264,17 +265,15 @@ def _onCheckEvent(self, evt: wx.CommandEvent): self._ensureEnableState(evt.IsChecked()) def _ensureEnableState(self, shouldBeEnabled: bool): - currentlyEnabled = bool(self._getProvider()) + currentlyEnabled = bool(self._providerControl.getProviderInstance()) if shouldBeEnabled and not currentlyEnabled: - log.debug("init provider") confirmed = self.confirmInitWithUser() if confirmed: - self._initProvider() + self._providerControl.startProvider() else: self._enabledCheckbox.SetValue(False) elif not shouldBeEnabled and currentlyEnabled: - log.debug("terminate provider") - self._terminateProvider() + self._providerControl.terminateProvider() def confirmInitWithUser(self) -> bool: settingsStorage = self._getSettingsStorage() From 59f8276191810c185a3c322eda74a0611dcdf1dc Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:22:00 +0100 Subject: [PATCH 047/116] Improve docs --- source/vision/providerBase.py | 48 ++++++++++++------- source/vision/visionHandler.py | 2 +- .../NVDAHighlighter.py | 12 +---- source/visionEnhancementProviders/readme.md | 5 +- .../screenCurtain.py | 3 +- 5 files changed, 41 insertions(+), 29 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 3fb0a2e5834..2cc3ff4427d 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -14,7 +14,7 @@ from baseObject import AutoPropertyObject from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import FrozenSet, Type, Optional, List, Union, Tuple +from typing import FrozenSet, Type, Optional, List, Union, Tuple, Any SupportedSettingType = Union[ List[driverHandler.DriverSetting], @@ -26,23 +26,26 @@ class VisionEnhancementProviderSettings(AutoSettings, ABC): """ Base class for settings for a vision enhancement provider. Ensure that the following are implemented: - - AutoSettings.getId - - AutoSettings.getTranslatedName + - AutoSettings.getId: + This is case sensitive. Used in the config file. + - AutoSettings.getTranslatedName: + The string that should appear in the GUI as the name + - AutoSettings._get_supportedSettings: + The "runtime" settings for your provider Although technically optional, derived classes probably need to implement: - - AutoSettings._get_preInitSettings - - AutoSettings._get_supportedSettings + - AutoSettings._get_preInitSettings: + The settings always configurable for your provider """ supportedSettings: SupportedSettingType # Typing for autoprop L{_get_supportedSettings} def __init__(self): super().__init__() - # ensure that settings are loaded at construction time. - self.initSettings() + self.initSettings() # ensure that settings are loaded at construction time. @classmethod def _getConfigSection(cls) -> str: - # all providers should be in the "vision" section. - return "vision" + return "vision" # all providers should be in the "vision" section. + class VisionProviderStateControl: """ Stub showing the interface for controling the start/termination of a single provider. @@ -80,11 +83,15 @@ def getProviderInfo(self): class VisionEnhancementProvider(AutoPropertyObject): """A class for vision enhancement providers. Derived classes should implement: - - terminate - - registerEventExtensionPoints - - canStart - - getSettings - To provide a custom GUI, return a SettingsPanel class type from: + - terminate: + How to shutdown the provider + - registerEventExtensionPoints: + Allows the provider to receive updates form NVDA + - canStart: + Checks startup dependencies are satisfied + - getSettings: + Returns your implementation of VisionEnhancementProviderSettings + Optional: To provide a custom GUI, return a SettingsPanel class type from: - getSettingsPanelClass """ cachePropertiesByDefault = True @@ -102,9 +109,18 @@ def getSettings(cls) -> VisionEnhancementProviderSettings: ... @classmethod - def getSettingsPanelClass(cls) -> Optional[Type]: + def getSettingsPanelClass(cls) -> Optional[Any]: """Returns the instance to be used in order to construct a settings panel for the provider. - @return: Optional[SettingsPanel] + The returned class must have a constructor which accepts: + - parent: wx.Window + - providerControl: VisionProviderStateControl + EG: + ``` python + class mySettingsPanel(gui.settingsDialogs.SettingsPanel): + def __init__(self, parent: wx.Window, providerControl: VisionProviderStateControl): + super().__init__(parent=parent) + ``` + @rtype: Optional[SettingsPanel] @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. """ return None diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index a21c8e938a8..963df45c204 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -110,7 +110,7 @@ class VisionHandler(AutoPropertyObject): It performs the following tasks: * It keeps track of active vision enhancement providers in the L{providers} dictionary. - * It processes initialization and termnation of providers. + * It processes initialization and termination of providers. * It receives certain events from the core of NVDA, delegating them to the appropriate extension points. """ diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 7c50ba1cb49..9fe04786a7f 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -305,10 +305,8 @@ def _updateEnabledState(self): if any(settingsToTriggerActivation): if all(settingsToTriggerActivation): self._enabledCheckbox.Set3StateValue(wx.CHK_CHECKED) - log.debug("all") else: self._enabledCheckbox.Set3StateValue(wx.CHK_UNDETERMINED) - log.debug("some") self._ensureEnableState(True) else: self._enabledCheckbox.Set3StateValue(wx.CHK_UNCHECKED) @@ -351,7 +349,6 @@ class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): @classmethod def getSettings(cls) -> NVDAHighlighterSettings: - log.debug(f"getting settings: {cls._settings.__class__!r}") return cls._settings @classmethod # impl required by vision.providerBase.VisionEnhancementProvider @@ -376,6 +373,7 @@ def registerEventExtensionPoints(self, extensionPoints): def __init__(self): super().__init__() + log.debug("Starting NVDAHighlighter") self.contextToRectMap = {} winGDI.gdiPlusInitialize() self.window = None @@ -383,12 +381,8 @@ def __init__(self): self._highlighterThread.daemon = True self._highlighterThread.start() - # Demonstrate adding runtime settings, to test this: - # - make getSettingsPanelClass return None - # - un-comment equivelent line in terminate (restoring settings to non-runtime version) - # self.__class__._settings = NVDAHighlighterSettings_Runtime() - def terminate(self): + log.debug("Terminating NVDAHighlighter") if self._highlighterThread: if not winUser.user32.PostThreadMessageW(self._highlighterThread.ident, winUser.WM_QUIT, 0, 0): raise WinError() @@ -396,8 +390,6 @@ def terminate(self): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() - # see comment in __init__ - # self.__class__._settings = NVDAHighlighterSettings() def _run(self): if vision._isDebug(): diff --git a/source/visionEnhancementProviders/readme.md b/source/visionEnhancementProviders/readme.md index 2e5a12f05dd..4fe396e4c64 100644 --- a/source/visionEnhancementProviders/readme.md +++ b/source/visionEnhancementProviders/readme.md @@ -1,6 +1,9 @@ ## Vision Enhancement Providers -These modules use the "vision framework" (see source/vision/) to augment visual information presented to the user. +These modules use the "vision framework" to augment visual information presented to the user. +For more information about the implementation of a provider see vision.providerBase.VisionEnhancementProvider +(in source/vision/providerBase.py) + Two of the built-in examples are: - NVDA Highlighter which will react to changes in focus and draw a rectangle outline around the focused object. - Screen Curtain which when enabled makes the screen black for privacy reasons. diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 911b506dc86..3c1edf6bc79 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -311,12 +311,13 @@ def getSettings(cls) -> ScreenCurtainSettings: def __init__(self): super().__init__() - log.debug(f"ScreenCurtain", stack_info=True) + log.debug(f"Starting ScreenCurtain") Magnification.MagInitialize() Magnification.MagShowSystemCursor(False) Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) def terminate(self): + log.debug(f"Terminating ScreenCurtain") super().terminate() Magnification.MagShowSystemCursor(True) Magnification.MagUninitialize() From edd78ce13b9aa2aad220b5afc4d8df25b914617b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:34:37 +0100 Subject: [PATCH 048/116] Provider ID no longer has to match module name --- source/vision/providerBase.py | 2 +- source/visionEnhancementProviders/exampleProvider_autoGui.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 2cc3ff4427d..af46ed2f100 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -27,7 +27,7 @@ class VisionEnhancementProviderSettings(AutoSettings, ABC): Base class for settings for a vision enhancement provider. Ensure that the following are implemented: - AutoSettings.getId: - This is case sensitive. Used in the config file. + This is case sensitive. Used in the config file. Does not have to match the module name. - AutoSettings.getTranslatedName: The string that should appear in the GUI as the name - AutoSettings._get_supportedSettings: diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index f0c40f26d62..e607aa4b972 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -45,7 +45,7 @@ class AutoGuiTestSettings(VisionEnhancementProviderSettings): @classmethod def getId(cls) -> str: - return "exampleProvider_autoGui" + return "exampleOfAutoGui" # Note: this does not have to match the name of the module. @classmethod def getTranslatedName(cls) -> str: From 46ef9c44b5633f054d574646fe2048e75af5f05a Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:48:54 +0100 Subject: [PATCH 049/116] Fix linting errors --- source/gui/settingsDialogs.py | 13 ++++++++----- source/vision/__init__.py | 3 ++- source/vision/providerBase.py | 2 +- source/vision/providerInfo.py | 1 - source/vision/visionHandler.py | 6 +++--- .../visionEnhancementProviders/NVDAHighlighter.py | 2 +- .../exampleProvider_autoGui.py | 4 ++-- source/visionEnhancementProviders/screenCurtain.py | 2 +- 8 files changed, 18 insertions(+), 15 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 9ff485dbb20..aae5a7c20dd 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -12,6 +12,7 @@ import copy import re import wx +from vision.providerBase import VisionEnhancementProviderSettings from wx.lib import scrolledpanel from wx.lib.expando import ExpandoTextCtrl import wx.lib.newevent @@ -34,7 +35,8 @@ import brailleTables import brailleInput import vision -from typing import Callable, List, Type, Optional, Any +import vision.providerBase +from typing import Callable, List, Optional, Any import core import keyboardHandler import characterProcessing @@ -3123,7 +3125,7 @@ def terminateProvider( self._parent ) - def getProviderInstance(self) -> Optional[vision.VisionEnhancementProvider]: + def getProviderInstance(self) -> Optional[vision.providerBase.VisionEnhancementProvider]: return vision.handler.providers.get(self._providerInfo.providerId, None) def getProviderInfo(self) -> vision.providerInfo.ProviderInfo: @@ -3251,7 +3253,7 @@ def onSave(self): log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = list( vision.visionHandler.getProviderInfo(providerId) - for providerId in vision.handler.providers + for providerId in vision.handler.providers ) @@ -3260,6 +3262,8 @@ class VisionProviderSubPanel_Settings( SettingsPanel ): + _settingsCallable: Callable[[], VisionEnhancementProviderSettings] + def __init__( self, parent: wx.Window, @@ -3267,7 +3271,7 @@ def __init__( settingsCallable: Callable[[], vision.providerBase.VisionEnhancementProviderSettings] ): """ - @param providerCallable: A callable that returns an instance to a VisionEnhancementProvider. + @param settingsCallable: A callable that returns an instance to a VisionEnhancementProviderSettings. This will usually be a weakref, but could be any callable taking no arguments. """ self._settingsCallable = settingsCallable @@ -3275,7 +3279,6 @@ def __init__( def getSettings(self) -> AutoSettings: settings = self._settingsCallable() - log.debug(f"getting settings: {settings.__class__!r}") return settings def makeSettings(self, settingsSizer): diff --git a/source/vision/__init__.py b/source/vision/__init__.py index ec6689e3500..25dfd4abee6 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -10,9 +10,9 @@ Add-ons can provide their own provider using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ -from vision.providerBase import VisionEnhancementProvider from vision.providerInfo import ProviderInfo from .constants import Role +from . import visionHandler from .visionHandler import VisionHandler, getProviderClass import visionEnhancementProviders import config @@ -20,6 +20,7 @@ handler: Optional[VisionHandler] = None + def initialize(): global handler config.addConfigDirsToPythonPackagePath(visionEnhancementProviders) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index af46ed2f100..975cadb7464 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -14,7 +14,7 @@ from baseObject import AutoPropertyObject from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import FrozenSet, Type, Optional, List, Union, Tuple, Any +from typing import FrozenSet, Optional, List, Union, Tuple, Any SupportedSettingType = Union[ List[driverHandler.DriverSetting], diff --git a/source/vision/providerInfo.py b/source/vision/providerInfo.py index e5bc67aadc8..f71d85d54af 100644 --- a/source/vision/providerInfo.py +++ b/source/vision/providerInfo.py @@ -9,7 +9,6 @@ ProviderIdT = str ModuleNameT = str TranslatedNameT = str -#RolesT = List[Role] @dataclass diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 963df45c204..c9e5ae89702 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -50,8 +50,8 @@ def getProviderClass( def getProviderList( - onlyStartable: bool = True - ) -> List[providerInfo.ProviderInfo]: + onlyStartable: bool = True +) -> List[providerInfo.ProviderInfo]: """Gets a list of available vision enhancement names with their descriptions as well as supported roles. @param onlyStartable: excludes all providers for which the check method returns C{False}. @return: list of tuples with provider names, provider descriptions, and supported roles. @@ -92,7 +92,7 @@ def getProviderList( # make it impossible to list all the others. log.error("", exc_info=True) # Sort the providers alphabetically by name. - providerList.sort(key=lambda info: info.translatedName.lower()) + providerList.sort(key=lambda info: info.translatedName.lower()) return providerList diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 9fe04786a7f..3f4aebc0627 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -5,7 +5,7 @@ # Copyright (C) 2018-2019 NV Access Limited, Babbage B.V., Takuya Nishimoto """Default highlighter based on GDI Plus.""" -from typing import Callable, Optional, Tuple +from typing import Optional, Tuple import vision from vision.constants import Role, Context diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index e607aa4b972..c7464d80ed2 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -155,8 +155,8 @@ def _initRuntimeOnlySettings(self): # load and set values from the external source, this will override values loaded from config. settings.runtimeOnlySetting_externalValueLoad = self._getValueFromDeviceOrOtherApplication( - "runtimeOnlySetting_externalValueLoad" - ) + "runtimeOnlySetting_externalValueLoad" + ) def _getValueFromDeviceOrOtherApplication(self, settingId: str) -> Any: """ This method might connect to another application / device and fetch default values.""" diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 3c1edf6bc79..f5a630bc95f 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -19,7 +19,7 @@ VisionEnhancementProviderSettings, SupportedSettingType, ) -from typing import Optional, Type, Callable +from typing import Optional, Type class MAGCOLOREFFECT(Structure): From 8d812df229099c5569e0e742d2b99492592fb2f4 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:54:09 +0100 Subject: [PATCH 050/116] Remove unused role concept --- source/vision/__init__.py | 1 - source/vision/constants.py | 22 ------------------- source/vision/providerBase.py | 7 +----- source/vision/visionHandler.py | 5 ++--- .../NVDAHighlighter.py | 3 +-- 5 files changed, 4 insertions(+), 34 deletions(-) diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 25dfd4abee6..311d82648cd 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -11,7 +11,6 @@ using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ from vision.providerInfo import ProviderInfo -from .constants import Role from . import visionHandler from .visionHandler import VisionHandler, getProviderClass import visionEnhancementProviders diff --git a/source/vision/constants.py b/source/vision/constants.py index 5e4ab323a9d..92c59f82250 100644 --- a/source/vision/constants.py +++ b/source/vision/constants.py @@ -10,28 +10,6 @@ from enum import Enum -class Role(str, Enum): - """ - A role that could be fulfilled by a vision enhancement provider. - """ - # This should be a string enum when Python 3 arrives. - MAGNIFIER = "magnifier" - HIGHLIGHTER = "highlighter" - COLORENHANCER = "colorEnhancer" - - -ROLE_DESCRIPTIONS = { - # Translators: The name for a vision enhancement provider that magnifies (a part of) the screen. - Role.MAGNIFIER: _("Magnifier"), - # Translators: The name for a vision enhancement provider that highlights important areas on screen, - # such as the focus, caret or review cursor location. - Role.HIGHLIGHTER: _("Highlighter"), - # Translators: The name for a vision enhancement provider that enhances the color presentation. - # (i.e. color inversion, gray scale coloring, etc.) - Role.COLORENHANCER: _("Color enhancer"), -} - - class Context(str, Enum): """Context for events received by providers. Typically this informs of the cause of the event. diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 975cadb7464..6c45899d957 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -12,9 +12,8 @@ from autoSettingsUtils.autoSettings import AutoSettings from baseObject import AutoPropertyObject -from .constants import Role from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import FrozenSet, Optional, List, Union, Tuple, Any +from typing import Optional, List, Union, Tuple, Any SupportedSettingType = Union[ List[driverHandler.DriverSetting], @@ -95,10 +94,6 @@ class VisionEnhancementProvider(AutoPropertyObject): - getSettingsPanelClass """ cachePropertiesByDefault = True - #: The roles supported by this provider. - #: This attribute is currently not used, - #: but might be later for presentational purposes. - supportedRoles: FrozenSet[Role] = frozenset() @classmethod def getSettings(cls) -> VisionEnhancementProviderSettings: diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index c9e5ae89702..aa7705845ee 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -52,10 +52,9 @@ def getProviderClass( def getProviderList( onlyStartable: bool = True ) -> List[providerInfo.ProviderInfo]: - """Gets a list of available vision enhancement names with their descriptions as well as supported roles. + """Gets a list of available vision enhancement provider information @param onlyStartable: excludes all providers for which the check method returns C{False}. - @return: list of tuples with provider names, provider descriptions, and supported roles. - See L{constants.Role} for the available roles. + @return: List of available providers """ providerList = [] for loader, moduleName, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 3f4aebc0627..b105a3d1846 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -8,7 +8,7 @@ from typing import Optional, Tuple import vision -from vision.constants import Role, Context +from vision.constants import Context from vision.providerBase import SupportedSettingType from vision.util import getContextRect from windowUtils import CustomWindow @@ -205,7 +205,6 @@ class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderSetti name = "NVDAHighlighter" # Translators: Description for NVDA's built-in screen highlighter. description = _("NVDA Highlighter") - supportedRoles = frozenset([Role.HIGHLIGHTER]) # Default settings for parameters highlightFocus = False highlightNavigator = False From f5ef6227c846e40599e5a863175acdde6011f672 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 08:57:04 +0100 Subject: [PATCH 051/116] Fix instance/class used in getSettingsPanelClass docs --- source/vision/providerBase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 6c45899d957..2e6701da5f4 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -105,7 +105,7 @@ def getSettings(cls) -> VisionEnhancementProviderSettings: @classmethod def getSettingsPanelClass(cls) -> Optional[Any]: - """Returns the instance to be used in order to construct a settings panel for the provider. + """Returns the class to be used in order to construct a settingsPanel instance for the provider. The returned class must have a constructor which accepts: - parent: wx.Window - providerControl: VisionProviderStateControl From 06b296d7645911c359c6fc2721c13f88b4c45517 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 09:10:38 +0100 Subject: [PATCH 052/116] Clarify return types of starting/stopping a provider --- source/gui/settingsDialogs.py | 23 ++++++----------------- source/vision/__init__.py | 6 +++--- source/vision/providerBase.py | 20 ++++++++++---------- source/vision/visionHandler.py | 4 ++-- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index aae5a7c20dd..2fb2c91f6f8 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3042,10 +3042,9 @@ def __init__( def startProvider( self, - ) -> bool: + ) -> None: """Initializes the provider in a way that is gui friendly, showing an error if appropriate. - @returns: Whether initialization succeeded. """ success = True initErrors = [] @@ -3077,12 +3076,11 @@ def startProvider( wx.OK | wx.ICON_WARNING, self._parent ) - return success def terminateProvider( self, verbose: bool = False - ): + ) -> None: """Terminates one or more providers in a way that is gui friendly, @verbose: Whether to show a termination error. @returns: Whether initialization succeeded for all providers. @@ -3182,33 +3180,24 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): def safeInitProviders( self, providers: List[vision.providerInfo.ProviderInfo] - ) -> bool: + ) -> None: """Initializes one or more providers in a way that is gui friendly, showing an error if appropriate. - @returns: Whether initialization succeeded for all providers. """ - results = [True] for provider in providers: - results.append( - VisionProviderStateControl(self, provider).startProvider() - ) - return all(results) + VisionProviderStateControl(self, provider).startProvider() def safeTerminateProviders( self, providers: List[vision.providerInfo.ProviderInfo], verbose: bool = False - ): + ) -> None: """Terminates one or more providers in a way that is gui friendly, @verbose: Whether to show a termination error. @returns: Whether termination succeeded for all providers. """ - results = [True] for provider in providers: - results.append( - VisionProviderStateControl(self, provider).terminateProvider() - ) - return all(results) + VisionProviderStateControl(self, provider).terminateProvider(verbose=verbose) def refreshPanel(self): self.Freeze() diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 311d82648cd..9f6b6b83ffb 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -20,19 +20,19 @@ handler: Optional[VisionHandler] = None -def initialize(): +def initialize() -> None: global handler config.addConfigDirsToPythonPackagePath(visionEnhancementProviders) handler = VisionHandler() -def pumpAll(): +def pumpAll() -> None: """Runs tasks at the end of each core cycle.""" if handler and handler.extensionPoints: handler.extensionPoints.post_coreCycle.notify() -def terminate(): +def terminate() -> None: global handler handler.terminate() handler = None diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 2e6701da5f4..a678f9feca3 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -47,22 +47,22 @@ def _getConfigSection(cls) -> str: class VisionProviderStateControl: - """ Stub showing the interface for controling the start/termination of a single provider. + """ Stub showing the interface for controlling the start/termination of a single provider. Implementors of this class should handle the outcome when things go wrong. """ @abstractmethod - def startProvider(self) -> bool: - """Initializes the provider in a way that is gui friendly, + def startProvider(self) -> None: + """Initializes the provider in a way that is GUI friendly, showing an error if appropriate. - @returns: True on initialization success. + @note: Use getProviderInstance to determine success """ @abstractmethod - def terminateProvider(self, verbose: bool = False): - """Terminates one or more providers in a way that is gui friendly, + def terminateProvider(self, verbose: bool = False) -> None: + """Terminates one or more providers in a way that is GUI friendly, @verbose: Whether to show a termination error. - @returns: Whether initialization succeeded for all providers. + @note: Use getProviderInstance to determine success """ @abstractmethod @@ -120,7 +120,7 @@ def __init__(self, parent: wx.Window, providerControl: VisionProviderStateContro """ return None - def reinitialize(self): + def reinitialize(self) -> None: """Reinitialize a vision enhancement provider, reusing the same instance. This base implementation simply calls terminate and __init__ consecutively. """ @@ -128,7 +128,7 @@ def reinitialize(self): self.__init__() @abstractmethod - def terminate(self): + def terminate(self) -> None: """Terminate this driver. This should be used for any required clean up. @precondition: L{initialize} has been called. @@ -137,7 +137,7 @@ def terminate(self): ... @abstractmethod - def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints): + def registerEventExtensionPoints(self, extensionPoints: EventExtensionPoints) -> None: """ Called at provider initialization time, this method should register the provider to the several event extension points that it is interested in. diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index aa7705845ee..6ab5cd8f45c 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -131,7 +131,7 @@ def terminateProvider( self, provider: providerInfo.ProviderInfo, saveSettings: bool = True - ): + ) -> None: """Terminates a currently active provider. When termnation fails, an exception is raised. Yet, the provider wil lbe removed from the providers dictionary, @@ -182,7 +182,7 @@ def initializeProvider( self, provider: providerInfo.ProviderInfo, temporary: bool = False - ): + ) -> None: """ Enables and activates the supplied provider. @param provider: The provider to initialise. From ae5d6868b71719b2ae0ef074d3d3a8ce198a65f8 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 09:19:41 +0100 Subject: [PATCH 053/116] Give NVDA highlighter enable checkbox a better name and accelerator --- source/visionEnhancementProviders/NVDAHighlighter.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index b105a3d1846..2248c9ee305 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -202,9 +202,6 @@ def refresh(self): class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderSettings): - name = "NVDAHighlighter" - # Translators: Description for NVDA's built-in screen highlighter. - description = _("NVDA Highlighter") # Default settings for parameters highlightFocus = False highlightNavigator = False @@ -217,7 +214,7 @@ def getId(cls) -> str: @classmethod def getTranslatedName(cls) -> str: # Translators: Description for NVDA's built-in screen highlighter. - return _("NVDA Highlighter") + return _("Focus Highlight") @classmethod def _get_preInitSettings(cls) -> SupportedSettingType: @@ -258,7 +255,7 @@ def _buildGui(self): self, # Translators: The label for a checkbox that enables / disables focus highlighting # in the NVDA Highlighter vision settings panel. - label=_("Highlight focus"), + label=_("&Enable Highlighting"), style=wx.CHK_3STATE ) From 122784f9c978927b36cdcfd99bbe5c20d8e476bb Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 4 Nov 2019 14:57:51 +0100 Subject: [PATCH 054/116] Fix bad merge --- source/visionEnhancementProviders/screenCurtain.py | 1 - 1 file changed, 1 deletion(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 053882c93e6..bf57935a2ad 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -86,7 +86,6 @@ class Magnification: _MagShowSystemCursorArgTypes ) MagShowSystemCursor.errcheck = _errCheck -<<<<<<< HEAD # Translators: Description of a vision enhancement provider that disables output to the screen, From cd4b5637858ce4ca411a9b1e35342b0de6bf78d9 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Mon, 4 Nov 2019 15:00:33 +0100 Subject: [PATCH 055/116] Fix lint error not caught by previous version of flake8tabs --- source/gui/settingsDialogs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index caf5587fc21..3e3c8822638 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1309,8 +1309,8 @@ def makeSettings(self, settingsSizer): # read text in that dialect). autoDialectSwitchingText = _("Automatic dialect switching (when supported)") self.autoDialectSwitchingCheckbox = settingsSizerHelper.addItem( - wx.CheckBox(self, label=autoDialectSwitchingText - )) + wx.CheckBox(self, label=autoDialectSwitchingText) + ) self.autoDialectSwitchingCheckbox.SetValue( config.conf["speech"]["autoDialectSwitching"] ) From 8761bdd637e0cfa99249f26aaae7e519537a73bf Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 15:23:29 +0100 Subject: [PATCH 056/116] fixup merge --- source/visionEnhancementProviders/screenCurtain.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index bf57935a2ad..ca95897800c 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -50,8 +50,6 @@ class Magnification: # Get full screen color effect _MagGetFullscreenColorEffectFuncType = WINFUNCTYPE(BOOL, POINTER(MAGCOLOREFFECT)) _MagGetFullscreenColorEffectArgTypes = ((2, "effect"),) - _MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL) - _MagShowSystemCursorArgTypes = ((1, "showCursor"),) # show system cursor _MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL) @@ -315,14 +313,13 @@ def __init__(self): super().__init__() log.debug(f"Starting ScreenCurtain") Magnification.MagInitialize() - Magnification.MagShowSystemCursor(False) Magnification.MagSetFullscreenColorEffect(TRANSFORM_BLACK) Magnification.MagShowSystemCursor(False) def terminate(self): log.debug(f"Terminating ScreenCurtain") try: - super(VisionEnhancementProvider, self).terminate() + super().terminate() finally: Magnification.MagShowSystemCursor(True) Magnification.MagUninitialize() From 36342528d01cb284c1a27bef38a3d86fecc82d97 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 11:19:12 +0100 Subject: [PATCH 057/116] Fix case error in name of internal function --- source/autoSettingsUtils/autoSettings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 77d410abad3..cf19cb5cfaa 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -91,7 +91,7 @@ def _initSpecificSettings( config.conf[section][settingsId] = {} # Make sure the config spec is up to date, so the config validator does its work. config.conf[section][settingsId].spec.update( - cls._getConfigSPecForSettings(settings) + cls._getConfigSpecForSettings(settings) ) # Make sure the clsOrInst has attributes for every setting for setting in settings: @@ -139,7 +139,7 @@ def isSupported(self, settingID) -> bool: return False @classmethod - def _getConfigSPecForSettings( + def _getConfigSpecForSettings( cls, settings: SupportedSettingType ) -> Dict: @@ -152,7 +152,7 @@ def _getConfigSPecForSettings( return spec def getConfigSpec(self): - return self._getConfigSPecForSettings(self.supportedSettings) + return self._getConfigSpecForSettings(self.supportedSettings) @classmethod def _saveSpecificSettings( From 4007756db68332e04a11e9c6831fe4d20bc49b20 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 12:19:10 +0100 Subject: [PATCH 058/116] Add copyright headers, and fix docs --- source/autoSettingsUtils/autoSettings.py | 16 +++++++-------- source/autoSettingsUtils/driverSetting.py | 24 ++++++++++++++++++++--- source/autoSettingsUtils/utils.py | 11 ++++++++++- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index cf19cb5cfaa..4a8b42a672b 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -103,8 +103,8 @@ def _initSpecificSettings( cls._loadSpecificSettings(clsOrInst, settings) def initSettings(self): - """Initializes the configuration for this driver. - This method is called when initializing the driver. + """Initializes the configuration for this AutoSettings instance. + This method is called when initializing the AutoSettings instance. """ self._initSpecificSettings(self, self.supportedSettings) @@ -113,7 +113,7 @@ def initSettings(self): @classmethod def _get_preInitSettings(cls) -> SupportedSettingType: - """The settings supported by the driver at pre initialisation time. + """The settings supported by the AutoSettings instance at pre initialisation time. """ return [] @@ -124,14 +124,14 @@ def _get_preInitSettings(cls) -> SupportedSettingType: _abstract_supportedSettings = True def _get_supportedSettings(self) -> SupportedSettingType: - """The settings supported by the driver. Abstract. + """The settings supported by the AutoSettings instance. Abstract. When overriding this property, subclasses are encouraged to extend the getter method to ensure that L{preInitSettings} is part of the list of supported settings. """ return self.preInitSettings def isSupported(self, settingID) -> bool: - """Checks whether given setting is supported by the driver. + """Checks whether given setting is supported by the AutoSettings instance. """ for s in self.supportedSettings: if s.id == settingID: @@ -185,8 +185,8 @@ def _saveSpecificSettings( def saveSettings(self): """ - Saves the current settings for the driver to the configuration. - This method is also executed when the driver is loaded for the first time, + Saves the current settings for the AutoSettings instance to the configuration. + This method is also executed when the AutoSettings instance is loaded for the first time, in order to populate the configuration with the initial settings.. """ self._saveSpecificSettings(self, self.supportedSettings) @@ -233,7 +233,7 @@ def _loadSpecificSettings( def loadSettings(self, onlyChanged: bool = False): """ - Loads settings for this driver from the configuration. + Loads settings for this AutoSettings instance from the configuration. This method assumes that the instance has attributes o/properties corresponding with the name of every setting in L{supportedSettings}. @param onlyChanged: When loading settings, only apply those for which diff --git a/source/autoSettingsUtils/driverSetting.py b/source/autoSettingsUtils/driverSetting.py index 6ddb617275a..05b7819ec58 100644 --- a/source/autoSettingsUtils/driverSetting.py +++ b/source/autoSettingsUtils/driverSetting.py @@ -1,10 +1,25 @@ -from typing import Optional +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + +"""Classes used to represent settings for Drivers and other AutoSettings instances + Naming of these classes is historical, kept for backwards compatibility purposes. +""" + +from typing import Optional from baseObject import AutoPropertyObject class DriverSetting(AutoPropertyObject): - """Represents a synthesizer or braille display setting such as voice, variant or dot firmness. + """As a base class, represents a setting to be shown in GUI and saved to config. + + GUI representation is a string selection GUI control, a wx.Choice control. + + Used for synthesizer or braille display setting such as voice, variant or dot firmness as + well as for settings in Vision Providers """ id: str displayName: str @@ -53,7 +68,9 @@ def __init__( class NumericDriverSetting(DriverSetting): - """Represents a numeric driver setting such as rate, volume, pitch or dot firmness.""" + """Represents a numeric driver setting such as rate, volume, pitch or dot firmness. + GUI representation is a slider control. + """ defaultVal: int @@ -103,6 +120,7 @@ def __init__( class BooleanDriverSetting(DriverSetting): """Represents a boolean driver setting such as rate boost or automatic time sync. + GUI representation is a wx.Checkbox """ defaultVal: bool diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py index 18d3551deae..5be9aab437a 100644 --- a/source/autoSettingsUtils/utils.py +++ b/source/autoSettingsUtils/utils.py @@ -1,3 +1,12 @@ +# -*- coding: UTF-8 -*- +# A part of NonVisual Desktop Access (NVDA) +# This file is covered by the GNU General Public License. +# See the file COPYING for more details. +# Copyright (C) 2019 NV Access Limited + +"""Utility methods for Driver and AutoSettings instances +""" + def paramToPercent(current, min, max): """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. @@ -32,7 +41,7 @@ class UnsupportedConfigParameterError(NotImplementedError): class StringParameterInfo(object): """ - The base class used to represent a value of a string driver setting. + Used to represent a value of a DriverSetting instance. """ def __init__(self, id, displayName): From 6bbea60dd06147987e8577e2c911666e911aca2b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 12:20:15 +0100 Subject: [PATCH 059/116] Use single definition for SupportedSettingType Since it is a iterable, len does not work directly. Convert to list first. --- source/autoSettingsUtils/autoSettings.py | 6 ++---- source/driverHandler.py | 3 --- source/synthDriverHandler.py | 7 ++++--- source/vision/providerBase.py | 10 ++-------- 4 files changed, 8 insertions(+), 18 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 4a8b42a672b..4b7519cce63 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -7,7 +7,7 @@ """autoSettings for add-ons""" from abc import abstractmethod from copy import deepcopy -from typing import Union, Dict, Type, Any, Iterable +from typing import Dict, Type, Any, Iterable import config from autoSettingsUtils.utils import paramToPercent, percentToParam, UnsupportedConfigParameterError @@ -15,9 +15,7 @@ from logHandler import log from .driverSetting import DriverSetting -SupportedSettingType: Type = Union[ - Iterable[DriverSetting] -] +SupportedSettingType: Type = Iterable[DriverSetting] class AutoSettings(AutoPropertyObject): diff --git a/source/driverHandler.py b/source/driverHandler.py index 407b4a80bfb..20f3daa78b8 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -12,9 +12,6 @@ ) # F401: the following imports, while unused in this file, are provided for backwards compatibility. -from autoSettingsUtils.autoSettings import ( # noqa: F401 - SupportedSettingType, -) from autoSettingsUtils.driverSetting import ( # noqa: F401 DriverSetting, BooleanDriverSetting, diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index 4caba389f32..62eb7382323 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -395,12 +395,13 @@ def loadSettings(self, onlyChanged=False): ).format(self.name)) def _get_initialSettingsRingSetting (self): - if not self.isSupported("rate") and len(self.supportedSettings)>0: + supportedSettings = list(self.supportedSettings) + if not self.isSupported("rate") and len(supportedSettings)>0: #Choose first as an initial one - for i,s in enumerate(self.supportedSettings): + for i, s in enumerate(supportedSettings): if s.availableInSettingsRing: return i return None - for i,s in enumerate(self.supportedSettings): + for i, s in enumerate(supportedSettings): if s.id == "rate": return i return None diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index a678f9feca3..6593130dd63 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -7,18 +7,12 @@ """Module within the vision framework that contains the base vision enhancement provider class. """ -import driverHandler from abc import abstractmethod, ABC -from autoSettingsUtils.autoSettings import AutoSettings +from autoSettingsUtils.autoSettings import AutoSettings, SupportedSettingType from baseObject import AutoPropertyObject from .visionHandlerExtensionPoints import EventExtensionPoints -from typing import Optional, List, Union, Tuple, Any - -SupportedSettingType = Union[ - List[driverHandler.DriverSetting], - Tuple[driverHandler.DriverSetting] -] +from typing import Optional, Any class VisionEnhancementProviderSettings(AutoSettings, ABC): From 24e648e94e3340239fc4487d008aed04f8c0ecfe Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 12:31:00 +0100 Subject: [PATCH 060/116] Remove copied base __init__ implementation in Driver class --- source/autoSettingsUtils/autoSettings.py | 3 +++ source/driverHandler.py | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index 4b7519cce63..a9524b621f7 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -32,6 +32,9 @@ class AutoSettings(AutoPropertyObject): """ def __init__(self): + """Perform any initialisation + @note: registers with the config save action extension point + """ super().__init__() self._registerConfigSaveAction() diff --git a/source/driverHandler.py b/source/driverHandler.py index 20f3daa78b8..77a87f35c0a 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -54,7 +54,6 @@ def __init__(self): @postcondition: This driver can be used. """ super(Driver, self).__init__() - self._registerConfigSaveAction() def terminate(self, saveSettings: bool = True): """Terminate this driver. @@ -102,7 +101,7 @@ def _percentToParam(cls, percent, min, max): """ return percentToParam(percent, min, max) -# Impl for abstract methods in AutoSettings class + # Impl for abstract methods in AutoSettings class @classmethod def getId(cls) -> str: return cls.name From 520b9e4ec9b0c33e343431c2e8f8fcaae106e52e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 12:32:37 +0100 Subject: [PATCH 061/116] Comments for dialog announcement in toggle Screen curtain gesture --- source/globalCommands.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/source/globalCommands.py b/source/globalCommands.py index d22157f8e95..c151b866878 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2367,6 +2367,12 @@ def script_toggleScreenCurtain(self, gesture): # Already in the process of enabling the screen curtain, exit early. # Ensure that the dialog is in the foreground, and read it again. self._waitingOnScreenCurtainWarningDialog.Raise() + + # Key presses interrupt speech, so it maybe that the dialog wasn't + # announced properly (if the user triggered the gesture more + # than once). So we speak the objects to imitate the dialog getting + # focus again. It might be useful to have something like this in a + # script: see https://github.com/nvaccess/nvda/issues/9147#issuecomment-454278313 speech.cancelSpeech() speech.speakObject( api.getForegroundObject(), From 5ddfbc6421d7daa80163f3d8f7297fc7ea070010 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 4 Nov 2019 19:17:10 +0100 Subject: [PATCH 062/116] Fix lint error --- source/synthDriverHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/synthDriverHandler.py b/source/synthDriverHandler.py index 62eb7382323..78d52ff911a 100644 --- a/source/synthDriverHandler.py +++ b/source/synthDriverHandler.py @@ -396,7 +396,7 @@ def loadSettings(self, onlyChanged=False): def _get_initialSettingsRingSetting (self): supportedSettings = list(self.supportedSettings) - if not self.isSupported("rate") and len(supportedSettings)>0: + if not self.isSupported("rate") and len(supportedSettings) > 0: #Choose first as an initial one for i, s in enumerate(supportedSettings): if s.availableInSettingsRing: return i From b5e0ba4919bfd7c143868fe5d5c07ab39c6597d0 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 5 Nov 2019 11:37:29 +0100 Subject: [PATCH 063/116] Use type hinting rather than docstring --- source/gui/settingsDialogs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 3e3c8822638..098fa53a70f 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -299,11 +299,10 @@ def _buildGui(self): self.SetSizer(self.mainSizer) @abstractmethod - def makeSettings(self, sizer): + def makeSettings(self, sizer) -> wx.BoxSizer: """Populate the panel with settings controls. Subclasses must override this method. @param sizer: The sizer to which to add the settings controls. - @type sizer: wx.BoxSizer """ raise NotImplementedError From e682631f70ce36db48576a798008d9c496145f74 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 5 Nov 2019 11:39:16 +0100 Subject: [PATCH 064/116] Rename DriverSettingsMixin to AutoSettingsMixin Provides better consistency with the AutoSettings class. An alias mapping DriverSettingsMixin to AutoSettingsMixin is provided for backwards compatibility, however DriverSettingsMixin should be considered deprecated. Use AutoSettingsMixin instead. --- source/gui/settingsDialogs.py | 22 ++++++++++++------- .../NVDAHighlighter.py | 4 ++-- .../screenCurtain.py | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 098fa53a70f..2a7cc25a843 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1044,7 +1044,7 @@ def __call__(self,evt): ) -class DriverSettingsMixin(metaclass=ABCMeta): +class AutoSettingsMixin(metaclass=ABCMeta): """ Mixin class that provides support for driver specific gui settings. Derived classes should implement: @@ -1061,7 +1061,7 @@ def __init__(self, *args, **kwargs): """ self.sizerDict = {} self.lastControl = None - super(DriverSettingsMixin, self).__init__(*args, **kwargs) + super(AutoSettingsMixin, self).__init__(*args, **kwargs) # because settings instances can be of type L{Driver} as well, we have to handle # showing settings for non-instances. Because of this, we must reacquire a reference # to the settings class whenever we wish to use it (via L{getSettings}) in case the instance changes. @@ -1272,9 +1272,15 @@ def onPanelActivated(self): self.settingsSizer.Clear(delete_windows=True) self._currentSettingsRef = weakref.ref(self.getSettings()) self.makeSettings(self.settingsSizer) - super(DriverSettingsMixin, self).onPanelActivated() + super(AutoSettingsMixin, self).onPanelActivated() -class VoiceSettingsPanel(DriverSettingsMixin, SettingsPanel): + +#: DriverSettingsMixin name is provided or backwards compatibility. +# The name DriverSettingsMixin should be considered deprecated, use AutoSettingsMixin instead. +DriverSettingsMixin = AutoSettingsMixin + + +class VoiceSettingsPanel(AutoSettingsMixin, SettingsPanel): # Translators: This is the label for the voice settings panel. title = _("Voice") @@ -1397,7 +1403,7 @@ def makeSettings(self, settingsSizer): ) def onSave(self): - DriverSettingsMixin.onSave(self) + AutoSettingsMixin.onSave(self) config.conf["speech"]["autoLanguageSwitching"] = self.autoLanguageSwitchingCheckbox.IsChecked() config.conf["speech"]["autoDialectSwitching"] = self.autoDialectSwitchingCheckbox.IsChecked() @@ -2834,7 +2840,7 @@ def onOk(self, evt): self.Parent.updateCurrentDisplay() super(BrailleDisplaySelectionDialog, self).onOk(evt) -class BrailleSettingsSubPanel(DriverSettingsMixin, SettingsPanel): +class BrailleSettingsSubPanel(AutoSettingsMixin, SettingsPanel): @property def driver(self): @@ -3016,7 +3022,7 @@ def makeSettings(self, settingsSizer): log.debug("Finished making settings, now at %.2f seconds from start"%(time.time() - startTime)) def onSave(self): - DriverSettingsMixin.onSave(self) + AutoSettingsMixin.onSave(self) config.conf["braille"]["translationTable"] = self.outTableNames[self.outTableList.GetSelection()] brailleInput.handler.table = self.inTables[self.inTableList.GetSelection()] config.conf["braille"]["expandAtCursor"] = self.expandAtCursorCheckBox.GetValue() @@ -3267,7 +3273,7 @@ def onSave(self): class VisionProviderSubPanel_Settings( - DriverSettingsMixin, + AutoSettingsMixin, SettingsPanel ): diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 530472842df..39dacd31a88 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -232,7 +232,7 @@ def _get_supportedSettings(self) -> SupportedSettingType: class NVDAHighlighterGuiPanel( - gui.DriverSettingsMixin, + gui.AutoSettingsMixin, gui.SettingsPanel ): _enableCheckSizer: wx.BoxSizer @@ -278,7 +278,7 @@ def _buildGui(self): self.SetSizer(self.mainSizer) def getSettings(self) -> NVDAHighlighterSettings: - # DriverSettingsMixin uses self.driver to get / set attributes matching the names of the settings. + # AutoSettingsMixin uses self.driver to get / set attributes matching the names of the settings. # We want them set on this class. return VisionEnhancementProvider.getSettings() diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index ca95897800c..3c6d7f2844e 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -207,7 +207,7 @@ def _onShowEvt(self, evt): class ScreenCurtainGuiPanel( - gui.DriverSettingsMixin, + gui.AutoSettingsMixin, gui.SettingsPanel, ): From 0ed5b0d0eee02404f8f9fed9a32d3b00089d5564 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 5 Nov 2019 11:53:37 +0100 Subject: [PATCH 065/116] Add requirements doc string for classes using the AutoSettingsMixin --- source/gui/settingsDialogs.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 2a7cc25a843..d802ae3ea99 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1050,6 +1050,12 @@ class AutoSettingsMixin(metaclass=ABCMeta): Derived classes should implement: - L{getSettings} - L{settingsSizer} + Derived classes likely need to inherit from L{SettingsPanel}, in particular + the following methods must be provided: + - makeSettings + - onPanelActivated + @note: This mixin uses self.lastControl and self.sizerDict to keep track of the + controls added / and maintain ordering. If you plan to maintain other controls in the same panel care will need to be taken. """ def __init__(self, *args, **kwargs): @@ -1092,7 +1098,7 @@ def _makeSliderSettingControl( @param settingsStorage: where to get initial values / set values. This param must have an attribute with a name matching setting.id. In most cases it will be of type L{AutoSettings} - @returns: wx.BoxSizer containing newly created controls. + @return: wx.BoxSizer containing newly created controls. """ labeledControl = guiHelper.LabeledControlHelper( self, From 34552c45371426b104b3c9882b0a8bc6eefac2b0 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 5 Nov 2019 11:54:07 +0100 Subject: [PATCH 066/116] Remove debug logging from AutoSettingsMixin --- source/gui/settingsDialogs.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index d802ae3ea99..cf186968656 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1055,7 +1055,8 @@ class AutoSettingsMixin(metaclass=ABCMeta): - makeSettings - onPanelActivated @note: This mixin uses self.lastControl and self.sizerDict to keep track of the - controls added / and maintain ordering. If you plan to maintain other controls in the same panel care will need to be taken. + controls added / and maintain ordering. + If you plan to maintain other controls in the same panel care will need to be taken. """ def __init__(self, *args, **kwargs): @@ -1208,9 +1209,9 @@ def updateDriverSettings(self, changedSetting=None): if not settingsInst.isSupported(name): self.settingsSizer.Hide(sizer) # Create new controls, update already existing - log.debug(f"Current sizerDict: {self.sizerDict!r}") - - log.debug(f"Current supportedSettings: {self.getSettings().supportedSettings!r}") + if(gui._isDebug()): + log.debug(f"Current sizerDict: {self.sizerDict!r}") + log.debug(f"Current supportedSettings: {self.getSettings().supportedSettings!r}") for setting in settingsInst.supportedSettings: if setting.id == changedSetting: # Changing a setting shouldn't cause that setting's own values to change. @@ -1238,7 +1239,6 @@ def updateDriverSettings(self, changedSetting=None): if isinstance(setting, NumericDriverSetting): settingMaker = self._makeSliderSettingControl elif isinstance(setting, BooleanDriverSetting): - log.debug(f"creating a new bool driver setting: {setting.id}") settingMaker = self._makeBooleanSettingControl else: settingMaker = self._makeStringSettingControl From ddd371e2fe9e123546d6bac70cacf44597156a76 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Tue, 5 Nov 2019 17:13:03 +0100 Subject: [PATCH 067/116] Fix some issues in docs --- source/gui/settingsDialogs.py | 5 +++++ source/vision/providerBase.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index cf186968656..91ba6078916 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3064,6 +3064,11 @@ def onNoMessageTimeoutChange(self, evt): class VisionProviderStateControl(vision.providerBase.VisionProviderStateControl): + """ + Gives settings panels for vision enhancement providers a way to control a + single vision enhancement provider, handling any error conditions in + a UX friendly way. + """ def __init__( self, parent: wx.Window, diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 6593130dd63..74458db47e1 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -54,14 +54,14 @@ def startProvider(self) -> None: @abstractmethod def terminateProvider(self, verbose: bool = False) -> None: - """Terminates one or more providers in a way that is GUI friendly, + """Terminates the provider in a way that is GUI friendly, @verbose: Whether to show a termination error. @note: Use getProviderInstance to determine success """ @abstractmethod def getProviderInstance(self): - """Gets an instance of the provider if it already exists + """Gets an instance for the provider if it already exists @rtype: Optional[VisionEnhancementProvider] """ From ef1932b63a6efb66c4728e3b14dcf19a2471f61d Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 6 Nov 2019 19:57:59 +0100 Subject: [PATCH 068/116] Fix multiple provider error handling. One message is now shown. Code is shared better between starting single providers and starting multiple providers. --- source/gui/settingsDialogs.py | 180 +++++++++++++++++++++------------- source/vision/providerBase.py | 26 ++--- 2 files changed, 126 insertions(+), 80 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 91ba6078916..0538a8b8a3f 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3063,6 +3063,64 @@ def onNoMessageTimeoutChange(self, evt): self.messageTimeoutEdit.Enable(not evt.IsChecked()) +def showStartErrorForProviders( + parent: wx.Window, + providers: List[vision.providerInfo.ProviderInfo], +) -> None: + if not providers: + return + + if len(providers) == 1: + providerName = providers[0].translatedName + # Translators: This message is presented when + # NVDA is unable to load a single vision enhancement provider. + message = _(f"Could not load the {providerName} vision enhancement provider") + else: + providerNames = ", ".join(provider.translatedName for provider in providers) + # Translators: This message is presented when + # NVDA is unable to load multiple vision enhancement providers. + message = _( + f"Could not load the following vision enhancement providers:" + f"\n{providerNames}" + ) + gui.messageBox( + message, + # Translators: The title of the vision enhancement provider error message box. + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + parent, + ) + + +def showTerminationErrorForProviders( + parent: wx.Window, + providers: List[vision.providerInfo.ProviderInfo], +) -> None: + if not providers: + return + + if len(providers) == 1: + providerName = providers[0].translatedName + # Translators: This message is presented when + # NVDA is unable to gracefully terminate a single vision enhancement provider. + message = _(f"Could not gracefully terminate the {providerName} vision enhancement provider") + else: + providerNames = ", ".join(provider.translatedName for provider in providers) + # Translators: This message is presented when + # NVDA is unable to terminate multiple vision enhancement providers. + message = _( + "Could not gracefully terminate the following vision enhancement providers:\n" + f"{providerNames }" + ) + gui.messageBox( + message, + # Translators: The title of the vision enhancement provider error message box. + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + parent, + ) + + class VisionProviderStateControl(vision.providerBase.VisionProviderStateControl): """ Gives settings panels for vision enhancement providers a way to control a @@ -3077,97 +3135,74 @@ def __init__( self._providerInfo = providerInfo self._parent = parent + def getProviderInfo(self) -> vision.providerInfo.ProviderInfo: + return self._providerInfo + + def getProviderInstance(self) -> Optional[vision.providerBase.VisionEnhancementProvider]: + return vision.handler.providers.get(self._providerInfo.providerId, None) + def startProvider( self, - ) -> None: - """Initializes the provider in a way that is gui friendly, - showing an error if appropriate. + shouldPromptOnError: bool = True + ) -> bool: + """Initializes the provider, prompting user with the error if necessary. + @param shouldPromptOnError: True if the user should be presented with any errors that may occur. + @return: True on success + """ + success = self._doStartProvider() + if not success and shouldPromptOnError: + showStartErrorForProviders(self._parent, [self._providerInfo, ]) + return success + + def terminateProvider( + self, + shouldPromptOnError: bool = True + ) -> bool: + """Terminate the provider, prompting user with the error if necessary. + @param shouldPromptOnError: True if the user should be presented with any errors that may occur. + @return: True on success + """ + success = self._doTerminate() + if not success and shouldPromptOnError: + showTerminationErrorForProviders(self._parent, [self._providerInfo, ]) + return success + + def _doStartProvider(self) -> bool: + """Attempt to start the provider, catching any errors. + @return True on successful termination. """ - success = True - initErrors = [] try: vision.handler.initializeProvider(self._providerInfo) + return True except Exception: - initErrors.append(self._providerInfo.providerId) log.error( f"Could not initialize the {self._providerInfo.providerId} vision enhancement provider", exc_info=True ) - success = False - if not success and initErrors: - if len(initErrors) == 1: - # Translators: This message is presented when - # NVDA is unable to load a single vision enhancement provider. - message = _("Could not load the {provider} vision enhancement provider").format( - provider=initErrors[0] - ) - else: - initErrorsList = ", ".join(initErrors) - # Translators: This message is presented when - # NVDA is unable to load multiple vision enhancement providers. - message = _(f"Could not load the following vision enhancement providers:\n{initErrorsList}") - gui.messageBox( - message, - # Translators: The title of the vision enhancement provider error message box. - _("Vision Enhancement Provider Error"), - wx.OK | wx.ICON_WARNING, - self._parent - ) + return False - def terminateProvider( - self, - verbose: bool = False - ) -> None: - """Terminates one or more providers in a way that is gui friendly, - @verbose: Whether to show a termination error. - @returns: Whether initialization succeeded for all providers. + def _doTerminate(self) -> bool: + """Attempt to terminate the provider, catching any errors. + @return True on successful termination. """ - terminateErrors = [] try: # Terminating a provider from the gui should never save the settings. # This is because termination happens on the fly when unchecking check boxes. # Saving settings would be harmful if a user opens the vision panel, # then changes some settings and disables the provider. vision.handler.terminateProvider(self._providerInfo, saveSettings=False) + return True except Exception: - terminateErrors.append(self._providerInfo.providerId) log.error( f"Could not terminate the {self._providerInfo.providerId} vision enhancement provider", exc_info=True ) - - if terminateErrors: - if verbose: - if len(terminateErrors) == 1: - # Translators: This message is presented when - # NVDA is unable to gracefully terminate a single vision enhancement provider. - message = _( - "Could not gracefully terminate the {provider} vision enhancement provider" - ).format(provider=list(terminateErrors)[0]) - else: - terminateErrorsList = ", ".join(terminateErrors) - # Translators: This message is presented when - # NVDA is unable to termiante multiple vision enhancement providers. - message = _( - "Could not gracefully terminate the following vision enhancement providers:\n" - f"{terminateErrorsList}" - ) - gui.messageBox( - message, - # Translators: The title of the vision enhancement provider error message box. - _("Vision Enhancement Provider Error"), - wx.OK | wx.ICON_WARNING, - self._parent - ) - - def getProviderInstance(self) -> Optional[vision.providerBase.VisionEnhancementProvider]: - return vision.handler.providers.get(self._providerInfo.providerId, None) - - def getProviderInfo(self) -> vision.providerInfo.ProviderInfo: - return self._providerInfo + return False class VisionSettingsPanel(SettingsPanel): + settingsSizerHelper: guiHelper.BoxSizerHelper + providerPanelInstances: List[SettingsPanel] initialProviders: List[vision.providerInfo.ProviderInfo] # Translators: This is the label for the vision panel title = _("Vision") @@ -3221,8 +3256,13 @@ def safeInitProviders( """Initializes one or more providers in a way that is gui friendly, showing an error if appropriate. """ + errorProviders: List[vision.providerInfo.ProviderInfo] = [] for provider in providers: - VisionProviderStateControl(self, provider).startProvider() + with VisionProviderStateControl(self, provider) as control: + success = control.startProvider(shouldPromptOnError=False) + if not success: + errorProviders.append(provider) + showStartErrorForProviders(self, errorProviders) def safeTerminateProviders( self, @@ -3233,8 +3273,14 @@ def safeTerminateProviders( @verbose: Whether to show a termination error. @returns: Whether termination succeeded for all providers. """ + errorProviders: List[vision.providerInfo.ProviderInfo] = [] for provider in providers: - VisionProviderStateControl(self, provider).terminateProvider(verbose=verbose) + with VisionProviderStateControl(self, provider) as control: + success = control.terminateProvider(shouldPromptOnError=False) + if not success: + errorProviders.append(provider) + if verbose: + showTerminationErrorForProviders(self, errorProviders) def refreshPanel(self): self.Freeze() diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 74458db47e1..ddfda5a848b 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -46,17 +46,10 @@ class VisionProviderStateControl: """ @abstractmethod - def startProvider(self) -> None: - """Initializes the provider in a way that is GUI friendly, - showing an error if appropriate. - @note: Use getProviderInstance to determine success + def getProviderInfo(self): """ - - @abstractmethod - def terminateProvider(self, verbose: bool = False) -> None: - """Terminates the provider in a way that is GUI friendly, - @verbose: Whether to show a termination error. - @note: Use getProviderInstance to determine success + @return: The provider info + @rtype: providerInfo.ProviderInfo """ @abstractmethod @@ -66,10 +59,17 @@ def getProviderInstance(self): """ @abstractmethod - def getProviderInfo(self): + def startProvider(self, shouldPromptOnError: bool) -> bool: + """Initializes the provider, prompting user with the error if necessary. + @param shouldPromptOnError: True if the user should be presented with any errors that may occur. + @return: True on success """ - @return: The provider info - @rtype: providerInfo.ProviderInfo + + @abstractmethod + def terminateProvider(self, shouldPromptOnError: bool) -> bool: + """Terminate the provider, prompting user with the error if necessary. + @param shouldPromptOnError: True if the user should be presented with any errors that may occur. + @return: True on success """ From 94b28c99402699df15a8acd2ca3fda6a744a0232 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 6 Nov 2019 19:58:37 +0100 Subject: [PATCH 069/116] Ensure destruction of warning dialog for screencurtain --- source/visionEnhancementProviders/screenCurtain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 3c6d7f2844e..11ce9a5e625 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -280,14 +280,14 @@ def confirmInitWithUser(self) -> bool: if not settingsStorage.warnOnLoad: return True parent = self - dlg = WarnOnLoadDialog( + with WarnOnLoadDialog( screenCurtainSettingsStorage=settingsStorage, parent=parent - ) - res = dlg.ShowModal() - # WarnOnLoadDialog can change settings, reload them - self.updateDriverSettings() - return res == wx.YES + ) as dlg: + res = dlg.ShowModal() + # WarnOnLoadDialog can change settings, reload them + self.updateDriverSettings() + return res == wx.YES class ScreenCurtainProvider(vision.providerBase.VisionEnhancementProvider): From 3b886f93dc7ce7d4bf773d957c8ca392a2192240 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 6 Nov 2019 19:59:06 +0100 Subject: [PATCH 070/116] Improve readability of make settings method. --- source/gui/settingsDialogs.py | 46 ++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 0538a8b8a3f..60c6421cab0 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3215,12 +3215,35 @@ def _getProviderInfos(self) -> List[vision.providerInfo.ProviderInfo]: vision.visionHandler.getProviderInfo(providerId) for providerId in vision.handler.providers ) + def _createProviderSettingsPanel( + self, + providerInfo: vision.providerInfo.ProviderInfo + ) -> Optional[SettingsPanel]: + settingsPanelCls = providerInfo.providerClass.getSettingsPanelClass() + if not settingsPanelCls: + if gui._isDebug(): + log.debug(f"Using default panel for providerId: {providerInfo.providerId}") + settingsPanelCls = VisionProviderSubPanel_Wrapper + else: + if gui._isDebug(): + log.debug(f"Using custom panel for providerId: {providerInfo.providerId}") + + providerControl = VisionProviderStateControl(parent=self, providerInfo=providerInfo) + try: + return settingsPanelCls( + parent=self, + providerControl=providerControl + ) + # E722: bare except used since we can not know what exceptions a provider might throw. + # We should be able to continue despite a buggy provider. + except: # noqa: E722 + log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) + return None + def makeSettings(self, settingsSizer: wx.BoxSizer): self.initialProviders = self._getProviderInfos() self.providerPanelInstances = [] - self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) - self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) for providerInfo in vision.getProviderList(): @@ -3228,24 +3251,13 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), flag=wx.EXPAND ) - providerControl = VisionProviderStateControl(parent=self, providerInfo=providerInfo) + if len(self.providerPanelInstances) > 0: + settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) - settingsPanelCls = providerInfo.providerClass.getSettingsPanelClass() - if not settingsPanelCls: - log.debug(f"Using default panel for providerId: {providerInfo.providerId}") - settingsPanelCls = VisionProviderSubPanel_Wrapper - else: - log.debug(f"Using custom panel for providerId: {providerInfo.providerId}") - try: - settingsPanel = settingsPanelCls(parent=self, providerControl=providerControl) - # E722: bare except used since we can not know what exceptions a provider might throw. - # We should be able to continue despite a buggy provider. - except: # noqa: E722 - log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) + settingsPanel = self._createProviderSettingsPanel(providerInfo) + if not settingsPanel: continue - if len(self.providerPanelInstances) > 0: - settingsSizer.AddSpacer(guiHelper.SPACE_BETWEEN_VERTICAL_DIALOG_ITEMS) providerSizer.Add(settingsPanel, flag=wx.EXPAND) self.providerPanelInstances.append(settingsPanel) From 07a5637eb0d2fb3a35c0c1f5e5b29eed9a825333 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:29:36 +0100 Subject: [PATCH 071/116] Fix ScreenCurtain enable checkbox state after initialisation error --- source/visionEnhancementProviders/screenCurtain.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 11ce9a5e625..2d193da0d00 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -268,9 +268,7 @@ def _ensureEnableState(self, shouldBeEnabled: bool): currentlyEnabled = bool(self._providerControl.getProviderInstance()) if shouldBeEnabled and not currentlyEnabled: confirmed = self.confirmInitWithUser() - if confirmed: - self._providerControl.startProvider() - else: + if not confirmed or not self._providerControl.startProvider(): self._enabledCheckbox.SetValue(False) elif not shouldBeEnabled and currentlyEnabled: self._providerControl.terminateProvider() From 0697bad18ef988fd3caa275b55da8782dcfe9505 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:32:38 +0100 Subject: [PATCH 072/116] Fix initial state of screenCurtain enabled checkbox --- source/visionEnhancementProviders/screenCurtain.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 2d193da0d00..52153b3b35c 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -232,6 +232,8 @@ def _buildGui(self): # Translators: option to enable screen curtain in the vision settings panel label=_("Make screen black (immediate effect)") ) + isProviderActive = bool(self._providerControl.getProviderInstance()) + self._enabledCheckbox.SetValue(isProviderActive) self.mainSizer.Add(self._enabledCheckbox) self.mainSizer.AddSpacer(size=self.scaleSize(10)) From edb662f5292dbf57f292d34a5fec45cf8fb60d1b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:36:11 +0100 Subject: [PATCH 073/116] Refactor updateDriverSettings in in AutoSettingsMixin Extract implementation details into helper methods to highlight high level logic. --- source/gui/settingsDialogs.py | 86 +++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 60c6421cab0..7c84b5684f2 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1046,7 +1046,7 @@ def __call__(self,evt): class AutoSettingsMixin(metaclass=ABCMeta): """ - Mixin class that provides support for driver specific gui settings. + Mixin class that provides support for driver/vision provider specific gui settings. Derived classes should implement: - L{getSettings} - L{settingsSizer} @@ -1209,7 +1209,7 @@ def updateDriverSettings(self, changedSetting=None): if not settingsInst.isSupported(name): self.settingsSizer.Hide(sizer) # Create new controls, update already existing - if(gui._isDebug()): + if gui._isDebug(): log.debug(f"Current sizerDict: {self.sizerDict!r}") log.debug(f"Current supportedSettings: {self.getSettings().supportedSettings!r}") for setting in settingsInst.supportedSettings: @@ -1217,46 +1217,56 @@ def updateDriverSettings(self, changedSetting=None): # Changing a setting shouldn't cause that setting's own values to change. continue if setting.id in self.sizerDict: # update a value - self.settingsSizer.Show(self.sizerDict[setting.id]) - if isinstance(setting, NumericDriverSetting): - getattr(self, f"{setting.id}Slider").SetValue( - getattr(settingsStorage, setting.id) - ) - elif isinstance(setting, BooleanDriverSetting): - getattr(self, f"{setting.id}Checkbox").SetValue( - getattr(settingsStorage, setting.id) - ) - else: - options = getattr(self, f"_{setting.id}s") - lCombo = getattr(self, f"{setting.id}List") - try: - cur = getattr(settingsStorage, setting.id) - indexOfItem = [x.id for x in options].index(cur) - lCombo.SetSelection(indexOfItem) - except ValueError: - pass + self._updateValueForControl(setting, settingsStorage) else: # create a new control - if isinstance(setting, NumericDriverSetting): - settingMaker = self._makeSliderSettingControl - elif isinstance(setting, BooleanDriverSetting): - settingMaker = self._makeBooleanSettingControl - else: - settingMaker = self._makeStringSettingControl - try: - s = settingMaker(setting, settingsStorage) - except UnsupportedConfigParameterError: - log.debugWarning(f"Unsupported setting {setting.id}; ignoring", exc_info=True) - continue - self.sizerDict[setting.id] = s - self.settingsSizer.Insert( - len(self.sizerDict) - 1, - s, - border=10, - flag=wx.BOTTOM - ) + self._createNewControl(setting, settingsStorage) # Update graphical layout of the dialog self.settingsSizer.Layout() + def _createNewControl(self, setting, settingsStorage): + settingMaker = self._getSettingMaker(setting) + try: + s = settingMaker(setting, settingsStorage) + except UnsupportedConfigParameterError: + log.debugWarning(f"Unsupported setting {setting.id}; ignoring", exc_info=True) + else: + self.sizerDict[setting.id] = s + self.settingsSizer.Insert( + len(self.sizerDict) - 1, + s, + border=10, + flag=wx.BOTTOM + ) + + def _getSettingMaker(self, setting): + if isinstance(setting, NumericDriverSetting): + settingMaker = self._makeSliderSettingControl + elif isinstance(setting, BooleanDriverSetting): + settingMaker = self._makeBooleanSettingControl + else: + settingMaker = self._makeStringSettingControl + return settingMaker + + def _updateValueForControl(self, setting, settingsStorage): + self.settingsSizer.Show(self.sizerDict[setting.id]) + if isinstance(setting, NumericDriverSetting): + getattr(self, f"{setting.id}Slider").SetValue( + getattr(settingsStorage, setting.id) + ) + elif isinstance(setting, BooleanDriverSetting): + getattr(self, f"{setting.id}Checkbox").SetValue( + getattr(settingsStorage, setting.id) + ) + else: + options = getattr(self, f"_{setting.id}s") + lCombo = getattr(self, f"{setting.id}List") + try: + cur = getattr(settingsStorage, setting.id) + indexOfItem = [x.id for x in options].index(cur) + lCombo.SetSelection(indexOfItem) + except ValueError: + pass + def onDiscard(self): # unbind change events for string settings as wx closes combo boxes on cancel settingsInst = self.getSettings() From 9a3dd87d7fe1e89be84f30a2ae0ac10e81bae60b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:53:03 +0100 Subject: [PATCH 074/116] Remove noqa comment for bare Except Instead catch Exception. --- source/gui/settingsDialogs.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 7c84b5684f2..93a694c0c51 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -47,7 +47,7 @@ updateCheck = None import inputCore from . import nvdaControls -from driverHandler import * +from autoSettingsUtils.utils import UnsupportedConfigParameterError from autoSettingsUtils.autoSettings import AutoSettings from autoSettingsUtils.driverSetting import BooleanDriverSetting, NumericDriverSetting, DriverSetting from UIAUtils import shouldUseUIAConsole @@ -3244,9 +3244,9 @@ def _createProviderSettingsPanel( parent=self, providerControl=providerControl ) - # E722: bare except used since we can not know what exceptions a provider might throw. + # Broad except used since we can not know what exceptions a provider might throw. # We should be able to continue despite a buggy provider. - except: # noqa: E722 + except Exception: log.debug(f"Error creating providerPanel: {settingsPanelCls!r}", exc_info=True) return None @@ -3318,9 +3318,9 @@ def onDiscard(self): for panel in self.providerPanelInstances: try: panel.onDiscard() - # E722: bare except used since we can not know what exceptions a provider might throw. + # Broad except used since we can not know what exceptions a provider might throw. # We should be able to continue despite a buggy provider. - except: # noqa: E722 + except Exception: log.debug(f"Error discarding providerPanel: {panel.__class__!r}", exc_info=True) providersToInitialize = [ @@ -3341,9 +3341,9 @@ def onSave(self): for panel in self.providerPanelInstances: try: panel.onSave() - # E722: bare except used since we can not know what exceptions a provider might throw. + # Broad except used since we can not know what exceptions a provider might throw. # We should be able to continue despite a buggy provider. - except: # noqa: E722 + except Exception: log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = list( vision.visionHandler.getProviderInfo(providerId) @@ -3425,9 +3425,9 @@ def _createProviderSettings(self): settingsCallable=getSettingsCallable ) self._providerSettingsSizer.Add(self._providerSettings, flag=wx.EXPAND, proportion=1.0) - # E722: bare except used since we can not know what exceptions a provider might throw. + # Broad except used since we can not know what exceptions a provider might throw. # We should be able to continue despite a buggy provider. - except: # noqa: E722 + except Exception: log.error("unable to create provider settings", exc_info=True) return False return True From a35cdc27db704f3f8011b7c4f24e7d0f9e25083e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:54:02 +0100 Subject: [PATCH 075/116] Change error message for a provider GUI that can not be created --- source/gui/settingsDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 93a694c0c51..17d2af64e00 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3435,7 +3435,7 @@ def _createProviderSettings(self): def _nonEnableableGUI(self, evt): wx.MessageBox( # Translators: Shown when there is an error showing the GUI for a vision enhancement provider - _("Unable to configure GUI for Vision Enhancement Provider, it can not be enabled."), + _("Unable to configure user interface for Vision Enhancement Provider, it can not be enabled."), parent=self, ) self._checkBox.SetValue(False) From a8d51654e9dfcd0edaf70348d1660393e43b32da Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 18:58:12 +0100 Subject: [PATCH 076/116] Simplify _enableToggle logic --- source/gui/settingsDialogs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 17d2af64e00..beebbac4c68 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3441,12 +3441,12 @@ def _nonEnableableGUI(self, evt): self._checkBox.SetValue(False) def _enableToggle(self, evt): - if not evt.IsChecked(): - self._providerControl.terminateProvider() + if evt.IsChecked(): + self._providerControl.startProvider() self._providerSettings.updateDriverSettings() self._providerSettings.onPanelActivated() else: - self._providerControl.startProvider() + self._providerControl.terminateProvider() self._providerSettings.updateDriverSettings() self._providerSettings.onPanelActivated() self._sendLayoutUpdatedEvent() From e3f3998473beecefd6678c7946fbdcc947fbeaf1 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:00:27 +0100 Subject: [PATCH 077/116] Simplify and reorder imports --- source/vision/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 9f6b6b83ffb..3471367ffb0 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -10,9 +10,9 @@ Add-ons can provide their own provider using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ -from vision.providerInfo import ProviderInfo from . import visionHandler from .visionHandler import VisionHandler, getProviderClass +from .providerInfo import ProviderInfo import visionEnhancementProviders import config from typing import List, Optional From 5b6e86d53c635629f621989340588a9a2d34b083 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:05:53 +0100 Subject: [PATCH 078/116] Remove package level getProviderList method --- source/gui/settingsDialogs.py | 2 +- source/vision/__init__.py | 10 ---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index beebbac4c68..639dab25fb2 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3256,7 +3256,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) - for providerInfo in vision.getProviderList(): + for providerInfo in vision.visionHandler.getProviderList(): providerSizer = self.settingsSizerHelper.addItem( wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), flag=wx.EXPAND diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 3471367ffb0..1a85f44c63d 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -38,15 +38,5 @@ def terminate() -> None: handler = None -def getProviderList( - onlyStartable: bool = True -) -> List[ProviderInfo]: - """Gets a list of available vision enhancement providers - @param onlyStartable: excludes all providers for which the check method returns C{False}. - @return: Details of providers available - """ - return visionHandler.getProviderList(onlyStartable) - - def _isDebug() -> bool: return config.conf["debugLog"]["vision"] From 5c1c93adbc4fa5393365015208e8a689e93c50bf Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:07:13 +0100 Subject: [PATCH 079/116] Fix typo in comment --- source/vision/visionHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 6ab5cd8f45c..2bf28eff031 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -160,7 +160,7 @@ def terminateProvider( # therefore it is unknown what to expect. exception = e # Copy the configured providers before mutating the list. - # If we don't, configobj won't be aware of changes the list. + # If we don't, configobj won't be aware of changes in the list. configuredProviders: List = config.conf['vision']['providers'][:] try: configuredProviders.remove(providerId) From 25ad61ad979b1ebadf684c0d2e3e060c16c0efeb Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:14:27 +0100 Subject: [PATCH 080/116] Clarify comment --- source/visionEnhancementProviders/NVDAHighlighter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 39dacd31a88..f493eb74251 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -278,8 +278,9 @@ def _buildGui(self): self.SetSizer(self.mainSizer) def getSettings(self) -> NVDAHighlighterSettings: - # AutoSettingsMixin uses self.driver to get / set attributes matching the names of the settings. - # We want them set on this class. + # AutoSettingsMixin uses the getSettings method (via getSettingsStorage) to get the instance which is + # used to get / set attributes. The attributes must match the id's of the settings. + # We want them set on our settings instance. return VisionEnhancementProvider.getSettings() def makeSettings(self, sizer: wx.BoxSizer): From 5ca437d0388f6e3e1460ae9780ac0a32359044e0 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:17:43 +0100 Subject: [PATCH 081/116] Use US spelling for initialize --- source/vision/visionHandler.py | 4 ++-- source/visionEnhancementProviders/screenCurtain.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 2bf28eff031..571738d51be 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -185,7 +185,7 @@ def initializeProvider( ) -> None: """ Enables and activates the supplied provider. - @param provider: The provider to initialise. + @param provider: The provider to initialize. @param temporary: Whether the selected provider is enabled temporarily (e.g. as a fallback). This defaults to C{False}. If C{True}, no changes will be performed to the configuration. @@ -286,6 +286,6 @@ def handleConfigProfileSwitch(self) -> None: def initialFocus(self) -> None: if not api.getDesktopObject(): - # focus/review hasn't yet been initialised. + # focus/review hasn't yet been initialized. return self.handleGainFocus(api.getFocusObject()) diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 52153b3b35c..aba56146601 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -55,7 +55,7 @@ class Magnification: _MagShowSystemCursorFuncType = WINFUNCTYPE(BOOL, BOOL) _MagShowSystemCursorArgTypes = ((1, "showCursor"),) - # initialise + # initialize _MagInitializeFuncType = WINFUNCTYPE(BOOL) MagInitialize = _MagInitializeFuncType(("MagInitialize", _magnification)) MagInitialize.errcheck = _errCheck From 2ba8a2c28ccc7086477e20fb9a85c56ce5a7755e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:19:35 +0100 Subject: [PATCH 082/116] Clarify comments on getSettingsPanelClass(cls) --- source/visionEnhancementProviders/NVDAHighlighter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index f493eb74251..2751a08e4ce 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -350,14 +350,11 @@ def getSettings(cls) -> NVDAHighlighterSettings: @classmethod # impl required by vision.providerBase.VisionEnhancementProvider def getSettingsPanelClass(cls): - """Returns the instance to be used in order to construct a settings panel for the provider. + """Returns the class to be used in order to construct a settings panel for the provider. @return: Optional[SettingsPanel] @remarks: When None is returned, L{gui.settingsDialogs.VisionProviderSubPanel_Wrapper} is used. """ - # When using custom panel, dont change settings dynamically - # see comment in __init__ return NVDAHighlighterGuiPanel - # return None @classmethod # impl required by proivderBase.VisionEnhancementProvider def canStart(cls) -> bool: From b1362e9af7d53c210fa59659ffcc25ac34cb5254 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:22:16 +0100 Subject: [PATCH 083/116] Fix spelling of NVDAHighlighter class --- source/visionEnhancementProviders/NVDAHighlighter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 2751a08e4ce..d3faa27747e 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -326,12 +326,12 @@ def _onCheckEvent(self, evt: wx.CommandEvent): self.updateDriverSettings() else: self._updateEnabledState() - providerInst: Optional[NVDAHightlighter] = self._providerControl.getProviderInstance() + providerInst: Optional[NVDAHighlighter] = self._providerControl.getProviderInstance() if providerInst: providerInst.refresh() -class NVDAHightlighter(vision.providerBase.VisionEnhancementProvider): +class NVDAHighlighter(vision.providerBase.VisionEnhancementProvider): _ContextStyles = { Context.FOCUS: DASH_BLUE, Context.NAVIGATOR: SOLID_PINK, @@ -448,4 +448,4 @@ def _get_enabledContexts(self): ) -VisionEnhancementProvider = NVDAHightlighter +VisionEnhancementProvider = NVDAHighlighter From ecfb700ed0e5f5ca08c38190a35be0bddb25694e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:23:59 +0100 Subject: [PATCH 084/116] Fix unused imports --- source/vision/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 1a85f44c63d..7fba27f05a6 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -10,12 +10,10 @@ Add-ons can provide their own provider using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ -from . import visionHandler from .visionHandler import VisionHandler, getProviderClass -from .providerInfo import ProviderInfo import visionEnhancementProviders import config -from typing import List, Optional +from typing import Optional handler: Optional[VisionHandler] = None From c5116af3579bdd2f0435710f75de6f746e4596a8 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:31:08 +0100 Subject: [PATCH 085/116] Remove duplicated methods Driver inherits from AutoSettings, which already provides these methods. Moved the documentation to the AutoSettings impl. --- source/autoSettingsUtils/autoSettings.py | 17 +++++++++++++ source/driverHandler.py | 31 +----------------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index a9524b621f7..e9284dc5e02 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -244,8 +244,25 @@ def loadSettings(self, onlyChanged: bool = False): @classmethod def _paramToPercent(cls, current, min, max): + """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. + @param current: The current value. + @type current: int + @param min: The minimum value. + @type current: int + @param max: The maximum value. + @type max: int + """ return paramToPercent(current, min, max) @classmethod def _percentToParam(cls, percent, min, max): + """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum + raw parameter values. + @param percent: The current percentage. + @type percent: int + @param min: The minimum raw parameter value. + @type min: int + @param max: The maximum raw parameter value. + @type max: int + """ return percentToParam(percent, min, max) diff --git a/source/driverHandler.py b/source/driverHandler.py index 77a87f35c0a..74a26833166 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -6,10 +6,6 @@ """Handler for driver functionality that is global to synthesizers and braille displays.""" from autoSettingsUtils.autoSettings import AutoSettings -from autoSettingsUtils.utils import ( - paramToPercent, - percentToParam -) # F401: the following imports, while unused in this file, are provided for backwards compatibility. from autoSettingsUtils.driverSetting import ( # noqa: F401 @@ -76,38 +72,13 @@ def check(cls): """ return False - - @classmethod - def _paramToPercent(cls, current, min, max): - """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. - @param current: The current value. - @type current: int - @param min: The minimum value. - @type current: int - @param max: The maximum value. - @type max: int - """ - return paramToPercent(current, min, max) - - @classmethod - def _percentToParam(cls, percent, min, max): - """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum raw parameter values. - @param percent: The current percentage. - @type percent: int - @param min: The minimum raw parameter value. - @type min: int - @param max: The maximum raw parameter value. - @type max: int - """ - return percentToParam(percent, min, max) - # Impl for abstract methods in AutoSettings class @classmethod def getId(cls) -> str: return cls.name @classmethod - def getTranslatedName(cls) -> str: # todo rename to getTranslatedName + def getTranslatedName(cls) -> str: return cls.description @classmethod From ee52b651cf88890ffbb752aefee8838315410391 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 7 Nov 2019 19:34:58 +0100 Subject: [PATCH 086/116] Fix missing call to super from terminate --- source/visionEnhancementProviders/NVDAHighlighter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index d3faa27747e..51f0bf53ba9 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -387,6 +387,7 @@ def terminate(self): self._highlighterThread = None winGDI.gdiPlusTerminate() self.contextToRectMap.clear() + super().terminate() def _run(self): if vision._isDebug(): From 4a02c38eebaacbc0b7d273934c1ca05e34462811 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 10:55:43 +0100 Subject: [PATCH 087/116] Change getSettings to abstract Implementors of VisionEnhancementProvider must provide a settings instance. --- source/vision/providerBase.py | 5 ++--- .../visionEnhancementProviders/NVDAHighlighter.py | 14 +++++++++----- .../exampleProvider_autoGui.py | 3 ++- source/visionEnhancementProviders/screenCurtain.py | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index ddfda5a848b..9b4c1130557 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -9,7 +9,7 @@ from abc import abstractmethod, ABC -from autoSettingsUtils.autoSettings import AutoSettings, SupportedSettingType +from autoSettingsUtils.autoSettings import AutoSettings from baseObject import AutoPropertyObject from .visionHandlerExtensionPoints import EventExtensionPoints from typing import Optional, Any @@ -29,8 +29,6 @@ class VisionEnhancementProviderSettings(AutoSettings, ABC): - AutoSettings._get_preInitSettings: The settings always configurable for your provider """ - supportedSettings: SupportedSettingType # Typing for autoprop L{_get_supportedSettings} - def __init__(self): super().__init__() self.initSettings() # ensure that settings are loaded at construction time. @@ -90,6 +88,7 @@ class VisionEnhancementProvider(AutoPropertyObject): cachePropertiesByDefault = True @classmethod + @abstractmethod def getSettings(cls) -> VisionEnhancementProviderSettings: """ @remarks: The L{VisionEnhancementProviderSettings} class should be implemented to define the settings diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 51f0bf53ba9..778e36f23dd 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -9,8 +9,9 @@ import vision from vision.constants import Context -from vision.providerBase import SupportedSettingType +from autoSettingsUtils.autoSettings import SupportedSettingType from vision.util import getContextRect +from vision.visionHandlerExtensionPoints import EventExtensionPoints from windowUtils import CustomWindow import wx import gui @@ -344,11 +345,11 @@ class NVDAHighlighter(vision.providerBase.VisionEnhancementProvider): enabledContexts: Tuple[Context] # type info for autoprop: L{_get_enableContexts} - @classmethod + @classmethod # override def getSettings(cls) -> NVDAHighlighterSettings: return cls._settings - @classmethod # impl required by vision.providerBase.VisionEnhancementProvider + @classmethod # override def getSettingsPanelClass(cls): """Returns the class to be used in order to construct a settings panel for the provider. @return: Optional[SettingsPanel] @@ -356,11 +357,14 @@ def getSettingsPanelClass(cls): """ return NVDAHighlighterGuiPanel - @classmethod # impl required by proivderBase.VisionEnhancementProvider + @classmethod # override def canStart(cls) -> bool: return True - def registerEventExtensionPoints(self, extensionPoints): + def registerEventExtensionPoints( # override + self, + extensionPoints: EventExtensionPoints + ) -> None: extensionPoints.post_focusChange.register(self.handleFocusChange) extensionPoints.post_reviewMove.register(self.handleReviewMove) extensionPoints.post_browseModeMove.register(self.handleBrowseModeMove) diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/exampleProvider_autoGui.py index c7464d80ed2..f2232acde3a 100644 --- a/source/visionEnhancementProviders/exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/exampleProvider_autoGui.py @@ -7,7 +7,8 @@ import driverHandler import wx from autoSettingsUtils.utils import StringParameterInfo -from vision.providerBase import VisionEnhancementProviderSettings, SupportedSettingType +from autoSettingsUtils.autoSettings import SupportedSettingType +from vision.providerBase import VisionEnhancementProviderSettings from typing import Optional, Type, Any, List """Example provider, which demonstrates using the automatically constructed GUI. diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index aba56146601..36b458c715f 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -11,13 +11,13 @@ import winVersion from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError from ctypes.wintypes import BOOL -import driverHandler +from autoSettingsUtils.driverSetting import BooleanDriverSetting +from autoSettingsUtils.autoSettings import SupportedSettingType import wx import gui from logHandler import log from vision.providerBase import ( VisionEnhancementProviderSettings, - SupportedSettingType, ) from typing import Optional, Type @@ -112,7 +112,7 @@ def getTranslatedName(cls) -> str: @classmethod def _get_preInitSettings(cls) -> SupportedSettingType: return [ - driverHandler.BooleanDriverSetting( + BooleanDriverSetting( "warnOnLoad", warnOnLoadCheckBoxText, defaultVal=True From 64794f0415751374440b8c0ed30c2bf391040c3b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 11:20:57 +0100 Subject: [PATCH 088/116] Catch any error on re-init terminate method inside providerInst.reinitialize may raise. Even though all callers to VisionHandler.initializeProvider should handle exceptions anyway, adding a try block to log it and re-raise clarifies the control flow. --- source/vision/visionHandler.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 571738d51be..19abee30dd5 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -189,16 +189,21 @@ def initializeProvider( @param temporary: Whether the selected provider is enabled temporarily (e.g. as a fallback). This defaults to C{False}. If C{True}, no changes will be performed to the configuration. + @note: On error, an an Exception is raised. """ providerId = provider.providerId providerInst = self.providers.pop(providerId, None) if providerInst is not None: - providerInst.reinitialize() + try: + providerInst.reinitialize() + except Exception as e: + log.error(f"Error while re-initialising {providerId}") + raise e else: providerCls = provider.providerClass if not providerCls.canStart(): raise exceptions.ProviderInitException( - f"Trying to initialize provider {providerId!r} which reported being unable to start" + f"Trying to initialize provider {providerId} which reported being unable to start" ) # Initialize the provider. providerInst = providerCls() From fe4bc91a93e1cefe7260e0edc3bd63c1faaef62c Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 11:38:36 +0100 Subject: [PATCH 089/116] Fix lint --- source/gui/settingsDialogs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 639dab25fb2..b55af870c0c 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2856,6 +2856,7 @@ def onOk(self, evt): self.Parent.updateCurrentDisplay() super(BrailleDisplaySelectionDialog, self).onOk(evt) + class BrailleSettingsSubPanel(AutoSettingsMixin, SettingsPanel): @property From 20813a2e0e6241810ebb9ab65416639ed9639088 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 12:57:26 +0100 Subject: [PATCH 090/116] Fix translation test --- source/gui/settingsDialogs.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index b55af870c0c..a0dc1eceafb 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -35,6 +35,7 @@ import brailleTables import brailleInput import vision +import vision.providerInfo import vision.providerBase from typing import Callable, List, Optional, Any import core @@ -3088,12 +3089,9 @@ def showStartErrorForProviders( message = _(f"Could not load the {providerName} vision enhancement provider") else: providerNames = ", ".join(provider.translatedName for provider in providers) - # Translators: This message is presented when - # NVDA is unable to load multiple vision enhancement providers. - message = _( - f"Could not load the following vision enhancement providers:" - f"\n{providerNames}" - ) + # Translators: This message is presented when NVDA is unable to + # load multiple vision enhancement providers. + message = _(f"Could not load the following vision enhancement providers:\n{providerNames}") gui.messageBox( message, # Translators: The title of the vision enhancement provider error message box. From d587e5a204c96547f795a5c3cb2a05106f452281 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 13:01:12 +0100 Subject: [PATCH 091/116] Rename example provider So that it no longer appears in NVDA settings. --- .../{exampleProvider_autoGui.py => _exampleProvider_autoGui.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename source/visionEnhancementProviders/{exampleProvider_autoGui.py => _exampleProvider_autoGui.py} (100%) diff --git a/source/visionEnhancementProviders/exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py similarity index 100% rename from source/visionEnhancementProviders/exampleProvider_autoGui.py rename to source/visionEnhancementProviders/_exampleProvider_autoGui.py From 3f3f7c07e576e4857e1a8c0d0575b3bbea3ba9c2 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Fri, 8 Nov 2019 13:02:01 +0100 Subject: [PATCH 092/116] Add instructions to test the example provider --- source/visionEnhancementProviders/_exampleProvider_autoGui.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index f2232acde3a..389dcaefdcd 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -11,7 +11,8 @@ from vision.providerBase import VisionEnhancementProviderSettings from typing import Optional, Type, Any, List -"""Example provider, which demonstrates using the automatically constructed GUI. +"""Example provider, which demonstrates using the automatically constructed GUI. Rename this file, removing + the first underscore to test it with NVDA. For examples of overriding the GUI and using a custom implementation, see NVDAHighlighter or ScreenCurtain. From 62820ca86764e8746dba0f8b00078ff20e750bcf Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:20:10 +0100 Subject: [PATCH 093/116] Clean up param/percent conv functions Restore type hints, remove types from doc-string. Remove unncessary cast --- source/autoSettingsUtils/autoSettings.py | 12 +++--------- source/autoSettingsUtils/utils.py | 8 ++++---- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index e9284dc5e02..cdb58f003c7 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -243,26 +243,20 @@ def loadSettings(self, onlyChanged: bool = False): self._loadSpecificSettings(self, self.supportedSettings, onlyChanged) @classmethod - def _paramToPercent(cls, current, min, max): + def _paramToPercent(cls, current: int, min: int, max: int) -> int: """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. @param current: The current value. - @type current: int @param min: The minimum value. - @type current: int @param max: The maximum value. - @type max: int """ return paramToPercent(current, min, max) @classmethod - def _percentToParam(cls, percent, min, max): + def percentToParam(cls, percent: int, min: int, max: int) -> int: """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum - raw parameter values. + raw parameter values. @param percent: The current percentage. - @type percent: int @param min: The minimum raw parameter value. - @type min: int @param max: The maximum raw parameter value. - @type max: int """ return percentToParam(percent, min, max) diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py index 5be9aab437a..0bbc1e01d35 100644 --- a/source/autoSettingsUtils/utils.py +++ b/source/autoSettingsUtils/utils.py @@ -8,7 +8,7 @@ """ -def paramToPercent(current, min, max): +def paramToPercent(current: int, min: int, max: int) -> int: """Convert a raw parameter value to a percentage given the current, minimum and maximum raw values. @param current: The current value. @type current: int @@ -17,10 +17,10 @@ def paramToPercent(current, min, max): @param max: The maximum value. @type max: int """ - return int(round(float(current - min) / (max - min) * 100)) + return round(float(current - min) / (max - min) * 100) -def percentToParam(percent, min, max): +def percentToParam(percent: int, min: int, max: int) -> int: """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum raw parameter values. @param percent: The current percentage. @@ -30,7 +30,7 @@ def percentToParam(percent, min, max): @param max: The maximum raw parameter value. @type max: int """ - return int(round(float(percent) / 100 * (max - min) + min)) + return round(float(percent) / 100 * (max - min) + min) class UnsupportedConfigParameterError(NotImplementedError): From e52d2b6006b6942edb5646a2a7be679bc9f2262e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:21:15 +0100 Subject: [PATCH 094/116] Add missing translator comments --- source/gui/settingsDialogs.py | 15 +++++++++++---- tests/checkPot.py | 1 - 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index a0dc1eceafb..cefef29a237 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2845,10 +2845,17 @@ def onOk(self, evt): port = self.possiblePorts[self.portsList.GetSelection()][0] config.conf["braille"][display]["port"] = port if not braille.handler.setDisplayByName(display): - # Translators: This message is presented when - # NVDA is unable to load the selected - # braille display. - gui.messageBox(_("Could not load the %s display.")%display, _("Braille Display Error"), wx.OK|wx.ICON_WARNING, self) + + gui.messageBox( + # Translators: The message in a dialog presented when NVDA is unable to load the selected + # braille display. + message=_(f"Could not load the {display} display."), + # Translators: The title in a dialog presented when NVDA is unable to load the selected + # braille display. + caption=_("Braille Display Error"), + style=wx.OK | wx.ICON_WARNING, + parent=self + ) return if self.IsModal(): diff --git a/tests/checkPot.py b/tests/checkPot.py index 10a03c3ff91..60e60012ea0 100644 --- a/tests/checkPot.py +++ b/tests/checkPot.py @@ -81,7 +81,6 @@ 'Insufficient Privileges', 'Synthesizer Error', 'Dictionary Entry Error', - 'Braille Display Error', 'word', 'Taskbar', '%s items', From 223d4ddb71e1e48718e8e1e2d939cff040262e97 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:21:57 +0100 Subject: [PATCH 095/116] remove unused properties --- source/autoSettingsUtils/utils.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/source/autoSettingsUtils/utils.py b/source/autoSettingsUtils/utils.py index 0bbc1e01d35..8ffaa689149 100644 --- a/source/autoSettingsUtils/utils.py +++ b/source/autoSettingsUtils/utils.py @@ -43,16 +43,13 @@ class StringParameterInfo(object): """ Used to represent a value of a DriverSetting instance. """ + id: str + displayName: str - def __init__(self, id, displayName): + def __init__(self, id: str, displayName: str): """ @param id: The unique identifier of the value. - @type id: str @param displayName: The name of the value, visible to the user. - @type displayName: str """ self.id = id self.displayName = displayName - # Keep backwards compatibility - self.ID = id - self.name = displayName From 6b4e7ddfbf2683dd57913ce7cb33a9939a8675fb Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:22:13 +0100 Subject: [PATCH 096/116] Fix incorrect type hint --- source/gui/settingsDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index cefef29a237..2893021bf48 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -300,7 +300,7 @@ def _buildGui(self): self.SetSizer(self.mainSizer) @abstractmethod - def makeSettings(self, sizer) -> wx.BoxSizer: + def makeSettings(self, sizer: wx.BoxSizer): """Populate the panel with settings controls. Subclasses must override this method. @param sizer: The sizer to which to add the settings controls. From 9c25e259bf34422fc4fb1072511313d14dbf6587 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:22:30 +0100 Subject: [PATCH 097/116] Fix doc-string --- source/vision/providerBase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 9b4c1130557..5f50afa2505 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -122,7 +122,7 @@ def reinitialize(self) -> None: @abstractmethod def terminate(self) -> None: - """Terminate this driver. + """Terminate this provider. This should be used for any required clean up. @precondition: L{initialize} has been called. @postcondition: This provider can no longer be used. From 6436b1d9d41cbe24649cdf1d1da60e40c0626709 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 13:26:28 +0100 Subject: [PATCH 098/116] Use ABC from base class --- source/vision/providerBase.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 5f50afa2505..028ea087891 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -7,7 +7,7 @@ """Module within the vision framework that contains the base vision enhancement provider class. """ -from abc import abstractmethod, ABC +from abc import abstractmethod from autoSettingsUtils.autoSettings import AutoSettings from baseObject import AutoPropertyObject @@ -15,7 +15,7 @@ from typing import Optional, Any -class VisionEnhancementProviderSettings(AutoSettings, ABC): +class VisionEnhancementProviderSettings(AutoSettings): """ Base class for settings for a vision enhancement provider. Ensure that the following are implemented: From f8da0c592e34fb527ee0c63cdd7752c07a1394dd Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 15:13:17 +0100 Subject: [PATCH 099/116] Refactor provider list generation cache provider lists. --- source/vision/visionHandler.py | 78 +++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 34 deletions(-) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 19abee30dd5..2ab7ed28a01 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -49,56 +49,66 @@ def getProviderClass( raise initialException -def getProviderList( - onlyStartable: bool = True -) -> List[providerInfo.ProviderInfo]: - """Gets a list of available vision enhancement provider information - @param onlyStartable: excludes all providers for which the check method returns C{False}. - @return: List of available providers - """ - providerList = [] +def _getProvidersFromFileSystem(): for loader, moduleName, isPkg in pkgutil.iter_modules(visionEnhancementProviders.__path__): if moduleName.startswith('_'): continue try: + # Get each piece of info in a new statement so any exceptions raised identifies the line correctly. provider = getProviderClass(moduleName) - except Exception: - # Purposely catch everything. - # A provider can raise whatever exception it likes, - # therefore it is unknown what to expect. + providerSettings = provider.getSettings() + providerId = providerSettings.getId() + translatedName = providerSettings.getTranslatedName() + yield providerInfo.ProviderInfo( + providerId=providerId, + moduleName=moduleName, + translatedName=translatedName, + providerClass=provider + ) + except Exception: # Purposely catch everything as we don't know what a provider might raise. log.error( f"Error while importing vision enhancement provider module {moduleName}", exc_info=True ) continue + + +allProviders: List[providerInfo.ProviderInfo] + + +def getProviderList( + onlyStartable: bool = True, + reloadFromSystem: bool = False, +) -> List[providerInfo.ProviderInfo]: + """Gets a list of available vision enhancement provider information + @param onlyStartable: excludes all providers for which the check method returns C{False}. + @return: List of available providers + """ + global allProviders + if reloadFromSystem or not allProviders: + allProviders = list(_getProvidersFromFileSystem()) + # Sort the providers alphabetically by name. + allProviders.sort(key=lambda info: info.translatedName.lower()) + + providerList = [] + for provider in allProviders: try: - if not onlyStartable or provider.canStart(): - providerSettings = provider.getSettings() - providerList.append( - providerInfo.ProviderInfo( - providerId=providerSettings.getId(), - moduleName=moduleName, - translatedName=providerSettings.getTranslatedName(), - providerClass=provider - ) - ) + providerCanStart = provider.providerClass.canStart() + except Exception: # Purposely catch everything as we don't know what a provider might raise. + log.error(f"Error calling canStart for provider {provider.moduleName}", exc_info=True) + else: + if not onlyStartable or providerCanStart: + providerList.append(provider) else: log.debugWarning( - f"Excluding Vision enhancement provider module {moduleName} which is unable to start" + f"Excluding Vision enhancement provider module {provider.moduleName} which is unable to start" ) - except Exception: - # Purposely catch everything else as we don't want one failing provider - # make it impossible to list all the others. - log.error("", exc_info=True) - # Sort the providers alphabetically by name. - providerList.sort(key=lambda info: info.translatedName.lower()) return providerList def getProviderInfo(providerId: providerInfo.ProviderIdT) -> Optional[providerInfo.ProviderInfo]: - # This mechanism of getting the provider list and looking it up is particularly inefficient, but, before - # refactoring, confirm that getProviderList is / isn't cached. - for p in getProviderList(onlyStartable=False): + global allProviders + for p in allProviders: if p.providerId == providerId: return p raise LookupError(f"Provider with id ({providerId}) does not exist.") @@ -133,8 +143,8 @@ def terminateProvider( saveSettings: bool = True ) -> None: """Terminates a currently active provider. - When termnation fails, an exception is raised. - Yet, the provider wil lbe removed from the providers dictionary, + When termination fails, an exception is raised. + Yet, the provider will be removed from the providers dictionary, so its instance goes out of scope and wil lbe garbage collected. @param provider: The provider to terminate. @param saveSettings: Whether settings should be saved on termination. From 0753c54c1743964f4e6f51d22abd06e6a70d52e2 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 17:26:01 +0100 Subject: [PATCH 100/116] Make getProviderClass private Instead it is exposed through ProviderInfo class --- source/globalCommands.py | 2 +- source/vision/__init__.py | 2 +- source/vision/visionHandler.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index c151b866878..71a21749119 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2418,7 +2418,7 @@ def script_toggleScreenCurtain(self, gesture): scriptCount in (0, 1) # 1 press (temp enable) or 2 presses (enable) ): # Check if screen curtain is available, exit early if not. - if not vision.getProviderClass(screenCurtainId).canStart(): + if not screenCurtainProviderInfo.providerClass.canStart(): # Translators: Reported when the screen curtain is not available. message = _("Screen curtain not available") self._toggleScreenCurtainMessage = message diff --git a/source/vision/__init__.py b/source/vision/__init__.py index 7fba27f05a6..70fbd8b8627 100644 --- a/source/vision/__init__.py +++ b/source/vision/__init__.py @@ -10,7 +10,7 @@ Add-ons can provide their own provider using modules in the visionEnhancementProviders package containing a L{VisionEnhancementProvider} class. """ -from .visionHandler import VisionHandler, getProviderClass +from .visionHandler import VisionHandler import visionEnhancementProviders import config from typing import Optional diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 2ab7ed28a01..2e36b2b2ddd 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -25,7 +25,7 @@ from . import exceptions -def getProviderClass( +def _getProviderClass( moduleName: str, caseSensitive: bool = True ) -> Type[VisionEnhancementProvider]: @@ -55,7 +55,7 @@ def _getProvidersFromFileSystem(): continue try: # Get each piece of info in a new statement so any exceptions raised identifies the line correctly. - provider = getProviderClass(moduleName) + provider = _getProviderClass(moduleName) providerSettings = provider.getSettings() providerId = providerSettings.getId() translatedName = providerSettings.getTranslatedName() From 226d436d15737fd4af6d436f5397030f205a2598 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 18:20:31 +0100 Subject: [PATCH 101/116] Move getProviderList and getProviderInfo functions Make them instance methods on VisionHandler for greater API consistency. There is also very little sense in calling these before the handler is initialised. --- source/globalCommands.py | 2 +- source/gui/settingsDialogs.py | 8 ++-- source/vision/visionHandler.py | 87 +++++++++++++++++----------------- 3 files changed, 49 insertions(+), 48 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index 71a21749119..b80c052e056 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2358,7 +2358,7 @@ def script_toggleScreenCurtain(self, gesture): from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider screenCurtainId = ScreenCurtainProvider.getSettings().getId() - screenCurtainProviderInfo = vision.visionHandler.getProviderInfo(screenCurtainId) + screenCurtainProviderInfo = vision.handler.getProviderInfo(screenCurtainId) alreadyRunning = screenCurtainId in vision.handler.providers GlobalCommands._tempEnableScreenCurtain = scriptCount == 0 diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 2893021bf48..a73fbcda2ce 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3228,7 +3228,7 @@ class VisionSettingsPanel(SettingsPanel): def _getProviderInfos(self) -> List[vision.providerInfo.ProviderInfo]: return list( - vision.visionHandler.getProviderInfo(providerId) for providerId in vision.handler.providers + vision.handler.getProviderInfo(providerId) for providerId in vision.handler.providers ) def _createProviderSettingsPanel( @@ -3262,7 +3262,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) - for providerInfo in vision.visionHandler.getProviderList(): + for providerInfo in vision.handler.getProviderList(reloadFromSystem=True): providerSizer = self.settingsSizerHelper.addItem( wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), flag=wx.EXPAND @@ -3338,7 +3338,7 @@ def onDiscard(self): providerInfo.providerId for providerInfo in self.initialProviders ] providersToTerminate = [ - vision.visionHandler.getProviderInfo(providerId) for providerId in vision.handler.providers + vision.handler.getProviderInfo(providerId) for providerId in vision.handler.providers if providerId not in initialProviderIds ] self.safeTerminateProviders(providersToTerminate) @@ -3352,7 +3352,7 @@ def onSave(self): except Exception: log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) self.initialProviders = list( - vision.visionHandler.getProviderInfo(providerId) + vision.handler.getProviderInfo(providerId) for providerId in vision.handler.providers ) diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 2e36b2b2ddd..1fd225e3efa 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -73,47 +73,6 @@ def _getProvidersFromFileSystem(): continue -allProviders: List[providerInfo.ProviderInfo] - - -def getProviderList( - onlyStartable: bool = True, - reloadFromSystem: bool = False, -) -> List[providerInfo.ProviderInfo]: - """Gets a list of available vision enhancement provider information - @param onlyStartable: excludes all providers for which the check method returns C{False}. - @return: List of available providers - """ - global allProviders - if reloadFromSystem or not allProviders: - allProviders = list(_getProvidersFromFileSystem()) - # Sort the providers alphabetically by name. - allProviders.sort(key=lambda info: info.translatedName.lower()) - - providerList = [] - for provider in allProviders: - try: - providerCanStart = provider.providerClass.canStart() - except Exception: # Purposely catch everything as we don't know what a provider might raise. - log.error(f"Error calling canStart for provider {provider.moduleName}", exc_info=True) - else: - if not onlyStartable or providerCanStart: - providerList.append(provider) - else: - log.debugWarning( - f"Excluding Vision enhancement provider module {provider.moduleName} which is unable to start" - ) - return providerList - - -def getProviderInfo(providerId: providerInfo.ProviderIdT) -> Optional[providerInfo.ProviderInfo]: - global allProviders - for p in allProviders: - if p.providerId == providerId: - return p - raise LookupError(f"Provider with id ({providerId}) does not exist.") - - class VisionHandler(AutoPropertyObject): """The singleton vision handler is the core of the vision framework. It performs the following tasks: @@ -134,9 +93,51 @@ def postGuiInit(self) -> None: This is executed on the main thread by L{__init__} using the events queue. This ensures that the gui is fully initialized before providers are initialized that might rely on it. """ + self._updateAllProvidersList() self.handleConfigProfileSwitch() config.post_configProfileSwitch.register(self.handleConfigProfileSwitch) + _allProviders: List[providerInfo.ProviderInfo] = [] + + def _updateAllProvidersList(self): + self._allProviders = list(_getProvidersFromFileSystem()) + # Sort the providers alphabetically by name. + self._allProviders.sort(key=lambda info: info.translatedName.lower()) + + def getProviderList( + self, + onlyStartable: bool = True, + reloadFromSystem: bool = False, + ) -> List[providerInfo.ProviderInfo]: + """Gets a list of available vision enhancement provider information + @param onlyStartable: excludes all providers for which the check method returns C{False}. + @param reloadFromSystem: ensure the list is fresh. Providers may have been added to the file system. + @return: List of available providers + """ + if reloadFromSystem or not self._allProviders: + self._updateAllProvidersList() + + providerList = [] + for provider in self._allProviders: + try: + providerCanStart = provider.providerClass.canStart() + except Exception: # Purposely catch everything as we don't know what a provider might raise. + log.error(f"Error calling canStart for provider {provider.moduleName}", exc_info=True) + else: + if not onlyStartable or providerCanStart: + providerList.append(provider) + else: + log.debugWarning( + f"Excluding Vision enhancement provider module {provider.moduleName} which is unable to start" + ) + return providerList + + def getProviderInfo(self, providerId: providerInfo.ProviderIdT) -> Optional[providerInfo.ProviderInfo]: + for p in self._allProviders: + if p.providerId == providerId: + return p + raise LookupError(f"Provider with id ({providerId}) does not exist.") + def terminateProvider( self, provider: providerInfo.ProviderInfo, @@ -282,7 +283,7 @@ def handleConfigProfileSwitch(self) -> None: providersToTerminate = curProviders - configuredProviders for providerId in providersToTerminate: try: - providerInfo = getProviderInfo(providerId) + providerInfo = self.getProviderInfo(providerId) self.terminateProvider(providerInfo) except Exception: log.error( @@ -291,7 +292,7 @@ def handleConfigProfileSwitch(self) -> None: ) for providerId in providersToInitialize: try: - providerInfo = getProviderInfo(providerId) + providerInfo = self.getProviderInfo(providerId) self.initializeProvider(providerInfo) except Exception: log.error( From 71d30da319db56748a1519a98c78897e0860b9b3 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 18:39:23 +0100 Subject: [PATCH 102/116] Fix no contextmanager --- source/gui/settingsDialogs.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index a73fbcda2ce..d6e007491a0 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3303,8 +3303,7 @@ def safeTerminateProviders( """ errorProviders: List[vision.providerInfo.ProviderInfo] = [] for provider in providers: - with VisionProviderStateControl(self, provider) as control: - success = control.terminateProvider(shouldPromptOnError=False) + success = VisionProviderStateControl(self, provider).terminateProvider(shouldPromptOnError=False) if not success: errorProviders.append(provider) if verbose: From eb4498f7744cb0b9c2d9d3e91cce451ef625a29b Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 18:59:20 +0100 Subject: [PATCH 103/116] Fix documentation for runtime settings --- source/vision/providerBase.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 028ea087891..df4966f5481 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -22,12 +22,22 @@ class VisionEnhancementProviderSettings(AutoSettings): - AutoSettings.getId: This is case sensitive. Used in the config file. Does not have to match the module name. - AutoSettings.getTranslatedName: - The string that should appear in the GUI as the name + The string that should appear in the GUI as the name. - AutoSettings._get_supportedSettings: - The "runtime" settings for your provider + The "runtime" settings for your provider. By default this just returns L{_get_preInitSettings}. + The implementation must handle how to modify the returned settings based on external (software, + hardware) dependencies. Although technically optional, derived classes probably need to implement: - AutoSettings._get_preInitSettings: - The settings always configurable for your provider + The settings that are always configurable for your provider. + @note + If the vision enhancement provider has settings, it will provide an implementation of this class. + The provider will hold a reference to an instance of this class, this is accessed through the class method + L{VisionEnhancementProvider.getSettings}. + One way to handle settings that are strictly runtime: + - During initialization, the vision enhancement provider can instruct the settings instance what it should + expose using the L{utoSettings._get_supportedSettings} property. + - "_exampleProvider_autoGui.py" provides an example of this. """ def __init__(self): super().__init__() From 5be590a8c49f6b4f755951c0d2dd45d7786aadd7 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 19:23:20 +0100 Subject: [PATCH 104/116] Fix error on termination handling for autoGui providers --- source/gui/settingsDialogs.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index d6e007491a0..e2a72b1d38f 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3128,13 +3128,13 @@ def showTerminationErrorForProviders( "Could not gracefully terminate the following vision enhancement providers:\n" f"{providerNames }" ) - gui.messageBox( - message, - # Translators: The title of the vision enhancement provider error message box. - _("Vision Enhancement Provider Error"), - wx.OK | wx.ICON_WARNING, - parent, - ) + gui.messageBox( + message, + # Translators: The title of the vision enhancement provider error message box. + _("Vision Enhancement Provider Error"), + wx.OK | wx.ICON_WARNING, + parent, + ) class VisionProviderStateControl(vision.providerBase.VisionProviderStateControl): @@ -3446,14 +3446,18 @@ def _nonEnableableGUI(self, evt): self._checkBox.SetValue(False) def _enableToggle(self, evt): - if evt.IsChecked(): - self._providerControl.startProvider() - self._providerSettings.updateDriverSettings() - self._providerSettings.onPanelActivated() - else: - self._providerControl.terminateProvider() - self._providerSettings.updateDriverSettings() - self._providerSettings.onPanelActivated() + shouldBeRunning = evt.IsChecked() + if shouldBeRunning and not self._providerControl.startProvider(): + self._checkBox.SetValue(False) + return + elif not shouldBeRunning and not self._providerControl.terminateProvider(): + # When there is an error on termination, don't leave the checkbox checked. + # The provider should not be left configured to startup. + self._checkBox.SetValue(False) + return + # Able to successfully start / terminate: + self._providerSettings.updateDriverSettings() + self._providerSettings.onPanelActivated() self._sendLayoutUpdatedEvent() def onDiscard(self): From 2701d410ad152da25361b9a2c99fc8eef8b8ffb9 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Wed, 13 Nov 2019 19:24:33 +0100 Subject: [PATCH 105/116] Fix parent windows binding to AutoSettingsMixin controls This matches how checkboxes are handled. --- source/gui/settingsDialogs.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index e2a72b1d38f..3b600c87953 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -1018,6 +1018,7 @@ def driver(self): return self._driverRef() def __call__(self,evt): + evt.Skip() # allow other handlers to also process this event. val=evt.GetSelection() setattr(self.driver,self.setting.id,val) @@ -1028,6 +1029,7 @@ def __init__(self,driver,setting,container): super(StringDriverSettingChanger,self).__init__(driver,setting) def __call__(self,evt): + evt.Skip() # allow other handlers to also process this event. # Quick workaround to deal with voice changes. if self.setting.id == "voice": # Cancel speech first so that the voice will change immediately instead of the change being queued. From b4962c538e757412a31c9004bbd7c8085b79032c Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 14 Nov 2019 09:21:19 +0100 Subject: [PATCH 106/116] AutoSettings.percentToParam should be private, with an underscore --- source/autoSettingsUtils/autoSettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index cdb58f003c7..c58d4cc4ee5 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -252,7 +252,7 @@ def _paramToPercent(cls, current: int, min: int, max: int) -> int: return paramToPercent(current, min, max) @classmethod - def percentToParam(cls, percent: int, min: int, max: int) -> int: + def _percentToParam(cls, percent: int, min: int, max: int) -> int: """Convert a percentage to a raw parameter value given the current percentage and the minimum and maximum raw parameter values. @param percent: The current percentage. From 16717ff706d7f490d55307b841f6d5d7d63eb950 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 14 Nov 2019 09:36:09 +0100 Subject: [PATCH 107/116] Make VisionHandler.providers private Provide alternernative convenience functions to cater to the uses of the providers property. This allows us to more easily change the structures used for storing active providers / providerInfo / and their mapping with providerId. --- source/globalCommands.py | 2 +- source/gui/settingsDialogs.py | 20 ++++++------------- source/inputCore.py | 2 +- source/scriptHandler.py | 2 +- source/vision/visionHandler.py | 35 ++++++++++++++++++++++++---------- 5 files changed, 34 insertions(+), 27 deletions(-) diff --git a/source/globalCommands.py b/source/globalCommands.py index b80c052e056..b209fab0aaf 100755 --- a/source/globalCommands.py +++ b/source/globalCommands.py @@ -2359,7 +2359,7 @@ def script_toggleScreenCurtain(self, gesture): from visionEnhancementProviders.screenCurtain import ScreenCurtainProvider screenCurtainId = ScreenCurtainProvider.getSettings().getId() screenCurtainProviderInfo = vision.handler.getProviderInfo(screenCurtainId) - alreadyRunning = screenCurtainId in vision.handler.providers + alreadyRunning = bool(vision.handler.getProviderInstance(screenCurtainProviderInfo)) GlobalCommands._tempEnableScreenCurtain = scriptCount == 0 diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 3b600c87953..96671f8a001 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3157,7 +3157,7 @@ def getProviderInfo(self) -> vision.providerInfo.ProviderInfo: return self._providerInfo def getProviderInstance(self) -> Optional[vision.providerBase.VisionEnhancementProvider]: - return vision.handler.providers.get(self._providerInfo.providerId, None) + return vision.handler.getProviderInstance(self._providerInfo) def startProvider( self, @@ -3228,11 +3228,6 @@ class VisionSettingsPanel(SettingsPanel): # Translators: This is a label appearing on the vision settings panel. panelDescription = _("Configure visual aides.") - def _getProviderInfos(self) -> List[vision.providerInfo.ProviderInfo]: - return list( - vision.handler.getProviderInfo(providerId) for providerId in vision.handler.providers - ) - def _createProviderSettingsPanel( self, providerInfo: vision.providerInfo.ProviderInfo @@ -3259,7 +3254,7 @@ def _createProviderSettingsPanel( return None def makeSettings(self, settingsSizer: wx.BoxSizer): - self.initialProviders = self._getProviderInfos() + self.initialProviders = vision.handler.getActiveProviderInfos() self.providerPanelInstances = [] self.settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer) self.settingsSizerHelper.addItem(wx.StaticText(self, label=self.panelDescription)) @@ -3332,15 +3327,15 @@ def onDiscard(self): providersToInitialize = [ provider for provider in self.initialProviders - if provider.providerId not in vision.handler.providers + if not bool(vision.handler.getProviderInstance(provider)) ] self.safeInitProviders(providersToInitialize) initialProviderIds = [ providerInfo.providerId for providerInfo in self.initialProviders ] providersToTerminate = [ - vision.handler.getProviderInfo(providerId) for providerId in vision.handler.providers - if providerId not in initialProviderIds + provider for provider in vision.handler.getActiveProviderInfos() + if provider.providerId not in initialProviderIds ] self.safeTerminateProviders(providersToTerminate) @@ -3352,10 +3347,7 @@ def onSave(self): # We should be able to continue despite a buggy provider. except Exception: log.debug(f"Error saving providerPanel: {panel.__class__!r}", exc_info=True) - self.initialProviders = list( - vision.handler.getProviderInfo(providerId) - for providerId in vision.handler.providers - ) + self.initialProviders = vision.handler.getActiveProviderInfos() class VisionProviderSubPanel_Settings( diff --git a/source/inputCore.py b/source/inputCore.py index bbb28780fea..9bc890eb82d 100644 --- a/source/inputCore.py +++ b/source/inputCore.py @@ -597,7 +597,7 @@ def __init__(self, obj, ancestors): # Vision enhancement provider import vision - for provider in vision.handler.providers.values(): + for provider in vision.handler.getActiveProviderInstances(): if isinstance(provider, baseObject.ScriptableObject): self.addObj(provider) diff --git a/source/scriptHandler.py b/source/scriptHandler.py index 0d52d65f329..09da9f6d477 100644 --- a/source/scriptHandler.py +++ b/source/scriptHandler.py @@ -103,7 +103,7 @@ def findScript(gesture): return func # Vision enhancement provider level - for provider in vision.handler.providers.values(): + for provider in vision.handler.getActiveProviderInstances(): if isinstance(provider, baseObject.ScriptableObject): func = _getObjScript(provider, gesture, globalMapScripts) if func: diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index 1fd225e3efa..c0736ca27fb 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -77,14 +77,14 @@ class VisionHandler(AutoPropertyObject): """The singleton vision handler is the core of the vision framework. It performs the following tasks: - * It keeps track of active vision enhancement providers in the L{providers} dictionary. + * It keeps track of active vision enhancement _providers in the L{_providers} dictionary. * It processes initialization and termination of providers. * It receives certain events from the core of NVDA, delegating them to the appropriate extension points. """ def __init__(self): - self.providers: Dict[providerInfo.ProviderIdT, VisionEnhancementProvider] = dict() + self._providers: Dict[providerInfo.ProviderIdT, VisionEnhancementProvider] = dict() self.extensionPoints: EventExtensionPoints = EventExtensionPoints() queueHandler.queueFunction(queueHandler.eventQueue, self.postGuiInit) @@ -138,6 +138,21 @@ def getProviderInfo(self, providerId: providerInfo.ProviderIdT) -> Optional[prov return p raise LookupError(f"Provider with id ({providerId}) does not exist.") + def getActiveProviderInstances(self): + return list(self._providers.values()) + + def getActiveProviderInfos(self) -> List[providerInfo.ProviderInfo]: + activeProviderInfos = [ + self.getProviderInfo(p) for p in self._providers + ] + return list(activeProviderInfos) + + def getProviderInstance( + self, + provider: providerInfo.ProviderInfo + ) -> Optional[VisionEnhancementProvider]: + return self._providers.get(provider.providerId) + def terminateProvider( self, provider: providerInfo.ProviderInfo, @@ -151,8 +166,8 @@ def terminateProvider( @param saveSettings: Whether settings should be saved on termination. """ providerId = provider.providerId - # Remove the provider from the providers dictionary. - providerInstance = self.providers.pop(providerId, None) + # Remove the provider from the _providers dictionary. + providerInstance = self._providers.pop(providerId, None) if not providerInstance: raise exceptions.ProviderTerminateException( f"Tried to terminate uninitialized provider {providerId!r}" @@ -181,7 +196,7 @@ def terminateProvider( # As we cant rely on providers to de-register themselves from extension points when terminating them, # Re-create our extension points instance and ask active providers to reregister. self.extensionPoints = EventExtensionPoints() - for providerInst in self.providers.values(): + for providerInst in self._providers.values(): try: providerInst.registerEventExtensionPoints(self.extensionPoints) except Exception: @@ -203,7 +218,7 @@ def initializeProvider( @note: On error, an an Exception is raised. """ providerId = provider.providerId - providerInst = self.providers.pop(providerId, None) + providerInst = self._providers.pop(providerId, None) if providerInst is not None: try: providerInst.reinitialize() @@ -233,7 +248,7 @@ def initializeProvider( raise registerEventExtensionPointsException if not temporary and providerId not in config.conf['vision']['providers']: config.conf['vision']['providers'] = config.conf['vision']['providers'][:] + [providerId] - self.providers[providerId] = providerInst + self._providers[providerId] = providerInst try: self.initialFocus() except Exception: @@ -246,9 +261,9 @@ def initializeProvider( def terminate(self) -> None: self.extensionPoints = None config.post_configProfileSwitch.unregister(self.handleConfigProfileSwitch) - for instance in self.providers.values(): + for instance in self._providers.values(): instance.terminate() - self.providers.clear() + self._providers.clear() def handleUpdate(self, obj, property: str) -> None: self.extensionPoints.post_objectUpdate.notify(obj=obj, property=property) @@ -278,7 +293,7 @@ def handleMouseMove(self, obj, x: int, y: int) -> None: def handleConfigProfileSwitch(self) -> None: configuredProviders = set(config.conf['vision']['providers']) - curProviders = set(self.providers) + curProviders = set(self._providers) providersToInitialize = configuredProviders - curProviders providersToTerminate = curProviders - configuredProviders for providerId in providersToTerminate: From 7ebc5111b303f28e9b2fe125ccaaf5718bc842f4 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 14 Nov 2019 10:35:52 +0100 Subject: [PATCH 108/116] Update user guide --- user_docs/en/userGuide.t2t | 66 +++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index 9299ebaf68b..eb7b5232506 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -293,7 +293,7 @@ For example, if you are typing into an editable text field, the editable text fi The most common way of navigating around Windows with NVDA is to simply move the system focus using standard Windows keyboard commands, such as pressing tab and shift+tab to move forward and back between controls, pressing alt to get to the menu bar and then using the arrows to navigate menus, and using alt+tab to move between running applications. As you do this, NVDA will report information about the object with focus, such as its name, type, value, state, description, keyboard shortcut and positional information. -When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the current system focus is also exposed visually. +When Focus Highlight #VisionFocusHighlight] is enabled, the location of the current system focus is also exposed visually. There are some key commands that are useful when moving with the System focus: %kc:beginInclude @@ -349,7 +349,7 @@ Similarly, a toolbar contains controls, so you must move inside the toolbar to a The object currently being reviewed is called the navigator object. Once you navigate to an object, you can review its content using the [text review commands #ReviewingText] while in [Object review mode #ObjectReview]. -When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the current navigator object is also exposed visually. +When Focus Highlight #VisionFocusHighlight] is enabled, the location of the current navigator object is also exposed visually. By default, the navigator object moves along with the System focus, though this behavior can be toggled on and off. Note that braille follows both the [focus #SystemFocus] and [caret #SystemCaret] as well as object navigation and text review by default. @@ -496,7 +496,7 @@ Browse mode is also optionally available for Microsoft Word documents. In browse mode, the content of the document is made available in a flat representation that can be navigated with the cursor keys as if it were a normal text document. All of NVDA's [system caret #SystemCaret] key commands will work in this mode; e.g. say all, report formatting, table navigation commands, etc. -When the [NVDA Highlighter #VisionNVDAHighlighter] is enabled, the location of the virtual browse mode caret is also exposed visually. +When Focus Highlight #VisionFocusHighlight] is enabled, the location of the virtual browse mode caret is also exposed visually. Information such as whether text is a link, heading, etc. is reported along with the text as you move. Sometimes, you will need to interact directly with controls in these documents. @@ -749,15 +749,15 @@ Pressing dot 7 + dot 8 translates any braille input, but without adding a space + Vision +[Vision] While NVDA is primairly aimed at blind or vision impaired people how primarily use speech and/or braille to operate a computer, it also provides built-in facilities to change the contents of the screen. -Within NVDA, such a facility is called a vision enhancement provider. +Within NVDA, such a visual aid is called a vision enhancement provider. NVDA offers several built-in vision enhancement providers which are described below. Additional vision enhancement providers can be provided in [NVDA add-ons #AddonsManager]. NVDA's vision settings can be changed in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. -++ NVDA Highlighter ++[VisionNVDAHighlighter] -NVDA Highlighter can help to identify the [system focus #SystemFocus], [navigator object #ObjectNavigation] and [browse mode #BrowseMode] positions. +++ Focus Highlight ++[VisionFocusHighlight] +Focus Highlight can help to identify the [system focus #SystemFocus], [navigator object #ObjectNavigation] and [browse mode #BrowseMode] positions. These positions are highlighted with a colored rectangle outline. - Solid blue highlights a combined navigator object and system focus location (e.g. because [the navigator object follows the system focus #ReviewCursorFollowFocus]). - Dashed blue highlights just the system focus object. @@ -765,21 +765,14 @@ These positions are highlighted with a colored rectangle outline. - Solid yellow highlights the virtual caret used in browse mode (where there is no physical caret such as in web browsers). - -When the NVDA Highlighter is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsVisionEnhancementProviderSettings] +When Focus Highlight is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsVisionEnhancementProviderSettings] ++ Screen Curtain ++[VisionScreenCurtain] As a blind or vision impaired user, it is often not possible or necessary to see the contents of the screen. Furthermore, it might be hard to ensure that there isn't someone looking over your shoulder. For this situation, NVDA contains a feature called "screen curtain" which can be enabled to make the screen black. -Enable the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. -A warning that your screen will become black after activation will be displayed. -Before continuing (selecting "Yes"), ensure you have enabled speech / braille and will be able to control your computer without the use of the screen. -Select "No" if you no longer wish to enable the Screen Curtain. -If you are sure, you can choose the Yes button to enable the screen curtain. -If not, choosing No will disable the screen curtain again. -If you no longer want to see this warning message every time, you can change this behavior in the dialog that displays the message. -When the screen curtain is enabled, you can always restore the warning message from [the vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. +You can enable the Screen Curtain in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog. + Content Recognition +[ContentRecognition] When authors don't provide sufficient information for a screen reader user to determine the content of something, various tools can be used to attempt to recognize the content from an image. @@ -1361,26 +1354,39 @@ This option won't be available if your braille display only supports automatic p You may consult the documentation for your braille display in the section [Supported Braille Displays #SupportedBrailleDisplays] to check for more details on the supported types of communication and available ports. +++ Vision +++[VisionSettings] -The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [vision enhancement providers #Vision]. -This settings category contains the following options: +The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [visual aides #Vision]. -==== Vision enhancement providers ====[VisionSettingsVisionEnhancementProviders] -The checkboxes in this list control allow you to enable vision enhancement providers that are either built into NVDA or provided using add-ons. +Note that the available options in this category could be extended by [NVDA add-ons #AddonsManager]. +By default, this settings category contains the following options: -NVDA has the following providers built in: -- [NVDA Highlighter #VisionNVDAHighlighter] -- [Screen Curtain #VisionScreenCurtain] +==== Focus Highlight ====[VisionSettingsFocusHighlight] +The check boxes in the Focus Highlight grouping control the behavior of NVDA's built-in Focus Highlight #VisionFocusHighlight] facility. + +- Enable Highlighting: Toggles Focus Highlight on and off. +- Highlight system focus: toggles whether the [system focus #SystemFocus] will be highlighted. +- 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. - -When you enable a disabled provider, the provider will be automatically enabled as soon as you check the check box. -When you disable an enabled provider, the provider will be automatically disabled as soon as you uncheck the check box. -If you accidentally enable or disable a provider, you can always restore the previous situation by choosing the Cancel button. +Note that checking and unchecking the "Enable Highlighting" check box wil also change the state of the tree other check boxes accordingly. +Therefore, if "Enable Highlighting" is off and you check the 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. + +==== Screen Curtain ====[VisionSettingsScreenCurtain] +You can enable the [Screen Curtain #VisionScreenCurtain] by checking the "Make screen black (immediate effect)" check box. +A warning that your screen will become black after activation will be displayed. +Before continuing (selecting "Yes"), ensure you have enabled speech / braille and will be able to control your computer without the use of the screen. +Select "No" if you no longer wish to enable the Screen Curtain. +If you are sure, you can choose the Yes button to enable the screen curtain. +If you no longer want to see this warning message every time, you can change this behavior in the dialog that displays the message. +You can always restore the warning by checking the "Always show a warning when loading Screen Curtain" check box next to the "Make screen black" check box. + +To toggle the SCreen Curtain from anywhere, please assign a custom gesture using the [Input Gestures dialog #InputGestures]. -==== Vision enhancement provider settings ====[VisionSettingsVisionEnhancementProviderSettings] -When you enable a provider in the list control and this provider has adjustable settings, these settings are automatically shown in the vision category. -For example, when you enable the NVDA Highlighter, navigating the dialog with tab brings you to several settings that allow you to enable highlighting the focus or navigator object. +==== Settings for third party visual aides ====[VisionSettingsThirdPartyVisualAides] +Additional vision enhancement providers can be provided in [NVDA add-ons #AddonsManager]. +When these providers have adjustable settings, they will be shown in this settings category in separate groupings. For the supported settings per provider, please refer to de documentation for that provider. -For the built in providers, supported settings are listed in the [vision #Vision] section of this user guide. +++ Keyboard (NVDA+control+k) +++[KeyboardSettings] The Keyboard category in the NVDA Settings dialog contains options that sets how NVDA behaves as you use and type on your keyboard. @@ -1742,7 +1748,7 @@ If you wish to distribute custom code to others, you should package it as an NVD ==== Open Developer Scratchpad Directory ====[AdvancedSettingsOpenScratchpadDir] This button opens the directory where you can place custom code while developing it. This button is only enabled if NVDA is configured to enable loading custom code from the Developer Scratchpad Directory. - + ==== Use UI automation to access Microsoft Word document controls when available ====[AdvancedSettingsUseUiaForWord] When this option is enabled, NVDA will try to use the Microsoft UI Automation accessibility api in order to fetch information from Microsoft Word document controls. This includes in Microsoft Word itself, and also the Microsoft Outlook message viewer and composer. From ef9b4faee5fc75b3fe8a19aa8e4efa34c7c9730e Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 14 Nov 2019 10:40:06 +0100 Subject: [PATCH 109/116] Fix square brackets --- user_docs/en/userGuide.t2t | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index eb7b5232506..b61f7a09c76 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -293,7 +293,7 @@ For example, if you are typing into an editable text field, the editable text fi The most common way of navigating around Windows with NVDA is to simply move the system focus using standard Windows keyboard commands, such as pressing tab and shift+tab to move forward and back between controls, pressing alt to get to the menu bar and then using the arrows to navigate menus, and using alt+tab to move between running applications. As you do this, NVDA will report information about the object with focus, such as its name, type, value, state, description, keyboard shortcut and positional information. -When Focus Highlight #VisionFocusHighlight] is enabled, the location of the current system focus is also exposed visually. +When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the current system focus is also exposed visually. There are some key commands that are useful when moving with the System focus: %kc:beginInclude @@ -349,7 +349,7 @@ Similarly, a toolbar contains controls, so you must move inside the toolbar to a The object currently being reviewed is called the navigator object. Once you navigate to an object, you can review its content using the [text review commands #ReviewingText] while in [Object review mode #ObjectReview]. -When Focus Highlight #VisionFocusHighlight] is enabled, the location of the current navigator object is also exposed visually. +When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the current navigator object is also exposed visually. By default, the navigator object moves along with the System focus, though this behavior can be toggled on and off. Note that braille follows both the [focus #SystemFocus] and [caret #SystemCaret] as well as object navigation and text review by default. @@ -496,7 +496,7 @@ Browse mode is also optionally available for Microsoft Word documents. In browse mode, the content of the document is made available in a flat representation that can be navigated with the cursor keys as if it were a normal text document. All of NVDA's [system caret #SystemCaret] key commands will work in this mode; e.g. say all, report formatting, table navigation commands, etc. -When Focus Highlight #VisionFocusHighlight] is enabled, the location of the virtual browse mode caret is also exposed visually. +When [Focus Highlight #VisionFocusHighlight] is enabled, the location of the virtual browse mode caret is also exposed visually. Information such as whether text is a link, heading, etc. is reported along with the text as you move. Sometimes, you will need to interact directly with controls in these documents. @@ -765,7 +765,7 @@ These positions are highlighted with a colored rectangle outline. - Solid yellow highlights the virtual caret used in browse mode (where there is no physical caret such as in web browsers). - -When Focus Highlight is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsVisionEnhancementProviderSettings] +When Focus Highlight is enabled in the [vision category #VisionSettings] of the [NVDA Settings #NVDASettings] dialog, you can [change whether or not to highlight the focus, navigator object or browse mode caret #VisionSettingsFocusHighlight] ++ Screen Curtain ++[VisionScreenCurtain] As a blind or vision impaired user, it is often not possible or necessary to see the contents of the screen. @@ -1360,7 +1360,7 @@ Note that the available options in this category could be extended by [NVDA add- By default, this settings category contains the following options: ==== Focus Highlight ====[VisionSettingsFocusHighlight] -The check boxes in the Focus Highlight grouping control the behavior of NVDA's built-in Focus Highlight #VisionFocusHighlight] facility. +The check boxes in the Focus Highlight grouping control the behavior of NVDA's built-in [Focus Highlight #VisionFocusHighlight] facility. - Enable Highlighting: Toggles Focus Highlight on and off. - Highlight system focus: toggles whether the [system focus #SystemFocus] will be highlighted. @@ -1369,7 +1369,7 @@ The check boxes in the Focus Highlight grouping control the behavior of NVDA's b - Note that checking and unchecking the "Enable Highlighting" check box wil also change the state of the tree other check boxes accordingly. -Therefore, if "Enable Highlighting" is off and you check the check box, the other tree check boxes will also be checked automatically. +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. ==== Screen Curtain ====[VisionSettingsScreenCurtain] From a9f179a07ef97d296d8b11dedec0df25878539c2 Mon Sep 17 00:00:00 2001 From: Leonard de Ruijter Date: Thu, 14 Nov 2019 12:21:18 +0100 Subject: [PATCH 110/116] User docs: visual aide> visual aid --- user_docs/en/userGuide.t2t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/user_docs/en/userGuide.t2t b/user_docs/en/userGuide.t2t index b61f7a09c76..802d4249dbd 100644 --- a/user_docs/en/userGuide.t2t +++ b/user_docs/en/userGuide.t2t @@ -1354,7 +1354,7 @@ This option won't be available if your braille display only supports automatic p You may consult the documentation for your braille display in the section [Supported Braille Displays #SupportedBrailleDisplays] to check for more details on the supported types of communication and available ports. +++ Vision +++[VisionSettings] -The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [visual aides #Vision]. +The Vision category in the NVDA Settings dialog allows you to enable, disable and configure [visual aids #Vision]. Note that the available options in this category could be extended by [NVDA add-ons #AddonsManager]. By default, this settings category contains the following options: @@ -1383,7 +1383,7 @@ You can always restore the warning by checking the "Always show a warning when l To toggle the SCreen Curtain from anywhere, please assign a custom gesture using the [Input Gestures dialog #InputGestures]. -==== Settings for third party visual aides ====[VisionSettingsThirdPartyVisualAides] +==== Settings for third party visual aids ====[VisionSettingsThirdPartyVisualAids] Additional vision enhancement providers can be provided in [NVDA add-ons #AddonsManager]. When these providers have adjustable settings, they will be shown in this settings category in separate groupings. For the supported settings per provider, please refer to de documentation for that provider. From 01dfbaba21954b3b07c29083072b35359d828363 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 14 Nov 2019 12:30:23 +0100 Subject: [PATCH 111/116] Fix spelling: aides vs aids --- source/gui/settingsDialogs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 96671f8a001..37a07ddb3a3 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3226,7 +3226,7 @@ class VisionSettingsPanel(SettingsPanel): title = _("Vision") # Translators: This is a label appearing on the vision settings panel. - panelDescription = _("Configure visual aides.") + panelDescription = _("Configure visual aids.") def _createProviderSettingsPanel( self, From 0a09c26ce57d98de76ba3abf01169f03f2cafd22 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Thu, 14 Nov 2019 15:30:13 +0100 Subject: [PATCH 112/116] Handle providers with no options with auto gui --- source/gui/settingsDialogs.py | 69 +++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 37a07ddb3a3..09f30d6dcdd 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -310,7 +310,7 @@ def makeSettings(self, sizer: wx.BoxSizer): def onPanelActivated(self): """Called after the panel has been activated (i.e. de corresponding category is selected in the list of categories). For example, this might be used for resource intensive tasks. - Sub-classes should extendthis method. + Sub-classes should extend this method. """ self.Show() @@ -1083,6 +1083,14 @@ def __init__(self, *args, **kwargs): def getSettings(self) -> AutoSettings: ... + @abstractmethod + def makeSettings(self, sizer: wx.BoxSizer): + """Populate the panel with settings controls. + @note: Normally classes also inherit from settingsDialogs.SettingsPanel. + @param sizer: The sizer to which to add the settings controls. + """ + ... + def _getSettingsStorage(self) -> Any: """ Override to change storage object for setting values.""" return self.getSettings() @@ -1283,7 +1291,7 @@ def onDiscard(self): def onSave(self): self.getSettings().saveSettings() - def onPanelActivated(self): + def refreshGui(self): if not self._currentSettingsRef(): if gui._isDebug(): log.debug("refreshing panel") @@ -1291,7 +1299,13 @@ def onPanelActivated(self): self.settingsSizer.Clear(delete_windows=True) self._currentSettingsRef = weakref.ref(self.getSettings()) self.makeSettings(self.settingsSizer) - super(AutoSettingsMixin, self).onPanelActivated() + + def onPanelActivated(self): + """Called after the panel has been activated + @note: Normally classes also inherit from settingsDialogs.SettingsPanel. + """ + self.refreshGui() + super().onPanelActivated() #: DriverSettingsMixin name is provided or backwards compatibility. @@ -3378,6 +3392,10 @@ def makeSettings(self, settingsSizer): # Construct vision enhancement provider settings self.updateDriverSettings() + @property + def hasOptions(self) -> bool: + return bool(self.sizerDict) + class VisionProviderSubPanel_Wrapper( SettingsPanel @@ -3396,25 +3414,42 @@ def __init__( super().__init__(parent=parent) def makeSettings(self, settingsSizer): - # Translators: Enable checkbox on a vision enhancement provider on the vision settings category panel - checkBox = wx.CheckBox(self, label=_("Enable")) - settingsSizer.Add(checkBox) - settingsSizer.AddSpacer(size=self.scaleSize(10)) + self._checkBox = wx.CheckBox( + self, + # Translators: Enable checkbox on a vision enhancement provider on the vision settings category panel + label=_("Enable") + ) + settingsSizer.Add(self._checkBox) + self._optionsSizer = wx.BoxSizer(orient=wx.VERTICAL) + self._optionsSizer.AddSpacer(size=self.scaleSize(10)) # Translators: Options label on a vision enhancement provider on the vision settings category panel - settingsSizer.Add(wx.StaticText(self, label=_("Options:"))) - settingsSizer.Add( + self._optionsText = wx.StaticText(self, label=_("Options:")) + self._optionsSizer.Add(self._optionsText) + self._optionsSizer.Add( self._providerSettingsSizer, border=self.scaleSize(15), flag=wx.LEFT | wx.EXPAND, proportion=1.0 ) - self._checkBox: wx.CheckBox = checkBox - if self._providerControl.getProviderInstance(): - checkBox.SetValue(True) + settingsSizer.Add( + self._optionsSizer, + flag=wx.EXPAND, + proportion=1.0 + ) + self._checkBox.SetValue(bool(self._providerControl.getProviderInstance())) if self._createProviderSettings(): - checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) + self._checkBox.Bind(wx.EVT_CHECKBOX, self._enableToggle) + else: + self._checkBox.Bind(wx.EVT_CHECKBOX, self._nonEnableableGUI) + self._updateOptionsVisibility() + + def _updateOptionsVisibility(self): + hasProviderOptions = bool(self._providerSettings) and self._providerSettings.hasOptions + if hasProviderOptions: + self.settingsSizer.Show(self._optionsSizer, recursive=True) else: - checkBox.Bind(wx.EVT_CHECKBOX, self._nonEnableableGUI) + self.settingsSizer.Hide(self._optionsSizer, recursive=True) + self._sendLayoutUpdatedEvent() def _createProviderSettings(self): try: @@ -3443,16 +3478,18 @@ def _enableToggle(self, evt): shouldBeRunning = evt.IsChecked() if shouldBeRunning and not self._providerControl.startProvider(): self._checkBox.SetValue(False) + self._updateOptionsVisibility() return elif not shouldBeRunning and not self._providerControl.terminateProvider(): # When there is an error on termination, don't leave the checkbox checked. # The provider should not be left configured to startup. self._checkBox.SetValue(False) + self._updateOptionsVisibility() return # Able to successfully start / terminate: self._providerSettings.updateDriverSettings() - self._providerSettings.onPanelActivated() - self._sendLayoutUpdatedEvent() + self._providerSettings.refreshGui() + self._updateOptionsVisibility() def onDiscard(self): if self._providerSettings: From 77ac29a5c5a817f592b1036063e7beb12ebdae8e Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 18 Nov 2019 12:29:59 +0100 Subject: [PATCH 113/116] Remove saveSettings param from Driver.terminate It was introduced with this feature, but is not used. --- source/driverHandler.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/source/driverHandler.py b/source/driverHandler.py index 74a26833166..732ee300e09 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -51,15 +51,13 @@ def __init__(self): """ super(Driver, self).__init__() - def terminate(self, saveSettings: bool = True): - """Terminate this driver. + def terminate(self): + """Save settings and terminate this driver. This should be used for any required clean up. - @param saveSettings: Whether settings should be saved on termination. @precondition: L{initialize} has been called. @postcondition: This driver can no longer be used. """ - if saveSettings: - self.saveSettings() + self.saveSettings() self._unregisterConfigSaveAction() @classmethod From c4b8492e6eee990ed99409052943ae94d577dba9 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 18 Nov 2019 13:09:53 +0100 Subject: [PATCH 114/116] Remove preInitSettings property This was introduced with this feature. Not being used by the framework, and seems likely to mislead. --- source/autoSettingsUtils/autoSettings.py | 18 ++---------------- source/vision/providerBase.py | 6 ++---- .../NVDAHighlighter.py | 6 +----- .../_exampleProvider_autoGui.py | 7 +++++-- .../screenCurtain.py | 8 ++------ 5 files changed, 12 insertions(+), 33 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index c58d4cc4ee5..e05f71584d2 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -25,10 +25,7 @@ class AutoSettings(AutoPropertyObject): Derived classes must implement: - getId - getTranslatedName - - _get_supportedSettings - you may just call super from this implementation, - it will return _get_preInitSettings - Derived classes should use the following to return settings if possible: - - _get_preInitSettings + - _get_supportedSettings """ def __init__(self): @@ -109,15 +106,6 @@ def initSettings(self): """ self._initSpecificSettings(self, self.supportedSettings) - #: type hinting for _get_preInitSettings - preInitSettings: SupportedSettingType - - @classmethod - def _get_preInitSettings(cls) -> SupportedSettingType: - """The settings supported by the AutoSettings instance at pre initialisation time. - """ - return [] - #: Typing for auto property L{_get_supportedSettings} supportedSettings: SupportedSettingType @@ -126,10 +114,8 @@ def _get_preInitSettings(cls) -> SupportedSettingType: def _get_supportedSettings(self) -> SupportedSettingType: """The settings supported by the AutoSettings instance. Abstract. - When overriding this property, subclasses are encouraged to extend the getter method - to ensure that L{preInitSettings} is part of the list of supported settings. """ - return self.preInitSettings + return [] def isSupported(self, settingID) -> bool: """Checks whether given setting is supported by the AutoSettings instance. diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index df4966f5481..8a73f961869 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -24,12 +24,10 @@ class VisionEnhancementProviderSettings(AutoSettings): - AutoSettings.getTranslatedName: The string that should appear in the GUI as the name. - AutoSettings._get_supportedSettings: - The "runtime" settings for your provider. By default this just returns L{_get_preInitSettings}. + The settings for your provider, the returned list is permitted to change during + start / termination of the provider. The implementation must handle how to modify the returned settings based on external (software, hardware) dependencies. - Although technically optional, derived classes probably need to implement: - - AutoSettings._get_preInitSettings: - The settings that are always configurable for your provider. @note If the vision enhancement provider has settings, it will provide an implementation of this class. The provider will hold a reference to an instance of this class, this is accessed through the class method diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 778e36f23dd..73beef7db5f 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -217,8 +217,7 @@ def getTranslatedName(cls) -> str: # Translators: Description for NVDA's built-in screen highlighter. return _("Focus Highlight") - @classmethod - def _get_preInitSettings(cls) -> SupportedSettingType: + def _get_supportedSettings(self) -> SupportedSettingType: return [ driverHandler.BooleanDriverSetting( 'highlight%s' % (context[0].upper() + context[1:]), @@ -228,9 +227,6 @@ def _get_preInitSettings(cls) -> SupportedSettingType: for context in _supportedContexts ] - def _get_supportedSettings(self) -> SupportedSettingType: - return super().supportedSettings - class NVDAHighlighterGuiPanel( gui.AutoSettingsMixin, diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index 389dcaefdcd..9c1c12da196 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -54,7 +54,10 @@ def getTranslatedName(cls) -> str: return "Example Provider with Auto Gui" # Should normally be translated with _() method. @classmethod - def _get_preInitSettings(cls) -> SupportedSettingType: + def getPreInitSettings(cls) -> SupportedSettingType: + """Get settings that can be configured before the provider is initialized. + This is a class method because it does not rely on any instance state in this class. + """ return [ driverHandler.BooleanDriverSetting( "shouldDoX", # value stored in matching property name on class @@ -116,7 +119,7 @@ def _getAvailableRuntimeSettings(self) -> SupportedSettingType: def _get_supportedSettings(self) -> SupportedSettingType: settings = [] - settings.extend(self.preInitSettings) + settings.extend(self.getPreInitSettings()) settings.extend(self._getAvailableRuntimeSettings()) return settings diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 36b458c715f..e74b28c52dd 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -109,8 +109,7 @@ def getId(cls) -> str: def getTranslatedName(cls) -> str: return screenCurtainTranslatedName - @classmethod - def _get_preInitSettings(cls) -> SupportedSettingType: + def _get_supportedSettings(self) -> SupportedSettingType: return [ BooleanDriverSetting( "warnOnLoad", @@ -119,9 +118,6 @@ def _get_preInitSettings(cls) -> SupportedSettingType: ), ] - def _get_supportedSettings(self) -> SupportedSettingType: - return super().supportedSettings - warnOnLoadText = _( # Translators: A warning shown when activating the screen curtain. @@ -188,7 +184,7 @@ def _exitDialog(self, result: int): if result == wx.YES: settingsStorage = self._settingsStorage settingsStorage.warnOnLoad = self.showWarningOnLoadCheckBox.IsChecked() - settingsStorage._saveSpecificSettings(settingsStorage, settingsStorage.preInitSettings) + settingsStorage._saveSpecificSettings(settingsStorage, settingsStorage.supportedSettings) self.EndModal(result) def _onDialogActivated(self, evt): From e31e3a2994f891a4e4a4762561b177e0559952ce Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 18 Nov 2019 13:45:10 +0100 Subject: [PATCH 115/116] Rename getTranalatedName -> getDisplayName - class AutoSettings - data class ProviderInfo This is more consistent with class DriverSettings It communicates the intended usage of the string better. --- source/autoSettingsUtils/autoSettings.py | 4 ++-- source/driverHandler.py | 2 +- source/gui/settingsDialogs.py | 10 +++++----- source/vision/providerBase.py | 2 +- source/vision/providerInfo.py | 4 ++-- source/vision/visionHandler.py | 6 +++--- source/visionEnhancementProviders/NVDAHighlighter.py | 2 +- .../_exampleProvider_autoGui.py | 2 +- source/visionEnhancementProviders/screenCurtain.py | 2 +- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/source/autoSettingsUtils/autoSettings.py b/source/autoSettingsUtils/autoSettings.py index e05f71584d2..f9386a361db 100644 --- a/source/autoSettingsUtils/autoSettings.py +++ b/source/autoSettingsUtils/autoSettings.py @@ -24,7 +24,7 @@ class AutoSettings(AutoPropertyObject): standard GUI for these settings. Derived classes must implement: - getId - - getTranslatedName + - getDisplayName - _get_supportedSettings """ @@ -60,7 +60,7 @@ def getId(cls) -> str: @classmethod @abstractmethod - def getTranslatedName(cls) -> str: + def getDisplayName(cls) -> str: """ @return: The translated name for this collection of settings. This is for use in the GUI to represent the group of these settings. diff --git a/source/driverHandler.py b/source/driverHandler.py index 732ee300e09..cee0ea31f53 100644 --- a/source/driverHandler.py +++ b/source/driverHandler.py @@ -76,7 +76,7 @@ def getId(cls) -> str: return cls.name @classmethod - def getTranslatedName(cls) -> str: + def getDisplayName(cls) -> str: return cls.description @classmethod diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 09f30d6dcdd..370a973cf1c 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -3106,12 +3106,12 @@ def showStartErrorForProviders( return if len(providers) == 1: - providerName = providers[0].translatedName + providerName = providers[0].displayName # Translators: This message is presented when # NVDA is unable to load a single vision enhancement provider. message = _(f"Could not load the {providerName} vision enhancement provider") else: - providerNames = ", ".join(provider.translatedName for provider in providers) + providerNames = ", ".join(provider.displayName for provider in providers) # Translators: This message is presented when NVDA is unable to # load multiple vision enhancement providers. message = _(f"Could not load the following vision enhancement providers:\n{providerNames}") @@ -3132,12 +3132,12 @@ def showTerminationErrorForProviders( return if len(providers) == 1: - providerName = providers[0].translatedName + providerName = providers[0].displayName # Translators: This message is presented when # NVDA is unable to gracefully terminate a single vision enhancement provider. message = _(f"Could not gracefully terminate the {providerName} vision enhancement provider") else: - providerNames = ", ".join(provider.translatedName for provider in providers) + providerNames = ", ".join(provider.displayName for provider in providers) # Translators: This message is presented when # NVDA is unable to terminate multiple vision enhancement providers. message = _( @@ -3275,7 +3275,7 @@ def makeSettings(self, settingsSizer: wx.BoxSizer): for providerInfo in vision.handler.getProviderList(reloadFromSystem=True): providerSizer = self.settingsSizerHelper.addItem( - wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.translatedName), wx.VERTICAL), + wx.StaticBoxSizer(wx.StaticBox(self, label=providerInfo.displayName), wx.VERTICAL), flag=wx.EXPAND ) if len(self.providerPanelInstances) > 0: diff --git a/source/vision/providerBase.py b/source/vision/providerBase.py index 8a73f961869..57e11e1c1b8 100644 --- a/source/vision/providerBase.py +++ b/source/vision/providerBase.py @@ -21,7 +21,7 @@ class VisionEnhancementProviderSettings(AutoSettings): Ensure that the following are implemented: - AutoSettings.getId: This is case sensitive. Used in the config file. Does not have to match the module name. - - AutoSettings.getTranslatedName: + - AutoSettings.getDisplayName: The string that should appear in the GUI as the name. - AutoSettings._get_supportedSettings: The settings for your provider, the returned list is permitted to change during diff --git a/source/vision/providerInfo.py b/source/vision/providerInfo.py index f71d85d54af..bcc9bd1f30e 100644 --- a/source/vision/providerInfo.py +++ b/source/vision/providerInfo.py @@ -8,12 +8,12 @@ ProviderIdT = str ModuleNameT = str -TranslatedNameT = str +DisplayNameT = str @dataclass class ProviderInfo: providerId: ProviderIdT moduleName: ModuleNameT - translatedName: TranslatedNameT + displayName: DisplayNameT providerClass: Type[providerBase.VisionEnhancementProvider] diff --git a/source/vision/visionHandler.py b/source/vision/visionHandler.py index c0736ca27fb..64c53d849a8 100644 --- a/source/vision/visionHandler.py +++ b/source/vision/visionHandler.py @@ -58,11 +58,11 @@ def _getProvidersFromFileSystem(): provider = _getProviderClass(moduleName) providerSettings = provider.getSettings() providerId = providerSettings.getId() - translatedName = providerSettings.getTranslatedName() + displayName = providerSettings.getDisplayName() yield providerInfo.ProviderInfo( providerId=providerId, moduleName=moduleName, - translatedName=translatedName, + displayName=displayName, providerClass=provider ) except Exception: # Purposely catch everything as we don't know what a provider might raise. @@ -102,7 +102,7 @@ def postGuiInit(self) -> None: def _updateAllProvidersList(self): self._allProviders = list(_getProvidersFromFileSystem()) # Sort the providers alphabetically by name. - self._allProviders.sort(key=lambda info: info.translatedName.lower()) + self._allProviders.sort(key=lambda info: info.displayName.lower()) def getProviderList( self, diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index 73beef7db5f..d8ee1e1b20a 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -213,7 +213,7 @@ def getId(cls) -> str: return "NVDAHighlighter" @classmethod - def getTranslatedName(cls) -> str: + def getDisplayName(cls) -> str: # Translators: Description for NVDA's built-in screen highlighter. return _("Focus Highlight") diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index 9c1c12da196..c0fec51f317 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -50,7 +50,7 @@ def getId(cls) -> str: return "exampleOfAutoGui" # Note: this does not have to match the name of the module. @classmethod - def getTranslatedName(cls) -> str: + def getDisplayName(cls) -> str: return "Example Provider with Auto Gui" # Should normally be translated with _() method. @classmethod diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index e74b28c52dd..46d02206149 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -106,7 +106,7 @@ def getId(cls) -> str: return "screenCurtain" @classmethod - def getTranslatedName(cls) -> str: + def getDisplayName(cls) -> str: return screenCurtainTranslatedName def _get_supportedSettings(self) -> SupportedSettingType: From 4daa3dec3b33c16d84493e3f98cea7c9dd9619d5 Mon Sep 17 00:00:00 2001 From: Reef Turner Date: Mon, 18 Nov 2019 13:45:32 +0100 Subject: [PATCH 116/116] Fix consistency of imports. --- source/visionEnhancementProviders/NVDAHighlighter.py | 7 ++++--- .../_exampleProvider_autoGui.py | 7 +++---- source/visionEnhancementProviders/screenCurtain.py | 8 +++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/source/visionEnhancementProviders/NVDAHighlighter.py b/source/visionEnhancementProviders/NVDAHighlighter.py index d8ee1e1b20a..701a79d67d5 100644 --- a/source/visionEnhancementProviders/NVDAHighlighter.py +++ b/source/visionEnhancementProviders/NVDAHighlighter.py @@ -7,11 +7,12 @@ """Default highlighter based on GDI Plus.""" from typing import Optional, Tuple +from autoSettingsUtils.autoSettings import SupportedSettingType import vision from vision.constants import Context -from autoSettingsUtils.autoSettings import SupportedSettingType from vision.util import getContextRect from vision.visionHandlerExtensionPoints import EventExtensionPoints +from vision import providerBase from windowUtils import CustomWindow import wx import gui @@ -202,7 +203,7 @@ def refresh(self): _supportedContexts = (Context.FOCUS, Context.NAVIGATOR, Context.BROWSEMODE) -class NVDAHighlighterSettings(vision.providerBase.VisionEnhancementProviderSettings): +class NVDAHighlighterSettings(providerBase.VisionEnhancementProviderSettings): # Default settings for parameters highlightFocus = False highlightNavigator = False @@ -328,7 +329,7 @@ def _onCheckEvent(self, evt: wx.CommandEvent): providerInst.refresh() -class NVDAHighlighter(vision.providerBase.VisionEnhancementProvider): +class NVDAHighlighter(providerBase.VisionEnhancementProvider): _ContextStyles = { Context.FOCUS: DASH_BLUE, Context.NAVIGATOR: SOLID_PINK, diff --git a/source/visionEnhancementProviders/_exampleProvider_autoGui.py b/source/visionEnhancementProviders/_exampleProvider_autoGui.py index c0fec51f317..e132e56fc6f 100644 --- a/source/visionEnhancementProviders/_exampleProvider_autoGui.py +++ b/source/visionEnhancementProviders/_exampleProvider_autoGui.py @@ -3,12 +3,11 @@ # See the file COPYING for more details. # Copyright (C) 2019 NV Access Limited -import vision +from vision import providerBase import driverHandler import wx from autoSettingsUtils.utils import StringParameterInfo from autoSettingsUtils.autoSettings import SupportedSettingType -from vision.providerBase import VisionEnhancementProviderSettings from typing import Optional, Type, Any, List """Example provider, which demonstrates using the automatically constructed GUI. Rename this file, removing @@ -22,7 +21,7 @@ """ -class AutoGuiTestSettings(VisionEnhancementProviderSettings): +class AutoGuiTestSettings(providerBase.VisionEnhancementProviderSettings): #: dictionary of the setting id's available when provider is running. _availableRuntimeSettings = [ @@ -124,7 +123,7 @@ def _get_supportedSettings(self) -> SupportedSettingType: return settings -class AutoGuiTestProvider(vision.providerBase.VisionEnhancementProvider): +class AutoGuiTestProvider(providerBase.VisionEnhancementProvider): _settings = AutoGuiTestSettings() @classmethod diff --git a/source/visionEnhancementProviders/screenCurtain.py b/source/visionEnhancementProviders/screenCurtain.py index 46d02206149..387a4f782d3 100644 --- a/source/visionEnhancementProviders/screenCurtain.py +++ b/source/visionEnhancementProviders/screenCurtain.py @@ -8,6 +8,7 @@ """ import vision +from vision import providerBase import winVersion from ctypes import Structure, windll, c_float, POINTER, WINFUNCTYPE, WinError from ctypes.wintypes import BOOL @@ -16,9 +17,6 @@ import wx import gui from logHandler import log -from vision.providerBase import ( - VisionEnhancementProviderSettings, -) from typing import Optional, Type @@ -97,7 +95,7 @@ class Magnification: ) -class ScreenCurtainSettings(VisionEnhancementProviderSettings): +class ScreenCurtainSettings(providerBase.VisionEnhancementProviderSettings): warnOnLoad: bool @@ -286,7 +284,7 @@ def confirmInitWithUser(self) -> bool: return res == wx.YES -class ScreenCurtainProvider(vision.providerBase.VisionEnhancementProvider): +class ScreenCurtainProvider(providerBase.VisionEnhancementProvider): _settings = ScreenCurtainSettings() @classmethod