Module speech
[hide private]
[frames] | no frames]

Source Code for Module speech

#speech.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-2010 Michael Curran , James Teh , Peter Vágner , Aleksey Sadovoy 

"""High-level functions to speak information.
""" 

import colors
import globalVars
from logHandler import log
import api
import controlTypes
import config
import tones
import synthDriverHandler
from synthDriverHandler import *
import re
import textInfos
import queueHandler
import speechDictHandler
import characterProcessing
import languageHandler

speechMode_off=0
speechMode_beeps=1
speechMode_talk=2
#: How speech should be handled; one of speechMode_off, speechMode_beeps or speechMode_talk.
speechMode=speechMode_talk
speechMode_beeps_ms=15
beenCanceled=True
isPaused=False
curWordChars=[]

REASON_FOCUS=1
REASON_MOUSE=2
REASON_QUERY=3
REASON_CHANGE=4
REASON_MESSAGE=5
REASON_SAYALL=6
REASON_CARET=7
REASON_DEBUG=8
REASON_ONLYCACHE=9
REASON_FOCUSENTERED=10

#: The string used to separate distinct chunks of text when multiple chunks should be spoken without pauses.
CHUNK_SEPARATOR = "  "

oldTreeLevel=None
oldTableID=None
oldRowNumber=None
oldColumnNumber=None

def initialize():
        """Loads and sets the synth driver configured in nvda.ini."""
        synthDriverHandler.initialize()
        setSynth(config.conf["speech"]["synth"])

def terminate():
        setSynth(None)
        speechViewerObj=None

#: If a chunk of text contains only these characters, it will be considered blank.
BLANK_CHUNK_CHARS = frozenset((" ", "\n", "\r", "\0", u"\xa0"))
def isBlank(text):
        """Determine whether text should be reported as blank.
        @param text: The text in question.
        @type text: str
        @return: C{True} if the text is blank, C{False} if not.
        @rtype: bool
        """
        return not text or set(text) <= BLANK_CHUNK_CHARS

RE_CONVERT_WHITESPACE = re.compile("[\0\r\n]")

def processText(locale,text,symbolLevel):
        text = speechDictHandler.processText(text)
        text = characterProcessing.processSpeechSymbols(locale, text, symbolLevel)
        text = RE_CONVERT_WHITESPACE.sub(u" ", text)
        return text.strip()

def getLastSpeechIndex():
        """Gets the last index passed by the synthesizer. Indexing is used so that its possible to find out when a certain peace of text has been spoken yet. Usually the character position of the text is passed to speak functions as the index.
@returns: the last index encountered
@rtype: int
"""
        return getSynth().lastIndex

def cancelSpeech():
        """Interupts the synthesizer from currently speaking"""
        global beenCanceled, isPaused, _speakSpellingGenerator
        # Import only for this function to avoid circular import.
        import sayAllHandler
        sayAllHandler.stop()
        speakWithoutPauses._pendingSpeechSequence=[]
        speakWithoutPauses.lastSentIndex=None
        if _speakSpellingGenerator:
                _speakSpellingGenerator.close()
        if beenCanceled:
                return
        elif speechMode==speechMode_off:
                return
        elif speechMode==speechMode_beeps:
                return
        getSynth().cancel()
        beenCanceled=True
        isPaused=False

def pauseSpeech(switch):
        global isPaused, beenCanceled
        getSynth().pause(switch)
        isPaused=switch
        beenCanceled=False

def speakMessage(text,index=None):
        """Speaks a given message.
@param text: the message to speak
@type text: string
@param index: the index to mark this current text with, its best to use the character position of the text if you know it 
@type index: int
"""
        speakText(text,index=index,reason=REASON_MESSAGE)

def getCurrentLanguage():
        language=getSynth().language if config.conf['speech']['autoLanguageSwitching'] else None
        if language:
                language=languageHandler.normalizeLanguage(language)
        if not language:
                language=languageHandler.getLanguage()
        return language

def spellTextInfo(info,useCharacterDescriptions=False):
        """Spells the text from the given TextInfo, honouring any LangChangeCommand objects it finds if autoLanguageSwitching is enabled."""
        if not config.conf['speech']['autoLanguageSwitching']:
                speakSpelling(info.text,useCharacterDescriptions=useCharacterDescriptions)
                return
        curLanguage=None
        for field in info.getTextWithFields({}):
                if isinstance(field,basestring):
                        speakSpelling(field,curLanguage,useCharacterDescriptions=useCharacterDescriptions)
                elif isinstance(field,textInfos.FieldCommand) and field.command=="formatChange":
                        curLanguage=field.field.get('language')

_speakSpellingGenerator=None

def speakSpelling(text,locale=None,useCharacterDescriptions=False):
        global beenCanceled, _speakSpellingGenerator
        import speechViewer
        if speechViewer.isActive:
                speechViewer.appendText(text)
        if speechMode==speechMode_off:
                return
        elif speechMode==speechMode_beeps:
                tones.beep(config.conf["speech"]["beepSpeechModePitch"],speechMode_beeps_ms)
                return
        if isPaused:
                cancelSpeech()
        beenCanceled=False
        defaultLanguage=getCurrentLanguage()
        if not locale or (not config.conf['speech']['autoDialectSwitching'] and locale.split('_')[0]==defaultLanguage.split('_')[0]):
                locale=defaultLanguage

        if not text:
                return getSynth().speak((_("blank"),))
        if not text.isspace():
                text=text.rstrip()
        if _speakSpellingGenerator and _speakSpellingGenerator.gi_frame:
                _speakSpellingGenerator.send((text,locale,useCharacterDescriptions))
        else:
                _speakSpellingGenerator=_speakSpellingGen(text,locale,useCharacterDescriptions)
                try:
                        # Speak the first character before this function returns.
                        next(_speakSpellingGenerator)
                except StopIteration:
                        return
                queueHandler.registerGeneratorObject(_speakSpellingGenerator)

def _speakSpellingGen(text,locale,useCharacterDescriptions):
        synth=getSynth()
        synthConfig=config.conf["speech"][synth.name]
        buf=[(text,locale,useCharacterDescriptions)]
        for text,locale,useCharacterDescriptions in buf:
                textLength=len(text)
                for count,char in enumerate(text): 
                        uppercase=char.isupper()
                        charDesc=None
                        if useCharacterDescriptions:
                                charDesc=characterProcessing.getCharacterDescription(locale,char.lower())
                        if charDesc:
                                #Consider changing to multiple synth speech calls
                                char=charDesc[0] if textLength>1 else u"\u3001".join(charDesc)
                        else:
                                char=characterProcessing.processSpeechSymbol(locale,char)
                        if uppercase and synthConfig["sayCapForCapitals"]:
                                char=_("cap %s")%char
                        if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]:
                                oldPitch=synthConfig["pitch"]
                                synth.pitch=max(0,min(oldPitch+synthConfig["capPitchChange"],100))
                        index=count+1
                        log.io("Speaking character %r"%char)
                        speechSequence=[LangChangeCommand(locale)] if config.conf['speech']['autoLanguageSwitching'] else []
                        if len(char) == 1 and synthConfig["useSpellingFunctionality"]:
                                speechSequence.append(CharacterModeCommand(True))
                        if index is not None:
                                speechSequence.append(IndexCommand(index))
                        speechSequence.append(char)
                        synth.speak(speechSequence)
                        if uppercase and synth.isSupported("pitch") and synthConfig["capPitchChange"]:
                                synth.pitch=oldPitch
                        while textLength>1 and (isPaused or getLastSpeechIndex()!=index):
                                for x in xrange(2):
                                        args=yield
                                        if args: buf.append(args)
                        if uppercase and  synthConfig["beepForCapitals"]:
                                tones.beep(2000,50)
                args=yield
                if args: buf.append(args)

def speakObjectProperties(obj,reason=REASON_QUERY,index=None,**allowedProperties):
        if speechMode==speechMode_off:
                return
        #Fetch the values for all wanted properties
        newPropertyValues={}
        positionInfo=None
        for name,value in allowedProperties.iteritems():
                if name.startswith('positionInfo_') and value:
                        if positionInfo is None:
                                positionInfo=obj.positionInfo
                elif value:
                        try:
                                newPropertyValues[name]=getattr(obj,name)
                        except NotImplementedError:
                                pass
        if positionInfo:
                if allowedProperties.get('positionInfo_level',False) and 'level' in positionInfo:
                        newPropertyValues['positionInfo_level']=positionInfo['level']
                if allowedProperties.get('positionInfo_indexInGroup',False) and 'indexInGroup' in positionInfo:
                        newPropertyValues['positionInfo_indexInGroup']=positionInfo['indexInGroup']
                if allowedProperties.get('positionInfo_similarItemsInGroup',False) and 'similarItemsInGroup' in positionInfo:
                        newPropertyValues['positionInfo_similarItemsInGroup']=positionInfo['similarItemsInGroup']
        #Fetched the cached properties and update them with the new ones
        oldCachedPropertyValues=getattr(obj,'_speakObjectPropertiesCache',{}).copy()
        cachedPropertyValues=oldCachedPropertyValues.copy()
        cachedPropertyValues.update(newPropertyValues)
        obj._speakObjectPropertiesCache=cachedPropertyValues
        #If we should only cache we can stop here
        if reason==REASON_ONLYCACHE:
                return
        #If only speaking change, then filter out all values that havn't changed
        if reason==REASON_CHANGE:
                for name in set(newPropertyValues)&set(oldCachedPropertyValues):
                        if newPropertyValues[name]==oldCachedPropertyValues[name]:
                                del newPropertyValues[name]
                        elif name=="states": #states need specific handling
                                oldStates=oldCachedPropertyValues[name]
                                newStates=newPropertyValues[name]
                                newPropertyValues['states']=newStates-oldStates
                                newPropertyValues['negativeStates']=oldStates-newStates
        #properties such as states need to know the role to speak properly, give it as a _ name
        newPropertyValues['_role']=newPropertyValues.get('role',obj.role)
        # The real states are needed also, as the states entry might be filtered.
        newPropertyValues['_states']=obj.states
        #Get the speech text for the properties we want to speak, and then speak it
        text=getSpeechTextForProperties(reason,**newPropertyValues)
        if text:
                speakText(text,index=index)

def speakObject(obj,reason=REASON_QUERY,index=None):
        from NVDAObjects import NVDAObjectTextInfo
        isEditable=(reason!=REASON_FOCUSENTERED and obj.TextInfo!=NVDAObjectTextInfo and (obj.role in (controlTypes.ROLE_EDITABLETEXT,controlTypes.ROLE_TERMINAL) or controlTypes.STATE_EDITABLE in obj.states))
        allowProperties={'name':True,'role':True,'states':True,'value':True,'description':True,'keyboardShortcut':True,'positionInfo_level':True,'positionInfo_indexInGroup':True,'positionInfo_similarItemsInGroup':True,"rowNumber":True,"columnNumber":True,"columnCount":True,"rowCount":True}

        if reason==REASON_FOCUSENTERED:
                allowProperties["value"]=False
                allowProperties["keyboardShortcut"]=False
                allowProperties["positionInfo_level"]=False
                # Aside from excluding some properties, focus entered should be spoken like focus.
                reason=REASON_FOCUS

        if not config.conf["presentation"]["reportObjectDescriptions"]:
                allowProperties["description"]=False
        if not config.conf["presentation"]["reportKeyboardShortcuts"]:
                allowProperties["keyboardShortcut"]=False
        if not config.conf["presentation"]["reportObjectPositionInformation"]:
                allowProperties["positionInfo_level"]=False
                allowProperties["positionInfo_indexInGroup"]=False
                allowProperties["positionInfo_similarItemsInGroup"]=False
        if reason!=REASON_QUERY:
                allowProperties["rowCount"]=False
                allowProperties["columnCount"]=False
                if (not config.conf["documentFormatting"]["reportTables"]
                                or not config.conf["documentFormatting"]["reportTableCellCoords"]
                                or obj.tableCellCoordsInName):
                        allowProperties["rowNumber"]=False
                        allowProperties["columnNumber"]=False
        if isEditable:
                allowProperties['value']=False

        speakObjectProperties(obj,reason=reason,index=index,**allowProperties)
        if reason!=REASON_ONLYCACHE and isEditable:
                try:
                        info=obj.makeTextInfo(textInfos.POSITION_SELECTION)
                        if not info.isCollapsed:
                                speakSelectionMessage(_("selected %s"),info.text)
                        else:
                                info.expand(textInfos.UNIT_LINE)
                                speakTextInfo(info,unit=textInfos.UNIT_LINE,reason=reason)
                except:
                        newInfo=obj.makeTextInfo(textInfos.POSITION_ALL)
                        speakTextInfo(newInfo,unit=textInfos.UNIT_PARAGRAPH,reason=reason)

def speakText(text,index=None,reason=REASON_MESSAGE,symbolLevel=None):
        """Speaks some text.
        @param text: The text to speak.
        @type text: str
        @param index: The index to mark this text with, which can be used later to determine whether this piece of text has been spoken.
        @type index: int
        @param reason: The reason for this speech; one of the REASON_* constants.
        @param symbolLevel: The symbol verbosity level; C{None} (default) to use the user's configuration.
        """
        speechSequence=[]
        if index is not None:
                speechSequence.append(IndexCommand(index))
        if text is not None:
                if isBlank(text):
                        text=_("blank")
                speechSequence.append(text)
        speak(speechSequence,symbolLevel=symbolLevel)

RE_INDENTATION_SPLIT = re.compile(r"^([^\S\r\n\f\v]*)(.*)$", re.UNICODE | re.DOTALL)
def splitTextIndentation(text):
        """Splits indentation from the rest of the text.
        @param text: The text to split.
        @type text: basestring
        @return: Tuple of indentation and content.
        @rtype: (basestring, basestring)
        """
        return RE_INDENTATION_SPLIT.match(text).groups()

RE_INDENTATION_CONVERT = re.compile(r"(?P\s)(?P=char)*", re.UNICODE)
def getIndentationSpeech(indentation):
        """Retrieves the phrase to be spoken for a given string of indentation.
        @param indentation: The string of indentation.
        @type indentation: basestring
        @return: The phrase to be spoken.
        @rtype: unicode
        """
        # Translators: no indent is spoken when the user moves from a line that has indentation, to one that 
        # does not.
        if not indentation:
                return _("no indent")

        res = []
        locale=languageHandler.getLanguage()
        for m in RE_INDENTATION_CONVERT.finditer(indentation):
                raw = m.group()
                symbol = characterProcessing.processSpeechSymbol(locale, raw[0])
                count = len(raw)
                if symbol == raw[0]:
                        # There is no replacement for this character, so do nothing.
                        res.append(raw)
                elif count == 1:
                        res.append(symbol)
                else:
                        res.append(u"{count} {symbol}".format(count=count, symbol=symbol))

        return " ".join(res)

def speak(speechSequence,symbolLevel=None):
        """Speaks a sequence of text and speech commands
        @param speechSequence: the sequence of text and L{SpeechCommand} objects to speak
        @param symbolLevel: The symbol verbosity level; C{None} (default) to use the user's configuration.
        """
        if not speechSequence: #Pointless - nothing to speak 
                return
        import speechViewer
        if speechViewer.isActive:
                for item in speechSequence:
                        if isinstance(item,basestring):
                                speechViewer.appendText(item)
        global beenCanceled, curWordChars
        curWordChars=[]
        if speechMode==speechMode_off:
                return
        elif speechMode==speechMode_beeps:
                tones.beep(config.conf["speech"]["beepSpeechModePitch"],speechMode_beeps_ms)
                return
        if isPaused:
                cancelSpeech()
        beenCanceled=False
        #Filter out redundant LangChangeCommand objects 
        #And also fill in default values
        autoLanguageSwitching=config.conf['speech']['autoLanguageSwitching']
        autoDialectSwitching=config.conf['speech']['autoDialectSwitching']
        curLanguage=defaultLanguage=getCurrentLanguage()
        prevLanguage=None
        defaultLanguageRoot=defaultLanguage.split('_')[0]
        oldSpeechSequence=speechSequence
        speechSequence=[]
        for item in oldSpeechSequence:
                if isinstance(item,LangChangeCommand):
                        if not autoLanguageSwitching: continue
                        curLanguage=item.lang
                        if not curLanguage or (not autoDialectSwitching and curLanguage.split('_')[0]==defaultLanguageRoot):
                                curLanguage=defaultLanguage
                elif isinstance(item,basestring):
                        if not item: continue
                        if autoLanguageSwitching and curLanguage!=prevLanguage:
                                speechSequence.append(LangChangeCommand(curLanguage))
                                prevLanguage=curLanguage
                        speechSequence.append(item)
                else:
                        speechSequence.append(item)
        if not speechSequence:
                # After normalisation, the sequence is empty.
                # There's nothing to speak.
                return
        log.io("Speaking %r" % speechSequence)
        if symbolLevel is None:
                symbolLevel=config.conf["speech"]["symbolLevel"]
        curLanguage=defaultLanguage
        for index in xrange(len(speechSequence)):
                item=speechSequence[index]
                if autoLanguageSwitching and isinstance(item,LangChangeCommand):
                        curLanguage=item.lang
                if isinstance(item,basestring):
                        speechSequence[index]=processText(curLanguage,item,symbolLevel)+" "
        getSynth().speak(speechSequence)

def speakSelectionMessage(message,text):
        if len(text) < 512:
                speakMessage(message % text)
        else:
                speakMessage(message % _("%d characters") % len(text))

def speakSelectionChange(oldInfo,newInfo,speakSelected=True,speakUnselected=True,generalize=False):
        """Speaks a change in selection, either selected or unselected text.
        @param oldInfo: a TextInfo instance representing what the selection was before
        @type oldInfo: L{textInfos.TextInfo}
        @param newInfo: a TextInfo instance representing what the selection is now
        @type newInfo: L{textInfos.TextInfo}
        @param generalize: if True, then this function knows that the text may have changed between the creation of the oldInfo and newInfo objects, meaning that changes need to be spoken more generally, rather than speaking the specific text, as the bounds may be all wrong.
        @type generalize: boolean
        """
        selectedTextList=[]
        unselectedTextList=[]
        if newInfo.isCollapsed and oldInfo.isCollapsed:
                return
        startToStart=newInfo.compareEndPoints(oldInfo,"startToStart")
        startToEnd=newInfo.compareEndPoints(oldInfo,"startToEnd")
        endToStart=newInfo.compareEndPoints(oldInfo,"endToStart")
        endToEnd=newInfo.compareEndPoints(oldInfo,"endToEnd")
        if speakSelected and oldInfo.isCollapsed:
                selectedTextList.append(newInfo.text)
        elif speakUnselected and newInfo.isCollapsed:
                unselectedTextList.append(oldInfo.text)
        else:
                if startToEnd>0 or endToStart<0:
                        if speakSelected and not newInfo.isCollapsed:
                                selectedTextList.append(newInfo.text)
                        if speakUnselected and not oldInfo.isCollapsed:
                                unselectedTextList.append(oldInfo.text)
                else:
                        if speakSelected and startToStart<0 and not newInfo.isCollapsed:
                                tempInfo=newInfo.copy()
                                tempInfo.setEndPoint(oldInfo,"endToStart")
                                selectedTextList.append(tempInfo.text)
                        if speakSelected and endToEnd>0 and not newInfo.isCollapsed:
                                tempInfo=newInfo.copy()
                                tempInfo.setEndPoint(oldInfo,"startToEnd")
                                selectedTextList.append(tempInfo.text)
                        if startToStart>0 and not oldInfo.isCollapsed:
                                tempInfo=oldInfo.copy()
                                tempInfo.setEndPoint(newInfo,"endToStart")
                                unselectedTextList.append(tempInfo.text)
                        if endToEnd<0 and not oldInfo.isCollapsed:
                                tempInfo=oldInfo.copy()
                                tempInfo.setEndPoint(newInfo,"startToEnd")
                                unselectedTextList.append(tempInfo.text)
        locale=languageHandler.getLanguage()
        if speakSelected:
                if not generalize:
                        for text in selectedTextList:
                                if  len(text)==1:
                                        text=characterProcessing.processSpeechSymbol(locale,text)
                                speakSelectionMessage(_("selecting %s"),text)
                elif len(selectedTextList)>0:
                        text=newInfo.text
                        if len(text)==1:
                                text=characterProcessing.processSpeechSymbol(locale,text)
                        speakSelectionMessage(_("selected %s"),text)
        if speakUnselected:
                if not generalize:
                        for text in unselectedTextList:
                                if  len(text)==1:
                                        text=characterProcessing.processSpeechSymbol(locale,text)
                                speakSelectionMessage(_("unselecting %s"),text)
                elif len(unselectedTextList)>0:
                        speakMessage(_("selection removed"))
                        if not newInfo.isCollapsed:
                                text=newInfo.text
                                if len(text)==1:
                                        text=characterProcessing.processSpeechSymbol(locale,text)
                                speakSelectionMessage(_("selected %s"),text)

def speakTypedCharacters(ch):
        global curWordChars;
        if api.isTypingProtected():
                realChar="*"
        else:
                realChar=ch
        if ch.isalnum():
                curWordChars.append(realChar)
        elif ch=="\b":
                # Backspace, so remove the last character from our buffer.
                del curWordChars[-1:]
        elif len(curWordChars)>0:
                typedWord="".join(curWordChars)
                curWordChars=[]
                if log.isEnabledFor(log.IO):
                        log.io("typed word: %s"%typedWord)
                if config.conf["keyboard"]["speakTypedWords"]: 
                        speakText(typedWord)
        if config.conf["keyboard"]["speakTypedCharacters"] and ord(ch)>=32:
                speakSpelling(realChar)

silentRolesOnFocus=set([
        controlTypes.ROLE_PANE,
        controlTypes.ROLE_ROOTPANE,
        controlTypes.ROLE_FRAME,
        controlTypes.ROLE_UNKNOWN,
        controlTypes.ROLE_APPLICATION,
        controlTypes.ROLE_TABLECELL,
        controlTypes.ROLE_LISTITEM,
        controlTypes.ROLE_MENUITEM,
        controlTypes.ROLE_CHECKMENUITEM,
        controlTypes.ROLE_TREEVIEWITEM,
])

silentValuesForRoles=set([
        controlTypes.ROLE_CHECKBOX,
        controlTypes.ROLE_RADIOBUTTON,
        controlTypes.ROLE_LINK,
        controlTypes.ROLE_MENUITEM,
        controlTypes.ROLE_APPLICATION,
])

def processPositiveStates(role, states, reason, positiveStates):
        positiveStates = positiveStates.copy()
        # The user never cares about certain states.
        if role==controlTypes.ROLE_EDITABLETEXT:
                positiveStates.discard(controlTypes.STATE_EDITABLE)
        if role!=controlTypes.ROLE_LINK:
                positiveStates.discard(controlTypes.STATE_VISITED)
        positiveStates.discard(controlTypes.STATE_SELECTABLE)
        positiveStates.discard(controlTypes.STATE_FOCUSABLE)
        positiveStates.discard(controlTypes.STATE_CHECKABLE)
        if controlTypes.STATE_DRAGGING in positiveStates:
                # It's obvious that the control is draggable if it's being dragged.
                positiveStates.discard(controlTypes.STATE_DRAGGABLE)
        if role == controlTypes.ROLE_COMBOBOX:
                # Combo boxes inherently have a popup, so don't report it.
                positiveStates.discard(controlTypes.STATE_HASPOPUP)
        if role in (controlTypes.ROLE_LINK, controlTypes.ROLE_BUTTON, controlTypes.ROLE_CHECKBOX, controlTypes.ROLE_RADIOBUTTON, controlTypes.ROLE_TOGGLEBUTTON, controlTypes.ROLE_MENUITEM, controlTypes.ROLE_TAB, controlTypes.ROLE_SLIDER, controlTypes.ROLE_DOCUMENT):
                # This control is clearly clickable according to its role
                # or reporting clickable just isn't useful.
                positiveStates.discard(controlTypes.STATE_CLICKABLE)
        if reason == REASON_QUERY:
                return positiveStates
        positiveStates.discard(controlTypes.STATE_DEFUNCT)
        positiveStates.discard(controlTypes.STATE_MODAL)
        positiveStates.discard(controlTypes.STATE_FOCUSED)
        positiveStates.discard(controlTypes.STATE_OFFSCREEN)
        positiveStates.discard(controlTypes.STATE_INVISIBLE)
        if reason != REASON_CHANGE:
                positiveStates.discard(controlTypes.STATE_LINKED)
                if role in (controlTypes.ROLE_LISTITEM, controlTypes.ROLE_TREEVIEWITEM, controlTypes.ROLE_MENUITEM, controlTypes.ROLE_TABLEROW) and controlTypes.STATE_SELECTABLE in states:
                        positiveStates.discard(controlTypes.STATE_SELECTED)
        if role != controlTypes.ROLE_EDITABLETEXT:
                positiveStates.discard(controlTypes.STATE_READONLY)
        if role == controlTypes.ROLE_CHECKBOX:
                positiveStates.discard(controlTypes.STATE_PRESSED)
        if role == controlTypes.ROLE_MENUITEM:
                # The user doesn't usually care if a menu item is expanded or collapsed.
                positiveStates.discard(controlTypes.STATE_COLLAPSED)
                positiveStates.discard(controlTypes.STATE_EXPANDED)
        return positiveStates

