Skip to content
Permalink
Browse files

Speech commands use sequences rather than strings (PR #10371)

In order to get many of the benefits of the speech refactor, we need to have speech sequences (rather than single strings) returned from our various getXSpeech methods. This will be necessary if we want to use sounds instead of text to indicate spelling errors, change voice parameters for emphasized text or links, etc.

* Definitions and all usages converted:
- getControlFieldSpeech
  - speech.getControlFieldSpeech
  - browseMode.BrowseModeDocumentTextInfo.getControlFieldSpeech
  - textInfos.TextInfo.getControlFieldSpeech
  - speech.getFormatFieldSpeech
- getFormatFieldSpeech
  - appModules.kindle.BookPageViewTextInfo.getFormatFieldSpeech
  - textInfos.TextInfo.getFormatFieldSpeech
  - treeInterceptorHandler.RootProxyTextInfo.getFormatFieldSpeech
- speech.getSpeechTextForProperties
- speech.getIndentationSpeech
- speech.getTableInfoSpeech

* Rename getSpeechTextForProperties to getPropertiesSpeech
* Unify line endings (use Windows line endings) in excel.py
* Add speechSequence checking
- Logs errors in speech sequences (unknown types, None entries, empty list entries)
- Must be enabled in the advanced panel, logging options.

* Remove separator argument from getFormatFieldSpeech method
* Include padding added to strings for speech in test
- In the future it would be better to test speech as structured data. This would allow us to test commands as well.

* Fix unnecessary newlines between speech items in Speech viewer
- SpeechViewer.appendText renamed to appendSpeechSequence.
- appendSpeechSequence takes a SpeechSequence instead of a str.
- This allows more control over the presentation of speech, which will separate speech items with a space, and conclude with a newline.

Fixes #10098
  • Loading branch information...
feerrenrut committed Nov 6, 2019
1 parent 8cd9582 commit 4b80c41312ab5323f07962f78e5fe2b79bfd6f51
@@ -227,52 +227,48 @@ def report(self,readUnit=None):

class ExcelChartQuickNavItem(ExcelQuickNavItem):

def __init__( self , nodeType , document , chartObject , chartCollection ):
def __init__(self, nodeType, document, chartObject, chartCollection):
self.chartIndex = chartObject.Index
if chartObject.Chart.HasTitle:

self.label = chartObject.Chart.ChartTitle.Text + " " + chartObject.TopLeftCell.address(False,False,1,False) + "-" + chartObject.BottomRightCell.address(False,False,1,False)

else:

self.label = chartObject.Name + " " + chartObject.TopLeftCell.address(False,False,1,False) + "-" + chartObject.BottomRightCell.address(False,False,1,False)

super( ExcelChartQuickNavItem ,self).__init__( nodeType , document , chartObject , chartCollection )
topLeftAddress = chartObject.TopLeftCell.address(False, False, 1, False)
bottomRightAddress = chartObject.BottomRightCell.address(False, False, 1, False)
if chartObject.Chart.HasTitle:
nameText = chartObject.Chart.ChartTitle.Text
else:
nameText = chartObject.Name
self.label = f"{nameText} {topLeftAddress}-{bottomRightAddress}"
super(ExcelChartQuickNavItem, self).__init__(
nodeType,
document,
chartObject,
chartCollection
)

def __lt__(self,other):
return self.chartIndex < other.chartIndex

def moveTo(self):
try:
self.excelItemObject.Activate()

# After activate(), though the chart object is selected,

# pressing arrow keys moves the object, rather than

# let use go inside for sub-objects. Somehow
# calling an COM function on a different object fixes that !

log.debugWarning( self.excelItemCollection.Count )

except(COMError):

pass
self.excelItemObject.Activate()
# After activate(), though the chart object is selected,
# pressing arrow keys moves the object, rather than
# let us go inside for sub-objects. Somehow
# calling a COM function on a different object fixes that!
log.debugWarning(self.excelItemCollection.Count)
except COMError:
pass
focus=api.getDesktopObject().objectWithFocus()
if not focus or not isinstance(focus,ExcelBase):
return
# Charts are not yet automatically detected with objectFromFocus, so therefore use selection
sel=focus._getSelection()
if not sel:
return
eventHandler.queueEvent("gainFocus",sel)
eventHandler.queueEvent("gainFocus", sel)


@property
def isAfterSelection(self):
activeCell = self.document.Application.ActiveCell
#log.debugWarning("active row: {} active column: {} current row: {} current column: {}".format ( activeCell.row , activeCell.column , self.excelCommentObject.row , self.excelCommentObject.column ) )

if self.excelItemObject.TopLeftCell.row == activeCell.row:
if self.excelItemObject.TopLeftCell.column > activeCell.column:
return False
@@ -289,13 +285,20 @@ def __lt__(self,other):
return self.excelItemObject.row < other.excelItemObject.row

def moveTo(self):
self.excelItemObject.Activate()
self.excelItemObject.Activate()
eventHandler.queueEvent("gainFocus",api.getDesktopObject().objectWithFocus())

@property
def isAfterSelection(self):
activeCell = self.document.Application.ActiveCell
log.debugWarning("active row: {} active column: {} current row: {} current column: {}".format ( activeCell.row , activeCell.column , self.excelItemObject.row , self.excelItemObject.column ) )
log.debugWarning(
"active row: {} active column: {} current row: {} current column: {}".format(
activeCell.row,
activeCell.column,
self.excelItemObject.row,
self.excelItemObject.column
)
)

if self.excelItemObject.row == activeCell.row:
if self.excelItemObject.column > activeCell.column:
@@ -374,7 +377,7 @@ class CommentExcelCollectionQuicknavIterator(ExcelQuicknavIterator):
def collectionFromWorksheet( self , worksheetObject ):
try:
return worksheetObject.cells.SpecialCells( xlCellTypeComments )
except(COMError):
except(COMError):
return None

def filter(self,item):
@@ -385,7 +388,7 @@ class FormulaExcelCollectionQuicknavIterator(ExcelQuicknavIterator):
def collectionFromWorksheet( self , worksheetObject ):
try:
return worksheetObject.cells.SpecialCells( xlCellTypeFormulas )
except(COMError):
except(COMError):

return None

@@ -420,7 +423,7 @@ def isAfterSelection(self):
return False
else:
return True


class SheetsExcelCollectionQuicknavIterator(ExcelQuicknavIterator):
"""
Allows iterating over an MS excel Sheets collection emitting L{QuickNavItem} object.
@@ -1433,10 +1436,14 @@ def reportFocus(self):
if isinstance(field,textInfos.FieldCommand) and isinstance(field.field,textInfos.FormatField):
formatField.update(field.field)
if not hasattr(self.parent,'_formatFieldSpeechCache'):
self.parent._formatFieldSpeechCache={}
text=speech.getFormatFieldSpeech(formatField,attrsCache=self.parent._formatFieldSpeechCache,formatConfig=formatConfig) if formatField else None
if text:
speech.speakText(text)
self.parent._formatFieldSpeechCache = textInfos.Field()
if formatField:
sequence = speech.getFormatFieldSpeech(
formatField,
attrsCache=self.parent._formatFieldSpeechCache,
formatConfig=formatConfig
)
speech.speak(sequence)
super(ExcelCell,self).reportFocus()

__gestures = {
@@ -1602,7 +1609,7 @@ class ExcelMergedCell(ExcelCell):

def _get_cellCoordsText(self):
return self.getCellAddress(self.excelCellObject.mergeArea)


def _get_rowSpan(self):
return self.excelCellObject.mergeArea.rows.count

@@ -2,14 +2,13 @@
#Copyright (C) 2016-2017 NV Access Limited
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
from typing import Optional, Dict

from comtypes import COMError
import time
from comtypes.hresult import S_OK
import appModuleHandler
import speech
import sayAllHandler
import eventHandler
import api
from scriptHandler import willSayAllResume, isScriptWaiting
import controlTypes
@@ -18,6 +17,7 @@
import browseMode
from browseMode import BrowseModeDocumentTreeInterceptor
import textInfos
from speech.types import SpeechSequence
from textInfos import DocumentWithPageTurns
from IAccessibleHandler import IAccessible2
from NVDAObjects.IAccessible import IAccessible
@@ -272,29 +272,55 @@ def getTextWithFields(self, formatConfig=None):
item.field.pop("content", None)
return items

def getFormatFieldSpeech(self, attrs, attrsCache=None, formatConfig=None, reason=None, unit=None, extraDetail=False , initialFormat=False, separator=speech.CHUNK_SEPARATOR):
out = ""
def getFormatFieldSpeech(
self,
attrs: textInfos.Field,
attrsCache: Optional[textInfos.Field] = None,
formatConfig: Optional[Dict[str, bool]] = None,
reason: Optional[str] = None,
unit: Optional[str] = None,
extraDetail: bool = False,
initialFormat: bool = False
) -> SpeechSequence:
out: SpeechSequence = []
comment = attrs.get("kindle-user-note")
if comment:
# For now, we report this the same way we do comments.
attrs["comment"] = comment
highlight = attrs.get("kindle-highlight")
oldHighlight = attrsCache.get("kindle-highlight") if attrsCache is not None else None
if oldHighlight != highlight:
# Translators: Reported when text is highlighted.
out += (_("highlight") if highlight
translation = (
# Translators: Reported when text is highlighted.
_("highlight") if highlight else
# Translators: Reported when text is not highlighted.
else _("no highlight")) + separator
_("no highlight")
)
out.append(translation)
popular = attrs.get("kindle-popular-highlight-count")
oldPopular = attrsCache.get("kindle-popular-highlight-count") if attrsCache is not None else None
if oldPopular != popular:
# Translators: Reported in Kindle when text has been identified as a popular highlight;
# i.e. it has been highlighted by several people.
# %s is replaced with the number of people who have highlighted this text.
out += (_("%s highlighted") % popular if popular
translation = (
# Translators: Reported in Kindle when text has been identified as a popular highlight;
# i.e. it has been highlighted by several people.
# %s is replaced with the number of people who have highlighted this text.
_("%s highlighted") % popular if popular else
# Translators: Reported when moving out of a popular highlight.
else _("out of popular highlight")) + separator
out += super(BookPageViewTextInfo, self).getFormatFieldSpeech(attrs, attrsCache=attrsCache, formatConfig=formatConfig, reason=reason, unit=unit, extraDetail=extraDetail , initialFormat=initialFormat, separator=separator)
_("out of popular highlight")
)
out.append(translation)

superSpeech = super(BookPageViewTextInfo, self).getFormatFieldSpeech(
attrs,
attrsCache=attrsCache,
formatConfig=formatConfig,
reason=reason,
unit=unit,
extraDetail=extraDetail,
initialFormat=initialFormat
)
out.extend(superSpeech)
textInfos._logBadSequenceTypes(out)
return out

def updateSelection(self):
@@ -2,10 +2,11 @@
# Copyright (C) 2009-2019 NV Access Limited, Leonard de Ruijter
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
from typing import Dict

import controlTypes

ariaRolesToNVDARoles={
ariaRolesToNVDARoles: Dict[str, int] = {
"description":controlTypes.ROLE_STATICTEXT,
"search":controlTypes.ROLE_SECTION,
"alert":controlTypes.ROLE_ALERT,
@@ -58,13 +59,13 @@
"treeitem":controlTypes.ROLE_TREEVIEWITEM,
}

ariaSortValuesToNVDAStates={
ariaSortValuesToNVDAStates: Dict[str, int] = {
'descending':controlTypes.STATE_SORTED_DESCENDING,
'ascending':controlTypes.STATE_SORTED_ASCENDING,
'other':controlTypes.STATE_SORTED,
}

landmarkRoles = {
landmarkRoles: Dict[str, str] = {
# Translators: Reported for the banner landmark, normally found on web pages.
"banner": pgettext("aria", "banner"),
# Translators: Reported for the complementary landmark, normally found on web pages.
@@ -84,7 +85,7 @@
"region": pgettext("aria", "region"),
}

htmlNodeNameToAriaRoles = {
htmlNodeNameToAriaRoles: Dict[str, str] = {
"header": "banner",
"nav": "navigation",
"main": "main",
@@ -9,6 +9,7 @@
import winsound
import time
import weakref

import wx
import core
from logHandler import log
@@ -1131,6 +1132,7 @@ def move():
# If it didn't, and it directly or indirectly called wx.Yield, it could start executing NVDA's core pump from within the yield, causing recursion.
core.callLater(100, move)


class BrowseModeDocumentTextInfo(textInfos.TextInfo):

def _get_focusableNVDAObjectAtStart(self):
@@ -221,6 +221,7 @@
louis = boolean(default=false)
timeSinceInput = boolean(default=false)
vision = boolean(default=false)
speech = boolean(default=false)
[uwpOcr]
language = string(default="")
@@ -3,6 +3,7 @@
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2007-2016 NV Access Limited, Babbage B.V.
from typing import Dict, Union, Set, Any, Optional, List

ROLE_UNKNOWN=0
ROLE_WINDOW=1
@@ -199,7 +200,7 @@
STATE_OVERFLOWING=0x10000000000
STATE_UNLOCKED=0x20000000000

roleLabels={
roleLabels: Dict[int, str] = {
# Translators: The word for an unknown control type.
ROLE_UNKNOWN:_("unknown"),
# Translators: The word for window of a program such as document window.
@@ -500,7 +501,7 @@
ROLE_ARTICLE: _("article"),
}

stateLabels={
stateLabels: Dict[int, str] = {
# Translators: This is presented when a control or document is unavailable.
STATE_UNAVAILABLE:_("unavailable"),
# Translators: This is presented when a control has focus.
@@ -639,7 +640,7 @@

#: Text to use for 'current' values. These describe if an item is the current item
#: within a particular kind of selection.
isCurrentLabels = {
isCurrentLabels: Dict[Union[bool, str], str] = {
# Translators: Presented when an item is marked as current in a collection of items
True:_("current"),
# Translators: Presented when a page item is marked as current in a collection of page items
@@ -776,24 +777,25 @@ def processNegativeStates(role, states, reason, negativeStates=None):
# Return all negative states which should be spoken, excluding the positive states.
return speakNegatives - states

def processAndLabelStates(role, states, reason, positiveStates=None, negativeStates=None, positiveStateLabelDict={}, negativeStateLabelDict={}):

def processAndLabelStates(
role: int,
states: Set[Any],
reason: str,
positiveStates: Optional[Set[Any]] = None,
negativeStates: Optional[Set[Any]] = None,
positiveStateLabelDict: Dict[int, str] = {},
negativeStateLabelDict: Dict[int, str] = {},
) -> List[str]:
"""Processes the states for an object and returns the appropriate state labels for both positive and negative states.
@param role: The role of the object to process states for (e.g. C{ROLE_CHECKBOX}.
@type role: int
@param states: The raw states for an object to process.
@type states: set
@param reason: The reason to process the states (e.g. C{REASON_FOCUS}.
@type reason: str
@param positiveStates: Used for C{REASON_CHANGE}, specifies states changed from negative to positive;
@type positiveStates: set
@param negativeStates: Used for C{REASON_CHANGE}, specifies states changed from positive to negative;
@type negativeStates: setpositiveStateLabelDict={}, negativeStateLabelDict
@param positiveStateLabelDict: Dictionary containing state identifiers as keys and associated positive labels as their values.
@type positiveStateLabelDict: dict
@param negativeStateLabelDict: Dictionary containing state identifiers as keys and associated negative labels as their values.
@type negativeStateLabelDict: dict
@return: The labels of the relevant positive and negative states.
@rtype: [str, ...]
"""
mergedStateLabels=[]
positiveStates = processPositiveStates(role, states, reason, positiveStates)

0 comments on commit 4b80c41

Please sign in to comment.
You can’t perform that action at this time.