diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index 37cd11a9599..1bc653d6a17 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -647,21 +647,41 @@ def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includ if debug: log.debug("NULL childRange. Skipping") continue - clippedStart=clippedEnd=False - if index==lastChildIndex and childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,textRange,UIAHandler.TextPatternRangeEndpoint_End)>=0: + clippedStart = False + clippedEnd = False + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_Start + ) <= 0: + if debug: + log.debug("Child completely before textRange. Skipping") + continue + if childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_Start, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) >= 0: if debug: log.debug("Child at or past end of textRange. Breaking") break - if index==lastChildIndex: - lastChildEndDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - if lastChildEndDelta>0: - if debug: - log.debug( - "textRange ended part way through the child. " - "Crop end of childRange to fit" - ) - childRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End,textRange,UIAHandler.TextPatternRangeEndpoint_End) - clippedEnd=True + lastChildEndDelta = childRange.CompareEndpoints( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + if lastChildEndDelta > 0: + if debug: + log.debug( + "textRange ended part way through the child. " + "Crop end of childRange to fit" + ) + childRange.MoveEndpointByRange( + UIAHandler.TextPatternRangeEndpoint_End, + textRange, + UIAHandler.TextPatternRangeEndpoint_End + ) + clippedEnd = True childStartDelta=childRange.CompareEndpoints(UIAHandler.TextPatternRangeEndpoint_Start,tempRange,UIAHandler.TextPatternRangeEndpoint_End) if childStartDelta>0: # plain text before this child @@ -893,7 +913,24 @@ def findOverlayClasses(self,clsList): clsList.append(edge.EdgeList) else: clsList.append(edge.EdgeNode) - elif self.role == controlTypes.ROLE_DOCUMENT and UIAAutomationId == "Microsoft.Windows.PDF.DocumentView": + elif self.windowClassName == "Chrome_RenderWidgetHostHWND": + from . import chromium + from . import web + if ( + self.UIATextPattern + and self.role == controlTypes.ROLE_DOCUMENT + and self.parent + and self.parent.role == controlTypes.ROLE_PANE + ): + clsList.append(chromium.ChromiumUIADocument) + else: + if self.role == controlTypes.ROLE_LIST: + clsList.append(web.List) + clsList.append(chromium.ChromiumUIA) + elif ( + self.role == controlTypes.ROLE_DOCUMENT + and self.UIAElement.cachedAutomationId == "Microsoft.Windows.PDF.DocumentView" + ): # PDFs from . import edge clsList.append(edge.EdgeHTMLRoot) diff --git a/source/NVDAObjects/UIA/chromium.py b/source/NVDAObjects/UIA/chromium.py new file mode 100644 index 00000000000..f93b01d9618 --- /dev/null +++ b/source/NVDAObjects/UIA/chromium.py @@ -0,0 +1,62 @@ +# 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) 2020 NV Access limited, Leonard de Ruijter + +import UIAHandler +from . import web +import controlTypes + + +class ChromiumUIATextInfo(web.UIAWebTextInfo): + + def _getFormatFieldAtRange(self, textRange, formatConfig, ignoreMixedValues=False): + formatField = super()._getFormatFieldAtRange(textRange, formatConfig, ignoreMixedValues=ignoreMixedValues) + # Headings are also exposed in the element tree, + # And therefore exposing in a formatField is redundant and causes duplicate reporting. + # So remove heading-level from the formatField if it exists. + try: + del formatField.field['heading-level'] + except KeyError: + pass + return formatField + + def _getControlFieldForObject(self, obj, isEmbedded=False, startOfNode=False, endOfNode=False): + field = super()._getControlFieldForObject( + obj, + isEmbedded=isEmbedded, + startOfNode=startOfNode, + endOfNode=endOfNode + ) + # use the value of comboboxes as content. + if obj.role == controlTypes.ROLE_COMBOBOX: + field['content'] = obj.value + # Layout tables do not have the UIA table pattern + if field['role'] == controlTypes.ROLE_TABLE: + if not obj._getUIACacheablePropertyValue(UIAHandler.UIA_IsTablePatternAvailablePropertyId): + field['table-layout'] = True + # Currently no way to tell if author has explicitly set name. + # Therefore always report the name if the control is not of a type that + # by definition uses its name for content. + # this may cause some duplicate speaking, + # But that is currently better than nothing at all. + if not field.get('nameIsContent') and field.get('name'): + field['alwaysReportName'] = True + return field + + +class ChromiumUIA(web.UIAWeb): + _TextInfo = ChromiumUIATextInfo + + +class ChromiumUIATreeInterceptor(web.UIAWebTreeInterceptor): + + def _get_documentConstantIdentifier(self): + return self.rootNVDAObject.parent._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId) + + +class ChromiumUIADocument(ChromiumUIA): + treeInterceptorClass = ChromiumUIATreeInterceptor + + def _get_shouldCreateTreeInterceptor(self): + return self.role == controlTypes.ROLE_DOCUMENT diff --git a/source/NVDAObjects/UIA/edge.py b/source/NVDAObjects/UIA/edge.py index cbd84185868..e3f380cfd25 100644 --- a/source/NVDAObjects/UIA/edge.py +++ b/source/NVDAObjects/UIA/edge.py @@ -19,231 +19,12 @@ import UIAHandler from UIABrowseMode import UIABrowseModeDocument, UIABrowseModeDocumentTextInfo, UIATextRangeQuickNavItem,UIAControlQuicknavIterator from UIAUtils import * -from . import UIA, UIATextInfo - -def splitUIAElementAttribs(attribsString): - """Split an UIA Element attributes string into a dict of attribute keys and values. - An invalid attributes string does not cause an error, but strange results may be returned. - @param attribsString: The UIA Element attributes string to convert. - @type attribsString: str - @return: A dict of the attribute keys and values, where values are strings - @rtype: {str: str} - """ - attribsDict = {} - tmp = "" - key = "" - inEscape = False - for char in attribsString: - if inEscape: - tmp += char - inEscape = False - elif char == "\\": - inEscape = True - elif char == "=": - # We're about to move on to the value, so save the key and clear tmp. - key = tmp - tmp = "" - elif char == ";": - # We're about to move on to a new attribute. - if key: - # Add this key/value pair to the dict. - attribsDict[key] = tmp - key = "" - tmp = "" - else: - tmp += char - # If there was no trailing semi-colon, we need to handle the last attribute. - if key: - # Add this key/value pair to the dict. - attribsDict[key] = tmp - return attribsDict - -class EdgeTextInfo(UIATextInfo): - - def _get_UIAElementAtStartWithReplacedContent(self): - """Fetches the deepest UIAElement at the start of the text range whos name has been overridden by the author (such as aria-label).""" - element=self.UIAElementAtStart - condition=createUIAMultiPropertyCondition({UIAHandler.UIA_ControlTypePropertyId:self.UIAControlTypesWhereNameIsContent}) - # A part from the condition given, we must always match on the root of the document so we know when to stop walking - runtimeID=VARIANT() - self.obj.UIAElement._IUIAutomationElement__com_GetCurrentPropertyValue(UIAHandler.UIA_RuntimeIdPropertyId,byref(runtimeID)) - condition=UIAHandler.handler.clientObject.createOrCondition(UIAHandler.handler.clientObject.createPropertyCondition(UIAHandler.UIA_RuntimeIdPropertyId,runtimeID),condition) - walker=UIAHandler.handler.clientObject.createTreeWalker(condition) - cacheRequest=UIAHandler.handler.clientObject.createCacheRequest() - cacheRequest.addProperty(UIAHandler.UIA_NamePropertyId) - cacheRequest.addProperty(UIAHandler.UIA_AriaPropertiesPropertyId) - element=walker.normalizeElementBuildCache(element,cacheRequest) - while element and not UIAHandler.handler.clientObject.compareElements(element,self.obj.UIAElement): - name=element.getCachedPropertyValue(UIAHandler.UIA_NamePropertyId) - if name: - ariaProperties=element.getCachedPropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - if ('label=' in ariaProperties) or ('labelledby=' in ariaProperties): - return element - try: - textRange=self.obj.UIATextPattern.rangeFromChild(element) - except COMError: - return - text = textRange.getText(-1) - if not text or text.isspace(): - return element - element=walker.getParentElementBuildCache(element,cacheRequest) - - def _moveToEdgeOfReplacedContent(self,back=False): - """If within replaced content (E.g. aria-label is used), moves to the first or last character covered, so that a following call to move in the same direction will move out of the replaced content, in order to ensure that the content only takes up one character stop.""" - element=self.UIAElementAtStartWithReplacedContent - if not element: - return - try: - textRange=self.obj.UIATextPattern.rangeFromChild(element) - except COMError: - return - if not back: - textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_Start, textRange, UIAHandler.TextPatternRangeEndpoint_End) - textRange.move(UIAHandler.TextUnit_Character, -1) - else: - textRange.MoveEndpointByRange(UIAHandler.TextPatternRangeEndpoint_End, textRange, UIAHandler.TextPatternRangeEndpoint_Start) - self._rangeObj=textRange - - def _collapsedMove(self,unit,direction,skipReplacedContent): - """A simple collapsed move (i.e. both ends move together), but whether it classes replaced content as one character stop can be configured via the skipReplacedContent argument.""" - if not skipReplacedContent: - return super(EdgeTextInfo,self).move(unit,direction) - if direction==0: - return - chunk=1 if direction>0 else -1 - finalRes=0 - while finalRes!=direction: - self._moveToEdgeOfReplacedContent(back=direction<0) - res=super(EdgeTextInfo,self).move(unit,chunk) - if res==0: - break - finalRes+=res - return finalRes +from . import UIA, web - def move(self,unit,direction,endPoint=None,skipReplacedContent=True): - if not endPoint: - return self._collapsedMove(unit,direction,skipReplacedContent) - else: - tempInfo=self.copy() - res=tempInfo.move(unit,direction,skipReplacedContent=skipReplacedContent) - if res!=0: - self.setEndPoint(tempInfo,"endToEnd" if endPoint=="end" else "startToStart") - return res - def _getControlFieldForObject(self,obj,isEmbedded=False,startOfNode=False,endOfNode=False): - field=super(EdgeTextInfo,self)._getControlFieldForObject(obj,isEmbedded=isEmbedded,startOfNode=startOfNode,endOfNode=endOfNode) - field['embedded']=isEmbedded - role=field.get('role') - # Fields should be treated as block for certain roles. - # This can affect whether the field is presented as a container (e.g. announcing entering and exiting) - if role in ( - controlTypes.ROLE_GROUPING, - controlTypes.ROLE_SECTION, - controlTypes.ROLE_PARAGRAPH, - controlTypes.ROLE_ARTICLE, - controlTypes.ROLE_LANDMARK, - controlTypes.ROLE_REGION, - ): - field['isBlock']=True - ariaProperties = splitUIAElementAttribs( - obj._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - ) - # ARIA roledescription and landmarks - field['roleText'] = ariaProperties.get('roledescription') - # provide landmarks - field['landmark']=obj.landmark - # Combo boxes with a text pattern are editable - if obj.role==controlTypes.ROLE_COMBOBOX and obj.UIATextPattern: - field['states'].add(controlTypes.STATE_EDITABLE) - # report if the field is 'current' - field['current']=obj.isCurrent - if obj.placeholder and obj._isTextEmpty: - field['placeholder']=obj.placeholder - # For certain controls, if ARIA overrides the label, then force the field's content (value) to the label - # Later processing in Edge's getTextWithFields will remove descendant content from fields with a content attribute. - hasAriaLabel = 'label' in ariaProperties - hasAriaLabelledby = 'labelledby' in ariaProperties - if field.get('nameIsContent'): - content="" - field.pop('name',None) - if hasAriaLabel or hasAriaLabelledby: - content=obj.name - if not content: - text=self.obj.makeTextInfo(obj).text - if not text or text.isspace(): - content=obj.name or field.pop('description',None) - if content: - field['content']=content - elif isEmbedded: - field['content']=obj.value - if field['role']==controlTypes.ROLE_GROUPING: - field['role']=controlTypes.ROLE_EMBEDDEDOBJECT - if not obj.value: - field['content']=obj.name - elif hasAriaLabel or hasAriaLabelledby: - field['alwaysReportName'] = True - # Give lists an item count - if obj.role==controlTypes.ROLE_LIST: - child=UIAHandler.handler.clientObject.ControlViewWalker.GetFirstChildElement(obj.UIAElement) - if child: - field['_childcontrolcount']=child.getCurrentPropertyValue(UIAHandler.UIA_SizeOfSetPropertyId) - return field - - def getTextWithFields(self,formatConfig=None): - # We don't want fields for collapsed ranges. - # This would normally be a general rule, but MS Word currently needs fields for collapsed ranges, thus this code is not in the base. - if self.isCollapsed: - return [] - fields=super(EdgeTextInfo,self).getTextWithFields(formatConfig) - seenText=False - curStarts=[] - # remove clickable state on descendants of controls with clickable state - clickableField=None - for field in fields: - if isinstance(field,textInfos.FieldCommand) and field.command=="controlStart": - states=field.field['states'] - if clickableField: - states.discard(controlTypes.STATE_CLICKABLE) - elif controlTypes.STATE_CLICKABLE in states: - clickableField=field.field - elif clickableField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field is clickableField: - clickableField=None - # Chop extra whitespace off the end incorrectly put there by Edge - numFields=len(fields) - index=0 - while index1 and isinstance(field,str) and field.isspace(): - prevField=fields[index-2] - if isinstance(prevField,textInfos.FieldCommand) and prevField.command=="controlEnd": - del fields[index-1:index+1] - index+=1 - # chop fields off the end incorrectly placed there by Edge - # This can happen if expanding to line covers element start chars at its end - startCount=0 - lastStartIndex=None - numFields=len(fields) - for index in range(numFields-1,-1,-1): - field=fields[index] - if isinstance(field,str): - break - elif isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and not field.field.get('embedded'): - startCount+=1 - lastStartIndex=index - if lastStartIndex: - del fields[lastStartIndex:lastStartIndex+(startCount*2)] - # Remove any content from fields with a content attribute - numFields=len(fields) - curField=None - for index in range(numFields-1,-1,-1): - field=fields[index] - if not curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd" and field.field.get('content'): - curField=field.field - endIndex=index - elif curField and isinstance(field,textInfos.FieldCommand) and field.command=="controlStart" and field.field is curField: - fields[index+1:endIndex]=" " - curField=None - return fields +class EdgeTextInfo(web.UIAWebTextInfo): + ... + class EdgeTextInfo_preGapRemoval(EdgeTextInfo): @@ -417,7 +198,8 @@ def _getTextWithFieldsForUIARange(self,rootElement,textRange,formatConfig,includ log.debug("Done walking parents to yield controlEnds and recurse unbalanced endRanges") log.debug("_getTextWithFieldsForUIARange (unbalanced) end") -class EdgeNode(UIA): + +class EdgeNode(web.UIAWeb): _edgeIsPreGapRemoval=winVersion.winVersion.build<15048 @@ -440,56 +222,6 @@ def getNormalizedUIATextRangeFromElement(self,UIAElement): charInfo.collapse(True) return textRange - def _get_role(self): - role=super(EdgeNode,self).role - if not isinstance(self,EdgeHTMLRoot) and role==controlTypes.ROLE_PANE and self.UIATextPattern: - return controlTypes.ROLE_INTERNALFRAME - ariaRole=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() - # #7333: It is valid to provide multiple, space separated aria roles in HTML - # The role used is the first role in the list that has an associated NVDA role in aria.ariaRolesToNVDARoles - for ariaRole in ariaRole.split(): - newRole=aria.ariaRolesToNVDARoles.get(ariaRole) - if newRole: - role=newRole - break - return role - - def _get_states(self): - states=super(EdgeNode,self).states - if self.role in (controlTypes.ROLE_STATICTEXT,controlTypes.ROLE_GROUPING,controlTypes.ROLE_SECTION,controlTypes.ROLE_GRAPHIC) and self.UIAInvokePattern: - states.add(controlTypes.STATE_CLICKABLE) - return states - - def _get_ariaProperties(self): - return splitUIAElementAttribs(self.UIAElement.currentAriaProperties) - - # RegEx to get the value for the aria-current property. This will be looking for a the value of 'current' - # in a list of strings like "something=true;current=date;". We want to capture one group, after the '=' - # character and before the ';' character. - # This could be one of: "false", "true", "page", "step", "location", "date", "time" - # "false" is ignored by the regEx and will not produce a match - RE_ARIA_CURRENT_PROP_VALUE = re.compile("current=(?!false)(\w+);") - - def _get_isCurrent(self): - ariaProperties=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaPropertiesPropertyId) - match = self.RE_ARIA_CURRENT_PROP_VALUE.search(ariaProperties) - log.debug("aria props = %s" % ariaProperties) - if match: - valueOfAriaCurrent = match.group(1) - log.debug("aria current value = %s" % valueOfAriaCurrent) - return valueOfAriaCurrent - return None - - def _get_roleText(self): - roleText = self.ariaProperties.get('roledescription', None) - if roleText: - return roleText - return super().roleText - - def _get_placeholder(self): - ariaPlaceholder = self.ariaProperties.get('placeholder', None) - return ariaPlaceholder - def _get__isTextEmpty(self): # NOTE: we can not check the result of the EdgeTextInfo move implementation to determine if we added # any characters to the range, since it seems to return 1 even when the text property has not changed. @@ -509,30 +241,10 @@ def _get__isTextEmpty(self): return True return False - def _get_landmark(self): - landmarkId=self._getUIACacheablePropertyValue(UIAHandler.UIA_LandmarkTypePropertyId) - if not landmarkId: # will be 0 for non-landmarks - return None - landmarkRole = UIAHandler.UIALandmarkTypeIdsToLandmarkNames.get(landmarkId) - if landmarkRole: - return landmarkRole - ariaRoles=self._getUIACacheablePropertyValue(UIAHandler.UIA_AriaRolePropertyId).lower() - # #7333: It is valid to provide multiple, space separated aria roles in HTML - # If multiple roles or even multiple landmark roles are provided, the first one is used - ariaRole = ariaRoles.split(" ")[0] - if ariaRole in aria.landmarkRoles and (ariaRole != 'region' or self.name): - return ariaRole - return None - - -class EdgeList(EdgeNode): - - # non-focusable lists are readonly lists (ensures correct NVDA presentation category) - def _get_states(self): - states=super(EdgeList,self).states - if controlTypes.STATE_FOCUSABLE not in states: - states.add(controlTypes.STATE_READONLY) - return states + +class EdgeList(web.List): + ... + class EdgeHTMLRootContainer(EdgeNode): @@ -543,6 +255,7 @@ def event_gainFocus(self): return return super(EdgeHTMLRootContainer,self).event_gainFocus() + class EdgeHeadingQuickNavItem(UIATextRangeQuickNavItem): @property @@ -555,6 +268,7 @@ def level(self): def isChild(self,parent): return self.level>parent.level + def EdgeHeadingQuicknavIterator(itemType,document,position,direction="next"): """ A helper for L{EdgeHTMLTreeInterceptor._iterNodesByType} that specifically yields L{EdgeHeadingQuickNavItem} objects found in the given document, starting the search from the given position, searching in the given direction. @@ -573,9 +287,8 @@ def EdgeHeadingQuicknavIterator(itemType,document,position,direction="next"): if item.level and (not levelString or levelString==str(item.level)): yield item -class EdgeHTMLTreeInterceptor(cursorManager.ReviewCursorManager,UIABrowseModeDocument): - TextInfo=UIABrowseModeDocumentTextInfo +class EdgeHTMLTreeInterceptor(web.UIAWebTreeInterceptor): def _get_documentConstantIdentifier(self): return self.rootNVDAObject.parent.name @@ -586,24 +299,6 @@ def _iterNodesByType(self,nodeType,direction="next",pos=None): else: return super(EdgeHTMLTreeInterceptor,self)._iterNodesByType(nodeType,direction=direction,pos=pos) - def shouldPassThrough(self,obj,reason=None): - # Enter focus mode for selectable list items ( and role=listbox) + if ( + reason == controlTypes.REASON_FOCUS + and obj.role == controlTypes.ROLE_LISTITEM + and controlTypes.STATE_SELECTABLE in obj.states + ): + return True + return super().shouldPassThrough(obj, reason=reason) diff --git a/source/_UIAHandler.py b/source/_UIAHandler.py index cbc465c5926..65a878896e6 100644 --- a/source/_UIAHandler.py +++ b/source/_UIAHandler.py @@ -69,9 +69,6 @@ "Button", # #8944: The Foxit UIA implementation is incomplete and should not be used for now. "FoxitDocWnd", - # All Chromium implementations (including Edge) should not be UIA, - # As their IA2 implementation is still better at the moment. - "Chrome_RenderWidgetHostHWND", ] # #8405: used to detect UIA dialogs prior to Windows 10 RS5. @@ -711,6 +708,20 @@ def _isUIAWindowHelper(self,hwnd): and not config.conf['UIA']['useInMSWordWhenAvailable'] ): return False + # Unless explicitly allowed, all Chromium implementations (including Edge) should not be UIA, + # As their IA2 implementation is still better at the moment. + elif ( + windowClass == "Chrome_RenderWidgetHostHWND" + and ( + config.conf['UIA']['allowInChromium'] == 3 # No + # Disabling is only useful if we can inject in-process (and use our older code) + or ( + appModule.helperLocalBindingHandle + and config.conf['UIA']['allowInChromium'] in (0, 1) # Only when necessary + ) + ) + ): + return False return bool(res) def isUIAWindow(self,hwnd): diff --git a/source/config/configSpec.py b/source/config/configSpec.py index 97f5f284582..8f2c0a11e53 100644 --- a/source/config/configSpec.py +++ b/source/config/configSpec.py @@ -220,6 +220,8 @@ enabled = boolean(default=true) useInMSWordWhenAvailable = boolean(default=false) winConsoleImplementation= option("auto", "legacy", "UIA", default="auto") + # 0:default, 1:Only when necessary, 2:yes, 3:no + allowInChromium = integer(0, 3, default=0) selectiveEventRegistration = boolean(default=false) [terminals] diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index 4267534c0fc..205d85afe72 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -2563,6 +2563,25 @@ def __init__(self, parent): self.winConsoleSpeakPasswordsCheckBox.SetValue(config.conf["terminals"]["speakPasswords"]) self.winConsoleSpeakPasswordsCheckBox.defaultValue = self._getDefaultValue(["terminals", "speakPasswords"]) + # Translators: This is the label for a checkbox in the + # Advanced settings panel. + label = _("Allow Microsoft Edge and other &Chromium based browsers to use UI Automation when available:") + # Translators: Label for the Allow Chromium to use uIA combobox in the Advanced settings panel. + onlyWhenNecessaryLabel = _("Only when necessary") + chromiumChoices = ( + # Translators: Label for the Allow Chromium to use uIA combobox in the Advanced settings panel. + # {} is replaced by the default option + _("Default ({})").format(onlyWhenNecessaryLabel), + onlyWhenNecessaryLabel, + # Translators: The label of a combobox option in the advanced settings panel. + _("Yes"), + # Translators: The label of a combobox option in the advanced settings panel. + _("no"), + ) + self.UIAInChromiumCombo = UIAGroup.addLabeledControl(label, wx.Choice, choices=chromiumChoices) + self.UIAInChromiumCombo.SetSelection(config.conf["UIA"]["allowInChromium"]) + self.UIAInChromiumCombo.defaultValue = self._getDefaultValue(["UIA", "allowInChromium"]) + # Translators: This is the label for a group of advanced options in the # Advanced settings panel label = _("Terminal programs") @@ -2734,6 +2753,7 @@ def haveConfigDefaultsBeenRestored(self): and self.UIAInMSWordCheckBox.IsChecked() == self.UIAInMSWordCheckBox.defaultValue and self.ConsoleUIACheckBox.IsChecked() == (self.ConsoleUIACheckBox.defaultValue == 'UIA') and self.winConsoleSpeakPasswordsCheckBox.IsChecked() == self.winConsoleSpeakPasswordsCheckBox.defaultValue + and self.UIAInChromiumCombo.selection == self.UIAInChromiumCombo.defaultValue and self.cancelExpiredFocusSpeechCombo.GetSelection() == self.cancelExpiredFocusSpeechCombo.defaultValue and self.keyboardSupportInLegacyCheckBox.IsChecked() == self.keyboardSupportInLegacyCheckBox.defaultValue and self.diffAlgoCombo.GetSelection() == self.diffAlgoCombo.defaultValue @@ -2747,6 +2767,7 @@ def restoreToDefaults(self): self.selectiveUIAEventRegistrationCheckBox.SetValue(self.selectiveUIAEventRegistrationCheckBox.defaultValue) self.UIAInMSWordCheckBox.SetValue(self.UIAInMSWordCheckBox.defaultValue) self.ConsoleUIACheckBox.SetValue(self.ConsoleUIACheckBox.defaultValue == 'UIA') + self.UIAInChromiumCombo.SetSelection(self.UIAInChromiumCombo.defaultValue) self.winConsoleSpeakPasswordsCheckBox.SetValue(self.winConsoleSpeakPasswordsCheckBox.defaultValue) self.cancelExpiredFocusSpeechCombo.SetSelection(self.cancelExpiredFocusSpeechCombo.defaultValue) self.keyboardSupportInLegacyCheckBox.SetValue(self.keyboardSupportInLegacyCheckBox.defaultValue) @@ -2765,6 +2786,7 @@ def onSave(self): else: config.conf['UIA']['winConsoleImplementation'] = "auto" config.conf["terminals"]["speakPasswords"] = self.winConsoleSpeakPasswordsCheckBox.IsChecked() + config.conf["UIA"]["allowInChromium"] = self.UIAInChromiumCombo.GetSelection() config.conf["featureFlag"]["cancelExpiredFocusSpeech"] = self.cancelExpiredFocusSpeechCombo.GetSelection() config.conf["terminals"]["keyboardSupportInLegacy"]=self.keyboardSupportInLegacyCheckBox.IsChecked() diffAlgoChoice = self.diffAlgoCombo.GetSelection() diff --git a/source/speech/__init__.py b/source/speech/__init__.py index b8348cad53c..5af92aed832 100755 --- a/source/speech/__init__.py +++ b/source/speech/__init__.py @@ -521,31 +521,23 @@ def getObjectSpeech( # noqa: C901 if shouldReportTextContent: try: info = obj.makeTextInfo(textInfos.POSITION_SELECTION) - if not info.isCollapsed: - # if there is selected text, then there is a value and we do not report placeholder - sequence.extend(getPreselectedTextSpeech(info.text)) - else: - info.expand(textInfos.UNIT_LINE) - textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason) - sequence.extend(placeholderSeq) - speechGen = getTextInfoSpeech( - info, - unit=textInfos.UNIT_LINE, - reason=controlTypes.REASON_CARET - ) - sequence.extend(_flattenNestedSequences(speechGen)) - except: # noqa E722 legacy bare except. Unknown what exceptions may be raised. - newInfo = obj.makeTextInfo(textInfos.POSITION_ALL) + except NotImplementedError: + info = None + if info and not info.isCollapsed: + # if there is selected text, then there is a value and we do not report placeholder + sequence.extend(getPreselectedTextSpeech(info.text)) + else: + if not info: + info = obj.makeTextInfo(textInfos.POSITION_FIRST) + info.expand(textInfos.UNIT_LINE) textEmpty, placeholderSeq = _getPlaceholderSpeechIfTextEmpty(obj, reason) - if textEmpty: - sequence.extend(placeholderSeq) - else: - speechGen = getTextInfoSpeech( - newInfo, - unit=textInfos.UNIT_PARAGRAPH, - reason=controlTypes.REASON_CARET, - ) - sequence.extend(_flattenNestedSequences(speechGen)) + sequence.extend(placeholderSeq) + speechGen = getTextInfoSpeech( + info, + unit=textInfos.UNIT_LINE, + reason=controlTypes.REASON_CARET + ) + sequence.extend(_flattenNestedSequences(speechGen)) elif role == controlTypes.ROLE_MATH: import mathPres mathPres.ensureInit()