def processNegativeStates(role, states, reason, negativeStates):
        speakNegatives = set()
        # Add the negative selected state if the control is selectable,
        # but only if it is either focused or this is something other than a change event.
        # The condition stops "not selected" from being spoken in some broken controls
        # when the state change for the previous focus is issued before the focus change.
        if role in (controlTypes.ROLE_LISTITEM, controlTypes.ROLE_TREEVIEWITEM, controlTypes.ROLE_TABLEROW) and controlTypes.STATE_SELECTABLE in states and (reason != REASON_CHANGE or controlTypes.STATE_FOCUSED in states):
                speakNegatives.add(controlTypes.STATE_SELECTED)
        # Restrict "not checked" in a similar way to "not selected".
        if (role in (controlTypes.ROLE_CHECKBOX, controlTypes.ROLE_RADIOBUTTON, controlTypes.ROLE_CHECKMENUITEM) or controlTypes.STATE_CHECKABLE in states)  and (controlTypes.STATE_HALFCHECKED not in states) and (reason != REASON_CHANGE or controlTypes.STATE_FOCUSED in states):
                speakNegatives.add(controlTypes.STATE_CHECKED)
        if reason == REASON_CHANGE:
                # We want to speak this state only if it is changing to negative.
                speakNegatives.add(controlTypes.STATE_DROPTARGET)
                # We were given states which have changed to negative.
                # Return only those supplied negative states which should be spoken;
                # i.e. the states in both sets.
                speakNegatives &= negativeStates
                if controlTypes.STATES_SORTED & negativeStates and not controlTypes.STATES_SORTED & states:
                        # If the object has just stopped being sorted, just report not sorted.
                        # The user doesn't care how it was sorted before.
                        speakNegatives.add(controlTypes.STATE_SORTED)
                return speakNegatives
        else:
                # This is not a state change; only positive states were supplied.
                # Return all negative states which should be spoken, excluding the positive states.
                return speakNegatives - states

