-
-
Notifications
You must be signed in to change notification settings - Fork 715
Improve selection and merged cell announcements in LibreOffice Calc 7.3 and above #12849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
b9f7ee1
f534f2d
cedd3f0
e135386
efad4d4
2db3707
5fa7c01
8da36f6
78c4402
bfe8453
d116976
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,82 +1,25 @@ | ||
| #appModules/soffice.py | ||
| #A part of NonVisual Desktop Access (NVDA) | ||
| #This file is covered by the GNU General Public License. | ||
| #See the file COPYING for more details. | ||
| #Copyright (C) 2006-2019 NV Access Limited, Bill Dengler | ||
| # A part of NonVisual Desktop Access (NVDA) | ||
| # This file is covered by the GNU General Public License. | ||
| # See the file COPYING for more details. | ||
| # Copyright (C) 2006-2021 NV Access Limited, Bill Dengler, Leonard de Ruijter | ||
|
|
||
| from comtypes import COMError | ||
| from comInterfaces import IAccessible2Lib as IA2 | ||
| import IAccessibleHandler | ||
| import appModuleHandler | ||
| import controlTypes | ||
| import textInfos | ||
| import colors | ||
| from compoundDocuments import CompoundDocument | ||
| from NVDAObjects.JAB import JAB, JABTextInfo | ||
| from NVDAObjects.IAccessible import IAccessible, IA2TextTextInfo | ||
| from NVDAObjects.behaviors import EditableText | ||
| from logHandler import log | ||
| import speech | ||
| import ui | ||
| import time | ||
| import api | ||
| import braille | ||
| import vision | ||
|
|
||
| def gridCoordStringToNumbers(coordString): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the removal of these functions and classes going to affect add-ons? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's very unlikely I think. Add-ons should avoid using appModules directly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @feerrenrut @michaelDCurran what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @feerrenrut - requested you review pending this discussion, and my concerns including this in 2021.3 |
||
| if not coordString or len(coordString)<2 or ' ' in coordString or coordString[0].isdigit() or not coordString[-1].isdigit(): | ||
| raise ValueError("bad coord string: %r"%coordString) | ||
| rowNum=0 | ||
| colNum=0 | ||
| coordStringRowStartIndex=None | ||
| for index,ch in enumerate(reversed(coordString)): | ||
| if not ch.isdigit(): | ||
| coordStringRowStartIndex=len(coordString)-index | ||
| break | ||
| rowNum=int(coordString[coordStringRowStartIndex:]) | ||
| for index,ch in enumerate(reversed(coordString[0:coordStringRowStartIndex])): | ||
| colNum+=((ord(ch.upper())-ord('A')+1)*(26**index)) | ||
| return rowNum,colNum | ||
|
|
||
| class JAB_OOTable(JAB): | ||
|
|
||
| def _get_rowCount(self): | ||
| return 0 | ||
|
|
||
| def _get_columnCount(self): | ||
| return 0 | ||
|
|
||
| class JAB_OOTableCell(JAB): | ||
|
|
||
| role=controlTypes.Role.TABLECELL | ||
|
|
||
| def _get_name(self): | ||
| name=super(JAB_OOTableCell,self).name | ||
| if name and name.startswith('Cell') and name[-2].isdigit(): | ||
| return None | ||
| return name | ||
|
|
||
| def _get_cellCoordsText(self): | ||
| name=super(JAB_OOTableCell,self).name | ||
| if name and name.startswith('Cell') and name[-2].isdigit(): | ||
| return name[5:-1] | ||
|
|
||
| def _get_value(self): | ||
| value=super(JAB_OOTableCell,self).value | ||
| if not value and issubclass(self.TextInfo,JABTextInfo): | ||
| value=self.makeTextInfo(textInfos.POSITION_ALL).text | ||
| return value | ||
|
|
||
| def _get_states(self): | ||
| states=super(JAB_OOTableCell,self).states | ||
| states.discard(controlTypes.State.EDITABLE) | ||
| return states | ||
|
|
||
| def _get_rowNumber(self): | ||
| try: | ||
| return gridCoordStringToNumbers(self.cellCoordsText)[0] | ||
| except ValueError: | ||
| return 0 | ||
|
|
||
| def _get_columnNumber(self): | ||
| try: | ||
| return gridCoordStringToNumbers(self.cellCoordsText)[1] | ||
| except ValueError: | ||
| return 0 | ||
|
|
||
| class SymphonyTextInfo(IA2TextTextInfo): | ||
|
|
||
|
|
@@ -151,7 +94,7 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True): | |
|
|
||
| # optimisation: Assume a hyperlink occupies a full attribute run. | ||
| try: | ||
| if obj.IAccessibleTextObject.QueryInterface(IA2.IAccessibleHypertext).hyperlinkIndex(offset) != -1: | ||
| if obj.IAccessibleTextObject.QueryInterface(IAccessibleHandler.IA2.IAccessibleHypertext).hyperlinkIndex(offset) != -1: | ||
| formatField["link"] = True | ||
| except COMError: | ||
| pass | ||
|
|
@@ -194,6 +137,7 @@ def _getStoryLength(self): | |
| # HACK: Account for the character faked in _getLineOffsets() so that move() will work. | ||
| return max(super(SymphonyTextInfo, self)._getStoryLength(), 1) | ||
|
|
||
|
|
||
| class SymphonyText(IAccessible, EditableText): | ||
| TextInfo = SymphonyTextInfo | ||
|
|
||
|
|
@@ -203,6 +147,7 @@ def _get_positionInfo(self): | |
| return {"level": int(level)} | ||
| return super(SymphonyText, self).positionInfo | ||
|
|
||
|
|
||
| class SymphonyTableCell(IAccessible): | ||
| """Silences particular states, and redundant column/row numbers""" | ||
|
|
||
|
|
@@ -213,26 +158,92 @@ def _get_cellCoordsText(self): | |
|
|
||
| name=None | ||
|
|
||
| def _get_hasSelection(self): | ||
| return ( | ||
| self.selectionContainer | ||
| and 1 < self.selectionContainer.getSelectedItemsCount() | ||
| ) | ||
|
|
||
| def _get_states(self): | ||
| states=super(SymphonyTableCell,self).states | ||
| states.discard(controlTypes.State.MULTILINE) | ||
| states.discard(controlTypes.State.EDITABLE) | ||
| if controlTypes.State.SELECTED not in states and {controlTypes.State.FOCUSED, controlTypes.State.SELECTABLE}.issubset(states): | ||
| if controlTypes.State.SELECTED not in states and controlTypes.State.FOCUSED in states: | ||
| # #8988: Cells in Libre Office do not have the selected state when a single cell is selected (i.e. has focus). | ||
| # Since #8898, the negative selected state is announced for table cells with the selectable state. | ||
| states.add(controlTypes.State.SELECTED) | ||
| if self.hasSelection: | ||
| # The selected state is never added to a focused object, even though it is selected. | ||
| # We assume our focus is in the selection. | ||
| states.add(controlTypes.State.SELECTED) | ||
| else: | ||
| # Remove the selectable state, since that ensures the negative selected state isn't spoken for focused cells. | ||
| states.discard(controlTypes.State.SELECTABLE) | ||
| if self.IA2Attributes.get('Formula'): | ||
| # #860: Recent versions of Calc expose has formula state via IAccessible 2. | ||
| states.add(controlTypes.State.HASFORMULA) | ||
| return states | ||
|
|
||
|
|
||
| class SymphonyIATableCell(SymphonyTableCell): | ||
| """An overlay class for cells implementing IAccessibleTableCell""" | ||
|
|
||
| def event_selectionAdd(self): | ||
| curFocus = api.getFocusObject() | ||
| if self.table and self.table == curFocus.table: | ||
| curFocus.announceSelectionChange() | ||
|
|
||
| def event_selectionRemove(self): | ||
| self.event_selectionAdd() | ||
|
|
||
| def announceSelectionChange(self): | ||
| if self is api.getFocusObject(): | ||
| speech.speakObjectProperties(self, states=True, cellCoordsText=True, reason=controlTypes.OutputReason.CHANGE) | ||
| braille.handler.handleUpdate(self) | ||
| vision.handler.handleUpdate(self, property="states") | ||
|
|
||
| def _get_cellCoordsText(self): | ||
| if self.hasSelection and controlTypes.State.FOCUSED in self.states: | ||
| selected, count = self.table.IAccessibleTable2Object.selectedCells | ||
| firstAccessible = selected[0].QueryInterface(IAccessibleHandler.IA2.IAccessible2) | ||
| firstAddress = firstAccessible.accName(0) | ||
| firstValue = firstAccessible.accValue(0) or '' | ||
| lastAccessible = selected[count - 1].QueryInterface(IAccessibleHandler.IA2.IAccessible2) | ||
| lastAddress = lastAccessible.accName(0) | ||
| lastValue = lastAccessible.accValue(0) or '' | ||
| # Translators: LibreOffice, report selected range of cell coordinates with their values | ||
| return _("{firstAddress} {firstValue} through {lastAddress} {lastValue}").format( | ||
| firstAddress=firstAddress, | ||
| firstValue=firstValue, | ||
| lastAddress=lastAddress, | ||
| lastValue=lastValue | ||
| ) | ||
| elif self.rowSpan > 1 or self.columnSpan > 1: | ||
| lastSelected = ( | ||
| (self.rowNumber - 1) + (self.rowSpan - 1), | ||
| (self.columnNumber - 1) + (self.columnSpan - 1) | ||
| ) | ||
| lastCellUnknown = self.table.IAccessibleTable2Object.cellAt(*lastSelected) | ||
| lastAccessible = lastCellUnknown.QueryInterface(IAccessibleHandler.IA2.IAccessible2) | ||
| lastAddress = lastAccessible.accName(0) | ||
| # Translators: LibreOffice, report range of cell coordinates | ||
| return _("{firstAddress} throuhg {lastAddress}").format( | ||
| firstAddress=self._get_name(), | ||
| lastAddress=lastAddress | ||
| ) | ||
| return super().cellCoordsText | ||
|
|
||
|
|
||
| class SymphonyTable(IAccessible): | ||
|
|
||
| def getSelectedItemsCount(self,maxCount=2): | ||
| # #8988: Neither accSelection nor IAccessibleTable2 is implemented on the LibreOffice tables. | ||
| # Returning 1 will suppress redundant selected announcements, | ||
| # while having the drawback of never announcing selected for selected cells. | ||
| return 1 | ||
| def _getSelectedItemsCount_accSelection(self, maxCount): | ||
| # accSelection is broken in LibreOffice. | ||
| raise NotImplementedError | ||
|
|
||
| def event_selectionWithIn(self): | ||
| curFocus = api.getFocusObject() | ||
| if self == curFocus.table: | ||
| curFocus.announceSelectionChange() | ||
|
|
||
|
|
||
| class SymphonyParagraph(SymphonyText): | ||
| """Removes redundant information that can be retreaved in other ways.""" | ||
|
|
@@ -246,28 +257,22 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList): | |
| windowClassName=obj.windowClassName | ||
| if isinstance(obj, IAccessible) and windowClassName in ("SALTMPSUBFRAME", "SALSUBFRAME", "SALFRAME"): | ||
| if role==controlTypes.Role.TABLECELL: | ||
| clsList.insert(0, SymphonyTableCell) | ||
| elif role==controlTypes.Role.TABLE: | ||
| if obj._IATableCell: | ||
| clsList.insert(0, SymphonyIATableCell) | ||
| else: | ||
| clsList.insert(0, SymphonyTableCell) | ||
| elif role==controlTypes.Role.TABLE and ( | ||
| hasattr(obj, "IAccessibleTable2Object") | ||
| or hasattr(obj, "IAccessibleTableObject") | ||
| ): | ||
| clsList.insert(0, SymphonyTable) | ||
| elif hasattr(obj, "IAccessibleTextObject"): | ||
| clsList.insert(0, SymphonyText) | ||
| if role==controlTypes.Role.PARAGRAPH: | ||
| clsList.insert(0, SymphonyParagraph) | ||
| if isinstance(obj, JAB) and windowClassName == "SALFRAME": | ||
| if role in (controlTypes.Role.PANEL,controlTypes.Role.LABEL): | ||
| parent=obj.parent | ||
| if parent and parent.role==controlTypes.Role.TABLE: | ||
| clsList.insert(0,JAB_OOTableCell) | ||
| elif role==controlTypes.Role.TABLE: | ||
| clsList.insert(0,JAB_OOTable) | ||
|
|
||
| def event_NVDAObject_init(self, obj): | ||
| windowClass = obj.windowClassName | ||
| if isinstance(obj, JAB) and windowClass == "SALFRAME": | ||
| # OpenOffice.org has some strange role mappings due to its use of JAB. | ||
| if obj.role == controlTypes.Role.CANVAS: | ||
| obj.role = controlTypes.Role.DOCUMENT | ||
|
|
||
| if windowClass in ("SALTMPSUBFRAME", "SALFRAME") and obj.role in (controlTypes.Role.DOCUMENT,controlTypes.Role.TEXTFRAME) and obj.description: | ||
| # This is a word processor document. | ||
| obj.description = None | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.