Repository: nvaccess/nvda

Issue:
Enabling UIA Support in Word Causes Paragraph Indentation Not to be Reported
<!-- Please read the text in this edit field before filling it in.
Please thoroughly read NVDA's wiki article on how to fill in this template, including how to provide the required files.
Issues may be closed if the required information is not present.
https://github.com/nvaccess/nvda/blob/master/devDocs/githubIssueTemplateExplanationAndExamples.md
Please also note that the NVDA project has a Citizen and Contributor Code of Conduct which can be found at https://github.com/nvaccess/nvda/blob/master/CODE_OF_CONDUCT.md. NV Access expects that all contributors and other community members read and abide by the rules set out in this document while participating or contributing to this project. This includes creating or commenting on issues and pull requests. 

Each of the questions and sections below start with multiple hash symbols (#). Place your answers and information on the blank line below each question.
-->

### Steps to reproduce:
1. Enable UIA support for Word in NVDA avnaced settings.
2. Open a word document.
3. Locate the indented first line of a paragraph.
4. Optionally, press NVDA+F to report formatting at current location.

### Actual behavior:
When I arrow down to the indented first line of a paragraph, NVDA doesn't announce indentation when UIA is enabled. When UIA is disabled, the problem is resolved. Pressing NVDA+F doesn't help.

### Expected behavior:
NVDA should report indentation when I locate the indented first line of a paragraph.

### System configuration
#### NVDA installed/portable/running from source:
Installed.

#### NVDA version:
2021.2

#### Windows version:
Windows 10 21H1 x64 build 19043.1266

#### Name and version of other software in use when reproducing the issue:
Office 16/365

#### Other information about your system:

### Other questions
#### Does the issue still occur after restarting your computer?
yes

#### Have you tried any other versions of NVDA? If so, please report their behaviors.
no

#### If NVDA add-ons are disabled, is your problem still occurring?

#### Does the issue still occur after you run the COM Registration Fixing Tool in NVDA's tools menu?
yes


Discussion:
Relates to #12770 - cc @michaelDCurran 

Resolving patch:
diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py
--- a/source/NVDAObjects/UIA/__init__.py
+++ b/source/NVDAObjects/UIA/__init__.py
@@ -25,7 +25,15 @@
 import api
 import textInfos
 from logHandler import log
-from UIAUtils import *
+from UIAUtils import (
+	BulkUIATextRangeAttributeValueFetcher,
+	UIATextRangeAttributeValueFetcher,
+	getChildrenWithCacheFromUIATextRange,
+	getEnclosingElementWithCacheFromUIATextRange,
+	iterUIARangeByUnit,
+	UIAMixedAttributeError,
+	UIATextRangeFromElement,
+)
 from NVDAObjects.window import Window
 from NVDAObjects import NVDAObjectTextInfo, InvalidNVDAObject
 from NVDAObjects.behaviors import (
@@ -44,11 +52,12 @@
 
 
 paragraphIndentIDs = {
-	UIAHandler.UIA_IndentationFirstLineAttributeId: "first-line-indent",
-	UIAHandler.UIA_IndentationLeadingAttributeId: "left-indent",
-	UIAHandler.UIA_IndentationTrailingAttributeId: "right-indent",
+	UIAHandler.UIA_IndentationFirstLineAttributeId,
+	UIAHandler.UIA_IndentationLeadingAttributeId,
+	UIAHandler.UIA_IndentationTrailingAttributeId,
 }
 
+
 class UIATextInfo(textInfos.TextInfo):
 
 	_cache_controlFieldNVDAObjectClass=True
@@ -232,20 +241,7 @@ def _getFormatFieldAtRange(self,textRange,formatConfig,ignoreMixedValues=False):
 			if val!=UIAHandler.handler.reservedNotSupportedValue:
 				formatField["style"]=val
 		if formatConfig["reportParagraphIndentation"]:
-			for ID, fieldAttr in paragraphIndentIDs.items():
-				val = fetcher.getValue(ID, ignoreMixedValues=ignoreMixedValues)
-				if isinstance(val, float):
-					# val is in points (1/72 of an inch)
-					val /= 72.0
-					if languageHandler.useImperialMeasurements():
-						# Translators: a measurement in inches
-						valText = _("{val:.2f} in").format(val=val)
-					else:
-						# Convert from inches to centermetres
-						val *= 2.54
-						# Translators: a measurement in centermetres
-						valText = _("{val:.2f} cm").format(val=val)
-					formatField[fieldAttr] = valText
+			formatField.update(self._getFormatFieldIndent(fetcher, ignoreMixedValues=ignoreMixedValues))
 		if formatConfig["reportAlignment"]:
 			val=fetcher.getValue(UIAHandler.UIA_HorizontalTextAlignmentAttributeId,ignoreMixedValues=ignoreMixedValues)
 			if val==UIAHandler.HorizontalTextAlignment_Left:
@@ -311,6 +307,72 @@ def _getFormatFieldAtRange(self,textRange,formatConfig,ignoreMixedValues=False):
 				pass
 		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{UIAUtils.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
+	
+	@staticmethod
+	def _getIndentValueDisplayString(val: float) -> str:
+		"""A function returning the string to display in formatting info.
+		@param val: an indent value measured in points, fetched via
+			an UIAHandler.UIA_Indentation*AttributeId attribute.
+		@return: The string used in formatting information to report the length of an indentation.
+		"""
+		
+		# convert points to inches (1pt = 1/72 in)
+		val /= 72.0
+		if languageHandler.useImperialMeasurements():
+			# Translators: a measurement in inches
+			valText = _("{val:.2f} in").format(val=val)
+		else:
+			# Convert from inches to centimetres
+			val *= 2.54
+			# Translators: a measurement in centimetres
+			valText = _("{val:.2f} cm").format(val=val)
+		return valText
+	
+	
 	def __init__(self,obj,position,_rangeObj=None):
 		super(UIATextInfo,self).__init__(obj,position)
 		if _rangeObj:


Test patch: