Re: Orca What happened to the Acroread script?



Joanmarie Diggs wrote:
Looks like another refactoring problem.  I'll send my full debug.out to
Rich as he's Mr. Refactor. <smile>

Yup, I'm guilty. A couple cases of self.isDesiredFocusedItem() need to be
orca_state.activeScript.isDesiredFocusedItem()

I've attached a new acroread.py script (untested) for you to try.

As an aside, I think we should adjust the acroread.py script so that
all the routines at the top, that are not part of the Script class, are in
the subclassed Script class. I can't see a reason why they shouldn't be, and that
would mean that self.isDesiredFocusedItem() would work (which is cleaner).

I can just see Joanie going "but, but, you did exactly this in the
StarOffice script. I was just copying you!" In that case, those routines
are used in the subclassed SpeechGenerator and BrailleGenerator
class, and the only way to solve this (when I initially wrote that script)
was to put them outside the Script class. Now that we have
orca_state.activeScript, those too should probably be moved down to
the Script class. (What do you think on the last part Will).
# Orca
#
# Copyright 2007 Sun Microsystems Inc. and Joanmarie Diggs
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Library General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.
#
# You should have received a copy of the GNU Library General Public
# License along with this library; if not, write to the
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.

"""Custom script for acroread"""

__id__        = "$Id:$"
__version__   = "$Revision:$"
__date__      = "$Date:$"
__copyright__ = "Copyright (c) 2007 Sun Microsystems Inc. and Joanmarie Diggs"
__license__   = "LGPL"

import orca.atspi as atspi
import orca.braille as braille
import orca.chnames as chnames
import orca.debug as debug
import orca.default as default
import orca.input_event as input_event
import orca.orca as orca
import orca.rolenames as rolenames
import orca.orca_state as orca_state
import orca.settings as settings
import orca.speech as speech

from orca.orca_i18n import _ # for gettext support

# To handle the multiple, identical object:text-caret-moved events
# and possible focus events that result from a single key press
#
currentInputEvent = None

# To handle the case when we get an object:text-caret-moved event
# for some text we just left, but which is still showing on the
# screen.
#
lastCaretMovedLine = None

# To minimize chattiness related to focused events when the Find
# toolbar is active.
#
findToolbarActive = False
findToolbarName = None
preFindLine = None

def getDocument(locusOfFocus):
    """ Obtains the Document object that contains the locusOfFocus.

    Arguments:
    - locusOfFocus: the locusOfFocus

    Returns: the Document object, if found.
    """

    document = None
    obj = locusOfFocus
    while obj.role != rolenames.ROLE_UNKNOWN:
        obj = obj.parent

    # This is probably it, but the parent of a text object
    # in a table also has a role of 'unknown' which in turn
    # has a parent with a role of 'unknown'.  The parent of
    # the Document object is a drawing area.
    #
    if obj.parent.role == rolenames.ROLE_DRAWING_AREA:
        document = obj
    else:
        while obj.role != rolenames.ROLE_TABLE:
            obj = obj.parent
        # For now, let's assume no nested tables! :-)
        #
        while obj.role != rolenames.ROLE_UNKNOWN:
            obj = obj.parent
        document = obj

    return document

def findNodeInDocument(obj):
    """ Obtains the location of an object with respect to the
    Document object.

    Arguments:
    - obj: the accessible whose location we're trying to obtain

    Returns: a list that represents the object's position,
    ordered from child to parent
    """

    nodeList = []
    document = getDocument(obj)
    while obj != document:
        nodeList.append(obj.index)
        obj = obj.parent
    return nodeList

def getNextTextObject(obj, nodeList=None):
    """A generator of objects with text in the Document object.
    Acroread organizes document content into a collection of
    individual objects that contain (or have associated) text
    and drawing areas which contain such objects along with
    additional drawing areas. The depth of the drawing areas
    in any given document or drawing area is unknown.

    Arguments:
    - obj:      an Accessible that contains children
    - nodeList: a list reflecting the current object's position
    """

    if nodeList:
        index = nodeList.pop()
    else:
        index = 0

    for i in range(index, obj.childCount):
        child = obj.child(i)
        for nextObject in getNextTextObject(child, nodeList):
            yield nextObject
        yield child

