view enso/ui/quasimode/__init__.py @ 18:09b7a34603c0

Made a few fixes related to the QUASIMODE_MAX_SUGGESTIONS variable.
author Atul Varma <varmaa@toolness.com>
date Fri, 22 Feb 2008 17:50:37 -0600
parents beba6a8243d6
children 09337777193c
line wrap: on
line source

# Copyright (c) 2008, Humanized, Inc.
# All rights reserved.
# 
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    1. Redistributions of source code must retain the above copyright
#       notice, this list of conditions and the following disclaimer.
#
#    2. Redistributions in binary form must reproduce the above copyright
#       notice, this list of conditions and the following disclaimer in the
#       documentation and/or other materials provided with the distribution.
#
#    3. Neither the name of Enso nor the names of its contributors may
#       be used to endorse or promote products derived from this
#       software without specific prior written permission.
# 
# THIS SOFTWARE IS PROVIDED BY Humanized, Inc. ``AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL Humanized, Inc. BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# ----------------------------------------------------------------------------
#
#   enso.ui.quasimode
#
# ----------------------------------------------------------------------------

"""
    Implements the Quasimode.

    This module implements a singleton class that represents the
    quasimode. It handles all quasimodal key events, and the logic for
    transitioning in and out of the quasimode.  When the quasimode
    terminates, it initiates the execution of the command, if any,
    that the user indicated while in the quasimode.  It also handles
    the various kinds of user "error", which primarily consist of "no command
    matches the text the user typed".
"""

# ----------------------------------------------------------------------------
# Imports
# ----------------------------------------------------------------------------

import weakref

from enso.ui import events
from enso.ui import commands
from enso.ui import messages
from enso import config
from enso import input

from enso.utils.strings import stringRatioBestMatch
from enso.utils.xml_tools import escapeXml
from enso.ui.quasimode.suggestionlist import TheSuggestionList
from enso.ui.quasimode.window import TheQuasimodeWindow

# Import the standard allowed key dictionary, which relates virtual
# key codes to character strings.
from enso.ui.quasimode.charmaps import STANDARD_ALLOWED_KEYCODES \
    as ALLOWED_KEYCODES


# ----------------------------------------------------------------------------
# TheQuasimode
# ----------------------------------------------------------------------------

