Skip to content

Commit 1692477

Browse files
authored
Merge b9f7ee1 into 2ae3980
2 parents 2ae3980 + b9f7ee1 commit 1692477

File tree

2 files changed

+111
-97
lines changed

2 files changed

+111
-97
lines changed

source/NVDAObjects/IAccessible/__init__.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1289,12 +1289,12 @@ def _get_selectionContainer(self):
12891289
return self.table
12901290
return super(IAccessible,self).selectionContainer
12911291

1292-
def _getSelectedItemsCount_accSelection(self,maxCount):
1292+
def _getSelectedItemsCount_accSelection(self, maxCount):
12931293
sel=self.IAccessibleObject.accSelection
12941294
if not sel:
12951295
raise NotImplementedError
12961296
# accSelection can return a child ID of a simple element, for instance in QT tree tables.
1297-
# Therefore treet this as a single selection.
1297+
# Therefore treat this as a single selection.
12981298
if isinstance(sel,int) and sel>0:
12991299
return 1
13001300
enumObj=sel.QueryInterface(IEnumVARIANT)
@@ -1310,24 +1310,33 @@ def _getSelectedItemsCount_accSelection(self,maxCount):
13101310
raise COMError(res,None,None)
13111311
return numItemsFetched.value if numItemsFetched.value <= maxCount else sys.maxsize
13121312

1313-
def getSelectedItemsCount(self,maxCount):
1314-
# To fetch the number of selected items, we first try MSAA's accSelection, but if that fails in any way, we fall back to using IAccessibleTable2's nSelectedCells, if we are on an IAccessible2 table.
1313+
def getSelectedItemsCount(self, maxCount=2):
1314+
# To fetch the number of selected items, we first try MSAA's accSelection,
1315+
# but if that fails in any way, we fall back to using IAccessibleTable2's nSelectedCells,
1316+
# if we are on an IAccessible2 table, or IAccessibleTable's nSelectedChildren,
1317+
# if we are on an IAccessible table.
13151318
# Currently Chrome does not implement accSelection, thus for Google Sheets we must use nSelectedCells when on a table.
1319+
# For older symphony based products, we use nSelectedChildren.
13161320
try:
13171321
return self._getSelectedItemsCount_accSelection(maxCount)
13181322
except (COMError,NotImplementedError) as e:
13191323
log.debug("Cannot fetch selected items count using accSelection, %s"%e)
13201324
pass
1321-
if hasattr(self,'IAccessibleTable2Object'):
1325+
if hasattr(self, 'IAccessibleTable2Object'):
13221326
try:
13231327
return self.IAccessibleTable2Object.nSelectedCells
13241328
except COMError as e:
1325-
log.debug("Error calling IAccessibleTable2::nSelectedCells, %s"%e)
1329+
log.debug(f"Error calling IAccessibleTable2::nSelectedCells, {e}")
1330+
pass
1331+
elif hasattr(self, 'IAccessibleTableObject'):
1332+
try:
1333+
return self.IAccessibleTableObject.nSelectedChildren
1334+
except COMError as e:
1335+
log.debug(f"Error calling IAccessibleTable::nSelectedCells, {e}")
13261336
pass
13271337
else:
13281338
log.debug("No means of getting a selection count from this IAccessible")
1329-
return super(IAccessible,self).getSelectedItemsCount(maxCount)
1330-
1339+
return super().getSelectedItemsCount(maxCount)
13311340

13321341
def _get_table(self):
13331342
if not isinstance(self.IAccessibleObject, IA2.IAccessible2):

source/appModules/soffice.py

Lines changed: 94 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,25 @@
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

76
from comtypes import COMError
8-
from comInterfaces import IAccessible2Lib as IA2
97
import IAccessibleHandler
108
import appModuleHandler
119
import controlTypes
1210
import textInfos
1311
import colors
1412
from compoundDocuments import CompoundDocument
15-
from NVDAObjects.JAB import JAB, JABTextInfo
1613
from NVDAObjects.IAccessible import IAccessible, IA2TextTextInfo
1714
from NVDAObjects.behaviors import EditableText
1815
from 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

8124
class 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+
197141
class 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+
206151
class 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+
229236
class 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

237248
class 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

Comments
 (0)