Skip to content

Commit

Permalink
Excel with UIA: announce merged cells (#12843)
Browse files Browse the repository at this point in the history
Summary of the issue:
When UIA is enabled for Excel, merged cells were not mentioned as such. Instead, only the first coordinates of the merged range were reported.

Description of how this pull request fixes the issue:
There is no way to fetch the last cell in the range from Excel itself. The UIA implementation in Excel also refuses to provide real row and column numbers. Therefore we have to convert the alphabetical column representation to a column number, then correct the column number for column span and convert it back to alphabetical representation.
  • Loading branch information
LeonarddeR authored Jun 20, 2022
1 parent c42a4ba commit 0976a98
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 8 deletions.
75 changes: 67 additions & 8 deletions source/NVDAObjects/UIA/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import ui
from logHandler import log
from . import UIA
import re


class ExcelCustomProperties:
Expand Down Expand Up @@ -131,6 +132,8 @@ class ExcelObject(UIA):

class ExcelCell(ExcelObject):

_coordinateRegEx = re.compile("([A-Z]+)([0-9]+)", re.IGNORECASE)

# selecting cells causes duplicate focus events
shouldAllowDuplicateUIAFocusEvent = True

Expand Down Expand Up @@ -411,6 +414,40 @@ def _get_states(self):
states.add(controlTypes.State.HASNOTE)
return states

@staticmethod
def _getColumnRepresentationForNumber(n: int) -> str:
"""
Convert a decimal number to its base alphabet representation.
See https://codereview.stackexchange.com/questions/182733/base-26-letters-and-base-10-using-recursion
for more details about the approach used.
"""
def modGenerator(x: int) -> Tuple[int, int]:
"""Generate digits from L{x} in base alphabet, least significants
bits first.
Since A is 1 rather than 0 in base alphabet, we are dealing with
L{x} - 1 at each iteration to be able to extract the proper digits.
"""
while x:
x, y = divmod(x - 1, 26)
yield y
return ''.join(
chr(ord("A") + i)
for i in modGenerator(n)
)[::-1]

@staticmethod
def _getNumberRepresentationForColumn(column: str) -> int:
"""
Convert an alphabet number to its decimal representation.
See https://codereview.stackexchange.com/questions/182733/base-26-letters-and-base-10-using-recursion
for more details about the approach used.
"""
return sum(
(ord(letter) - ord("A") + 1) * (26 ** i)
for i, letter in enumerate(reversed(column.upper()))
)

def _get_cellCoordsText(self):
if self._hasSelection():
sc = self._getUIACacheablePropertyValue(
Expand All @@ -423,7 +460,7 @@ def _get_cellCoordsText(self):

firstAddress = firstSelected.GetCurrentPropertyValue(
UIAHandler.UIA_NamePropertyId
).replace('"', '')
).replace('"', '').replace(' ', '')

firstValue = firstSelected.GetCurrentPropertyValue(
UIAHandler.UIA_ValueValuePropertyId
Expand All @@ -435,15 +472,15 @@ def _get_cellCoordsText(self):

lastAddress = lastSelected.GetCurrentPropertyValue(
UIAHandler.UIA_NamePropertyId
).replace('"', '')
).replace('"', '').replace(' ', '')

lastValue = lastSelected.GetCurrentPropertyValue(
UIAHandler.UIA_ValueValuePropertyId
)

cellCoordsTemplate = pgettext(
"excel-UIA",
# Translators: Excel, report range of cell coordinates
# Translators: Excel, report selected range of cell coordinates
"{firstAddress} {firstValue} through {lastAddress} {lastValue}"
)
return cellCoordsTemplate.format(
Expand All @@ -452,11 +489,33 @@ def _get_cellCoordsText(self):
lastAddress=lastAddress,
lastValue=lastValue
)
name = super().name
# Later builds of Excel 2016 quote the letter coordinate.
# We don't want the quotes.
name = name.replace('"', '')
return name
else:
name = super().name
# Later builds of Excel 2016 quote the letter coordinate.
# We don't want the quotes and also strip the space between column and row.
name = name.replace('"', '').replace(' ', '')
if self.rowSpan > 1 or self.columnSpan > 1:
# Excel does not offer information about merged cells
# but merges all merged cells into one UIA element named as the first cell in the merged range.
# We have to calculate the last address name manually.
firstAddress = name
firstColumn, firstRow = self._coordinateRegEx.match(firstAddress).groups()
firstRow = int(firstRow)
lastColumn = firstColumn if self.columnSpan == 1 else self._getColumnRepresentationForNumber(
self._getNumberRepresentationForColumn(firstColumn) + (self.columnSpan - 1)
)
lastRow = firstRow + (self.rowSpan - 1)
lastAddress = f"{lastColumn}{lastRow}"
cellCoordsTemplate = pgettext(
"excel-UIA",
# Translators: Excel, report merged range of cell coordinates
"{firstAddress} through {lastAddress}"
)
return cellCoordsTemplate.format(
firstAddress=firstAddress,
lastAddress=lastAddress,
)
return name

@script(
# Translators: the description for a script for Excel
Expand Down
2 changes: 2 additions & 0 deletions user_docs/en/changes.t2t
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ What's New in NVDA


== Changes ==
- When using UI Automation to access Microsoft Excel spreadsheet controls, NVDA is now able to report when a cell is merged. (#12843)
-


== Bug Fixes ==
Expand Down

0 comments on commit 0976a98

Please sign in to comment.