diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index daeedd82fb9..2364c459c55 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -13,6 +13,7 @@ Optional, Dict, Tuple, + Callable, ) import array from ctypes.wintypes import POINT @@ -181,9 +182,166 @@ def find(self, text, caseSensitive=False, reverse=False): return True return False - # C901 '_getFormatFieldAtRange' is too complex - # Note: when working on _getFormatFieldAtRange, look for opportunities to simplify - # and move logic out into smaller helper functions. + def _getFormatFieldFontName(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_FontNameAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + formatField["font-name"] = val + + def _getFormatFieldFontSize(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_FontSizeAttributeId) + if isinstance(val, numbers.Number): + # Translators: Abbreviation for points, a measurement of font size. + formatField["font-size"] = pgettext("font size", "%s pt") % float(val) + + def _getFormatFieldFontAttributes(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_FontWeightAttributeId) + if isinstance(val, int): + formatField["bold"] = val >= 700 + val = fetch(UIAHandler.UIA_IsItalicAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + formatField["italic"] = val + val = fetch(UIAHandler.UIA_UnderlineStyleAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + formatField["underline"] = bool(val) + val = fetch(UIAHandler.UIA_StrikethroughStyleAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + formatField["strikethrough"] = bool(val) + + def _getFormatFieldSuperscriptsAndSubscripts( + self, fetch: Callable[[int], int], formatField: textInfos.FormatField + ): + textPosition = None + val = fetch(UIAHandler.UIA_IsSuperscriptAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue and val: + textPosition = TextPosition.SUPERSCRIPT + else: + val = fetch(UIAHandler.UIA_IsSubscriptAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue and val: + textPosition = TextPosition.SUBSCRIPT + else: + textPosition = TextPosition.BASELINE + formatField["text-position"] = textPosition + + def _getFormatFieldStyle(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_StyleNameAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + formatField["style"] = val + + def _getFormatFieldIndent(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + """ + Helper function to get indent formatting from UIA, using the fetch function passed as parameter. + The indent formatting is reported according to MS Word's convention. + @param fetch: gets formatting information from UIA. + @return: The indent formatting informations corresponding to what has been retrieved via the fetcher. + """ + + val = fetch(UIAHandler.UIA_IndentationFirstLineAttributeId) + uiaIndentFirstLine = val if isinstance(val, float) else None + val = fetch(UIAHandler.UIA_IndentationLeadingAttributeId) + uiaIndentLeading = val if isinstance(val, float) else None + val = fetch(UIAHandler.UIA_IndentationTrailingAttributeId) + uiaIndentTrailing = val if isinstance(val, float) else None + if uiaIndentFirstLine is not None and uiaIndentLeading is not None: + reportedFirstLineIndent = uiaIndentFirstLine - uiaIndentLeading + if reportedFirstLineIndent > 0: # First line positive indent + reportedLeftIndent = uiaIndentLeading + reportedHangingIndent = None + elif reportedFirstLineIndent < 0: # First line negative indent + reportedLeftIndent = uiaIndentFirstLine + reportedHangingIndent = -reportedFirstLineIndent + reportedFirstLineIndent = None + else: + reportedLeftIndent = uiaIndentLeading + reportedFirstLineIndent = None + reportedHangingIndent = None + if reportedLeftIndent: + formatField["left-indent"] = self._getIndentValueDisplayString(reportedLeftIndent) + if reportedFirstLineIndent: + formatField["first-line-indent"] = self._getIndentValueDisplayString(reportedFirstLineIndent) + if reportedHangingIndent: + formatField["hanging-indent"] = self._getIndentValueDisplayString(reportedHangingIndent) + if uiaIndentTrailing: + formatField["right-indent"] = self._getIndentValueDisplayString(uiaIndentTrailing) + + def _getFormatFieldAlignment(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_HorizontalTextAlignmentAttributeId) + textAlign = textAlignLabels.get(val) + if textAlign: + formatField["text-align"] = textAlign + + def _getFormatFieldColor(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_BackgroundColorAttributeId) + if isinstance(val, int): + formatField["background-color"] = colors.RGB.fromCOLORREF(val) + val = fetch(UIAHandler.UIA_ForegroundColorAttributeId) + if isinstance(val, int): + formatField["color"] = colors.RGB.fromCOLORREF(val) + + def _getFormatFieldLineSpacing(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_LineSpacingAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + if val: + formatField["line-spacing"] = val + + def _getFormatFieldLinks(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + val = fetch(UIAHandler.UIA_LinkAttributeId) + if val != UIAHandler.handler.reservedNotSupportedValue: + if val: + formatField["link"] = True + + def _getFormatFieldHeadings(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + styleIDValue = fetch(UIAHandler.UIA_StyleIdAttributeId) + # #9842: styleIDValue can sometimes be a pointer to IUnknown. + # In Python 3, comparing an int with a pointer raises a TypeError. + if ( + isinstance(styleIDValue, int) + and UIAHandler.StyleId_Heading1 <= styleIDValue <= UIAHandler.StyleId_Heading9 + ): + formatField["heading-level"] = (styleIDValue - UIAHandler.StyleId_Heading1) + 1 + + def _getFormatFieldAnnotationTypes( + self, fetch: Callable[[int], int], formatField: textInfos.FormatField, formatConfig: Dict + ): + annotationTypes = fetch(UIAHandler.UIA_AnnotationTypesAttributeId) + # Some UIA implementations return a single value rather than a tuple. + # Always mutate to a tuple to allow for a generic x in y matching + if not isinstance(annotationTypes, tuple): + annotationTypes = (annotationTypes,) + if formatConfig["reportSpellingErrors"]: + if UIAHandler.AnnotationType_SpellingError in annotationTypes: + formatField["invalid-spelling"] = True + if UIAHandler.AnnotationType_GrammarError in annotationTypes: + formatField["invalid-grammar"] = True + if formatConfig["reportComments"]: + cats = self.obj._UIACustomAnnotationTypes + if cats.microsoftWord_draftComment.id and cats.microsoftWord_draftComment.id in annotationTypes: + formatField["comment"] = textInfos.CommentType.DRAFT + elif ( + cats.microsoftWord_resolvedComment.id + and cats.microsoftWord_resolvedComment.id in annotationTypes + ): + formatField["comment"] = textInfos.CommentType.RESOLVED + elif UIAHandler.AnnotationType_Comment in annotationTypes: + formatField["comment"] = True + if formatConfig["reportRevisions"]: + if UIAHandler.AnnotationType_InsertionChange in annotationTypes: + formatField["revision-insertion"] = True + elif UIAHandler.AnnotationType_DeletionChange in annotationTypes: + formatField["revision-deletion"] = True + if formatConfig["reportBookmarks"]: + cats = self.obj._UIACustomAnnotationTypes + if cats.microsoftWord_bookmark.id and cats.microsoftWord_bookmark.id in annotationTypes: + formatField["bookmark"] = True + + def _getFormatFieldCulture(self, fetch: Callable[[int], int], formatField: textInfos.FormatField): + cultureVal = fetch(UIAHandler.UIA_CultureAttributeId) + if cultureVal and isinstance(cultureVal, int): + try: + formatField["language"] = languageHandler.windowsLCIDToLocaleName(cultureVal) + except: # noqa: E722 + log.debugWarning("language error", exc_info=True) + pass + def _getFormatFieldAtRange( # noqa: C901 self, textRange: IUIAutomationTextRangeT, @@ -201,7 +359,6 @@ def _getFormatFieldAtRange( # noqa: C901 a mixed attribute value signifying that the caller may want to try again with a smaller range. @return: The formatting for the given text range. """ - formatField = textInfos.FormatField() if not isinstance(textRange, UIAHandler.IUIAutomationTextRange): raise ValueError("%s is not a text range" % textRange) fetchAnnotationTypes = ( @@ -222,21 +379,13 @@ def _getFormatFieldAtRange( # noqa: C901 if formatConfig["reportFontSize"]: IDs.add(UIAHandler.UIA_FontSizeAttributeId) if formatConfig["fontAttributeReporting"]: - IDs.update( - { - UIAHandler.UIA_FontWeightAttributeId, - UIAHandler.UIA_IsItalicAttributeId, - UIAHandler.UIA_UnderlineStyleAttributeId, - UIAHandler.UIA_StrikethroughStyleAttributeId, - }, - ) + IDs.add(UIAHandler.UIA_FontWeightAttributeId) + IDs.add(UIAHandler.UIA_IsItalicAttributeId) + IDs.add(UIAHandler.UIA_UnderlineStyleAttributeId) + IDs.add(UIAHandler.UIA_StrikethroughStyleAttributeId) if formatConfig["reportSuperscriptsAndSubscripts"]: - IDs.update( - { - UIAHandler.UIA_IsSuperscriptAttributeId, - UIAHandler.UIA_IsSubscriptAttributeId, - }, - ) + IDs.add(UIAHandler.UIA_IsSuperscriptAttributeId) + IDs.add(UIAHandler.UIA_IsSubscriptAttributeId) if formatConfig["reportParagraphIndentation"]: IDs.update(set(paragraphIndentIDs)) if formatConfig["reportAlignment"]: @@ -256,201 +405,38 @@ def _getFormatFieldAtRange( # noqa: C901 IDs.add(UIAHandler.UIA_AnnotationTypesAttributeId) IDs.add(UIAHandler.UIA_CultureAttributeId) fetcher = BulkUIATextRangeAttributeValueFetcher(textRange, IDs) + + def fetch(id): + return fetcher.getValue(id, ignoreMixedValues=ignoreMixedValues) + + formatField = textInfos.FormatField() if formatConfig["reportFontName"]: - val = fetcher.getValue(UIAHandler.UIA_FontNameAttributeId, ignoreMixedValues=ignoreMixedValues) - if val != UIAHandler.handler.reservedNotSupportedValue: - formatField["font-name"] = val + self._getFormatFieldFontName(fetch, formatField) if formatConfig["reportFontSize"]: - val = fetcher.getValue(UIAHandler.UIA_FontSizeAttributeId, ignoreMixedValues=ignoreMixedValues) - if isinstance(val, numbers.Number): - # Translators: Abbreviation for points, a measurement of font size. - formatField["font-size"] = pgettext("font size", "%s pt") % float(val) + self._getFormatFieldFontSize(fetch, formatField) if formatConfig["fontAttributeReporting"]: - val = fetcher.getValue(UIAHandler.UIA_FontWeightAttributeId, ignoreMixedValues=ignoreMixedValues) - if isinstance(val, int): - formatField["bold"] = val >= 700 - val = fetcher.getValue(UIAHandler.UIA_IsItalicAttributeId, ignoreMixedValues=ignoreMixedValues) - if val != UIAHandler.handler.reservedNotSupportedValue: - formatField["italic"] = val - val = fetcher.getValue( - UIAHandler.UIA_UnderlineStyleAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if val != UIAHandler.handler.reservedNotSupportedValue: - formatField["underline"] = bool(val) - val = fetcher.getValue( - UIAHandler.UIA_StrikethroughStyleAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if val != UIAHandler.handler.reservedNotSupportedValue: - formatField["strikethrough"] = bool(val) + self._getFormatFieldFontAttributes(fetch, formatField) if formatConfig["reportSuperscriptsAndSubscripts"]: - textPosition = None - val = fetcher.getValue( - UIAHandler.UIA_IsSuperscriptAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if val != UIAHandler.handler.reservedNotSupportedValue and val: - textPosition = TextPosition.SUPERSCRIPT - else: - val = fetcher.getValue( - UIAHandler.UIA_IsSubscriptAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if val != UIAHandler.handler.reservedNotSupportedValue and val: - textPosition = TextPosition.SUBSCRIPT - else: - textPosition = TextPosition.BASELINE - formatField["text-position"] = textPosition + self._getFormatFieldSuperscriptsAndSubscripts(fetch, formatField) if formatConfig["reportStyle"]: - val = fetcher.getValue(UIAHandler.UIA_StyleNameAttributeId, ignoreMixedValues=ignoreMixedValues) - if val != UIAHandler.handler.reservedNotSupportedValue: - formatField["style"] = val + self._getFormatFieldStyle(fetch, formatField) if formatConfig["reportParagraphIndentation"]: - formatField.update(self._getFormatFieldIndent(fetcher, ignoreMixedValues=ignoreMixedValues)) + self._getFormatFieldIndent(fetch, formatField) if formatConfig["reportAlignment"]: - val = fetcher.getValue( - UIAHandler.UIA_HorizontalTextAlignmentAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - textAlign = textAlignLabels.get(val) - if textAlign: - formatField["text-align"] = textAlign + self._getFormatFieldAlignment(fetch, formatField) if formatConfig["reportColor"]: - val = fetcher.getValue( - UIAHandler.UIA_BackgroundColorAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if isinstance(val, int): - formatField["background-color"] = colors.RGB.fromCOLORREF(val) - val = fetcher.getValue( - UIAHandler.UIA_ForegroundColorAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - if isinstance(val, int): - formatField["color"] = colors.RGB.fromCOLORREF(val) + self._getFormatFieldColor(fetch, formatField) if formatConfig["reportLineSpacing"]: - val = fetcher.getValue(UIAHandler.UIA_LineSpacingAttributeId, ignoreMixedValues=ignoreMixedValues) - if val != UIAHandler.handler.reservedNotSupportedValue: - if val: - formatField["line-spacing"] = val + self._getFormatFieldLineSpacing(fetch, formatField) if formatConfig["reportLinks"]: - val = fetcher.getValue(UIAHandler.UIA_LinkAttributeId, ignoreMixedValues=ignoreMixedValues) - if val != UIAHandler.handler.reservedNotSupportedValue: - if val: - formatField["link"] = True + self._getFormatFieldLinks(fetch, formatField) if formatConfig["reportHeadings"]: - styleIDValue = fetcher.getValue( - UIAHandler.UIA_StyleIdAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - # #9842: styleIDValue can sometimes be a pointer to IUnknown. - # In Python 3, comparing an int with a pointer raises a TypeError. - if ( - isinstance(styleIDValue, int) - and UIAHandler.StyleId_Heading1 <= styleIDValue <= UIAHandler.StyleId_Heading9 - ): - formatField["heading-level"] = (styleIDValue - UIAHandler.StyleId_Heading1) + 1 + self._getFormatFieldHeadings(fetch, formatField) if fetchAnnotationTypes: - annotationTypes = fetcher.getValue( - UIAHandler.UIA_AnnotationTypesAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - # Some UIA implementations return a single value rather than a tuple. - # Always mutate to a tuple to allow for a generic x in y matching - if not isinstance(annotationTypes, tuple): - annotationTypes = (annotationTypes,) - if formatConfig["reportSpellingErrors"]: - if UIAHandler.AnnotationType_SpellingError in annotationTypes: - formatField["invalid-spelling"] = True - if UIAHandler.AnnotationType_GrammarError in annotationTypes: - formatField["invalid-grammar"] = True - if formatConfig["reportComments"]: - cats = self.obj._UIACustomAnnotationTypes - if ( - cats.microsoftWord_draftComment.id - and cats.microsoftWord_draftComment.id in annotationTypes - ): - formatField["comment"] = textInfos.CommentType.DRAFT - elif ( - cats.microsoftWord_resolvedComment.id - and cats.microsoftWord_resolvedComment.id in annotationTypes - ): - formatField["comment"] = textInfos.CommentType.RESOLVED - elif UIAHandler.AnnotationType_Comment in annotationTypes: - formatField["comment"] = True - if formatConfig["reportRevisions"]: - if UIAHandler.AnnotationType_InsertionChange in annotationTypes: - formatField["revision-insertion"] = True - elif UIAHandler.AnnotationType_DeletionChange in annotationTypes: - formatField["revision-deletion"] = True - if formatConfig["reportBookmarks"]: - cats = self.obj._UIACustomAnnotationTypes - if cats.microsoftWord_bookmark.id and cats.microsoftWord_bookmark.id in annotationTypes: - formatField["bookmark"] = True - cultureVal = fetcher.getValue(UIAHandler.UIA_CultureAttributeId, ignoreMixedValues=ignoreMixedValues) - if cultureVal and isinstance(cultureVal, int): - try: - formatField["language"] = languageHandler.windowsLCIDToLocaleName(cultureVal) - except: # noqa: E722 - log.debugWarning("language error", exc_info=True) - pass + self._getFormatFieldAnnotationTypes(fetch, formatField, formatConfig) + self._getFormatFieldCulture(fetch, formatField) return textInfos.FieldCommand("formatChange", formatField) - def _getFormatFieldIndent( - self, - fetcher: UIATextRangeAttributeValueFetcher, - ignoreMixedValues: bool, - ) -> textInfos.FormatField: - """ - Helper function to get indent formatting from the fetcher passed as parameter. - The indent formatting is reported according to MS Word's convention. - @param fetcher: the UIA fetcher used to get all formatting information. - @param ignoreMixedValues: If True, formatting that is mixed according to UI Automation will not be included. - If False, L{UIAHandler.utils.MixedAttributeError} will be raised if UI Automation gives back - a mixed attribute value signifying that the caller may want to try again with a smaller range. - @return: The indent formatting informations corresponding to what has been retrieved via the fetcher. - """ - - formatField = textInfos.FormatField() - val = fetcher.getValue( - UIAHandler.UIA_IndentationFirstLineAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - uiaIndentFirstLine = val if isinstance(val, float) else None - val = fetcher.getValue( - UIAHandler.UIA_IndentationLeadingAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - uiaIndentLeading = val if isinstance(val, float) else None - val = fetcher.getValue( - UIAHandler.UIA_IndentationTrailingAttributeId, - ignoreMixedValues=ignoreMixedValues, - ) - uiaIndentTrailing = val if isinstance(val, float) else None - if uiaIndentFirstLine is not None and uiaIndentLeading is not None: - reportedFirstLineIndent = uiaIndentFirstLine - uiaIndentLeading - if reportedFirstLineIndent > 0: # First line positive indent - reportedLeftIndent = uiaIndentLeading - reportedHangingIndent = None - elif reportedFirstLineIndent < 0: # First line negative indent - reportedLeftIndent = uiaIndentFirstLine - reportedHangingIndent = -reportedFirstLineIndent - reportedFirstLineIndent = None - else: - reportedLeftIndent = uiaIndentLeading - reportedFirstLineIndent = None - reportedHangingIndent = None - if reportedLeftIndent: - formatField["left-indent"] = self._getIndentValueDisplayString(reportedLeftIndent) - if reportedFirstLineIndent: - formatField["first-line-indent"] = self._getIndentValueDisplayString(reportedFirstLineIndent) - if reportedHangingIndent: - formatField["hanging-indent"] = self._getIndentValueDisplayString(reportedHangingIndent) - if uiaIndentTrailing: - formatField["right-indent"] = self._getIndentValueDisplayString(uiaIndentTrailing) - return formatField - def _getIndentValueDisplayString(self, val: float) -> str: """A function returning the string to display in formatting info. @param val: an indent value measured in points, fetched via