Skip to content

Commit 465dce8

Browse files
Support for automatic row and column header reporting in Microsoft Word. Like Excel, NVDA+shift+c and NVDA+shift+r set the column headers or row headers respectively, Jaws-compatible headers are imported from the document, and these settings are stored in the word document in a Jaws-compatible format.
1 parent 86268dd commit 465dce8

2 files changed

Lines changed: 181 additions & 1 deletion

File tree

‎source/NVDAObjects/window/winword.py‎

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
from comtypes import COMError, GUID, BSTR
99
import comtypes.client
1010
import comtypes.automation
11+
import uuid
1112
import operator
1213
import locale
1314
import braille
15+
import scriptHandler
1416
import languageHandler
1517
import ui
1618
import NVDAHelper
@@ -25,6 +27,7 @@
2527
import textInfos.offsets
2628
import colors
2729
import controlTypes
30+
from tableUtils import HeaderCellInfo, HeaderCellTracker
2831
from . import Window
2932
from ..behaviors import EditableTextWithoutAutoSelectDetection
3033

@@ -348,7 +351,18 @@ def _normalizeControlField(self,field):
348351
field['name']=name
349352
field['alwaysReportName']=True
350353
field['role']=controlTypes.ROLE_FRAME
351-
return field
354+
# Hack support for lazy fetching of row and column header text values
355+
class ControlField(textInfos.ControlField):
356+
def get(d,name,default=None):
357+
if name=="table-rowheadertext":
358+
return self.obj.fetchAssociatedHeaderCellText(self._rangeObj.cells[1],False)
359+
elif name=="table-columnheadertext":
360+
return self.obj.fetchAssociatedHeaderCellText(self._rangeObj.cells[1],True)
361+
else:
362+
return super(ControlField,d).get(name,default)
363+
newField=ControlField()
364+
newField.update(field)
365+
return newField
352366

353367
def _normalizeFormatField(self,field,extraDetail=False):
354368
_startOffset=int(field.pop('_startOffset'))
@@ -508,6 +522,168 @@ def _get_states(self):
508522
states.add(controlTypes.STATE_MULTILINE)
509523
return states
510524

