diff --git a/source/gui/accPropServer.py b/source/gui/accPropServer.py deleted file mode 100644 index aabef6e367b..00000000000 --- a/source/gui/accPropServer.py +++ /dev/null @@ -1,178 +0,0 @@ -#accPropServer.py -#A part of NonVisual Desktop Access (NVDA) -#Copyright (C) 2017-2018 NV Access Limited, Derek Riemer, Babbage B.V. -#This file is covered by the GNU General Public License. -#See the file COPYING for more details. - -"""Implementation of IAccProcServer, so that customization of a wx control can be done very fast.""" -from ctypes.wintypes import BOOL -from typing import Optional, Tuple, Any, Union, Callable - -from logHandler import log -from comtypes.automation import S_OK, VARIANT, POINTER, c_int, c_double, _oleaut32 -from comtypes import COMObject, GUID -from comInterfaces.Accessibility import IAccPropServer, ANNO_CONTAINER, ANNO_THIS -from abc import ABCMeta, abstractmethod -import weakref -import winUser -import wx - -_VariantInit: Callable[[POINTER(VARIANT),], None] = _oleaut32.VariantInit -_VariantInit.argtypes = (POINTER(VARIANT),) - -AcceptedGetPropTypes = Union[ - bool, - int, c_int, - float, c_double, - str, - VARIANT, - # And others: L{comtpyes.automation.tagVariant._set_value} - ] - -class IAccPropServer_Impl(COMObject, metaclass=ABCMeta): - """Base class for implementing a COM interface for a hwnd based AccPropServer\ - to annotate a WX control. - The AccPropServer registers itself using the window handle of the WX control. - When the WX control is destroyed, the instance is automatically unregistered. - This should eventually be dropped in favor of WX' own annotation support, - blocked by wxWidgets/Phoenix#1129. - Please override the L{_GetPropValue} method, not L{GetPropValue}. - L{GetPropValue} wraps L{_getPropValue} to catch and log exceptions (Which for some reason NVDA's logger misses when they occur in GetPropValue). - You must also provide the L{properties} property. - """ - - _com_interfaces_ = [ - IAccPropServer - ] - - # Constants used with `IAccPropServer::GetPropValue` method see - # https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx - HAS_PROP = 1 # TRUE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - DOES_NOT_HAVE_PROP = 0 # FALSE - Constant for `BOOL* pfHasProp` out param of `IAccPropServer::GetPropValue` method - - # An array with the GUIDs of the properties that an AccPropServer should override - properties_GUIDPTR = [] - properties = [] - - def __init__(self, control, annotateProperties, annotateChildren=False): - """Initialize the instance of AccPropServer. - @param control: the WX control instance, so you can look up things in the _getPropValue method. - It's available on self.control. - @Type control: Subclass of wx.Window - @param annotateProperties The properties that should be annotated, see oleacc.py for constants. - @type annotateProperties List of oleacc constants. Internally these are converted to GUID pointers for the server. - @param annotateChildren: whether the WX control is a container which children should be annotated. - @type annotateChildren: bool - """ - self.properties = annotateProperties - self.properties_GUIDPTR = convertToGUIDPointerList(annotateProperties) - self.control = weakref.ref(control) - self.hwnd = control.GetHandle() - super(IAccPropServer_Impl, self).__init__() - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.SetHwndPropServer( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR), - pServer=self, - AnnoScope=ANNO_CONTAINER if annotateChildren else ANNO_THIS - ) - # clean up of accPropServices needs to happen when the control is destroyed. We can't rely on - # pythons `__del__` method to be called, and the wx framework does not call Destroy on child controls, - # automatically. Instead we can bind to the "window destroy" event for the control to do necessary - # cleanup (wxWidgets/Phoenix/#630). Not performing this cleanup results in a reference to the parent - # window, keeping it from being deleted correctly. This can cause a freeze on exit of NVDA. - control.Bind(wx.EVT_WINDOW_DESTROY, self._onDestroyControl, source=control) - - @abstractmethod - def _getPropValue( - self, - pIDString: str, - dwIDStringLen: int, - idProp: GUID - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """ Use this method to implement GetPropValue. - It is wrapped by the callback GetPropValue to handle exceptions, and ensure valid return types. - For instructions on implementing accPropServers, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd373681(v=vs.85).aspx . - For instructions specifically about this method, see https://msdn.microsoft.com/en-us/library/windows/desktop/dd318495(v=vs.85).aspx . - @param pIDString: Contains a string that identifies the property being requested. - If a single callback object is registered for annotating multiple accessible elements, - the identity string can be used to determine which element the request refers to. - If the accessible element is HWND-based, - IAccessibleHandler.accPropServices.DecomposeHwndIdentityString can be used - to extract the HWND/idObject/idChild from the identity string. - Note that, while one IAccPropServer implementation can annotate - multiple accessible elements, it is still bound to one wx.Control. - @param dwIDStringLen: Specifies the length of the identity string specified by the pIDString parameter. - @param idProp: Specifies a GUID indicating the desired property. One of the values from oleacc.PROPID_* - @return Use L{self._hasProp} to return correct values or return None if unable to supply the property. - """ - raise NotImplementedError - - def _hasProp( - self, - value: AcceptedGetPropTypes - ) -> Optional[Tuple[BOOL, AcceptedGetPropTypes]]: - """Constructs a tuple for the `IAccPropServer::GetPropValue` method, two elements: - 1. `VARIANT pvarValue` - 2. `BOOL pfHasProp` (either self.HAS_PROP or self.DOES_NOT_HAVE_PROP)""" - return value, self.HAS_PROP - - def GetPropValue( - self, this, # unused "this" used to indicate to comTypes we want a low level implementation - pIDString: str, - dwIDStringLen: int, - idProp: GUID, - pvarValue: POINTER(VARIANT), - pfGotProp: POINTER(BOOL) - ) -> int: - """ Exposed method to get a prop value. - see L{_getPropValue} for more details of args. - Uses a low-level approach, because comtypes tries to clear the VARIANT even though it is an out param. - When the pfHasProp part is FALSE / self.DOES_NOT_HAVE_PROP, then the pvarValue.vt part must be VT_EMPTY. - """ - # ensure exceptions don't leave this function. They will get get swallowed by the caller. - # instead catch and log exceptions. - try: - # Preset values for "no prop value", in case we return early. - pfGotProp.contents.value = self.DOES_NOT_HAVE_PROP - _VariantInit(pvarValue) - - ret = self._getPropValue(pIDString, dwIDStringLen, idProp) - if ret is None: - # We don't have the prop value, return early. - return S_OK - elif len(ret) != 2: - # We don't have the prop value, internal error. - raise RuntimeError("_getPropValue implementation must return None or two element tuple") - elif ret[1] != self.HAS_PROP: - # We don't have the prop value, return early. - return S_OK - - # we do have the prop value - pfGotProp.contents.value = self.HAS_PROP - pvarValue.contents.value = ret[0] - except Exception as e: # catch and log all exceptions so they are not swallowed by caller. - log.exception() - return S_OK - - def _onDestroyControl(self, evt): - evt.Skip() # Allow other handlers to process this event. - self._cleanup() - - def _cleanup(self): - # Import late to avoid circular import - from IAccessibleHandler import accPropServices - accPropServices.ClearHwndProps( - hwnd=self.hwnd, - idObject=winUser.OBJID_CLIENT, - idChild=0, - paProps=self.properties_GUIDPTR, - cProps=len(self.properties_GUIDPTR) - ) - -def convertToGUIDPointerList(propList): - return (GUID * len(propList))(*propList) diff --git a/source/gui/nvdaControls.py b/source/gui/nvdaControls.py index a0a08dcf326..b3ae5f682c2 100644 --- a/source/gui/nvdaControls.py +++ b/source/gui/nvdaControls.py @@ -9,7 +9,6 @@ import wx from comtypes import GUID from wx.lib.mixins import listctrl as listmix -from . import accPropServer from .dpiScalingHelper import DpiScalingHelperMixin from . import guiHelper import oleacc @@ -86,85 +85,35 @@ def OnSetFocus(self, evt): self.SetSelection(0, numChars) evt.Skip() -class AccPropertyOverride(accPropServer.IAccPropServer_Impl): - def __init__(self, control, propertyAnnotations): - """ - A simple class for overriding specific values on a control - :type propertyAnnotations: dict - """ - super(AccPropertyOverride, self).__init__( - control, - annotateProperties=list(propertyAnnotations.keys()), - annotateChildren=False - ) - self.propertyAnnotations = propertyAnnotations +class ListCtrlAccessible(wx.Accessible): + """WX Accessible implementation for checkable lists which aren't fully accessible.""" - def _getPropValue(self, pIDString, dwIDStringLen, idProp): - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None or not self.propertyAnnotations: - return None + def GetRole(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetRole(childId) + return (wx.ACC_OK, wx.ROLE_SYSTEM_CHECKBUTTON) - try: - val = self.propertyAnnotations[idProp] - if callable(val): - val = val() - return self._hasProp(val) - except KeyError: - pass - - return None - - def _cleanup(self): - # could contain references (via lambda) of our owner, set it to None to avoid a circular reference which - # would block destruction. - self.propertyAnnotations = None - super(AccPropertyOverride, self)._cleanup() - -class ListCtrlAccPropServer(accPropServer.IAccPropServer_Impl): - """AccPropServer for wx checkable lists which aren't fully accessible.""" - - def __init__(self, control): - super(ListCtrlAccPropServer, self).__init__( - control, - annotateProperties=[ - oleacc.PROPID_ACC_ROLE, # supposed to be checkbox, rather than list item - oleacc.PROPID_ACC_STATE # should include the checkable state and checked state if the item is checked. - ], - annotateChildren=True - ) + def GetState(self, childId): + if childId == winUser.CHILDID_SELF: + return super().GetState(childId) + states = wx.ACC_STATE_SYSTEM_SELECTABLE | wx.ACC_STATE_SYSTEM_FOCUSABLE + if self.Window.IsChecked(childId - 1): + states |= wx.ACC_STATE_SYSTEM_CHECKED + if self.Window.IsSelected(childId - 1): + # wx doesn't seem to have a method to check whether a list item is focused. + # Therefore, assume that a selected item is focused,which is the case in single select list boxes. + states |= wx.ACC_STATE_SYSTEM_SELECTED | wx.ACC_STATE_SYSTEM_FOCUSED + return (wx.ACC_OK, states) - def _getPropValue(self, pIDString: str, dwIDStringLen: int, idProp: GUID) -> Optional[Tuple[BOOL, Any]]: - control = self.control() # self.control held as a weak ref, ensure it stays alive for the duration of this method - if control is None: - return None - - # Import late to prevent circular import. - from IAccessibleHandler import accPropServices - handle, objid, childid = accPropServices.DecomposeHwndIdentityString(pIDString, dwIDStringLen) - if childid == winUser.CHILDID_SELF: - return None - - if idProp == oleacc.PROPID_ACC_ROLE: - return self._hasProp(oleacc.ROLE_SYSTEM_CHECKBUTTON) - - if idProp == oleacc.PROPID_ACC_STATE: - states = oleacc.STATE_SYSTEM_SELECTABLE|oleacc.STATE_SYSTEM_FOCUSABLE - if control.IsChecked(childid-1): - states |= oleacc.STATE_SYSTEM_CHECKED - if control.IsSelected(childid-1): - # wx doesn't seem to have a method to check whether a list item is focused. - # Therefore, assume that a selected item is focused,which is the case in single select list boxes. - states |= oleacc.STATE_SYSTEM_SELECTED | oleacc.STATE_SYSTEM_FOCUSED - return self._hasProp(states) class CustomCheckListBox(wx.CheckListBox): """Custom checkable list to fix a11y bugs in the standard wx checkable list box.""" def __init__(self, *args, **kwargs): super(CustomCheckListBox, self).__init__(*args, **kwargs) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register ourself with ourself's selected event, so that we can notify winEvent of the state change. self.Bind(wx.EVT_CHECKLISTBOX, self.notifyIAccessible) @@ -189,8 +138,8 @@ def __init__(self, parent, id=wx.ID_ANY, autoSizeColumn="LAST", pos=wx.DefaultPo ): AutoWidthColumnListCtrl.__init__(self, parent, id=id, pos=pos, size=size, style=style, autoSizeColumn=autoSizeColumn) listmix.CheckListCtrlMixin.__init__(self, check_image, uncheck_image, imgsz) - # Register object with COM to fix accessibility bugs in wx. - self.server = ListCtrlAccPropServer(self) + # Register a custom wx.Accessible implementation to fix accessibility incompleties + self.SetAccessible(ListCtrlAccessible(self)) # Register our hook to check/uncheck items with space. # Use wx.EVT_CHAR_HOOK, because EVT_LIST_KEY_DOWN isn't triggered for space. self.Bind(wx.EVT_CHAR_HOOK, self.onCharHook) diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 6bbb1fc7bd4..01db9055522 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -357,6 +357,22 @@ def _sendLayoutUpdatedEvent(self): event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) + +class SettingsPanelAccessible(wx.Accessible): + """ + WX Accessible implementation to set the role of a settings panel to property page, + as well as to set the accessible description based on the panel's description. + """ + + Window: SettingsPanel + + def GetRole(self, childId): + return (wx.ACC_OK, wx.ROLE_SYSTEM_PROPERTYPAGE) + + def GetDescription(self, childId): + return (wx.ACC_OK, self.Window.panelDescription) + + class MultiCategorySettingsDialog(SettingsDialog): """A settings dialog with multiple settings categories. A multi category settings dialog consists of a list view with settings categories on the left side, @@ -524,14 +540,7 @@ def _getCategoryPanel(self, catId): ).format(cls, panel.Size[0]) ) panel.SetLabel(panel.title) - import oleacc - panel.server = nvdaControls.AccPropertyOverride( - panel, - propertyAnnotations={ - oleacc.PROPID_ACC_ROLE: oleacc.ROLE_SYSTEM_PROPERTYPAGE, # change the role from pane to property page - oleacc.PROPID_ACC_DESCRIPTION: panel.panelDescription, # set a description - } - ) + panel.SetAccessible(SettingsPanelAccessible(panel)) return panel def postInit(self): diff --git a/user_docs/en/changes.t2t b/user_docs/en/changes.t2t index d4d9edcc55e..f1d6d595446 100644 --- a/user_docs/en/changes.t2t +++ b/user_docs/en/changes.t2t @@ -90,6 +90,7 @@ What's New in NVDA - `speech.re_last_pause` has been removed - please use `speech.SpeechWithoutPauses.re_last_pause` instead. (#12195) - `WelcomeDialog`, `LauncherDialog` and `AskAllowUsageStatsDialog` are moved to the `gui.startupDialogs`. (#12105) - `getDocFilePath` has been moved from `gui` to the `documentationUtils` module. (#12105) +- The gui.accPropServer module as well as the AccPropertyOverride and ListCtrlAccPropServer classes from the gui.nvdaControls module have been removed in favor of WX' native support for overriding accessibility properties. When enhancing accessibility of WX controls, implement wx.Accessible instead. (#12215) = 2020.4 =