def getTableAndDimensions(obj):
    """Get the table that this text object is in, along with its
    dimensions.

    Arguments:
    - obj: a text object within the Document object.

    Returns the table that this text object cell is in, along with
    the number of rows and columns.
    """

    table = None
    rows = 0
    columns = 0

    # HACK: Rows, columns, and cells are not labeled or assigned
    # roles.  However, the table structure and what can claim focus
    # SEEM to be consistent. So let's punt until things get properly
    # labeled.
    #
    rolesList = [rolenames.ROLE_TEXT, \
                 rolenames.ROLE_UNKNOWN, \
                 rolenames.ROLE_UNKNOWN, \
                 rolenames.ROLE_TABLE]
    if orca_state.activeScript.isDesiredFocusedItem(obj, rolesList):
        table = obj.parent.parent.parent
        rows = table.childCount
        columns = table.child(0).childCount

    return [table, rows, columns]

def getCellCoordinates(table, cell):
    """Get the coordinates of the specified text object with respect
     to the table that contains it.

    Arguments:
    - obj: a text object within a table

    Returns the row number and column number.
    """

    # HACK: Again, these things are not labeled or assigned roles,
    # so we're punting for now.
    #
    column = cell.parent.index + 1
    row = cell.parent.parent.index + 1

    return [row, column]

def isInFindToolbar(obj):
    """Examines the current object to identify if it is in the Find
    tool bar.  If so, it also sets findToolbarName so that we can
    identify this frame by name independent of localization.

    Arguments:
    - obj: an Accessible

    Returns True if the object is in the Find tool bar.
    """

    global findToolbarName

    inFindToolbar = False
    rolesList = [rolenames.ROLE_DRAWING_AREA, \
                 rolenames.ROLE_DRAWING_AREA, \
                 rolenames.ROLE_DRAWING_AREA, \
                 rolenames.ROLE_TOOL_BAR, \
                 rolenames.ROLE_PANEL, \
                 rolenames.ROLE_PANEL, \
                 rolenames.ROLE_FRAME]

    try:
        while obj.role != rolenames.ROLE_DRAWING_AREA:
            obj = obj.parent
        if orca_state.activeScript.isDesiredFocusedItem(obj, rolesList):
            inFindToolbar = True
            findToolbarName = self.getFrame(obj).name
    except:
        pass

    return inFindToolbar

########################################################################
#                                                                      #
# The acroread script class.                                           #
#                                                                      #
########################################################################