class _TheQuasimode:
    """
    Encapsulates the command quasimode state and event-handling.

    Future note: In code review, we realized that implementing the
    quasimode is an ideal case for the State pattern; the Quasimode
    singleton would have a private member for quasimode state, which
    would be an instance of one of two classes, InQuasimode or
    OutOfQuasimode, both descended from a QuasimodeState interface
    class.  Consequances of this include much cleaner transition code
    and separation of event handling into the two states.
    """

    def __init__( self ):
        """
        Initialize the quasimode.
        """
        
        # Boolean variable that records whether the quasimode key is
        # currently down, i.e., whether the user is "in the quasimode".
        self._inQuasimode = False

        # The QuasimodeWindow object that is responsible for
        # drawing the quasimode; set to None initially.
        # A QuasimodeWindow object is created at the beginning of
        # the quasimode, and destroyed at the completion of the
        # quasimode.
        self.__quasimodeWindow = None

        # The suggestion list object, which is responsible for
        # maintaining all the information about the auto-completed
        # command and suggested command names, and the text typed
        # by the user.
        self.__suggestionList = TheSuggestionList()

        # Boolean variable that should be set to True whenever an event
        # occurs that requires the quasimode to be redrawn, and which
        # should be set to False when the quasimode is drawn.
        self.__needsRedraw = False

        # Whether the next redraw should redraw the entire quasimodal
        # display, or only the description and user text.
        self.__nextRedrawIsFull = False

        # Register a key event responder, so that the quasimode can
        # actually respond to quasimode events.
        eventMan = events.eventManager
        eventMan.registerResponder( self.onKeyEvent, "key" )

        # Creates new event types that code can subscribe to, to find out
        # when the quasimode (or mode) is started and completed.
        eventMan.createEventType( "startQuasimode" )
        eventMan.createEventType( "endQuasimode" )

        # Read settings from config file: are we modal?
        # What key activates the quasimode?
        self.quasimodeKeycode = config.QUASIMODE_KEYCODE
        self.__isModal = config.IS_QUASIMODE_MODAL

        # Pass these settings on to the low-level C code:
        eventMan.setQuasimodeKeycode( input.KEYCODE_QUASIMODE_START,
                                      self.quasimodeKeycode )
        eventMan.setModality( self.__isModal )

        # Register "enter" and "escape" as exit keys:
        eventMan.setQuasimodeKeycode( input.KEYCODE_QUASIMODE_END,
                                      input.KEYCODE_RETURN )
        eventMan.setQuasimodeKeycode( input.KEYCODE_QUASIMODE_CANCEL,
                                      input.KEYCODE_ESCAPE )


    def getQuasimodeKey( self ):
        return self.quasimodeKeycode

    def setQuasimodeKey( self, keycode ):
        # LONGTERM TODO: make sure 'keycode' is a valid keycode.

        assert type( keycode ) == int

        config.QUASIMODE_KEYCODE = keycode
        
        self.quasimodeKeycode = keycode

        eventMan = events.eventManager
        eventMan.setQuasimodeKeycode( input.KEYCODE_QUASIMODE_START,
                                      keycode )

    def isModal( self ):
        return self.__isModal

    def setModal( self, isModal ):
        assert type( isModal ) == bool
        config.IS_QUASIMODE_MODAL = isModal
        
        self.__isModal = isModal
        eventMan = events.eventManager
        eventMan.setModality( isModal )

    def getSuggestionList( self ):
        return self.__suggestionList

    def onKeyEvent( self, eventType, keyCode ):
        """
        Handles a key event of particular type.
        """

        if eventType == input.EVENT_KEY_QUASIMODE:

            if keyCode == input.KEYCODE_QUASIMODE_START:
                assert not self._inQuasimode
                self.__quasimodeBegin()
            elif keyCode == input.KEYCODE_QUASIMODE_END:
                assert self._inQuasimode
                self.__quasimodeEnd()
            elif keyCode == input.KEYCODE_QUASIMODE_CANCEL:
                self.__suggestionList.clearState()
                self.__quasimodeEnd()

        elif eventType == input.EVENT_KEY_DOWN and self._inQuasimode:
            # The user has typed a character, and we need to redraw the
            # quasimode.
            self.__needsRedraw = True

            if keyCode == input.KEYCODE_TAB:
                self.__suggestionList.autoType()
            elif keyCode == input.KEYCODE_RETURN:
                self.__suggestionList.autoType()
            elif keyCode == input.KEYCODE_ESCAPE:
                self.__suggestionList.clearState()
            elif keyCode == input.KEYCODE_BACK:
                # Backspace has been pressed.
                self.__onBackspace()
            elif keyCode == input.KEYCODE_DOWN:
                # The user has pressed the down arrow; change which of the
                # suggestions is "active" (i.e., will be executed upon
                # termination of the quasimode)
                self.__suggestionList.cycleActiveSuggestion( 1 )
                self.__nextRedrawIsFull = True
            elif keyCode == input.KEYCODE_UP:
                # Up arrow; change which suggestion is active.
                self.__suggestionList.cycleActiveSuggestion( -1 )
                self.__nextRedrawIsFull = True
            elif ALLOWED_KEYCODES.has_key( keyCode ):
                # The user has typed a valid key to add to the userText.
                self.__addUserChar( keyCode )
            else:
                # The user has pressed a key that is not valid.
                pass


    def __addUserChar( self, keyCode ):
        """
        Adds the character corresponding to keyCode to the user text.
        """

        newCharacter = ALLOWED_KEYCODES[keyCode]
        oldUserText = self.__suggestionList.getUserText()
        self.__suggestionList.setUserText( oldUserText + newCharacter )

        # If the user had indicated one of the suggestions, then
        # typing a character snaps the active suggestion back to the
        # user text and auto-completion.
        self.__suggestionList.resetActiveSuggestion()


    def __onBackspace( self ):
        """
        Deletes one character, if possible, from the user text.
        """
        
        oldUserText = self.__suggestionList.getUserText()
        if len( oldUserText ) == 0:
            # There is no user text; backspace does nothing.
            return

        self.__suggestionList.setUserText( oldUserText[:-1] )

        # If the user had indicated anything on the suggestion list,
        # then hitting backspace snaps the active suggestion back to
        # the user text.
        self.__suggestionList.resetActiveSuggestion()
        

    def __quasimodeBegin( self ):
        """
        Executed when user presses the quasimode key.
        """

        assert self._inQuasimode == False

        if self.__quasimodeWindow == None:
            logging.info( "Created a new quasimode window!" )
            self.__quasimodeWindow = TheQuasimodeWindow()

        eventMan = events.eventManager
        eventMan.triggerEvent( "startQuasimode" )
        
        eventMan.registerResponder( self.__onTick, "timer" )

        self._inQuasimode = True
        self.__needsRedraw = True

        # Postcondition
        assert self._inQuasimode == True

    def __onTick( self, timePassed ):
        """
        Timer event responder.  Re-draws the quasimode, if it needs it.
        Only registered while in the quasimode.

        NOTE: Drawing the quasimode takes place in __onTick() for
        performance reasons.  If a user mashed down 10 keys in
        the space of a few milliseconds, and the quasimode was re-drawn
        on every single keystroke, then the quasimode could suddenly
        be lagging behind the user a half a second or more. 
        """

        unusedArgs( timePassed )

        assert self._inQuasimode == True

        if self.__needsRedraw:
            self.__needsRedraw = False
            self.__quasimodeWindow.update( self, self.__nextRedrawIsFull )
            self.__nextRedrawIsFull = False
        else:
            # If the quasimode hasn't changed, then continue drawing
            # any parts of it (such as the suggestion list) that
            # haven't been drawn/updated yet.
            self.__quasimodeWindow.continueDrawing()


    def __quasimodeEnd( self ):
        """
        Executed when user releases the quasimode key.
        """

        # The quasimode has terminated; remove the timer responder
        # function as an event responder.
        eventMan = events.eventManager
        eventMan.triggerEvent( "endQuasimode" )
        eventMan.removeResponder( self.__onTick )

        # LONGTERM TODO: Determine whether deleting or hiding is better.
        logging.info( "Deleting the quasimode window." )

        # Delete the Quasimode window.
        # NOTE: The object referred to by this private variable name is
        # only referred to by this class.  Assuming that calls from the
        # actual QuasimodeWindow object never result in references to
        # that object existing in other objects or modules, then
        # setting self.__quasimodeWindow to None results in the
        # object's reference count going to zero, and the object being
        # destroyed.
        # To verify this, we create a weakref to the window, and check
        # that it returns None.
        tempWindowRef = weakref.ref( self.__quasimodeWindow )
        self.__quasimodeWindow = None
        assert tempWindowRef() == None, "QuasimodeWindow wasn't destroyed!"

        activeCommand = self.__suggestionList.getActiveCommand()
        if activeCommand != None:
            cmdName = self.__suggestionList.getActiveCommandName()
            self.__executeCommand( activeCommand, cmdName )
        elif len( self.__suggestionList.getUserText() ) > 0:
            # The user typed some text, but there was no command match
            self.__showBadCommandMsg( self.__suggestionList.getUserText() )
            
        self._inQuasimode = False
        self.__suggestionList.clearState()

    def __executeCommand( self, cmd, cmdName ):
        """
        Attempts to execute the command.  Catches any errors raised by
        the command code and deals with them appropriately, e.g., by
        launching a bug report, informing the user, etc.

        Commands should deal with user-errors, like lack of selection,
        by displaying messages, etc.  Exceptions should only be raised
        when the command is actually broken, or code that the command
        calls is broken.
        """

        # The following message may be used by system tests.
        logging.info( "COMMAND EXECUTED: %s" % cmdName )
        try:
            cmd.run()
        except Exception:
            # An exception occured during the execution of the command.
            import Logging

            logging.error( "Command \"%s\" failed." % cmdName )
            Logging.logAndRaiseCurrentException()


    def __showBadCommandMsg( self, userText ):
        """
        Displays an error message telling the user that userText does
        not match any command.  Also, if there are any reasonable
        commands that were similar but not matching, offers those to
        the user as suggestions.
        """

        # Generate a caption for the message with a couple suggestions
        # for command names similar to the user's text
        caption = self.__commandSuggestionCaption( escapeXml( userText ) )
        badCmd = userText.lower()
        badCmd = escapeXml( badCmd )
        # Create and display a primary message.
        text = config.BAD_COMMAND_MSG
        text = text % ( badCmd, caption )

        msg = messages.Message( isPrimary = True,
                                isMini = False,
                                fullXml = text )
        msgMan = messages.messageManager
        msgMan.newMessage( msg )
        

    def __commandSuggestionCaption( self, userText ):
        """
        Creates and returns a caption suggesting one or two commands
        that are similar to userText.
        """
        
        # Retrieve one or two command name suggestions.
        cmdMan = commands.commandManager
        suggestions = cmdMan.retrieveSuggestions( userText )
        cmds = [ s.toText() for s in suggestions ]
        if len(cmds) > 0:
            ratioBestMatch = stringRatioBestMatch( userText.lower(), cmds )
            caption = config.ONE_SUGG_CAPTION
            caption = caption % ratioBestMatch
        else:
            # There were no suggestions; so we don't want a caption.
            caption = ""

        return caption


# ----------------------------------------------------------------------------
# Module Initilization
# ----------------------------------------------------------------------------

_quasimode = None

def init():
    global _quasimode

    logging.info( "Initing the Quasimode." )
    _quasimode = _TheQuasimode()

def shutdown():
    global _quasimode

    logging.info( "Shutting down the Quasimode." )
    _quasimode = None

def get():
    return _quasimode