Skip to content

Commit

Permalink
Merge b9f7ee1 into 2ae3980
Browse files Browse the repository at this point in the history
  • Loading branch information
LeonarddeR committed Sep 15, 2021
2 parents 2ae3980 + b9f7ee1 commit 1692477
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 97 deletions.
25 changes: 17 additions & 8 deletions source/NVDAObjects/IAccessible/__init__.py
Expand Up @@ -1289,12 +1289,12 @@ def _get_selectionContainer(self):
return self.table
return super(IAccessible,self).selectionContainer

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

def getSelectedItemsCount(self,maxCount):
# 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.
def getSelectedItemsCount(self, maxCount=2):
# 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, or IAccessibleTable's nSelectedChildren,
# if we are on an IAccessible table.
# Currently Chrome does not implement accSelection, thus for Google Sheets we must use nSelectedCells when on a table.
# For older symphony based products, we use nSelectedChildren.
try:
return self._getSelectedItemsCount_accSelection(maxCount)
except (COMError,NotImplementedError) as e:
log.debug("Cannot fetch selected items count using accSelection, %s"%e)
pass
if hasattr(self,'IAccessibleTable2Object'):
if hasattr(self, 'IAccessibleTable2Object'):
try:
return self.IAccessibleTable2Object.nSelectedCells
except COMError as e:
log.debug("Error calling IAccessibleTable2::nSelectedCells, %s"%e)
log.debug(f"Error calling IAccessibleTable2::nSelectedCells, {e}")
pass
elif hasattr(self, 'IAccessibleTableObject'):
try:
return self.IAccessibleTableObject.nSelectedChildren
except COMError as e:
log.debug(f"Error calling IAccessibleTable::nSelectedCells, {e}")
pass
else:
log.debug("No means of getting a selection count from this IAccessible")
return super(IAccessible,self).getSelectedItemsCount(maxCount)

return super().getSelectedItemsCount(maxCount)

def _get_table(self):
if not isinstance(self.IAccessibleObject, IA2.IAccessible2):
Expand Down
183 changes: 94 additions & 89 deletions source/appModules/soffice.py
@@ -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):
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):

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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"""

Expand All @@ -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."""
Expand All @@ -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
Expand Down

0 comments on commit 1692477

Please sign in to comment.