# Orca
#
# Copyright 2008-2009 Sun Microsystems Inc.
#
# 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., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

"""Utilities for obtaining tutorial utterances for objects.  In general,
there probably should be a singleton instance of the TutorialGenerator
class.  For those wishing to override the generators, however,
one can create a new instance and replace/extend the tutorial generators
as they see fit."""

__id__        = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2008-2009 Sun Microsystems Inc."
__license__   = "LGPL"

import pyatspi
import debug
import settings

from orca_i18n import _         # for gettext support

class TutorialGenerator:
    """Takes accessible objects and produces a tutorial string to speak
    for those objects.  See the getTutorialString method, which is the
    primary entry point.  Subclasses can feel free to override/extend
    the getTutorialGenerators instance field as they see fit."""

    def __init__(self, script):

        # The script that created us.  This allows us to ask the
        # script for information if we need it.
        #
        self._script = script
        # storing the last spoken message.
        self.lastTutorial = ""

        # Set up a dictionary that maps role names to functions
        # that generate tutorial strings for objects that implement that role.
        #
        self.tutorialGenerators = {}
        self.tutorialGenerators[pyatspi.ROLE_CHECK_BOX] = \
            self._getTutorialForCheckBox
        self.tutorialGenerators[pyatspi.ROLE_COMBO_BOX] = \
            self._getTutorialForComboBox
        self.tutorialGenerators[pyatspi.ROLE_FRAME] = \
            self._getTutorialForFrame
        self.tutorialGenerators[pyatspi.ROLE_ICON] = \
            self._getTutorialForIcon
        self.tutorialGenerators[pyatspi.ROLE_LAYERED_PANE] = \
            self._getTutorialForLayeredPane
        self.tutorialGenerators[pyatspi.ROLE_LIST] = \
            self._getTutorialForList
        self.tutorialGenerators[pyatspi.ROLE_LIST_ITEM] = \
            self._getTutorialForListItem
        self.tutorialGenerators[pyatspi.ROLE_PAGE_TAB] = \
            self._getTutorialForPageTab
        self.tutorialGenerators[pyatspi.ROLE_PARAGRAPH] = \
            self._getTutorialForText
        self.tutorialGenerators[pyatspi.ROLE_PASSWORD_TEXT] = \
            self._getTutorialForText
        self.tutorialGenerators[pyatspi.ROLE_ENTRY] = \
            self._getTutorialForText
        self.tutorialGenerators[pyatspi.ROLE_PUSH_BUTTON] = \
            self._getTutorialForPushButton
        self.tutorialGenerators[pyatspi.ROLE_SPIN_BUTTON] = \
            self._getTutorialForSpinButton
        self.tutorialGenerators[pyatspi.ROLE_TABLE_CELL] = \
            self._getTutorialForTableCellRow
        self.tutorialGenerators[pyatspi.ROLE_TEXT] = \
            self._getTutorialForText
        self.tutorialGenerators[pyatspi.ROLE_TOGGLE_BUTTON] = \
            self._getTutorialForCheckBox
        self.tutorialGenerators[pyatspi.ROLE_RADIO_BUTTON] = \
            self._getTutorialForRadioButton
        self.tutorialGenerators[pyatspi.ROLE_MENU]                = \
            self._getTutorialForMenu
        self.tutorialGenerators[pyatspi.ROLE_CHECK_MENU_ITEM]     = \
            self._getTutorialForCheckBox
        self.tutorialGenerators[pyatspi.ROLE_MENU_ITEM]           = \
            self._getTutorialForPushButton
        self.tutorialGenerators[pyatspi.ROLE_RADIO_MENU_ITEM]     = \
            self._getTutorialForCheckBox
        self.tutorialGenerators[pyatspi.ROLE_SLIDER]              = \
            self._getTutorialForSlider

    def _debugGenerator(self, generatorName, obj, alreadyFocused, utterances):
        """Prints debug.LEVEL_FINER information regarding
        the tutorial generator.

        Arguments:
        - generatorName: the name of the generator
        - obj: the object being presented
        - alreadyFocused: False if object just received focus
        - utterances: the generated text
        """

        debug.println(debug.LEVEL_FINER,
                      "GENERATOR: %s" % generatorName)
        debug.println(debug.LEVEL_FINER,
                      "           obj             = %s" % obj.name)
        debug.println(debug.LEVEL_FINER,
                      "           role            = %s" % obj.getRoleName())
        debug.println(debug.LEVEL_FINER,
                      "           alreadyFocused  = %s" % alreadyFocused)
        debug.println(debug.LEVEL_FINER,
                      "           utterances:")
        for text in utterances:
            debug.println(debug.LEVEL_FINER,
                    "               (%s)" % text)

    def _getDefaultTutorial(
        self, obj, alreadyFocused, forceTutorial, role=None):
        """The default tutorial generator returns the empty tutorial string
        because We have no associated tutorial function for the object.

        Arguments:
        - obj: an Accessible
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string
        - role: A role that should be used instead of the Accessible's
          possible role.

        Returns the empty list []
        """

        return []


    def _getTutorialForCheckBox(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a check box.

        Arguments:
        - obj: the check box
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is a tip for the user on how to toggle a checkbox.
        msg = _("Press space to toggle.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForCheckBox",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForComboBox(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a combobox.

        Arguments:
        - obj: the combo box
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is a tip for the user on how to interact
        # with a combobox.
        msg = _("Press space to expand, and use up and down to select an item.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForComboBox",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForFrame(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a frame.

        Arguments:
        - obj: the frame
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []
        name = self._script.utilities.displayedText(obj)
        if not name and obj.description:
            name = obj.description

        # Translators: If this application has more than one unfocused alert or
        # dialog window, inform user of how to refocus these.
        childWindowsMsg = _("Press alt+f6 to give focus to child windows.")

        # If this application has more than one unfocused alert or
        # dialog window, tell user how to give them focus.
        alertAndDialogCount = \
                    self._script.utilities.unfocusedAlertAndDialogCount(obj)
        if alertAndDialogCount > 0:
            utterances.append(childWindowsMsg)

        self._debugGenerator("_getTutorialForFrame",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForIcon(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for an icon.

        Arguments:
        - obj: the icon
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        if obj.parent.getRole() == pyatspi.ROLE_LAYERED_PANE:
            utterances = self._getTutorialForLayeredPane(obj.parent,
                                                         alreadyFocused,
                                                         forceTutorial)
        else:
            utterances = self._getDefaultTutorial(obj,
                                                  alreadyFocused,
                                                  forceTutorial)

        self._debugGenerator("_getTutorialForIcon",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForLayeredPane(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a layered pane.

        Arguments:
        - obj: the layered pane
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []
        name = self._script.utilities.displayedText(obj)
        if not name and obj.description:
            name = obj.description

        # Translators: this gives tips on how to navigate items in a
        # layered pane.
        msg = _("To move to items, use either " \
                "the arrow keys or type ahead searching.")
        utterances.append(msg)

        # Translators: this is the tutorial string for when first landing
        # on the desktop, describing how to access the system menus.
        desktopMsg = _("To get to the system menus press the alt+f1 key.")

        scriptName = self._script.name
        sibling = obj.parent.getChildAtIndex(0)
        if 'nautilus' in scriptName and obj == sibling:
            utterances.append(desktopMsg)

        if (not alreadyFocused and self.lastTutorial != utterances) \
            or forceTutorial:
            pass
        else:
            utterances = []

        self._debugGenerator("_getTutorialForLayeredPane",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForList(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a list.

        Arguments:
        - obj: the list
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []

        # Translators: this is the tutorial string when navigating lists.
        msg = _("Use up and down to select an item.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForList",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForListItem(self, obj, alreadyFocused, forceTutorial):
        """Get the  tutorial string for a listItem.

        Arguments:
        - obj: the listitem
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []

        # Translators: this represents the state of a node in a tree.
        # 'expanded' means the children are showing.
        # 'collapsed' means the children are not showing.
        # this string informs the user how to collapse the node.
        expandedMsg = _("To collapse, press shift plus left.")

        # Translators: this represents the state of a node in a tree.
        # 'expanded' means the children are showing.
        # 'collapsed' means the children are not showing.
        # this string informs the user how to expand the node.
        collapsedMsg = _("To expand, press shift plus right.")


        # If already in focus then the tree probably collapsed or expanded
        state = obj.getState()
        if state.contains(pyatspi.STATE_EXPANDABLE):
            if state.contains(pyatspi.STATE_EXPANDED):
                if (self.lastTutorial != [expandedMsg]) or forceTutorial:
                    utterances.append(expandedMsg)
            else:
                if (self.lastTutorial != [collapsedMsg]) or forceTutorial:
                    utterances.append(collapsedMsg)

        self._debugGenerator("_getTutorialForListItem",
                             obj,
                             alreadyFocused,
                             utterances)
        return utterances

    def _getTutorialForText(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a text object.

        Arguments:
        - obj: the text component
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """
        utterances = []
        # Translators: This is the tutorial string for when landing
        # on text fields.
        msg = _("Type in text.")

        if (not alreadyFocused or forceTutorial) and \
           not self._script.utilities.isReadOnlyTextArea(obj):
            utterances.append(msg)

        self._debugGenerator("_getTutorialForText",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForPageTab(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a page tab.

        Arguments:
        - obj: the page tab
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of tutorial utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is the tutorial string for landing
        # on a page tab, we are informing the
        # user how to navigate these.
        msg = _("Use left and right to view other tabs.")

        if (self.lastTutorial != [msg]) or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForPageTabList",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForPushButton(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a push button

        Arguments:
        - obj: the push button
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is the tutorial string for activating a push button.
        msg = _("To activate press space.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForPushButton",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForSpinButton(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a spin button.  If the object already has
        focus, then no tutorial is given.

        Arguments:
        - obj: the spin button
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is the tutorial string for when landing
        # on a spin button.
        msg = _("Use up or down arrow to select value." \
              " Or type in the desired numerical value.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForSpinButton",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForTableCell(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial utterances for a single table cell

        Arguments:
        - obj: the table
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []

        # Translators: this represents the state of a node in a tree.
        # 'expanded' means the children are showing.
        # 'collapsed' means the children are not showing.
        # this string informs the user how to collapse the node.
        expandedMsg = _("To collapse, press shift plus left.")

        # Translators: this represents the state of a node in a tree.
        # 'expanded' means the children are showing.
        # 'collapsed' means the children are not showing.
        # this string informs the user how to expand the node.
        collapsedMsg = _("To expand, press shift plus right.")

        # If this table cell has 2 children and one of them has a
        # 'toggle' action and the other does not, then present this
        # as a checkbox where:
        # 1) we get the checked state from the cell with the 'toggle' action
        # 2) we get the label from the other cell.
        # See Orca bug #376015 for more details.
        #
        if obj.childCount == 2:
            cellOrder = []
            hasToggle = [ False, False ]
            for i, child in enumerate(obj):
                try:
                    action = child.queryAction()
                except NotImplementedError:
                    continue
                else:
                    for j in range(0, action.nActions):
                        # Translators: this is the action name for
                        # the 'toggle' action. It must be the same
                        # string used in the *.po file for gail.
                        #
                        if action.getName(j) in ["toggle", _("toggle")]:
                            hasToggle[i] = True
                            break

            if hasToggle[0] and not hasToggle[1]:
                cellOrder = [ 1, 0 ]
            elif not hasToggle[0] and hasToggle[1]:
                cellOrder = [ 0, 1 ]
            if cellOrder:
                for i in cellOrder:
                    # Don't speak the label if just the checkbox state has
                    # changed.
                    #
                    if alreadyFocused and not hasToggle[i]:
                        pass
                    else:
                        utterances.extend( \
                            self._getTutorialForTableCell(obj[i],
                            alreadyFocused, forceTutorial))
                return utterances

        # [[[TODO: WDW - Attempt to infer the cell type.  There's a
        # bunch of stuff we can do here, such as check the EXPANDABLE
        # state, check the NODE_CHILD_OF relation, etc.  Logged as
        # bugzilla bug 319750.]]]
        #
        try:
            action = obj.queryAction()
        except NotImplementedError:
            action = None
        if action:
            for i in range(0, action.nActions):
                debug.println(debug.LEVEL_FINEST,
                    "tutorialgenerator._getTutorialForTableCell" \
                    + "looking at action %d" % i)

                # Translators: this is the action name for
                # the 'toggle' action. It must be the same
                # string used in the *.po file for gail.
                #
                if action.getName(i) in ["toggle", _("toggle")]:
                    utterances = self._getTutorialForCheckBox(obj,
                                  alreadyFocused, forceTutorial)
                    break

        state = obj.getState()
        if state.contains(pyatspi.STATE_EXPANDABLE):
            if state.contains(pyatspi.STATE_EXPANDED):
                if self.lastTutorial != [expandedMsg] or forceTutorial:
                    utterances.append(expandedMsg)
            else:
                if self.lastTutorial != [collapsedMsg] or forceTutorial:
                    utterances.append(collapsedMsg)

        self._debugGenerator("_getTutorialForTableCell",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForTableCellRow(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for the active table cell in the table row.

        Arguments:
        - obj: the table
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []

        if (not alreadyFocused):
            try:
                parent_table = obj.parent.queryTable()
            except NotImplementedError:
                parent_table = None
            if settings.readTableCellRow and parent_table \
                and not self._script.utilities.isLayoutOnly(obj.parent):
                parent = obj.parent
                index = self._script.utilities.cellIndex(obj)
                row = parent_table.getRowAtIndex(index)
                column = parent_table.getColumnAtIndex(index)

                # This is an indication of whether we should speak all the
                # table cells (the user has moved focus up or down a row),
                # or just the current one (focus has moved left or right in
                # the same row).
                #
                speakAll = True
                if "lastRow" in self._script.pointOfReference and \
                    "lastColumn" in self._script.pointOfReference:
                    pointOfReference = self._script.pointOfReference
                    speakAll = (pointOfReference["lastRow"] != row) or \
                        ((row == 0 or row == parent_table.nRows-1) and \
                           pointOfReference["lastColumn"] == column)

                utterances.extend(self._getTutorialForTableCell(obj,
                                        alreadyFocused, forceTutorial))
            else:
                utterances = self._getTutorialForTableCell(obj,
                  alreadyFocused, forceTutorial)
        else:
            utterances = self._getTutorialForTableCell(obj, alreadyFocused, \
              forceTutorial)

        self._debugGenerator("_getTutorialForTableCellRow",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def _getTutorialForRadioButton(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a radio button.

        Arguments:
        - obj: the radio button
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is a tip for the user, how to navigate radiobuttons.
        msg = _("Use arrow keys to change.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForRadioButton",
                             obj,
                             alreadyFocused,
                             utterances)
        return utterances

    def _getTutorialForMenu(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a menu.

        Arguments:
        - obj: the menu
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is a tip for the user, how to navigate menues.
        mainMenuMsg = _("To navigate, press left or right arrow. " \
                       "To move through items press up or down arrow.")

        # Translators: this is a tip for the user, how to
        # navigate into sub menues.
        subMenuMsg = _("To enter sub menu, press right arrow.")

        # Checking if we are a submenu,
        # we can't rely on our parent being just a menu.
        if obj.parent.name != "" and obj.parent.__class__ == obj.__class__:
            if (self.lastTutorial != [subMenuMsg]) or forceTutorial:
                utterances.append(subMenuMsg)
        else:
            if (self.lastTutorial != [mainMenuMsg]) or forceTutorial:
                utterances.append(mainMenuMsg)

        self._debugGenerator("_getTutorialForMenu",
                             obj,
                             alreadyFocused,
                             utterances)
        return utterances

    def _getTutorialForSlider(self, obj, alreadyFocused, forceTutorial):
        """Get the tutorial string for a slider.  If the object already has
        focus, then no tutorial is given.

        Arguments:
        - obj: the slider
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken for the object.
        """

        utterances = []
        # Translators: this is the tutorial string for when landing
        # on a slider.
        msg = _("To decrease press left arrow, to increase press right arrow." \
          " To go to minimum press home, and for maximum press end.")

        if (not alreadyFocused and self.lastTutorial != [msg]) \
           or forceTutorial:
            utterances.append(msg)

        self._debugGenerator("_getTutorialForSlider",
                             obj,
                             alreadyFocused,
                             utterances)

        return utterances

    def getTutorial(self, obj, alreadyFocused, forceTutorial=False):
        """Get the tutorial for an Accessible object.  This will look
        first to the specific tutorial generators and if this
        does not exist then return the empty tutorial.
        This method is the primary method
        that external callers of this class should use.

        Arguments:
        - obj: the object
        - alreadyFocused: False if object just received focus
        - forceTutorial: used for when whereAmI really needs the tutorial string

        Returns a list of utterances to be spoken.
        """

        if not settings.enableTutorialMessages:
            return []

        role = obj.getRole()
        if role in self.tutorialGenerators:
            generator = self.tutorialGenerators[role]
        else:
            generator = self._getDefaultTutorial
        msg = generator(obj, alreadyFocused, forceTutorial)
        utterances = [" ".join(msg)]
        if msg:
            self.lastTutorial = msg
        if forceTutorial:
            self.lastTutorial = ""

        self._debugGenerator("getTutorial",
                             obj,
                             alreadyFocused,
                             utterances)
        return utterances
