# Orca
#
# Copyright 2010 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., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.

"""Commonly-required utility methods needed by -- and potentially
   customized by -- application and toolkit scripts. They have
   been pulled out from the scripts because certain scripts had
   gotten way too large as a result of including these methods."""

__id__ = "$Id$"
__version__   = "$Revision$"
__date__      = "$Date$"
__copyright__ = "Copyright (c) 2010 Joanmarie Diggs."
__license__   = "LGPL"

import pyatspi

import orca.debug as debug
import orca.orca_state as orca_state
import orca.script_utilities as script_utilities

#############################################################################
#                                                                           #
# Utilities                                                                 #
#                                                                           #
#############################################################################

class Utilities(script_utilities.Utilities):

    def __init__(self, script):
        """Creates an instance of the Utilities class.

        Arguments:
        - script: the script with which this instance is associated.
        """

        script_utilities.Utilities.__init__(self, script)

    #########################################################################
    #                                                                       #
    # Utilities for finding, identifying, and comparing accessibles         #
    #                                                                       #
    #########################################################################

    def displayedText(self, obj):
        """Returns the text being displayed for an object. Overridden here
        because OpenOffice uses symbols (e.g. ">>" for buttons but exposes
        more useful information via the accessible's name.

        Arguments:
        - obj: the object

        Returns the text being displayed for an object or None if there isn't
        any text being shown.
        """

        if obj.getRole() == pyatspi.ROLE_PUSH_BUTTON and obj.name:
            return obj.name
        else:
            return script_utilities.Utilities.displayedText(self, obj)

    def isReadOnlyTextArea(self, obj):
        """Returns True if obj is a text entry area that is read only."""

        if not obj.getRole() == pyatspi.ROLE_TEXT:
            return False

        state = obj.getState()
        readOnly = state.contains(pyatspi.STATE_FOCUSABLE) \
                   and not state.contains(pyatspi.STATE_EDITABLE)
        debug.println(debug.LEVEL_ALL,
                      "soffice - isReadOnlyTextArea=%s for %s" \
                      % (readOnly, debug.getAccessibleDetails(obj)))

        return readOnly

    def isDuplicateEvent(self, event):
        """Returns True if we believe this event is a duplicate which we
        wish to ignore."""

        if not event:
            return False

        if event.type.startswith("object:text-caret-moved"):
            try:
                obj, offset = \
                    self._script.pointOfReference["lastCursorPosition"]
            except:
                return False
            else:
                # Doing an intentional equality check rather than calling
                # isSameObject() because we'd rather double-present an
                # object than not present it at all.
                #
                return obj == event.source and offset == event.detail1

        return False

    def isSameObject(self, obj1, obj2):
        same = script_utilities.Utilities.isSameObject(self, obj1, obj2)

        # Handle the case of false positives in dialog boxes resulting
        # from getIndexInParent() returning a bogus value. bgo#618790.
        #
        if same and (obj1 != obj2) and not obj1.name \
           and obj1.getRole() == pyatspi.ROLE_TABLE_CELL \
           and obj1.getIndexInParent() == obj2.getIndexInParent() == -1:
            top = self.topLevelObject(obj1)
            if top and top.getRole() == pyatspi.ROLE_DIALOG:
                same = False

        return same

    def frameAndDialog(self, obj):
        """Returns the frame and (possibly) the dialog containing
        the object. Overridden here for presentation of the title
        bar information: If the locusOfFocus is a spreadsheet cell,
        1) we are not in a dialog and 2) we need to present both the
        frame name and the sheet name. So we might as well return the
        sheet in place of the dialog so that the default code can do
        its thing.
        """

        if not self._script.isSpreadSheetCell(obj):
            return script_utilities.Utilities.frameAndDialog(self, obj)

        results = [None, None]

        parent = obj.parent
        while parent and (parent.parent != parent):
            if parent.getRole() == pyatspi.ROLE_FRAME:
                results[0] = parent
            if parent.getRole() == pyatspi.ROLE_TABLE:
                results[1] = parent
            parent = parent.parent

        return results

    def isFunctionalDialog(self, obj):
        """Returns true if the window is functioning as a dialog."""

        # The OOo Navigator window looks like a dialog, acts like a
        # dialog, and loses focus requiring the user to know that it's
        # there and needs Alt+F6ing into.  But officially it's a normal
        # window.

        # There doesn't seem to be (an efficient) top-down equivalent
        # of utilities.hasMatchingHierarchy(). But OOo documents have
        # root panes; this thing does not.
        #
        rolesList = [pyatspi.ROLE_FRAME,
                     pyatspi.ROLE_PANEL,
                     pyatspi.ROLE_PANEL,
                     pyatspi.ROLE_TOOL_BAR,
                     pyatspi.ROLE_PUSH_BUTTON]

        if obj.getRole() != rolesList[0]:
            # We might be looking at the child.
            #
            rolesList.pop(0)

        while obj and obj.childCount and len(rolesList):
            if obj.getRole() != rolesList.pop(0):
                return False
            obj = obj[0]

        return True

    def validParent(self, obj):
        """Returns the first valid parent/ancestor of obj. We need to do
        this in some applications and toolkits due to bogus hierarchies.

        See bugs:
        http://www.openoffice.org/issues/show_bug.cgi?id=78117
        http://bugzilla.gnome.org/show_bug.cgi?id=489490

        Arguments:
        - obj: the Accessible object
        """

        parent = obj.parent
        if parent and parent.getRole() in (pyatspi.ROLE_ROOT_PANE,
                                           pyatspi.ROLE_DIALOG):
            app = obj.getApplication()
            for frame in app:
                if frame.childCount < 1 \
                   or frame[0].getRole() not in (pyatspi.ROLE_ROOT_PANE,
                                                 pyatspi.ROLE_OPTION_PANE):
                    continue

                root_pane = frame[0]
                if obj in root_pane:
                    return root_pane

        return parent

    #########################################################################
    #                                                                       #
    # Impress-Specific Utilities                                            #
    #                                                                       #
    #########################################################################

    def drawingView(self, obj=orca_state.locusOfFocus):
        """Attempts to locate the Impress drawing view, which is the
        area in which slide editing occurs."""

        # TODO - JD: We should probably add this to the generatorCache.
        #
        docFrames = self.descendantsWithRole(
            self.topLevelObject(obj), pyatspi.ROLE_DOCUMENT_FRAME)
        docFrame = filter(lambda o: ":" in o.name and "/" in o.name, docFrames)
        if docFrame:
            return docFrame[0]

        return None

    def isDrawingView(self, obj):
        """Returns True if obj is the Impress Drawing View."""

        if obj and obj.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
            return (":" in obj.name and "/" in obj.name)

        return False

    def isInImpress(self, obj=orca_state.locusOfFocus):
        """Returns True if obj is in OOo Impress."""

        # Having checked English, Spanish, and Arabic, it would seem
        # that the Frame name will end with "Impress", unlocalized.
        #
        if obj:
            topLevel = self.topLevelObject(obj)
            if topLevel and topLevel.name.endswith("Impress"):
                return True

        return False

    def slideAndTaskPanes(self, obj=orca_state.locusOfFocus):
        """Attempts to locate the Impress slide pane and task pane."""

        drawingView = self.drawingView(obj)
        if not drawingView:
            return None, None

        parent = drawingView.parent
        if parent:
            parent = parent.parent
        if not parent:
            return None, None

        panes = self.descendantsWithRole(parent, pyatspi.ROLE_SPLIT_PANE)
        if not panes:
            return None, None

        slidePane = taskPane = None
        if self.descendantsWithRole(panes[0], pyatspi.ROLE_DOCUMENT_FRAME):
            slidePane = panes[0]
            if len(panes) == 2:
                taskPane = panes[1]
        else:
            taskPane = panes[0]
            if len(panes) == 2:
                slidePane = panes[1]

        return slidePane, taskPane

    def slideTitleAndPosition(self, obj):
        """Attempts to obtain the title, position of the slide which contains
        or is represented by obj.

        Returns a (title, position, count) tuple.
        """

        if obj.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
            dv = obj
        else:
            dv = self.ancestorWithRole(obj, [pyatspi.ROLE_DOCUMENT_FRAME], [])

        if not dv or not self.isDrawingView(dv):
            return "", 0, 0

        positionAndCount = dv.name.split(":")[1]
        position, count = positionAndCount.split("/")
        title = ""
        for child in dv:
            if not child.childCount:
                continue
            # We want an actual Title.
            #
            if child.name.startswith("ImpressTitle"):
                title = self.displayedText(child[0])
                break
            # But we'll live with a Subtitle if we can't find a title.
            # Unlike Titles, a single subtitle can be made up of multiple
            # accessibles.
            #
            elif child.name.startswith("ImpressSubtitle"):
                for line in child:
                    title = self.appendString(title, self.displayedText(line))

        return title, int(position), int(count)

    #########################################################################
    #                                                                       #
    # Utilities for working with the accessible text interface              #
    #                                                                       #
    #########################################################################

    def isWordMisspelled(self, obj, offset):
        """Identifies if the current word is flagged as misspelled by the
        application.

        Arguments:
        - obj: An accessible which implements the accessible text interface.
        - offset: Offset in the accessible's text for which to retrieve the
          attributes.

        Returns True if the word is flagged as misspelled.
        """

        attributes, start, end  = self.textAttributes(obj, offset, True)
        error = attributes.get("text-spelling")

        return error == "misspelled"

    def substring(self, obj, startOffset, endOffset):
        """Returns the substring of the given object's text specialization.

        NOTE: This is here to handle the problematic implementation of
        getText by OpenOffice.  See the bug discussion at:

           http://bugzilla.gnome.org/show_bug.cgi?id=356425)

        Once the OpenOffice issue has been resolved, this method probably
        should be removed.

        Arguments:
        - obj: an accessible supporting the accessible text specialization
        - startOffset: the starting character position
        - endOffset: the ending character position
        """

        text = obj.queryText().getText(0, -1).decode("UTF-8")
        if startOffset >= len(text):
            startOffset = len(text) - 1
        if endOffset == -1:
            endOffset = len(text)
        elif startOffset >= endOffset:
            endOffset = startOffset + 1
        string = text[max(0, startOffset):min(len(text), endOffset)]
        string = string.encode("UTF-8")

        return string

    def textAttributes(self, acc, offset, get_defaults=False):
        """Get the text attributes run for a given offset in a given accessible

        Arguments:
        - acc: An accessible.
        - offset: Offset in the accessible's text for which to retrieve the
        attributes.
        - get_defaults: Get the default attributes as well as the unique ones.
        Default is True

        Returns a dictionary of attributes, a start offset where the attributes
        begin, and an end offset. Returns ({}, 0, 0) if the accessible does not
        supprt the text attribute.
        """
        rv, start, end = script_utilities.Utilities.\
            textAttributes(self, acc, offset, get_defaults)

        # If there are no text attributes associated with the text at a
        # given offset, we might get some seriously bogus offsets, in
        # particular, an extremely large start offset and an extremely
        # large, but negative end offset. As a result, any text attributes
        # which are present on the line after the specified offset will
        # not be indicated by braille.py's getAttributeMask. Therefore,
        # we'll set the start offset to the character being examined,
        # and the end offset to the next character.
        #
        start = min(start, offset)
        if end < 0:
            debug.println(debug.LEVEL_WARNING,
                "soffice.script.py:getTextAttributes: detected a bogus " +
                "end offset. Start offset: %s, end offset: %s" % (start, end))
            end = offset + 1

        return rv, start, end

    #########################################################################
    #                                                                       #
    # Miscellaneous Utilities                                               #
    #                                                                       #
    #########################################################################

    def isAutoTextEvent(self, event):
        """Returns True if event is associated with text being autocompleted
        or autoinserted or autocorrected or autosomethingelsed.

        Arguments:
        - event: the accessible event being examined
        """

        if event.source.getRole() != pyatspi.ROLE_PARAGRAPH:
            return False

        lastKey, mods = self.lastKeyAndModifiers()
        if event.type.startswith("object:text-changed:insert"):
            if not event.any_data:
                return False

            if lastKey == "Tab" and event.any_data != "\t":
                return True

            if lastKey in ["BackSpace", "ISO_Left_Tab"]:
                return True

        if event.type.startswith("focus:"):
            if lastKey == "Return":
                try:
                    charCount = event.source.queryText().characterCount
                except:
                    charCount = 0
                return charCount > 0

        return False