525+
def populateHeaderCellTrackerFromBookmarks(self,headerCellTracker,bookmarks):
526+
for x in bookmarks:
527+
name=x.name
528+
lowerName=name.lower()
529+
isColumnHeader=isRowHeader=False
530+
if lowerName.startswith('title'):
531+
isColumnHeader=isRowHeader=True
532+
elif lowerName.startswith('columntitle'):
533+
isColumnHeader=True
534+
elif lowerName.startswith('rowtitle'):
535+
isRowHeader=True
536+
else:
537+
continue
538+
try:
539+
headerCell=x.range.cells.item(1)
540+
except COMError:
541+
continue
542+
headerCellTracker.addHeaderCellInfo(rowNumber=headerCell.rowIndex,columnNumber=headerCell.columnIndex,name=name,isColumnHeader=isColumnHeader,isRowHeader=isRowHeader)
543+
544+
_curHeaderCellTrackerTable=None
545+
_curHeaderCellTracker=None
546+
def getHeaderCellTrackerForTable(self,table):
547+
tableRange=table.range
548+
if not self._curHeaderCellTrackerTable or not tableRange.isEqual(self._curHeaderCellTrackerTable.range):
549+
self._curHeaderCellTracker=HeaderCellTracker()
550+
self.populateHeaderCellTrackerFromBookmarks(self._curHeaderCellTracker,tableRange.bookmarks)
551+
self._curHeaderCellTrackerTable=table
552+
return self._curHeaderCellTracker
553+
554+
def setAsHeaderCell(self,cell,isColumnHeader=False,isRowHeader=False):
555+
rowNumber=cell.rowIndex
556+
columnNumber=cell.columnIndex
557+
headerCellTracker=self.getHeaderCellTrackerForTable(cell.range.tables[1])
558+
oldInfo=headerCellTracker.getHeaderCellInfoAt(rowNumber,columnNumber)
559+
if oldInfo:
560+
if isColumnHeader and not oldInfo.isColumnHeader:
561+
oldInfo.isColumnHeader=True
562+
elif isRowHeader and not oldInfo.isRowHeader:
563+
oldInfo.isRowHeader=True
564+
else:
565+
return False
566+
isColumnHeader=oldInfo.isColumnHeader
567+
isRowHeader=oldInfo.isRowHeader
568+
if isColumnHeader and isRowHeader:
569+
name="Title_"
570+
elif isRowHeader:
571+
name="RowTitle_"
572+
elif isColumnHeader:
573+
name="ColumnTitle_"
574+
else:
575+
raise ValueError("One or both of isColumnHeader or isRowHeader must be True")
576+
name+=uuid.uuid4().hex
577+
if oldInfo:
578+
self.WinwordDocumentObject.bookmarks[oldInfo.name].delete()
579+
oldInfo.name=name
580+
else:
581+
headerCellTracker.addHeaderCellInfo(rowNumber=rowNumber,columnNumber=columnNumber,name=name,isColumnHeader=isColumnHeader,isRowHeader=isRowHeader)
582+
self.WinwordDocumentObject.bookmarks.add(name,cell.range)
583+
return True
584+
585+
def forgetHeaderCell(self,cell,isColumnHeader=False,isRowHeader=False):
586+
rowNumber=cell.rowIndex
587+
columnNumber=cell.columnIndex
588+
if not isColumnHeader and not isRowHeader:
589+
return False
590+
headerCellTracker=self.getHeaderCellTrackerForTable(cell.range.tables[1])
591+
info=headerCellTracker.getHeaderCellInfoAt(rowNumber,columnNumber)
592+
if not info:
593+
return False
594+
if isColumnHeader and info.isColumnHeader:
595+
info.isColumnHeader=False
596+
elif isRowHeader and info.isRowHeader:
597+
info.isRowHeader=False
598+
else:
599+
return False
600+
headerCellTracker.removeHeaderCellInfo(info)
601+
self.WinwordDocumentObject.bookmarks(info.name).delete()
602+
if info.isColumnHeader or info.isRowHeader:
603+
self.setAsHeaderCell(cell,isColumnHeader=info.isColumnHeader,isRowHeader=info.isRowHeader)
604+
return True
605+
606+
def fetchAssociatedHeaderCellText(self,cell,columnHeader=False):
607+
table=cell.range.tables[1]
608+
rowNumber=cell.rowIndex
609+
columnNumber=cell.columnIndex
610+
headerCellTracker=self.getHeaderCellTrackerForTable(table)
611+
for info in headerCellTracker.iterPossibleHeaderCellInfosFor(rowNumber,columnNumber,columnHeader=columnHeader):
612+
textList=[]
613+
if columnHeader:
614+
for headerRowNumber in xrange(info.rowNumber,info.rowNumber+info.rowSpan):
615+
headerCell=table.cell(headerRowNumber,columnNumber)
616+
textList.append(headerCell.range.text)
617+
else:
618+
for headerColumnNumber in xrange(info.columnNumber,info.columnNumber+info.colSpan):
619+
headerCell=table.cell(rowNumber,headerColumnNumber)
620+
textList.append(headerCell.range.text)
621+
text=" ".join(textList)
622+
if text:
623+
return text
624+
625+
def script_setColumnHeader(self,gesture):
626+
scriptCount=scriptHandler.getLastScriptRepeatCount()
627+
if not config.conf['documentFormatting']['reportTableHeaders']:
628+
# Translators: a message reported in the SetColumnHeader script for Microsoft Word.
629+
ui.message(_("Cannot set headers. Please enable reporting of table headers in Document Formatting Settings"))
630+
return
631+
try:
632+
cell=self.WinwordSelectionObject.cells[1]
633+
except COMError:
634+
# Translators: a message when trying to perform an action on a cell when not in one in Microsoft word
635+
ui.message(_("Not in a table cell"))
636+
return
637+
if scriptCount==0:
638+
if self.setAsHeaderCell(cell,isColumnHeader=True,isRowHeader=False):
639+
# Translators: a message reported in the SetColumnHeader script for Microsoft Word.
640+
ui.message(_("Set row {rowNumber} column {columnNumber} as start of column headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
641+
else:
642+
# Translators: a message reported in the SetColumnHeader script for Microsoft Word.
643+
ui.message(_("Already set row {rowNumber} column {columnNumber} as start of column headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
644+
elif scriptCount==1:
645+
if self.forgetHeaderCell(cell,isColumnHeader=True,isRowHeader=False):
646+
# Translators: a message reported in the SetColumnHeader script for Microsoft Word.
647+
ui.message(_("Removed row {rowNumber} column {columnNumber} from column headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
648+
else:
649+
# Translators: a message reported in the SetColumnHeader script for Microsoft Word.
650+
ui.message(_("Cannot find row {rowNumber} column {columnNumber} in column headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
651+
script_setColumnHeader.__doc__=_("Pressing once will set this cell as the first column header for any cells lower and to the right of it within this table. Pressing twice will forget the current column header for this cell.")
652+
653+
def script_setRowHeader(self,gesture):
654+
scriptCount=scriptHandler.getLastScriptRepeatCount()
655+
if not config.conf['documentFormatting']['reportTableHeaders']:
656+
# Translators: a message reported in the SetRowHeader script for Microsoft Word.
657+
ui.message(_("Cannot set headers. Please enable reporting of table headers in Document Formatting Settings"))
658+
return
659+
try:
660+
cell=self.WinwordSelectionObject.cells[1]
661+
except COMError:
662+
# Translators: a message when trying to perform an action on a cell when not in one in Microsoft word
663+
ui.message(_("Not in a table cell"))
664+
return
665+
if scriptCount==0:
666+
if self.setAsHeaderCell(cell,isColumnHeader=False,isRowHeader=True):
667+
# Translators: a message reported in the SetRowHeader script for Microsoft Word.
668+
ui.message(_("Set row {rowNumber} column {columnNumber} as start of row headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
669+
else:
670+
# Translators: a message reported in the SetRowHeader script for Microsoft Word.
671+
ui.message(_("Already set row {rowNumber} column {columnNumber} as start of row headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
672+
elif scriptCount==1:
673+
if self.forgetHeaderCell(cell,isColumnHeader=False,isRowHeader=True):
674+
# Translators: a message reported in the SetRowHeader script for Microsoft Word.
675+
ui.message(_("Removed row {rowNumber} column {columnNumber} from row headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
676+
else:
677+
# Translators: a message reported in the SetRowHeader script for Microsoft Word.
678+
ui.message(_("Cannot find row {rowNumber} column {columnNumber} in row headers").format(rowNumber=cell.rowIndex,columnNumber=cell.columnIndex))
679+
script_setRowHeader.__doc__=_("Pressing once will set this cell as the first row header for any cells lower and to the right of it within this table. Pressing twice will forget the current row header for this cell.")
680+
681+
def script_reportCurrentHeaders(self,gesture):
682+
cell=self.WinwordSelectionObject.cells[1]
683+
rowText=self.fetchAssociatedHeaderCellText(cell,False)
684+
columnText=self.fetchAssociatedHeaderCellText(cell,True)
685+
ui.message("Row %s, column %s"%(rowText or "empty",columnText or "empty"))
686+
511687
def _get_WinwordVersion(self):
512688
if not hasattr(self,'_WinwordVersion'):
513689
self._WinwordVersion=float(self.WinwordApplicationObject.version)
@@ -614,6 +790,9 @@ def script_previousColumn(self,gesture):
614790
__gestures = {
615791
"kb:tab": "tab",
616792
"kb:shift+tab": "tab",
793+
"kb:NVDA+shift+c":"setColumnHeader",
794+
"kb:NVDA+shift+r":"setRowHeader",
795+
"kb:NVDA+shift+h":"reportCurrentHeaders",
617796
"kb:control+alt+upArrow": "previousRow",
618797
"kb:control+alt+downArrow": "nextRow",
619798
"kb:control+alt+leftArrow": "previousColumn",

‎source/tableUtils.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
class HeaderCellInfo(object):
77
__slots__=['rowNumber','columnNumber','rowSpan','colSpan','minRowNumber','maxRowNumber','minColumnNumber','maxColumnNumber','name','isRowHeader','isColumnHeader']
88
def __init__(self,**kwargs):
9+
self.rowSpan=self.colSpan=1
910
self.minColumnNumber=self.maxColumnNumber=self.minRowNumber=self.maxRowNumber=None
1011
for name,value in kwargs.iteritems():
1112
setattr(self,name,value)

0 commit comments

Comments
 (0)