1- #appModules/soffice.py
2- #A part of NonVisual Desktop Access (NVDA)
3- #This file is covered by the GNU General Public License.
4- #See the file COPYING for more details.
5- #Copyright (C) 2006-2019 NV Access Limited, Bill Dengler
1+ # A part of NonVisual Desktop Access (NVDA)
2+ # This file is covered by the GNU General Public License.
3+ # See the file COPYING for more details.
4+ # Copyright (C) 2006-2021 NV Access Limited, Bill Dengler, Leonard de Ruijter
65
76from comtypes import COMError
8- from comInterfaces import IAccessible2Lib as IA2
97import IAccessibleHandler
108import appModuleHandler
119import controlTypes
1210import textInfos
1311import colors
1412from compoundDocuments import CompoundDocument
15- from NVDAObjects .JAB import JAB , JABTextInfo
1613from NVDAObjects .IAccessible import IAccessible , IA2TextTextInfo
1714from NVDAObjects .behaviors import EditableText
1815from logHandler import log
16+ import speech
17+ import ui
18+ import time
19+ import api
20+ import braille
21+ import vision
1922
20- def gridCoordStringToNumbers (coordString ):
21- if not coordString or len (coordString )< 2 or ' ' in coordString or coordString [0 ].isdigit () or not coordString [- 1 ].isdigit ():
22- raise ValueError ("bad coord string: %r" % coordString )
23- rowNum = 0
24- colNum = 0
25- coordStringRowStartIndex = None
26- for index ,ch in enumerate (reversed (coordString )):
27- if not ch .isdigit ():
28- coordStringRowStartIndex = len (coordString )- index
29- break
30- rowNum = int (coordString [coordStringRowStartIndex :])
31- for index ,ch in enumerate (reversed (coordString [0 :coordStringRowStartIndex ])):
32- colNum += ((ord (ch .upper ())- ord ('A' )+ 1 )* (26 ** index ))
33- return rowNum ,colNum
34-
35- class JAB_OOTable (JAB ):
36-
37- def _get_rowCount (self ):
38- return 0
39-
40- def _get_columnCount (self ):
41- return 0
42-
43- class JAB_OOTableCell (JAB ):
44-
45- role = controlTypes .Role .TABLECELL
46-
47- def _get_name (self ):
48- name = super (JAB_OOTableCell ,self ).name
49- if name and name .startswith ('Cell' ) and name [- 2 ].isdigit ():
50- return None
51- return name
52-
53- def _get_cellCoordsText (self ):
54- name = super (JAB_OOTableCell ,self ).name
55- if name and name .startswith ('Cell' ) and name [- 2 ].isdigit ():
56- return name [5 :- 1 ]
57-
58- def _get_value (self ):
59- value = super (JAB_OOTableCell ,self ).value
60- if not value and issubclass (self .TextInfo ,JABTextInfo ):
61- value = self .makeTextInfo (textInfos .POSITION_ALL ).text
62- return value
63-
64- def _get_states (self ):
65- states = super (JAB_OOTableCell ,self ).states
66- states .discard (controlTypes .State .EDITABLE )
67- return states
68-
69- def _get_rowNumber (self ):
70- try :
71- return gridCoordStringToNumbers (self .cellCoordsText )[0 ]
72- except ValueError :
73- return 0
74-
75- def _get_columnNumber (self ):
76- try :
77- return gridCoordStringToNumbers (self .cellCoordsText )[1 ]
78- except ValueError :
79- return 0
8023
8124class SymphonyTextInfo (IA2TextTextInfo ):
8225
@@ -151,7 +94,7 @@ def _getFormatFieldAndOffsets(self,offset,formatConfig,calculateOffsets=True):
15194
15295 # optimisation: Assume a hyperlink occupies a full attribute run.
15396 try :
154- if obj .IAccessibleTextObject .QueryInterface (IA2 .IAccessibleHypertext ).hyperlinkIndex (offset ) != - 1 :
97+ if obj .IAccessibleTextObject .QueryInterface (IAccessibleHandler . IA2 .IAccessibleHypertext ).hyperlinkIndex (offset ) != - 1 :
15598 formatField ["link" ] = True
15699 except COMError :
157100 pass
@@ -194,6 +137,7 @@ def _getStoryLength(self):
194137 # HACK: Account for the character faked in _getLineOffsets() so that move() will work.
195138 return max (super (SymphonyTextInfo , self )._getStoryLength (), 1 )
196139
140+
197141class SymphonyText (IAccessible , EditableText ):
198142 TextInfo = SymphonyTextInfo
199143
@@ -203,6 +147,7 @@ def _get_positionInfo(self):
203147 return {"level" : int (level )}
204148 return super (SymphonyText , self ).positionInfo
205149
150+
206151class SymphonyTableCell (IAccessible ):
207152 """Silences particular states, and redundant column/row numbers"""
208153
@@ -213,26 +158,92 @@ def _get_cellCoordsText(self):
213158
214159 name = None
215160
161+ def _get_hasSelection (self ):
162+ return (
163+ self .selectionContainer
164+ and 1 < self .selectionContainer .getSelectedItemsCount ()
165+ )
166+
216167 def _get_states (self ):
217168 states = super (SymphonyTableCell ,self ).states
218169 states .discard (controlTypes .State .MULTILINE )
219170 states .discard (controlTypes .State .EDITABLE )
220- if controlTypes .State .SELECTED not in states and { controlTypes .State .FOCUSED , controlTypes . State . SELECTABLE }. issubset ( states ) :
171+ if controlTypes .State .SELECTED not in states and controlTypes .State .FOCUSED in states :
221172 # #8988: Cells in Libre Office do not have the selected state when a single cell is selected (i.e. has focus).
222173 # Since #8898, the negative selected state is announced for table cells with the selectable state.
223- states .add (controlTypes .State .SELECTED )
174+ if self .hasSelection :
175+ # The selected state is never added to a focused object, even though it is selected.
176+ # We assume our focus is in the selection.
177+ states .add (controlTypes .State .SELECTED )
178+ else :
179+ # Remove the selectable state, since that ensures the negative selected state isn't spoken for focused cells.
180+ states .discard (controlTypes .State .SELECTABLE )
224181 if self .IA2Attributes .get ('Formula' ):
225182 # #860: Recent versions of Calc expose has formula state via IAccessible 2.
226183 states .add (controlTypes .State .HASFORMULA )
227184 return states
228185
186+
187+ class SymphonyIATableCell (SymphonyTableCell ):
188+ """An overlay class for cells implementing IAccessibleTableCell"""
189+
190+ def event_selectionAdd (self ):
191+ curFocus = api .getFocusObject ()
192+ if self .table and self .table == curFocus .table :
193+ curFocus .announceSelectionChange ()
194+
195+ def event_selectionRemove (self ):
196+ self .event_selectionAdd ()
197+
198+ def announceSelectionChange (self ):
199+ if self is api .getFocusObject ():
200+ speech .speakObjectProperties (self , states = True , cellCoordsText = True , reason = controlTypes .OutputReason .CHANGE )
201+ braille .handler .handleUpdate (self )
202+ vision .handler .handleUpdate (self , property = "states" )
203+
204+ def _get_cellCoordsText (self ):
205+ if self .hasSelection and controlTypes .State .FOCUSED in self .states :
206+ selected , count = self .table .IAccessibleTable2Object .selectedCells
207+ firstAccessible = selected [0 ].QueryInterface (IAccessibleHandler .IA2 .IAccessible2 )
208+ firstAddress = firstAccessible .accName (0 )
209+ firstValue = firstAccessible .accValue (0 ) or ''
210+ lastAccessible = selected [count - 1 ].QueryInterface (IAccessibleHandler .IA2 .IAccessible2 )
211+ lastAddress = lastAccessible .accName (0 )
212+ lastValue = lastAccessible .accValue (0 ) or ''
213+ # Translators: LibreOffice, report selected range of cell coordinates with their values
214+ return _ ("{firstAddress} {firstValue} through {lastAddress} {lastValue}" ).format (
215+ firstAddress = firstAddress ,
216+ firstValue = firstValue ,
217+ lastAddress = lastAddress ,
218+ lastValue = lastValue
219+ )
220+ elif self .rowSpan > 1 or self .columnSpan > 1 :
221+ lastSelected = (
222+ (self .rowNumber - 1 ) + (self .rowSpan - 1 ),
223+ (self .columnNumber - 1 ) + (self .columnSpan - 1 )
224+ )
225+ lastCellUnknown = self .table .IAccessibleTable2Object .cellAt (* lastSelected )
226+ lastAccessible = lastCellUnknown .QueryInterface (IAccessibleHandler .IA2 .IAccessible2 )
227+ lastAddress = lastAccessible .accName (0 )
228+ # Translators: LibreOffice, report range of cell coordinates
229+ return _ ("{firstAddress} throuhg {lastAddress}" ).format (
230+ firstAddress = self ._get_name (),
231+ lastAddress = lastAddress
232+ )
233+ return super ().cellCoordsText
234+
235+
229236class SymphonyTable (IAccessible ):
230237
231- def getSelectedItemsCount (self ,maxCount = 2 ):
232- # #8988: Neither accSelection nor IAccessibleTable2 is implemented on the LibreOffice tables.
233- # Returning 1 will suppress redundant selected announcements,
234- # while having the drawback of never announcing selected for selected cells.
235- return 1
238+ def _getSelectedItemsCount_accSelection (self , maxCount ):
239+ # accSelection is broken in LibreOffice.
240+ raise NotImplementedError
241+
242+ def event_selectionWithIn (self ):
243+ curFocus = api .getFocusObject ()
244+ if self == curFocus .table :
245+ curFocus .announceSelectionChange ()
246+
236247
237248class SymphonyParagraph (SymphonyText ):
238249 """Removes redundant information that can be retreaved in other ways."""
@@ -246,28 +257,22 @@ def chooseNVDAObjectOverlayClasses(self, obj, clsList):
246257 windowClassName = obj .windowClassName
247258 if isinstance (obj , IAccessible ) and windowClassName in ("SALTMPSUBFRAME" , "SALSUBFRAME" , "SALFRAME" ):
248259 if role == controlTypes .Role .TABLECELL :
249- clsList .insert (0 , SymphonyTableCell )
250- elif role == controlTypes .Role .TABLE :
260+ if obj ._IATableCell :
261+ clsList .insert (0 , SymphonyIATableCell )
262+ else :
263+ clsList .insert (0 , SymphonyTableCell )
264+ elif role == controlTypes .Role .TABLE and (
265+ hasattr (obj , "IAccessibleTable2Object" )
266+ or hasattr (obj , "IAccessibleTableObject" )
267+ ):
251268 clsList .insert (0 , SymphonyTable )
252269 elif hasattr (obj , "IAccessibleTextObject" ):
253270 clsList .insert (0 , SymphonyText )
254271 if role == controlTypes .Role .PARAGRAPH :
255272 clsList .insert (0 , SymphonyParagraph )
256- if isinstance (obj , JAB ) and windowClassName == "SALFRAME" :
257- if role in (controlTypes .Role .PANEL ,controlTypes .Role .LABEL ):
258- parent = obj .parent
259- if parent and parent .role == controlTypes .Role .TABLE :
260- clsList .insert (0 ,JAB_OOTableCell )
261- elif role == controlTypes .Role .TABLE :
262- clsList .insert (0 ,JAB_OOTable )
263273
264274 def event_NVDAObject_init (self , obj ):
265275 windowClass = obj .windowClassName
266- if isinstance (obj , JAB ) and windowClass == "SALFRAME" :
267- # OpenOffice.org has some strange role mappings due to its use of JAB.
268- if obj .role == controlTypes .Role .CANVAS :
269- obj .role = controlTypes .Role .DOCUMENT
270-
271276 if windowClass in ("SALTMPSUBFRAME" , "SALFRAME" ) and obj .role in (controlTypes .Role .DOCUMENT ,controlTypes .Role .TEXTFRAME ) and obj .description :
272277 # This is a word processor document.
273278 obj .description = None
0 commit comments