def speakTextInfo(info,useCache=True,formatConfig=None,unit=None,reason=REASON_QUERY,index=None):
        autoLanguageSwitching=config.conf['speech']['autoLanguageSwitching']
        extraDetail=unit in (textInfos.UNIT_CHARACTER,textInfos.UNIT_WORD)
        if not formatConfig:
                formatConfig=config.conf["documentFormatting"]
        reportIndentation=unit==textInfos.UNIT_LINE and formatConfig["reportLineIndentation"]

        speechSequence=[]
        #Fetch the last controlFieldStack, or make a blank one
        controlFieldStackCache=getattr(info.obj,'_speakTextInfo_controlFieldStackCache',[]) if useCache else []
        formatFieldAttributesCache=getattr(info.obj,'_speakTextInfo_formatFieldAttributesCache',{}) if useCache else {}
        textWithFields=info.getTextWithFields(formatConfig)
        # We don't care about node bounds, especially when comparing fields.
        # Remove them.
        for command in textWithFields:
                if not isinstance(command,textInfos.FieldCommand):
                        continue
                field=command.field
                if not field:
                        continue
                try:
                        del field["_startOfNode"]
                except KeyError:
                        pass
                try:
                        del field["_endOfNode"]
                except KeyError:
                        pass

        #Make a new controlFieldStack and formatField from the textInfo's initialFields
        newControlFieldStack=[]
        newFormatField=textInfos.FormatField()
        initialFields=[]
        for field in textWithFields:
                if isinstance(field,textInfos.FieldCommand) and field.command in ("controlStart","formatChange"):
                        initialFields.append(field.field)
                else:
                        break
        if len(initialFields)>0:
                del textWithFields[0:len(initialFields)]
        endFieldCount=0
        for field in reversed(textWithFields):
                if isinstance(field,textInfos.FieldCommand) and field.command=="controlEnd":
                        endFieldCount+=1
                else:
                        break
        if endFieldCount>0:
                del textWithFields[0-endFieldCount:]
        for field in initialFields:
                if isinstance(field,textInfos.ControlField):
                        newControlFieldStack.append(field)
                elif isinstance(field,textInfos.FormatField):
                        newFormatField.update(field)
                else:
                        raise ValueError("unknown field: %s"%field)
        #Calculate how many fields in the old and new controlFieldStacks are the same
        commonFieldCount=0
        for count in xrange(min(len(newControlFieldStack),len(controlFieldStackCache))):
                if newControlFieldStack[count]==controlFieldStackCache[count]:
                        commonFieldCount+=1
                else:
                        break

        #Get speech text for any fields in the old controlFieldStack that are not in the new controlFieldStack 
        endingBlock=False
        for count in reversed(xrange(commonFieldCount,len(controlFieldStackCache))):
                text=info.getControlFieldSpeech(controlFieldStackCache[count],controlFieldStackCache[0:count],"end_removedFromControlFieldStack",formatConfig,extraDetail,reason=reason)
                if text:
                        speechSequence.append(text)
                if not endingBlock and reason==REASON_SAYALL:
                        endingBlock=bool(int(controlFieldStackCache[count].get('isBlock',0)))
        if endingBlock:
                speechSequence.append(BreakCommand())
        # The TextInfo should be considered blank if we are only exiting fields (i.e. we aren't entering any new fields and there is no text).
        isTextBlank=True

        # Even when there's no speakable text, we still need to notify the synth of the index.
        if index is not None:
                speechSequence.append(IndexCommand(index))

        #Get speech text for any fields that are in both controlFieldStacks, if extra detail is not requested
        if not extraDetail:
                for count in xrange(commonFieldCount):
                        text=info.getControlFieldSpeech(newControlFieldStack[count],newControlFieldStack[0:count],"start_inControlFieldStack",formatConfig,extraDetail,reason=reason)
                        if text:
                                speechSequence.append(text)
                                isTextBlank=False

        #Get speech text for any fields in the new controlFieldStack that are not in the old controlFieldStack
        for count in xrange(commonFieldCount,len(newControlFieldStack)):
                text=info.getControlFieldSpeech(newControlFieldStack[count],newControlFieldStack[0:count],"start_addedToControlFieldStack",formatConfig,extraDetail,reason=reason)
                if text:
                        speechSequence.append(text)
                        isTextBlank=False
                commonFieldCount+=1

        #Fetch the text for format field attributes that have changed between what was previously cached, and this textInfo's initialFormatField.
        text=getFormatFieldSpeech(newFormatField,formatFieldAttributesCache,formatConfig,unit=unit,extraDetail=extraDetail)
        if text:
                speechSequence.append(text)
        if autoLanguageSwitching:
                language=newFormatField.get('language')
                speechSequence.append(LangChangeCommand(language))
                lastLanguage=language

        if unit in (textInfos.UNIT_CHARACTER,textInfos.UNIT_WORD) and len(textWithFields)>0 and len(textWithFields[0])==1 and len([x for x in textWithFields if isinstance(x,basestring)])==1:
                if any(isinstance(x,basestring) for x in speechSequence):
                        speak(speechSequence)
                speakSpelling(textWithFields[0],locale=language if autoLanguageSwitching else None)
                info.obj._speakTextInfo_controlFieldStackCache=list(newControlFieldStack)
                info.obj._speakTextInfo_formatFieldAttributesCache=formatFieldAttributesCache
                return

        #Move through the field commands, getting speech text for all controlStarts, controlEnds and formatChange commands
        #But also keep newControlFieldStack up to date as we will need it for the ends
        # Add any text to a separate list, as it must be handled differently.
        #Also make sure that LangChangeCommand objects are added before any controlField or formatField speech
        relativeSpeechSequence=[]
        inTextChunk=False
        allIndentation=""
        indentationDone=False
        for command in textWithFields:
                if isinstance(command,basestring):
                        if reportIndentation and not indentationDone:
                                indentation,command=splitTextIndentation(command)
                                # Combine all indentation into one string for later processing.
                                allIndentation+=indentation
                                if command:
                                        # There was content after the indentation, so there is no more indentation.
                                        indentationDone=True
                        if command:
                                if inTextChunk:
                                        relativeSpeechSequence[-1]+=command
                                else:
                                        relativeSpeechSequence.append(command)
                                        inTextChunk=True
                elif isinstance(command,textInfos.FieldCommand):
                        newLanguage=None
                        if  command.command=="controlStart":
                                # Control fields always start a new chunk, even if they have no field text.
                                inTextChunk=False
                                fieldText=info.getControlFieldSpeech(command.field,newControlFieldStack,"start_relative",formatConfig,extraDetail,reason=reason)
                                newControlFieldStack.append(command.field)
                        elif command.command=="controlEnd":
                                # Control fields always start a new chunk, even if they have no field text.
                                inTextChunk=False
                                fieldText=info.getControlFieldSpeech(newControlFieldStack[-1],newControlFieldStack[0:-1],"end_relative",formatConfig,extraDetail,reason=reason)
                                del newControlFieldStack[-1]
                                if commonFieldCount>len(newControlFieldStack):
                                        commonFieldCount=len(newControlFieldStack)
                        elif command.command=="formatChange":
                                fieldText=getFormatFieldSpeech(command.field,formatFieldAttributesCache,formatConfig,unit=unit,extraDetail=extraDetail)
                                if fieldText:
                                        inTextChunk=False
                                if autoLanguageSwitching:
                                        newLanguage=command.field.get('language')
                                        if lastLanguage!=newLanguage:
                                                # The language has changed, so this starts a new text chunk.
                                                inTextChunk=False
                        if not inTextChunk:
                                if fieldText:
                                        if autoLanguageSwitching and lastLanguage is not None:
                                                # Fields must be spoken in the default language.
                                                relativeSpeechSequence.append(LangChangeCommand(None))
                                                lastLanguage=None
                                        relativeSpeechSequence.append(fieldText)
                                if autoLanguageSwitching and newLanguage!=lastLanguage:
                                        relativeSpeechSequence.append(LangChangeCommand(newLanguage))
                                        lastLanguage=newLanguage
        if reportIndentation and allIndentation!=getattr(info.obj,"_speakTextInfo_lineIndentationCache",""):
                indentationSpeech=getIndentationSpeech(allIndentation)
                if autoLanguageSwitching and speechSequence[-1].lang is not None:
                        # Indentation must be spoken in the default language,
                        # but the initial format field specified a different language.
                        # Insert the indentation before the LangChangeCommand.
                        speechSequence.insert(-1, indentationSpeech)
                else:
                        speechSequence.append(indentationSpeech)
                info.obj._speakTextInfo_lineIndentationCache=allIndentation
        # Don't add this text if it is blank.
        relativeBlank=True
        for x in relativeSpeechSequence:
                if isinstance(x,basestring) and not isBlank(x):
                        relativeBlank=False
                        break
        if not relativeBlank:
                speechSequence.extend(relativeSpeechSequence)
                isTextBlank=False

        #Finally get speech text for any fields left in new controlFieldStack that are common with the old controlFieldStack (for closing), if extra detail is not requested
        if autoLanguageSwitching and lastLanguage is not None:
                speechSequence.append(LangChangeCommand(None))
                lastLanguage=None
        if not extraDetail:
                for count in reversed(xrange(min(len(newControlFieldStack),commonFieldCount))):
                        text=info.getControlFieldSpeech(newControlFieldStack[count],newControlFieldStack[0:count],"end_inControlFieldStack",formatConfig,extraDetail,reason=reason)
                        if text:
                                speechSequence.append(text)
                                isTextBlank=False

        # If there is nothing  that should cause the TextInfo to be considered non-blank, blank should be reported, unless we are doing a say all.
        if reason != REASON_SAYALL and isTextBlank:
                speechSequence.append(_("blank"))

        #Cache a copy of the new controlFieldStack for future use
        if useCache:
                info.obj._speakTextInfo_controlFieldStackCache=list(newControlFieldStack)
                info.obj._speakTextInfo_formatFieldAttributesCache=formatFieldAttributesCache

        if speechSequence:
                if reason==REASON_SAYALL:
                        speakWithoutPauses(speechSequence)
                else:
                        speak(speechSequence)

