|
8 | 8 | from comtypes import COMError, GUID, BSTR |
9 | 9 | import comtypes.client |
10 | 10 | import comtypes.automation |
| 11 | +import uuid |
11 | 12 | import operator |
12 | 13 | import locale |
13 | 14 | import braille |
| 15 | +import scriptHandler |
14 | 16 | import languageHandler |
15 | 17 | import ui |
16 | 18 | import NVDAHelper |
|
25 | 27 | import textInfos.offsets |
26 | 28 | import colors |
27 | 29 | import controlTypes |
| 30 | +from tableUtils import HeaderCellInfo, HeaderCellTracker |
28 | 31 | from . import Window |
29 | 32 | from ..behaviors import EditableTextWithoutAutoSelectDetection |
30 | 33 |
|
@@ -348,7 +351,18 @@ def _normalizeControlField(self,field): |
348 | 351 | field['name']=name |
349 | 352 | field['alwaysReportName']=True |
350 | 353 | 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 |
352 | 366 |
|
353 | 367 | def _normalizeFormatField(self,field,extraDetail=False): |
354 | 368 | _startOffset=int(field.pop('_startOffset')) |
@@ -508,6 +522,168 @@ def _get_states(self): |
508 | 522 | states.add(controlTypes.STATE_MULTILINE) |
509 | 523 | return states |
510 | 524 |
|
| 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 | + |
511 | 687 | def _get_WinwordVersion(self): |
512 | 688 | if not hasattr(self,'_WinwordVersion'): |
513 | 689 | self._WinwordVersion=float(self.WinwordApplicationObject.version) |
@@ -614,6 +790,9 @@ def script_previousColumn(self,gesture): |
614 | 790 | __gestures = { |
615 | 791 | "kb:tab": "tab", |
616 | 792 | "kb:shift+tab": "tab", |
| 793 | + "kb:NVDA+shift+c":"setColumnHeader", |
| 794 | + "kb:NVDA+shift+r":"setRowHeader", |
| 795 | + "kb:NVDA+shift+h":"reportCurrentHeaders", |
617 | 796 | "kb:control+alt+upArrow": "previousRow", |
618 | 797 | "kb:control+alt+downArrow": "nextRow", |
619 | 798 | "kb:control+alt+leftArrow": "previousColumn", |
|
0 commit comments