Mercurial > enso_core
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