# Orca
#
# Copyright 2004-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.

"""Provides the FocusTrackingPresenter for Orca."""

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

import gobject
import Queue
import threading
import time

import pyatspi
import braille
import default
import debug
import input_event
import orca
import orca_state
import presentation_manager
import settings
import speech

from orca_i18n import _ # for gettext support

class FocusTrackingPresenter(presentation_manager.PresentationManager):
    """Maintain a set of scripts for all running applications, and
    also keeps the notion of an activeScript.  All object events are
    passed to the associated script for that application, regardless if
    the application has keyboard focus or not.  All keyboard events are
    passed to the active script only if it has indicated interest in the
    event."""

    def __init__(self):

        presentation_manager.PresentationManager.__init__(self)

        # Dictionary that keeps count of event listeners by type.
        # This is a bit convoluted for now, but what happens is that
        # scripts tell the FocusTrackingPresenter to listen for
        # object events based on what they want, and the
        # FocusTrackingPresenter then eventually passes them to the
        # script.  Because both the FocusTrackingPresenter and scripts
        # are interested in object events, and the FocusTrackingPresenter
        # is what delves them out, we keep at most one listener to avoid
        # receiving the same event twice in a row.
        #
        self.registry        = pyatspi.Registry
        self._knownScripts   = {}
        self._knownAppSettings = {}
        self._oldAppSettings = None
        self._eventQueue     = Queue.Queue(0)
        self._gidleId        = 0
        self._gidleLock      = threading.Lock()
        self.noFocusTimestamp = 0.0

        self.setActiveScript(None, "__init__")

        # Initialize variable to make pylint happy.
        #
        self._defaultScript = None
        self._appStateInfo = None
        self._listenerCounts = None

        if settings.listenAllEvents:
            # Just listen to everything, always.
            self._registerAllEvents()

        if settings.debugEventQueue:
            self._enqueueEventCount = 0
            self._dequeueEventCount = 0

    ########################################################################
    #                                                                      #
    # METHODS FOR KEEPING TRACK OF LISTENERS REGISTERED WITH ATSPI         #
    #                                                                      #
    ########################################################################

    def _registerEventListener(self, eventType):
        """Tells this module to listen for the given event type.

        Arguments:
        - eventType: the event type.
        """

        if eventType in self._listenerCounts:
            self._listenerCounts[eventType] += 1
        else:
            self.registry.registerEventListener(self._enqueueEvent,
                                                eventType)
            self._listenerCounts[eventType] = 1

    def _deregisterEventListener(self, eventType):
        """Tells this module to stop listening for the given event type.

        Arguments:
        - eventType: the event type.
        """

        self._listenerCounts[eventType] -= 1
        if self._listenerCounts[eventType] == 0:
            self.registry.deregisterEventListener(self._enqueueEvent,
                                                  eventType)
            del self._listenerCounts[eventType]

    def _registerAllEvents(self):
        """Register all top-level event types except for "mouse".
        """
        topLevelEvents = filter(lambda et: ':' not in et and 'mouse' not in et,
                                pyatspi.EVENT_TREE)
        for event_type in topLevelEvents:
            self.registry.registerEventListener(
                self._enqueueEvent, event_type)

    def _deregisterAllEvents(self):
        topLevelEvents = filter(lambda et: ':' not in et and 'mouse' not in et,
                                pyatspi.EVENT_TREE)
        for event_type in topLevelEvents:
            self.registry.deregisterEventListener(
                self._enqueueEvent, event_type)

    def _registerEventListeners(self, script):
        """Tells the FocusTrackingPresenter to listen for all
        the event types of interest to the script.

        Arguments:
        - script: the script.
        """

        if settings.listenAllEvents:
            # We are always listening to everything.
            return

        for eventType in script.listeners.keys():
            self._registerEventListener(eventType)

    def _deregisterEventListeners(self, script):
        """Tells the FocusTrackingPresenter to stop listening for all the
        event types of interest to the script.

        Arguments:
        - script: the script.
        """
        if settings.listenAllEvents:
            # We are always listening to everything.
            return

        for eventType in script.listeners.keys():
            self._deregisterEventListener(eventType)

    ########################################################################
    #                                                                      #
    # METHODS FOR KEEPING TRACK OF KNOWN SCRIPTS.                          #
    #                                                                      #
    ########################################################################

    def _createScriptForToolkit(self, app, toolkitName):
        """For the given toolkit name, create a new script instance.

        Arguments:
        - app: the Python app
        - toolkitName: The name of the object or application toolkit

        Returns an instance of a Script if possible
        """

        script = None
        scriptPackages = settings.scriptPackages
        for package in scriptPackages:
            if package:
                name = '.'.join((package, toolkitName))
            else:
                name = toolkitName
            try:
                debug.println(
                    debug.LEVEL_FINE,
                    "Looking for toolkit script %s.py..." % toolkitName)
                module = __import__(name,
                                    globals(),
                                    locals(),
                                    [''])
                script = module.Script(app)
                debug.println(debug.LEVEL_FINE,
                              "...found %s.py" % name)
            except ImportError:
                debug.println(
                    debug.LEVEL_FINE,
                    "...could not find %s.py" % toolkitName)
            except:
                debug.printException(debug.LEVEL_SEVERE)
                debug.println(
                    debug.LEVEL_SEVERE,
                    "While attempting to import %s" % toolkitName)

        return script

    # The cache of the currently known scripts.  The key is the Python
    # Accessible application, and the value is the script for that app.
    #

    def _createScript(self, app, obj=None):
        """For the given application name, create a new script instance.
        We'll first see if a mapping from appName to module name exists.
        If it does, we use that.  If it doesn't, we try the app name.
        If all else fails, we fall back to the default script.
        """

        script = None
        objToolkit = self._toolkitForObject(obj)

        if settings.enableCustomScripts and not objToolkit:
            # Look for custom scripts first.
            #
            # We'll use the LEVEL_FINEST level for debug output here as
            # it really isn't an error if the script cannot be found.
            # But, sometimes a script cannot be "found" because it has
            # a syntax error in it, so we want to give script writers
            # a vehicle for debugging these types of things.
            #
            scriptPackages = settings.scriptPackages

            moduleName = settings.getScriptModuleName(app)
            module = None

            if moduleName and len(moduleName):
                for package in scriptPackages:
                    if len(package):
                        name = package + "." + moduleName
                    else:
                        name = moduleName
                    try:
                        debug.println(debug.LEVEL_FINEST,
                                      "Looking for script at %s.py..." % name)
                        module = __import__(name,
                                            globals(),
                                            locals(),
                                            [''])
                        debug.println(debug.LEVEL_FINEST,
                                      "...found %s.py" % name)
                        break
                    except ImportError:
                        debug.println(debug.LEVEL_FINEST,
                                      "...could not find %s.py" % name)
                    except:
                        debug.printException(debug.LEVEL_SEVERE)
                        debug.println(debug.LEVEL_SEVERE,
                                      "While attempting to import %s" % name)
            if module:
                try:
                    script = module.Script(app)
                except:
                    # We do not want the getScript method to fail.  If it does,
                    # we want to let the script developer know what went wrong,
                    # but we also want to move along without crashing Orca.
                    #
                    debug.printException(debug.LEVEL_SEVERE)

        if not script and app and objToolkit:
            script = self._createScriptForToolkit(app, objToolkit)

        # If there is no custom script for an application, try seeing if
        # there is a script for the toolkit of the application.  If there
        # is, then try to use it.  If there isn't, then fall back to the
        # default script. Note that this search is restricted to the "orca"
        # package for now.
        #
        if (not script) and app and getattr(app, "toolkitName", None):
            script = self._createScriptForToolkit(app, app.toolkitName)

        if not script:
            script = default.Script(app)

        return script

    def getScript(self, app, obj=None):
        """Get a script for an app (and make it if necessary).  This is used
        instead of a simple calls to Script's constructor.

        Arguments:
        - app: the Python app

        Returns an instance of a Script.
        """

        script = None

        objToolkit = self._toolkitForObject(obj)
        if objToolkit:
            script = self._knownScripts.get(objToolkit)
            if not script:
                script = self._createScriptForToolkit(app, objToolkit)
                if script:
                    self._knownScripts[objToolkit] = script
                    self._registerEventListeners(script)
            if script:
                return script

        # We might not know what the app is.  In this case, just defer
        # to the default script for support.
        #
        if not app:
            if not self._defaultScript:
                self._defaultScript = default.Script(None)
                self._registerEventListeners(self._defaultScript)
            script = self._defaultScript
        elif app in self._knownScripts:
            script = self._knownScripts[app]
        else:
            script = self._createScript(app, obj)
            self._knownScripts[app] = script
            self._registerEventListeners(script)

        return script

    def setActiveScript(self, newScript, reason=None):
        """Set the new active script.

        Arguments:
        - newScript: the new script to be made active.
        """

        try:
            # If old ("factory") settings don't exist yet, save
            # a set, else restore the old application settings.
            #
            if not self._oldAppSettings:
                self._oldAppSettings = \
                    orca_state.activeScript.saveOldAppSettings()
            else:
                orca_state.activeScript.restoreOldAppSettings( \
                    self._oldAppSettings)

            orca_state.activeScript.deactivate()
        except:
            pass

        orca_state.activeScript = newScript

        try:
            orca_state.activeScript.activate()
        except:
            pass

        if orca_state.activeScript:
            debug.println(debug.LEVEL_FINE, "ACTIVE SCRIPT: %s (reason=%s)" \
                          % (orca_state.activeScript.name, reason))

    def _cleanupCache(self):
        """Looks for defunct accessible objects in the cache and removes them.
        """

        objectsRemoved = 0
        debug.println(debug.LEVEL_FINEST,
                      "_cleanupCache: %d objects removed." % objectsRemoved)

    def _cleanupGarbage(self):
        """Cleans up garbage on the heap."""
        import gc
        gc.collect()
        for obj in gc.garbage:
            try:
                if isinstance(obj, pyatspi.Accessibility.Accessible):
                    gc.garbage.remove(obj)
                    obj.__del__()
            except:
                pass

    def _reclaimScripts(self):
        """Compares the list of known scripts to the list of known apps,
        deleting any scripts as necessary.
        """

        # Sometimes the desktop can become unavailable.  This happens
        # often when synaptic is used to load new modules (see the bug
        # report http://bugzilla.gnome.org/show_bug.cgi?id=342022).
        # So...if this happens, we'll just move along.  The next
        # successful call to _reclaimScripts will reclaim anything we
        # didn't reclaim this time.
        #
        try:
            desktop = self.registry.getDesktop(0)

            for app in self._knownScripts.keys():
                if app not in desktop:
                    script = self._knownScripts[app]
                    self._deregisterEventListeners(script)

                    # Provide a bunch of hints to the garbage collector
                    # that we just don't care about this stuff any longer.
                    # Note the "app.app = None" - that helps remove a
                    # cycle of the application referring to itself.
                    #
                    del self._knownScripts[app]
                    del app
                    del script
        except:
            debug.printException(debug.LEVEL_FINEST)

    def _toolkitForObject(self, obj):
        """Returns the name of the toolkit associated with obj, if it is
        specified."""

        name = ''
        if obj:
            attrs = dict([attr.split(':', 1) for attr in obj.getAttributes()])
            name = attrs.get('toolkit', '')

        return name

    ########################################################################
    #                                                                      #
    # METHODS FOR KEEPING TRACK OF APPLICATION SETTINGS.                   #
    #                                                                      #
    ########################################################################

    def loadAppSettings(self, script):
        """Load the users application specific settings for an app.

        Arguments:
        - script: the current active script.
        """

        app = script.app
        settingsPackages = settings.settingsPackages
        moduleName = settings.getScriptModuleName(app)

        if moduleName and len(moduleName):
            for package in settingsPackages:
                if len(package):
                    name = package + "." + moduleName
                else:
                    name = moduleName
                try:
                    debug.println(debug.LEVEL_FINEST,
                                  "Looking for settings at %s.py..." % name)
                    if name not in self._knownAppSettings:
                        self._knownAppSettings[name] = \
                            __import__(name, globals(), locals(), [''])
                    reload(self._knownAppSettings[name])
                    debug.println(debug.LEVEL_FINEST,
                                  "...found %s.py" % name)

                    # Setup the user's application specific key bindings.
                    # (if any).
                    #
                    if hasattr(self._knownAppSettings[name],
                               "overrideAppKeyBindings"):
                        script.overrideAppKeyBindings = \
                            self._knownAppSettings[name].overrideAppKeyBindings
                        script.keyBindings = \
                         self._knownAppSettings[name].overrideAppKeyBindings( \
                            script, script.keyBindings)

                    # Setup the user's application specific pronunciations
                    # (if any).
                    #
                    if hasattr(self._knownAppSettings[name],
                               "overridePronunciations"):
                        script.overridePronunciations = \
                            self._knownAppSettings[name].overridePronunciations
                        script.app_pronunciation_dict = \
                         self._knownAppSettings[name].overridePronunciations( \
                            script, script.app_pronunciation_dict)

                    break
                except ImportError:
                    debug.println(debug.LEVEL_FINEST,
                                  "...could not find %s.py" % name)

    ########################################################################
    #                                                                      #
    # METHODS FOR PRE-PROCESSING AND MASSAGING AT-SPI OBJECT EVENTS        #
    # for processing by the rest of Orca.                                  #
    #                                                                      #
    ########################################################################

    def _processKeyboardEvent(self, keyboardEvent):
        """Processes the given keyboard event based on the keybinding from the
        currently active script.

        Arguments:
        - keyboardEvent: an instance of input_event.KeyboardEvent
        """
        if orca_state.activeScript:
            try:
                orca_state.activeScript.processKeyboardEvent(keyboardEvent)
            except:
                debug.printException(debug.LEVEL_WARNING)
                debug.printStack(debug.LEVEL_WARNING)

    def _processBrailleEvent(self, brailleEvent):
        """Called whenever a cursor key is pressed on the Braille display.

        Arguments:
        - brailleEvent: an instance of input_event.BrailleEvent
        """
        if orca_state.activeScript:
            try:
                orca_state.activeScript.processBrailleEvent(brailleEvent)
            except:
                debug.printException(debug.LEVEL_WARNING)
                debug.printStack(debug.LEVEL_WARNING)

    def _processObjectEvent(self, event):
        """Handles all events destined for scripts.

        Arguments:
        - e: an at-spi event.
        """

        debug.printObjectEvent(debug.LEVEL_FINEST, event)

        # [[[TODO: WDW - HACK to prevent gnome-panel from killing
        # itself.  It seems to do so after it issues some tool tip
        # events and Orca attempts to process them.  We're not really
        # doing anything with tool tips right now, so we just ignore
        # them.  Note that this is just a bandaid to the problem.  We
        # should do something better.  Please refer to bug 368626
        # http://bugzilla.gnome.org/show_bug.cgi?id=368626 to follow
        # this problem.]]]
        #
        try:
            if event.source.getRole() == pyatspi.ROLE_TOOL_TIP:
                if settings.presentToolTips:
                    pass
                elif isinstance(orca_state.lastInputEvent, \
                                input_event.KeyboardEvent) \
                     and orca_state.lastNonModifierKeyEvent.event_string \
                         == "F1":
                    # Always present tooltips initiated by the user
                    # pressing Control-F1 on the keyboard.  Mouse move
                    # events don't update orca_state.lastInputEvents,
                    # however, so it's possible the user accidentally
                    # nudged the mouse after pressing F1 and generated
                    # another tooltip event. If the current time minus
                    # the last keyboard event time is greater than 0.5
                    # seconds, than just ignore this tooltip event.
                    #
                    if (time.time() - orca_state.lastInputEvent.time) > 0.5:
                        return
                else:
                    return
        except:
            pass

        # Reclaim (delete) any scripts when desktop children go away.
        # The idea here is that a desktop child is an app. We also
        # generally do not like object:children-changed:remove events,
        # either.
        #
        if event.type.startswith("object:children-changed:remove"):
            if event.source == self.registry.getDesktop(0):
                self._reclaimScripts()
                self._cleanupCache()
                if settings.debugMemoryUsage:
                    self._cleanupGarbage()
            return

        # Clean up any flat review context so that Orca does not get
        # confused (see bgo#609633)
        #
        if event.type.startswith("window:deactivate") \
           and orca_state.activeScript \
           and orca_state.activeScript.flatReviewContext \
           and orca_state.activeScript.app == event.host_application:
            orca_state.activeScript.drawOutline(-1, 0, 0, 0)
            orca_state.activeScript.flatReviewContext = None

        try:
            # We don't want to touch a defunct object.  It's useless and it
            # can cause hangs.
            #
            if event.source:
                state = event.source.getState()
                if state.contains(pyatspi.STATE_DEFUNCT):
                    debug.println(debug.LEVEL_FINEST,
                                  "IGNORING DEFUNCT OBJECT")
                    if event.type.startswith("window:deactivate"):
                        orca.setLocusOfFocus(event, None)
                        orca_state.activeWindow = None
                    return

            if (not debug.eventDebugFilter) \
                or (debug.eventDebugFilter \
                    and debug.eventDebugFilter.match(event.type)):
                if not event.type.startswith("mouse:"):
                    debug.printDetails(debug.LEVEL_FINEST, "    ", event.source)

        except LookupError:
            if event.type.startswith("window:deactivate"):
                debug.println(debug.LEVEL_FINEST,
                              "LookupError in deactivate event; clearing focus")
                orca.setLocusOfFocus(event, None)
                orca_state.activeWindow = None
                return
            debug.printException(debug.LEVEL_WARNING)
            debug.println(debug.LEVEL_WARNING,
                          "LookupError above while processing event: %s" %\
                          event.type)
            return
        except:
            debug.printException(debug.LEVEL_WARNING)
            return

        if not event.source:
            debug.println(debug.LEVEL_WARNING,
                          "ERROR: received an event with no source.")
            return

        # We can sometimes get COMM_FAILURES even if the object has not
        # gone away.  This happens a lot with the Java access bridge.
        # So...we will try a few times before giving up.
        #
        # [[[TODO: WDW - might want to consider re-introducing the reload
        # feature of scripts somewhere around here.  I pulled it out as
        # part of the big refactor to make scripts object-oriented. Logged
        # as bugzilla bug 319777.]]]
        #
        retryCount = 0
        oldLocusOfFocus = orca_state.locusOfFocus
        try:
            # If we've received a mouse event, then don't try to get
            # event.source.getApplication() because the top most parent
            # is of role unknown, which will cause an ERROR message to be
            # displayed. See Orca bug #409731 for more details.
            #
            if not event.type.startswith("mouse:"):
                app = event.host_application or event.source.getApplication()
                s = self.getScript(app, event.source)
            else:
                s = orca_state.activeScript
            if not s:
                return
        except:
            s = None
            debug.printException(debug.LEVEL_WARNING)
            debug.println(debug.LEVEL_WARNING,
                          "ERROR: received an event, but Script is None")

        while s and retryCount <= s.commFailureAttemptLimit:
            try:
                state = event.source.getState()
                if not state.contains(pyatspi.STATE_ICONIFIED):
                    eType = event.type
                    setNewActiveScript = eType == "window:activate"

                    reason = None
                    if not reason and setNewActiveScript:
                        reason = "window:activate event"

                    # [[[TODO: WDW - HACK we look for frame that get
                    # focus: as a means to detect active scripts
                    # because yelp does this.  Actually, yelp is a bit
                    # odd in that it calls itself 'yelp' then changes
                    # its application name and id to the Gecko toolkit
                    # in use, and then issues a focus: event on the
                    # main window, which is a frame.]]]
                    #
                    setNewActiveScript = setNewActiveScript \
                        or (eType.startswith("focus") \
                            and (event.source.getRole() == pyatspi.ROLE_FRAME))

                    if not reason and setNewActiveScript:
                        reason = "frame received focus"

                    # Added in a further check. We look for modal panels
                    # that are now showing (such as gnome-screensaver-dialog).
                    # See bug #530368 for more details.
                    #
                    setNewActiveScript = setNewActiveScript \
                        or (eType.startswith("object:state-changed:showing")
                            and (event.source.getRole() == pyatspi.ROLE_PANEL)
                            and state.contains(pyatspi.STATE_MODAL))

                    if not reason and setNewActiveScript:
                        reason = "modal panel is showing"

                    # Also, we might be running into a gnome-panel
                    # applet, which is indicated by a host application
                    # with no children.  See bug #536985.
                    #
                    #setNewActiveScript = setNewActiveScript \
                    #    or (event.host_application \
                    #        and len(event.host_application) == 0 \
                    #        and orca_state.activeScript \
                    #        and (orca_state.activeScript.app \
                    #             != event.host_application))
                    #
                    #if not reason and setNewActiveScript:
                    #    reason = "bizarre applet behavior"

                    # Or, we might just be getting a focus event.  In this
                    # case, assume the window has focus and we missed an
                    # event for it somehow.
                    #
                    if not setNewActiveScript:
                        if eType.startswith("focus") \
                           or (eType.startswith("object:state-changed:focused")\
                               and event.detail1):
                            setNewActiveScript = \
                                orca_state.activeScript \
                                and event.host_application \
                                and (orca_state.activeScript.app \
                                     != event.host_application)

                    # One last check -- let's make sure the new script
                    # thinks it should become active.
                    #
                    if setNewActiveScript:
                        app = event.host_application \
                            or event.source.getApplication()
                        theScript = self.getScript(app, event.source)
                        setNewActiveScript = theScript.isActivatableEvent(event)
                        if not reason and setNewActiveScript:
                            reason = "script requested it"

                    if not reason and setNewActiveScript:
                        reason = "object received focus"

                    if setNewActiveScript:
                        # We'll let someone else decide if it's important
                        # to stop speech or not.
                        #speech.stop()

                        app = event.host_application \
                            or event.source.getApplication()
                        theScript = self.getScript(app, event.source)
                        self.setActiveScript(theScript, reason)

                        # Load in the application specific settings for the
                        # app for this event (if found).
                        #
                        self.loadAppSettings(orca_state.activeScript)

                        # Tell BrlTTY which commands we care about.
                        #
                        braille.setupKeyRanges(\
                            orca_state.activeScript.brailleBindings.keys())
                    s.processObjectEvent(event)
                    if retryCount:
                        debug.println(debug.LEVEL_WARNING,
                                      "  SUCCEEDED AFTER %d TRIES" % retryCount)
                break
            except LookupError:
                debug.printException(debug.LEVEL_WARNING)
                debug.println(debug.LEVEL_WARNING,
                              "LookupError above while processing: %s" % \
                              event.type)
                retryCount += 1
                if retryCount <= s.commFailureAttemptLimit:
                    # We want the old locus of focus to be reset so
                    # the proper stuff will be spoken if the locus
                    # of focus changed during our last attempt at
                    # handling this event.
                    #
                    orca_state.locusOfFocus = oldLocusOfFocus
                    debug.println(debug.LEVEL_WARNING,
                                  "  TRYING AGAIN (%d)" % retryCount)
                    time.sleep(s.commFailureWaitTime)
                else:
                    debug.println(debug.LEVEL_WARNING,
                                  "  GIVING UP AFTER %d TRIES" \
                                  % (retryCount - 1))
            except:
                debug.printException(debug.LEVEL_WARNING)
                break

    def _enqueueEvent(self, e):
        """Handles all events destined for scripts.

        Arguments:
        - e: an at-spi event.
        """

        asyncMode = settings.asyncMode

        if settings.debugEventQueue:
            if self._enqueueEventCount:
                debug.println(debug.LEVEL_ALL,
                              "focus_tracking_presenter._enqueueEvent has " \
                              "been entered before exiting (count = %d)" \
                              % self._enqueueEventCount)
            self._enqueueEventCount += 1

        event = None
        if isinstance(e, input_event.KeyboardEvent):
            if e.type == pyatspi.KEY_PRESSED_EVENT:
                debug.println(debug.LEVEL_ALL,
                              "----------> QUEUEING KEYPRESS '%s' (%d)"
                              % (e.event_string, e.hw_code))
            elif e.type == pyatspi.KEY_RELEASED_EVENT:
                debug.println(debug.LEVEL_ALL,
                              "----------> QUEUEING KEYRELEASE '%s' (%d)"
                              % (e.event_string, e.hw_code))
            event = e
        elif isinstance(e, input_event.BrailleEvent):
            debug.println(debug.LEVEL_ALL,
                          "----------> QUEUEING BRAILLE COMMAND %s" \
                          % repr(e.event))
            event = e
        else:
            if e.type in settings.ignoredEventsList:
                if settings.debugEventQueue:
                    self._enqueueEventCount -= 1
                return
            # We ignore defunct objects.
            #
            if e.type.startswith("object:state-changed:defunct"):
                if settings.debugEventQueue:
                    self._enqueueEventCount -= 1
                return

            # We also generally do not like
            # object:property-change:accessible-parent events because
            # they indicate something is now whacked with the
            # hierarchy.
            #
            if e.type.startswith("object:property-change:accessible-parent"):
                if settings.debugEventQueue:
                    self._enqueueEventCount -= 1
                return

            # At this point in time, we only care when objects are
            # removed from the desktop.
            #
            if e.type.startswith("object:children-changed:remove") \
                and (e.source != self.registry.getDesktop(0)):
                if settings.debugEventQueue:
                    self._enqueueEventCount -= 1
                return

            # If the event doesn't have a source or that source is not marked
            # valid, then we don't care about this event. Just return.
            #

            event = e
            if not event.source:
                debug.println(debug.LEVEL_FINEST,
                      "---------> IGNORING INVALID EVENT %s" % e.type)
                if settings.debugEventQueue:
                    self._enqueueEventCount -= 1
                return

            if (not debug.eventDebugFilter) \
                or (debug.eventDebugFilter \
                    and debug.eventDebugFilter.match(e.type)):
                debug.println(debug.LEVEL_ALL,
                              "---------> QUEUEING EVENT %s" % e.type)

            # Some toolkits (e.g., Java - see bug #531869) may need to
            # have their events processed immediately.
            #
            try:
                if len(settings.synchronousToolkits) > 0 and \
                    event.host_application.toolkitName \
                    in settings.synchronousToolkits:
                    asyncMode = False
            except:
                pass

            app = event.host_application or event.source.getApplication()
            script = self.getScript(app, event.source)
            script.eventCache[event.type] = (event, time.time())

        if event:
            if settings.debugEventQueue:
                debug.println(debug.LEVEL_ALL,
                              "           acquiring lock...")
            self._gidleLock.acquire()
            if settings.debugEventQueue:
                debug.println(debug.LEVEL_ALL,
                              "           ...acquired")
                debug.println(debug.LEVEL_ALL,
                              "           calling queue.put...")
                debug.println(debug.LEVEL_ALL,
                              "           (full=%s)" % self._eventQueue.full())
            self._eventQueue.put(event)
            if settings.debugEventQueue:
                debug.println(debug.LEVEL_ALL,
                              "           ...put complete")
            if asyncMode and (not self._gidleId):
                if settings.gilSleepTime:
                    time.sleep(settings.gilSleepTime)
                self._gidleId = gobject.idle_add(self._dequeueEvent)

            if settings.debugEventQueue:
                debug.println(debug.LEVEL_ALL,
                              "           releasing lock...")
            self._gidleLock.release()
            if settings.debugEventQueue:
                debug.println(debug.LEVEL_ALL,
                              "           ...released")

            if not asyncMode:
                self._dequeueEvent()

        if settings.debugEventQueue:
            self._enqueueEventCount -= 1

        # [[[TODO: HACK - on some hangs, we see the event queue growing,
        # but the thread to take the events off is hung.  We might be
        # able to 'recover' by quitting when we see this happen.]]]
        #
        #if self._eventQueue.qsize() > 500:
        #    print "Looks like something has hung.  Here's the threads:"
        #    for someThread in threading.enumerate():
        #        print someThread.getName(), someThread.isAlive()
        #    print "Quitting Orca."
        #    orca.shutdown()

    def _dequeueEvent(self):
        """Handles all events destined for scripts.  Called by the GTK
        idle thread.
        """

        rerun = True

        if settings.debugEventQueue:
            debug.println(debug.LEVEL_ALL,
                          "Entering focus_tracking_presenter._dequeueEvent" \
                          + " %d" % self._dequeueEventCount)
            self._dequeueEventCount += 1

        try:
            event = self._eventQueue.get_nowait()

            if isinstance(event, input_event.KeyboardEvent):
                if event.type == pyatspi.KEY_PRESSED_EVENT:
                    debug.println(debug.LEVEL_ALL,
                                  "DEQUEUED KEYPRESS '%s' (%d) <----------" \
                                  % (event.event_string, event.hw_code))
                    pressRelease = "PRESS"
                elif event.type == pyatspi.KEY_RELEASED_EVENT:
                    debug.println(debug.LEVEL_ALL,
                                  "DEQUEUED KEYRELEASE '%s' (%d) <----------" \
                                  % (event.event_string, event.hw_code))
                    pressRelease = "RELEASE"
                debug.println(debug.eventDebugLevel,
                              "\nvvvvv PROCESS KEY %s EVENT %s vvvvv"\
                              % (pressRelease, event.event_string))
                self._processKeyboardEvent(event)
                debug.println(debug.eventDebugLevel,
                              "\n^^^^^ PROCESS KEY %s EVENT %s ^^^^^"\
                              % (pressRelease, event.event_string))
            elif isinstance(event, input_event.BrailleEvent):
                debug.println(debug.LEVEL_ALL,
                              "DEQUEUED BRAILLE COMMAND %s <----------" \
                              % repr(event.event))
                debug.println(debug.eventDebugLevel,
                              "\nvvvvv PROCESS BRAILLE EVENT %s vvvvv"\
                              % repr(event.event))
                self._processBrailleEvent(event)
                debug.println(debug.eventDebugLevel,
                              "\n^^^^^ PROCESS BRAILLE EVENT %s ^^^^^"\
                              % repr(event.event))
            else:
                if (not debug.eventDebugFilter) \
                    or (debug.eventDebugFilter \
                        and debug.eventDebugFilter.match(event.type)):
                    debug.println(debug.LEVEL_ALL,
                                  "DEQUEUED EVENT %s <----------" \
                                  % event.type)
                    debug.println(debug.eventDebugLevel,
                                  "\nvvvvv PROCESS OBJECT EVENT %s vvvvv" \
                                  % event.type)
                self._processObjectEvent(event)
                if (not debug.eventDebugFilter) \
                    or (debug.eventDebugFilter \
                        and debug.eventDebugFilter.match(event.type)):
                    debug.println(debug.eventDebugLevel,
                                  "^^^^^ PROCESS OBJECT EVENT %s ^^^^^\n" \
                                  % event.type)

            # [[[TODO: HACK - it would seem logical to only do this if we
            # discover the queue is empty, but this inroduces a hang for
            # some reason if done inside an acquire/release block for a
            # lock.  So...we do it here.]]]
            #
            noFocus = (not orca_state.activeScript) \
                      or ((not orca_state.locusOfFocus) \
                          and (self.noFocusTimestamp \
                               != orca_state.noFocusTimestamp))

            self._gidleLock.acquire()
            if self._eventQueue.empty():
                if noFocus:
                    if settings.gilSleepTime:
                        time.sleep(settings.gilSleepTime)
                    # Translators: this is intended to be a short phrase to
                    # speak and braille to tell the user that no component
                    # has keyboard focus.
                    #
                    message = _("No focus")
                    if settings.brailleVerbosityLevel == \
                        settings.VERBOSITY_LEVEL_VERBOSE:
                        braille.displayMessage(message)
                    if settings.speechVerbosityLevel == \
                        settings.VERBOSITY_LEVEL_VERBOSE:
                        speech.speak(message)
                    self.noFocusTimestamp = orca_state.noFocusTimestamp
                self._gidleId = 0
                rerun = False # destroy and don't call again
            self._gidleLock.release()
        except Queue.Empty:
            debug.println(debug.LEVEL_SEVERE,
                          "focus_tracking_presenter:_dequeueEvent: " \
                          + " the event queue is empty!")
            rerun = False # destroy and don't call again
        except:
            debug.printException(debug.LEVEL_SEVERE)

        if settings.debugEventQueue:
            self._dequeueEventCount -= 1
            debug.println(debug.LEVEL_ALL,
                          "Leaving focus_tracking_presenter._dequeueEvent" \
                          + " %d" % self._dequeueEventCount)

        return rerun

    def processKeyboardEvent(self, keyboardEvent):
        """Processes the given keyboard event based on the keybinding from the
        currently active script. This method is called synchronously from the
        at-spi registry and should be performant.  In addition, it must return
        True if it has consumed the event (and False if not).

        Arguments:
        - keyboardEvent: an instance of input_event.KeyboardEvent

        Returns True if the event should be consumed.
        """

        consume = False
        if orca_state.activeScript \
           and orca_state.activeScript.consumesKeyboardEvent(keyboardEvent):
            consume = not orca_state.bypassNextCommand
            if consume:
                self._enqueueEvent(keyboardEvent)

        return consume

    def processBrailleEvent(self, brailleEvent):
        """Called whenever a cursor key is pressed on the Braille display.

        Arguments:
        - brailleEvent: an instance of input_event.BrailleEvent

        Returns True if the command was consumed; otherwise False
        """

        if orca_state.activeScript \
           and orca_state.activeScript.consumesBrailleEvent(brailleEvent):
            self._enqueueEvent(brailleEvent)
            return True
        else:
            return False

    def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
        """Called when the visual object with focus changes.

        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 orca_state.activeScript:
            orca_state.activeScript.locusOfFocusChanged(event,
                                                        oldLocusOfFocus,
                                                        newLocusOfFocus)

    def visualAppearanceChanged(self, event, obj):
        """Called when the visual appearance of an object changes.
        This method should not be called for objects whose visual
        appearance changes solely because of focus -- locusOfFocusChanged
        is used for that.  Instead, it is intended mostly for objects
        whose notional 'value' has changed, such as a checkbox changing
        state, a progress bar advancing, a slider moving, text inserted,
        caret moved, etc.

        Arguments:
        - event: if not None, the Event that caused this to happen
        - obj: the Accessible whose visual appearance changed.
        """

        if orca_state.activeScript:
            orca_state.activeScript.visualAppearanceChanged(event, obj)

    def _saveAppStates(self):
        """Saves script and application state information."""
        self._appStateInfo = []
        for script in self._knownScripts.values():
            self._appStateInfo.append([script.app, script.getAppState()])

    def _restoreAppStates(self):
        """Restores script and application state information."""
        try:
            for [app, appState] in self._appStateInfo:
                script = self.getScript(app)
                script.setAppState(appState)
        except:
            pass

        self._appStateInfo = None

    def activate(self):
        """Called when this presentation manager is activated."""

        self._listenerCounts = {}
        self._knownScripts   = {}
        self._knownAppSettings = {}
        self._oldAppSettings = None
        self._defaultScript  = None

        self._restoreAppStates()

        self.setActiveScript(self.getScript(None), "activate")

        # Tell BrlTTY which commands we care about.
        #
        braille.setupKeyRanges(orca_state.activeScript.brailleBindings.keys())

        self._registerEventListener("window:activate")
        self._registerEventListener("window:deactivate")
        self._registerEventListener("object:children-changed:remove")

        win = orca_state.activeScript.utilities.activeWindow()
        if win:
            # Generate a fake window activation event so the application
            # can tell the user about itself.
            #
            class _FakeEvent:
                def __init__(self, source, eventType,
                             detail1, detail2, any_data):
                    self.source = source
                    self.type = eventType
                    self.detail1 = detail1
                    self.detail2 = detail2
                    self.any_data = any_data
                    self.host_application = None

            class _FakeData:
                def __init__(self):
                    pass
                def value(self):
                    return None

            fe = _FakeEvent(win, "window:activate", 0, 0, _FakeData())
            self._enqueueEvent(fe)

    def deactivate(self):
        """Called when this presentation manager is deactivated."""

        self._saveAppStates()

        for eventType in self._listenerCounts.keys():
            self.registry.deregisterEventListener(self._enqueueEvent,
                                                  eventType)
        self._listenerCounts = {}
        self._knownScripts   = {}
        self._knownAppSettings = {}
        self._oldAppSettings = None
        self._defaultScript  = None

        self.setActiveScript(None, "deactivate")