def getSpeechTextForProperties(reason=REASON_QUERY,**propertyValues):
        global oldTreeLevel, oldTableID, oldRowNumber, oldColumnNumber
        textList=[]
        name=propertyValues.get('name')
        if name:
                textList.append(name)
        if 'role' in propertyValues:
                role=propertyValues['role']
                speakRole=True
        elif '_role' in propertyValues:
                speakRole=False
                role=propertyValues['_role']
        else:
                speakRole=False
                role=controlTypes.ROLE_UNKNOWN
        value=propertyValues.get('value') if role not in silentValuesForRoles else None
        rowNumber=propertyValues.get('rowNumber')
        columnNumber=propertyValues.get('columnNumber')
        includeTableCellCoords=propertyValues.get('includeTableCellCoords',True)
        if speakRole and (reason not in (REASON_SAYALL,REASON_CARET,REASON_FOCUS) or not (name or value or rowNumber or columnNumber) or role not in silentRolesOnFocus):
                textList.append(controlTypes.speechRoleLabels[role])
        if value:
                textList.append(value)
        states=propertyValues.get('states')
        realStates=propertyValues.get('_states',states)
        if states is not None:
                positiveStates=processPositiveStates(role,realStates,reason,states)
                textList.extend([controlTypes.speechStateLabels[x] for x in positiveStates])
        if 'negativeStates' in propertyValues:
                negativeStates=propertyValues['negativeStates']
        else:
                negativeStates=None
        if negativeStates is not None or (reason != REASON_CHANGE and states is not None):
                negativeStates=processNegativeStates(role, realStates, reason, negativeStates)
                if controlTypes.STATE_DROPTARGET in negativeStates:
                        # "not drop target" doesn't make any sense, so use a custom message.
                        textList.append(_("done dragging"))
                        negativeStates.discard(controlTypes.STATE_DROPTARGET)
                textList.extend([_("not %s")%controlTypes.speechStateLabels[x] for x in negativeStates])
        if 'description' in propertyValues:
                textList.append(propertyValues['description'])
        if 'keyboardShortcut' in propertyValues:
                textList.append(propertyValues['keyboardShortcut'])
        indexInGroup=propertyValues.get('positionInfo_indexInGroup',0)
        similarItemsInGroup=propertyValues.get('positionInfo_similarItemsInGroup',0)
        if 00 and lastStartIndex