Mercurial > enso_core
changeset 8:5283b9fedbbe
Added enso.ui.quasimode.suggestionlist, and moved some more constants into enso.config.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Fri, 22 Feb 2008 07:17:15 -0600 |
parents | 119f063771bc |
children | 1d38d095bd32 |
files | enso/config.py enso/ui/quasimode/suggestionlist.py enso/ui/quasimode/window.py |
diffstat | 3 files changed, 417 insertions(+), 10 deletions(-) [+] |
line wrap: on
line diff
--- a/enso/config.py Fri Feb 22 06:56:58 2008 -0600 +++ b/enso/config.py Fri Feb 22 07:17:15 2008 -0600 @@ -2,11 +2,26 @@ # localization into account too (or we can make a separate module for # such strings). +# The keycode for the quasimode key. +# TODO: Where are keycodes specified? QUASIMODE_KEYCODE = 1 +# Whether the Quasimode is actually modal ("sticky"). IS_QUASIMODE_MODAL = False -OPENING_MSG_XML = "<p>Welcome to Enso.</p>" +# Amount of time, in seconds (float), to wait from the time +# that the quasimode begins drawing to the time that the +# suggestion list begins to be displayed. Setting this to a +# value greater than 0 will effectively create a +# "spring-loaded suggestion list" behavior. +QUASIMODE_SUGGESTION_DELAY = 0.2 + +# The maximum number of suggestions to display in the quasimode. +QUASIMODE_MAX_SUGGESTIONS = 6 + +# The minimum number of characters the user must type before the +# auto-completion mechanism engages. +QUASIMODE_MIN_AUTOCOMPLETE_CHARS = 1 # The message displayed when the user types some text that is not a command. BAD_COMMAND_MSG = "<p><command>%s</command> is not a command.</p>"\ @@ -15,3 +30,16 @@ # The captions for the above message, indicating commands that are related # to the command the user typed. ONE_SUGG_CAPTION = "<caption>Did you mean <command>%s</command>?</caption>" + +# The string that is displayed in the quasimode window when the user +# first enters the quasimode. +QUASIMODE_DEFAULT_HELP = u"Welcome to Enso! Enter a command, " \ + u"or type \u201chelp\u201d for assistance." + +# The string displayed when the user has typed some characters but there +# is no matching command. +QUASIMODE_NO_COMMAND_HELP = "There is no matching command. "\ + "Use backspace to delete characters." + +# Message XML for the Splash message shown when Enso first loads. +OPENING_MSG_XML = "<p>Welcome to Enso.</p>"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/enso/ui/quasimode/suggestionlist.py Fri Feb 22 07:17:15 2008 -0600 @@ -0,0 +1,385 @@ +# 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.suggestionlist +# +# ---------------------------------------------------------------------------- + +""" + Implements a SuggestionList to keep track of auto-completions, + suggestions, and other data related to typing in the quasimode. +""" + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +from enso.ui import commands +from enso.ui.commands.suggestions import AutoCompletion +from enso import config + + +# ---------------------------------------------------------------------------- +# The SuggestionList Singleton +# ---------------------------------------------------------------------------- + +class TheSuggestionList: + """ + A singleton class that encapsulates all of the textual information + created when a user types in the quasimode, including the user's + typed text, the auto-completion, any suggestions, and the command + description/help text. + """ + + # LONGTERM TODO: The trio of main data elements: + # ( __autoCompletion, __suggestions, __activeIndex ) + # These should never be updated except together; and they + # should never be accessed unless updated. Right now, this + # involves a nasty, hard-to-maintain cludge of an "update" + # mechanism. + # This class should be a new-style class, and these attributes + # should be properties whose getters appropriately update them. + # This eliminates the burden on client code to remember to call + # update near/around fetching these attributes, and will eliminate + # a source of errors. + + def __init__( self ): + """ + Initializes the SuggestionList. + """ + + # Set all of the member variables to their empty values. + self.clearState() + + + def clearState( self ): + """ + Clears all of the variables relating to the state of the + quasimode's generated information. + """ + + # The "user text". Together with the active index, constitutes + # the "source" information, i.e., the information from which + # all the rest is calculated. + self.__userText = "" + + # An index of the above suggestion list indicating which + # command name the user has indicated. + self.__activeIndex = 0 + + # The current auto-completion object. + self.__autoCompletion = AutoCompletion( originalText = "", + suggestedText = "" ) + + # The current list of suggestions. The 0th element is the + # auto-completion. + self.__suggestions = [ self.__autoCompletion ] + + # A boolean telling whether the suggestion list and + # auto-completion attributes above need to be updated. + self.__suggestionsDirty = False + + + def getUserText( self ): + return self.__userText + + + def setUserText( self, text ): + """ + Sets the user text based on the value of text. + + NOTE: The stored user text may not be simply a copy of text + typed by the user; for example, multiple contiguous spaces in + text may be reduced to a single space. + """ + + # Only single spaces are allowed in the user text; additional + # spaces are ignored. + while text.find( " "*2 ) != -1: + text = text.replace( " "*2, " " ) + + self.__userText = text + # One of the source variables has changed. + self.__markDirty() + + + def autoType( self ): + """ + Sets the stored user text to the value indicated by the + current autocompleted suggestion. + """ + + self.__update() + + completion = self.__suggestions[ self.__activeIndex ] + if completion == None: + return + + completion = completion.toText() + if len(completion) == 0: + return + self.__userText = completion + + # One of the source variables has changed. + self.resetActiveSuggestion() + self.__markDirty() + + + def __update( self ): + """ + While not good general coding style, this method deliberately + encapsulates all the calls necessary to update the internal + suggestion list and auto-completion objects, as such calls (by + their nature) involve a fair amount of string processing and can + be performance sensitive. + + It updates the __suggestions and __autoCompletion attributes + to reflect the current userText. + """ + + if self.__suggestionsDirty: + self.__suggestionsDirty = False + + # NOTE: in the next two lines, ".strip()" is called because the + # autcompletions and suggestions should ignore trailing whitespace. + self.__autoCompletion = self.__autoComplete( + self.getUserText().strip() + ) + self.__suggestions = self.__findSuggestions( + self.getUserText().strip() + ) + # We need to verify that it is a valid index; if the + # namespace changed, then the suggestionss in the above + # getSuggestions() line might be different than the + # suggestions were the last time the active index was + # updated. + maxIndex = max( [ len(self.__suggestions)-1, 0 ] ) + self.__activeIndex = min( [self.__activeIndex, maxIndex] ) + + + def __autoComplete( self, userText ): + """ + Uses the CommandManager to determine if userText auto-completes + to a command name, and what that command name is. + + Returns an AutoCompletion object; if the AutoCompletion object + is empty (i.e., the text representation has 0 length), then there + was no valid auto-completed command name. + """ + + if len( userText ) < config.QUASIMODE_MIN_AUTOCOMPLETE_CHARS: + autoCompletion = AutoCompletion( userText, "" ) + else: + cmdMan = commands.commandManager + autoCompletion = cmdMan.autoComplete( userText ) + if autoCompletion == None: + autoCompletion = AutoCompletion( userText, "" ) + + return autoCompletion + + + def __findSuggestions( self, userText ): + """ + Uses the command manager to determine if there are any inexact + but near matches of command names to userText. + + Returns a complete suggestion list, where the 0th element is + the auto-completion, and each subsequent element (if any) is a + suggestion different than the autocompletion for a command + name that is similar to userText. + """ + + if len( userText ) < config.QUASIMODE_MIN_AUTOCOMPLETE_CHARS: + return [ self.__autoCompletion ] + + cmdMan = commands.commandManager + + suggestions = cmdMan.retrieveSuggestions( userText ) + + # BEGIN: Performance-improving code. + # Eliminate most of the suggestions before sorting them. + threshold = 0.0 + restrictedSuggestions = suggestions[:] + oldRestrictedSuggestions = restrictedSuggestions + + # LONGTERM TODO: You may be able to optimize the algorithm + # even further in the following way: assuming that thresh(x) + # gives you the number of suggestions whose nearness is + # greater than x, first see if thresh( 0.5 ) > + # QUASIMODE_MAX_SUGGESTIONS; if so, see if thresh( 0.75 ) is, + # but if not, see if thresh( 0.25 ) is, and so forth. + while (len( restrictedSuggestions ) > + config.QUASIMODE_MAX_SUGGESTIONS): + threshold += 0.05 + oldRestrictedSuggestions = restrictedSuggestions + restrictedSuggestions = [ \ + s for s in oldRestrictedSuggestions \ + if s._nearness > threshold \ + ] + + # Use the second-to-last restricted suggestions, as + # the last restricted suggestions may actually have + # fewer than we want. + suggestions = oldRestrictedSuggestions + # END: Performance-improving code. + + # Because the Suggestion object implements __cmp__ to sort + # by nearness, we can simply sort the suggestions in place. + suggestions.sort() + suggestions = suggestions[:config.QUASIMODE_MAX_SUGGESTIONS] + + # Make the auto-completion the 0th suggestion, and not listed + # more than once. + auto = self.__autoCompletion + if len( auto.toText() ) > 0: + suggestions = [ s for s in suggestions + if not s.toText() == auto.toText() ] + return [ auto ] + suggestions + + + def __markDirty( self ): + """ + Sets an internal variable telling the class that the suggestion list + is "dirty", and should be updated before returning any information. + """ + + self.__suggestionsDirty = True + + + def getSuggestions( self ): + """ + In a pair with getAutoCompletion(), this method gets the latest + suggestion list, making sure that the internal variable is + updated. + """ + + self.__update() + + return self.__suggestions + + + def getAutoCompletion( self ): + """ + In a pair with getSuggestions(), this method gets the latest + auto-completion, making sure that the internal variable is updated. + """ + + self.__update() + + return self.__autoCompletion + + + def getDescription( self ): + """ + Determines and returns the description for the currently + active command. + """ + + if self.getActiveCommand() == None: + if len( self.getAutoCompletion().getSource() ) \ + < config.QUASIMODE_MIN_AUTOCOMPLETE_CHARS: + # The user hasn't typed enough to match a command. + descText = config.QUASIMODE_DEFAULT_HELP + else: + # There is no command to match the user's text. + descText = config.QUASIMODE_NO_COMMAND_HELP + else: + # The active index is more than one, so one of the elements + # of the suggestion list is active, and we are assured + # that the active command exists. + descText = self.getActiveCommand().getDescription() + + descText = descText.strip() + + # Postcondition + assert len(descText) > 0 + + + def getActiveCommand( self ): + """ + Returns the active command, i.e., the command object that + implements the command that is currently indicated to the + user, either as the auto-completed command, or as a highlighted + element on the suggestion list. If there is no active command, + then the function returns None. + """ + + activeName = self.getActiveCommandName() + + if activeName == "": + return None + else: + cmdMan = commands.commandManager + return cmdMan.getCommand( activeName ) + + + def getActiveCommandName( self ): + """ + Determines the command name of the "active" command, i.e., the + name that is indicated to the user as the command that will + be activated on exiting the quasimode. + """ + + self.__update() + activeSugg = self.__suggestions[self.__activeIndex] + return activeSugg.toText() + + + def cycleActiveSuggestion( self, distance ): + """ + Changes which of the suggestions is "active", i.e., which suggestion + will be activated when the user releases the CapsLock key. + + Used to implement the up/down arrow key behavior. + """ + + self.__activeIndex += distance + if len( self.getSuggestions() ) > 0: + truncateLength = len( self.getSuggestions() ) + self.__activeIndex = self.__activeIndex % truncateLength + else: + self.__activeIndex = 0 + # One of the source variables has changed. + self.__markDirty() + + + def getActiveIndex( self ): + return self.__activeIndex + + + def resetActiveSuggestion( self ): + """ + Sets the active suggestion to 0, i.e., the user's + text/auto-completion. + """ + + self.__activeIndex = 0 + # One of the source variables has changed. + self.__markDirty()
--- a/enso/ui/quasimode/window.py Fri Feb 22 06:56:58 2008 -0600 +++ b/enso/ui/quasimode/window.py Fri Feb 22 07:17:15 2008 -0600 @@ -70,6 +70,7 @@ from enso.ui.quasimode.layout import DESCRIPTION_SCALE from enso.ui.quasimode.layout import AUTOCOMPLETE_SCALE, SUGGESTION_SCALE from enso.ui.quasimode.suggestionlist import MAX_SUGGESTIONS +from enso import config # ---------------------------------------------------------------------------- @@ -181,7 +182,7 @@ haven't yet been drawn, such as the suggestion list. If 'ignoreTimeElapsed' is True, then the - TIME_UNTIL_SUGGESTIONS_SHOW constant will be ignored and any + QUASIMODE_SUGGESTION_DELAY constant will be ignored and any pending suggestion waiting to be drawn will be rendered. Returns whether a suggestion was drawn. @@ -190,17 +191,10 @@ called. """ - # Amount of time, in seconds (float), to wait from the time - # that the quasimode begins drawing to the time that the - # suggestion list begins to be displayed. Setting this to a - # value greater than 0 will effectively create a - # "spring-loaded suggestion list" behavior. - TIME_UNTIL_SUGGESTIONS_SHOW = 0.2 - if self.__suggestionsLeft: timeElapsed = time.time() - self.__drawStart if ( (not ignoreTimeElapsed) and - (timeElapsed < TIME_UNTIL_SUGGESTIONS_SHOW) ): + (timeElapsed < config.QUASIMODE_SUGGESTION_DELAY) ): return False try: suggestionDrawer = self.__suggestionsLeft.next()