class Script(default.Script):

    def __init__(self, app):
        """Creates a new script for the given application.

        Arguments:
        - app: the application to create a script for.
        """

        self.debugLevel = debug.LEVEL_FINEST
        default.Script.__init__(self, app)

        # Acroread documents are contained in an object whose rolename
        # is "Document".  "Link" is also capitalized in acroread.  We
        # need to make these known to Orca for speech and braille output.
        #
        self.ROLE_DOCUMENT = "Document"
        rolenames.rolenames[self.ROLE_DOCUMENT] = \
            rolenames.Rolename(self.ROLE_DOCUMENT,
                               _("doc"),
                               _("Document"),
                               _("document"))

        self.ROLE_LINK = "Link"
        rolenames.rolenames[self.ROLE_LINK] = \
            rolenames.Rolename(self.ROLE_LINK,
                                _("lnk"),
                                _("Link"),
                                _("link"))

    def setupInputEventHandlers(self):
        """Defines InputEventHandler fields for this script that can be
        called by the key and braille bindings. In this particular case,
        we just want to be able to define our own sayAll() method.
        """

        default.Script.setupInputEventHandlers(self)

        self.inputEventHandlers["sayAllHandler"] = \
            input_event.InputEventHandler(
                Script.sayAll,
                _("Speaks entire document."))

    def checkForTableBoundary (self, oldFocus, newFocus):
        """Check to see if we've crossed any table boundaries,
        speaking the appropriate details when we have.

        Arguments:
        - oldFocus: Accessible that is the old locus of focus
        - newFocus: Accessible that is the new locus of focus
        """

        if oldFocus == None or newFocus == None:
            return

        [oldFocusIsTable, oldFocusRows, oldFocusColumns] = \
                   getTableAndDimensions(oldFocus)
        [newFocusIsTable, newFocusRows, newFocusColumns] = \
                   getTableAndDimensions(newFocus)

        # [[[TODO: JD - It is possible to move focus into the object
        # that contains the object that contains the text object. We
        # need to detect this and adjust accordingly.]]]

        if not oldFocusIsTable and newFocusIsTable:
            # We've entered a table.  Announce the dimensions.
            #
            line = _("table with %d rows and %d columns.") % \
                    (newFocusRows, newFocusColumns)
            speech.speak(line)

        elif oldFocusIsTable and not newFocusIsTable:
            # We've left a table.  Announce this fact.
            #
            speech.speak(_("leaving table."))

        elif oldFocusIsTable and newFocusIsTable:
            # See if we've crossed a cell boundary.  If so, speak
            # what has changed (per Mike).
            #
            [oldRow, oldCol] = \
                   getCellCoordinates(oldFocusIsTable, oldFocus)
            [newRow, newCol] = \
                   getCellCoordinates(newFocusIsTable, newFocus)
            if newRow != oldRow:
                # We can't count on being in the first/last cell
                # of the new row -- only the first/last cell of
                # the new row that contains data.
                #
                line = _("row %d, column %d") % (newRow, newCol)
                speech.speak(line)
            elif newCol != oldCol:
                line = _("column %d") % newCol
                speech.speak(line)

    def onFocus(self, event):
        """Called whenever an object gets focus. Overridden in this script
        because we sometimes get a focus event in addition to caret-moved
        events when we change from one area in the document to another. We
        want to minimize the repetition of text along with the unnecessary
        speaking of object types (e.g. drawing area, text, etc.).

        Arguments:
        - event: the Event
        """

        global currentInputEvent
        currentInputEvent = None

        # We sometimes get focus events for items that don't --
        # or don't yet) have focus.  Ignore these.
        #
        if (event.source.role == rolenames.ROLE_CHECK_BOX or \
            event.source.role == rolenames.ROLE_PUSH_BUTTON or \
            event.source.role == rolenames.ROLE_RADIO_BUTTON) and \
           not event.source.state.count(atspi.Accessibility.STATE_FOCUSED):
            return

        if not event.source.state.count(atspi.Accessibility.STATE_SHOWING):
            return

        if not findToolbarActive and event.source.role == rolenames.ROLE_TEXT:
            if event.source.parent and \
               (event.source.parent.role == rolenames.ROLE_DRAWING_AREA or \
                event.source.parent.role == rolenames.ROLE_UNKNOWN):
                # We're going to get at least one (and likely several)
                # caret-moved events which will cause this to get spoken, 
                # so skip it for now.
                #
                return

        if event.source.role == rolenames.ROLE_DRAWING_AREA:
            # A drawing area can claim focus when visually what has focus is
            # a text object that is a child of the drawing area.  When this
            # occurs, Orca doesn't see the text.  Therefore, try to figure out
            # where we are based on where we were and what key we pressed.
            # Then set the event.source accordingly before handing things off
            # to the default script.
            #
            debug.println(self.debugLevel, "acroread: Drawing area bug")
            lastKey = None
            try:
                lastKey = orca_state.lastInputEvent.event_string
            except:
                pass
            LOFIndex = orca_state.locusOfFocus.index
            childIndex = None

            # [[[TODO: JD - These aren't all of the possibilities.  This is
            # very much a work in progress and of testing.]]]
            #
            if lastKey == "Up":
                childIndex = LOFIndex - 1
            elif lastKey == "Down":
                childIndex = LOFIndex + 1
            elif lastKey == "Right" or lastKey == "End":
                childIndex = LOFIndex
            elif lastKey == "Left" or lastKey == "Home":
                childIndex = LOFIndex

            if (childIndex >= 0):
                child = event.source.child(childIndex)
                event.source = child

        default.Script.onFocus(self, event)

    def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
        """Called when the visual object with focus changes. Overridden
        in this script to minimize the repetition of text along with
        the unnecessary speaking of object types.

        Arguments:
        - event: if not None, the Event that caused the change
        - oldLocusOfFocus: Accessible that is the old locus of focus
        - newLocusOfFocus: Accessible that is the new locus of focus
        """

        if not newLocusOfFocus or (oldLocusOfFocus == newLocusOfFocus):
            return

        # Eliminate unnecessary chattiness related to the Find toolbar.
        #
        if findToolbarActive:
            if newLocusOfFocus.role == rolenames.ROLE_TEXT:
                newText = self.getTextLineAtCaret(newLocusOfFocus)
                if newText == preFindLine:
                    orca.setLocusOfFocus(event, oldLocusOfFocus, False)
                    return
            if newLocusOfFocus.role == rolenames.ROLE_DRAWING_AREA:
                orca.setLocusOfFocus(event, oldLocusOfFocus, False)
                return

            utterances = \
                 self.speechGenerator.getSpeech(newLocusOfFocus, False)
            speech.speakUtterances(utterances)
            brailleRegions = \
                 self.brailleGenerator.getBrailleRegions(newLocusOfFocus)
            braille.displayRegions(brailleRegions)
            orca.setLocusOfFocus(event, newLocusOfFocus, False)
            return

        # Eliminate unnecessary chattiness in the Search panel.
        #
        if newLocusOfFocus.role == rolenames.ROLE_PUSH_BUTTON and \
           oldLocusOfFocus and oldLocusOfFocus.role == self.ROLE_LINK and \
           newLocusOfFocus.name == oldLocusOfFocus.name:
            return

        # Eliminate general document chattiness.
        #
        if newLocusOfFocus.role == self.ROLE_DOCUMENT or \
           newLocusOfFocus.role == rolenames.ROLE_DRAWING_AREA:
            orca.setLocusOfFocus(event, newLocusOfFocus, False)
            return

        elif newLocusOfFocus.role == self.ROLE_LINK:
            # It seems that this will be the only event we will get.  But
            # the default script's onFocus will result in unnecessary
            # verboseness: reporting the drawing area(s) in which this link
            # is contained, speaking the periods in a table of contents, etc.
            #
            utterances = self.speechGenerator.getSpeech(newLocusOfFocus, False)
            adjustedUtterances = []
            for utterance in utterances:
                adjustedUtterances.append(self.adjustForRepeats(utterance))
            speech.speakUtterances(adjustedUtterances)
            brailleRegions = \
                     self.brailleGenerator.getBrailleRegions(newLocusOfFocus)
            braille.displayRegions(brailleRegions)
            orca.setLocusOfFocus(event, newLocusOfFocus, False)
            return

        default.Script.locusOfFocusChanged(self, event,
                                           oldLocusOfFocus, newLocusOfFocus)

    def onCaretMoved(self, event):
        """Called whenever the caret moves.  Overridden in this script
        because we want to minimize the repetition of text and the speaking
        of erroneous events.

        Arguments:
        - event: the Event
        """

        global currentInputEvent, lastCaretMovedLine
        lastInputEvent = orca_state.lastInputEvent
        lastKey = lastInputEvent.event_string

        # A single keypress usually results in multiple, not necessarily
        # identical, caret-moved events.  Check to see if the events are
        # identical or very closely timed (time chosen based on testing).
        #
        if currentInputEvent and lastInputEvent:
            timeDiff = abs(currentInputEvent.time - lastInputEvent.time)
            if currentInputEvent == lastInputEvent or timeDiff < 0.2:
                return

        # Changing pages sometimes results in a caret-moved event for
        # text that may or may NOT have had focus recently.  Sometimes
        # we luck out and it's not showing.
        #
        if not event.source.state.count(atspi.Accessibility.STATE_SHOWING):
            return

        # Other times, it's showing, but happens to be the text we just
        # left. Since this SEEMS limited to page up/page down, let's be
        # conservative until we have evidence to the contrary.
        #
        textLine = self.getTextLineAtCaret(event.source)
        isOldLine = textLine == lastCaretMovedLine and \
                    (lastKey == "Page_Down" or lastKey == "Page_Up")
        if isOldLine:
            lastCaretMovedLine = None
            return
        else:
            lastCaretMovedLine = textLine

        # [[[TODO: JD - Sometimes it's showing AND we didn't just leave
        # it. This also seems to occur sometimes with the Find toolbar.]]]
        
        currentInputEvent = orca_state.lastInputEvent
        self.checkForTableBoundary(orca_state.locusOfFocus, event.source)
        default.Script.onCaretMoved(self, event)

    def onStateChanged(self, event):
        """Called whenever an object's state changes.

        Arguments:
        - event: the Event
        """

        global findToolbarActive

        if event.type == "object:state-changed:checked" and \
           event.source.role == rolenames.ROLE_RADIO_BUTTON:
            # Radio buttons in the Search panel are not automatically
            # selected when you arrow to them.  You have to press Space
            # to select the current radio button.  Watch for this.
            #
            orca.visualAppearanceChanged(event, event.source)
            return

        elif event.type == "object:state-changed:focused" and \
             event.detail1 == 1:
            if event.source.role == rolenames.ROLE_PUSH_BUTTON:
                # Try to minimize chattiness in the Search panel
                #
                utterances = \
                     self.speechGenerator.getSpeech(event.source, False)
                speech.speakUtterances(utterances)
                brailleRegions = \
                     self.brailleGenerator.getBrailleRegions(event.source)
                braille.displayRegions(brailleRegions)
                orca.setLocusOfFocus(event, event.source, False)
                return

            elif event.source.role == rolenames.ROLE_TEXT:
                # There's an excellent chance that the Find toolbar just
                # gained focus.  Check.
                #
                if isInFindToolbar(event.source):
                    findToolbarActive = True

        default.Script.onStateChanged(self, event)

    def onWindowDeactivated(self, event):
        """Called whenever a toplevel window is deactivated. Overridden
        in this script to deal with significant chattiness surrounding
        the use of the Find toolbar.

        Arguments:
        - event: the Event
        """

        global findToolbarActive, preFindLine
        locusOfFocus = orca_state.locusOfFocus

        if event.source.name == findToolbarName:
            findToolbarActive = False
        elif locusOfFocus.text:
            preFindLine = self.getTextLineAtCaret(locusOfFocus)

        default.Script.onWindowDeactivated(self, event)

    def textLines(self, obj, nodeList=None):
        """A generator that can be used to iterate over each line of a
        text object, starting at the caret offset. Overridden here
        because we are not getting any RELATION_FLOWS_TO from acroread.

        Arguments:
        - obj:      An Accessible that contains children.  Initially, the
                    document itself.
        - nodeList: A list reflecting the position of the current text object
        """

        for textObj in getNextTextObject(obj, nodeList):
            for [context, acss] in default.Script.textLines(textObj):
                yield [context, acss]

    def sayAll(self, inputEvent):
        """Speaks the contents of the document beginning with the present
        location.  Overridden in this script because the default sayAll
        only speaks the current text object.

        Arguments:
        - inputEvent: if not None, the input event that caused this action.
        """

        if orca_state.locusOfFocus:
            nodeList = findNodeInDocument(orca_state.locusOfFocus)
            document = getDocument(orca_state.locusOfFocus)
            # Note:  We get the correct progress callback, but acroread
            # doesn't seem to respond to setCaretOffset, so we cannot
            # update our location when sayAll is interrupted or finished.
            #
            speech.sayAll(self.textLines(document, nodeList),
                          self.__sayAllProgressCallback)
        else:
            default.Script.sayAll(self, inputEvent)

        return True

    def sayWord(self, obj):
        """Speaks the word at the caret.  Overridden here because we seem
        to be getting the details of the word we just left when moving
        forward with Control Right Arrow. Control Left Arrow works as
        expected with the default script with the exception of crossing
        over a blank line (which sometimes causes the word with focus to
        be repeated).  Both problems are addressed here.

        Arguments:
        - obj: an Accessible object that implements the AccessibleText
               interface
        """

        if not (obj.parent.role == rolenames.ROLE_DRAWING_AREA or \
                obj.parent.role == rolenames.ROLE_UNKNOWN):
            default.Script.sayWord(self, obj)

        else:
            text = obj.text
            offset = text.caretOffset
            lastKey = orca_state.lastInputEvent.event_string

            if lastKey == "Right":
                penultimateWord = orca_state.lastWord
                [lastWord, startOffset, endOffset] = \
                    text.getTextAtOffset(offset,
                                 atspi.Accessibility.TEXT_BOUNDARY_WORD_START)
                [word, startOffset, endOffset] = \
                    text.getTextAfterOffset(endOffset+1,
                                 atspi.Accessibility.TEXT_BOUNDARY_WORD_START)
                if len(penultimateWord) > 0:
                    lastCharPW = penultimateWord[len(penultimateWord) - 1]
                    if lastCharPW == "\n":
                        voice = self.voices[settings.DEFAULT_VOICE]
                        speech.speak(chnames.getCharacterName("\n"),
                                     voice,
                                     False)
                        if penultimateWord != lastWord:
                            word = lastWord

            if lastKey == "Left":
                lastWord = orca_state.lastWord
                [word, startOffset, endOffset] = \
                    text.getTextAtOffset(offset,
                                 atspi.Accessibility.TEXT_BOUNDARY_WORD_START)
                if len(word) > 0:
                    lastChar = word[len(word) - 1]
                    if lastChar == "\n" and lastWord != word:
                        voice = self.voices[settings.DEFAULT_VOICE]
                        speech.speak(chnames.getCharacterName("\n"),
                                     voice,
                                     False)
                    if lastWord == word:
                        return

            if self.getLinkIndex(obj, offset) >= 0:
                voice = self.voices[settings.HYPERLINK_VOICE]
            elif word.isupper():
                voice = self.voices[settings.UPPERCASE_VOICE]
            else:
                voice = self.voices[settings.DEFAULT_VOICE]

            word = self.adjustForRepeats(word)
            orca_state.lastWord = word
            speech.speak(word, voice)
            self.speakTextSelectionState(obj, startOffset, endOffset)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]