Mercurial > enso_core
changeset 27:943e412a8bd9
Added enso.ui.commands.factories.
author | Atul Varma <varmaa@toolness.com> |
---|---|
date | Sat, 23 Feb 2008 09:05:11 -0600 |
parents | 63b5f5f313b8 |
children | cff69f571315 |
files | enso/ui/commands/factories.py enso/ui/messages/__init__.py |
diffstat | 2 files changed, 462 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/enso/ui/commands/factories.py Sat Feb 23 09:05:11 2008 -0600 @@ -0,0 +1,444 @@ +# 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.commands.factories +# +# ---------------------------------------------------------------------------- + +""" + A couple useful CommandFactory implementations. + + GenericPrefixFactory is likely to be the base of most actual command + factory implementations, since it deals with the relatively common case + of a family of simple arguments, all sharing a common prefix. + + ArbitraryPostfixFactory can be used as the base for any command factory + implementation that can accept *any* postfix or argument, such as + "learn as open <blah>" + "learn as format <blah>" +""" + +# ---------------------------------------------------------------------------- +# Imports +# ---------------------------------------------------------------------------- + +import re + +from enso.ui.commands.suggestions import AutoCompletion, Suggestion +from enso.ui.commands.interfaces import AbstractCommandFactory, CommandObject +from enso.ui.messages import displayMessage + + +# ---------------------------------------------------------------------------- +# Private Utility Functions +# ---------------------------------------------------------------------------- + +def _equivalizeChars( userText ): + """ + Returns a regular expression in which certain characters are + replaced with equivalent character sets, e.g., "2" by "[2@]". + """ + + # TODO: These appear to only be equivalent characters for US + # keyboard layouts. + + EQUIVALENT_CHARS = { + "1" : "1!", + "2" : "2@", + "3" : "3#", + "4" : "4$", + "5" : "5%", + "6" : "6^", + "7" : "7&", + "8" : "8*", + "9" : "9(", + "0" : "0)", + "-" : "-_", + "=" : "=+", + ";" : ":;", + "-" : "-_", + "'" : "'\"", + } + + searchText = re.escape( userText ) + for char in EQUIVALENT_CHARS.keys(): + expr = EQUIVALENT_CHARS[char] + expr = re.escape( expr ) + expr = "[%s]" % expr + char = re.escape(char) + searchText = searchText.replace( char, expr ) + + # {{{searchText}}} is a pattern that may contain spaces. To + # determine whether a string matches searchText, we want any + # number of spaces in the string to match a single space in + # searchText. Therefore, we replace each space in searchText with + # a "multispace" match RE, i.e., a regular expression that will + # match one or more spaces. + space = re.escape( " " ) + multiSpace = "[%s]+" % space + searchText = searchText.replace( space, multiSpace ) + + return searchText + + +# ---------------------------------------------------------------------------- +# Prefix Command Factory +# ---------------------------------------------------------------------------- + +class NoArgumentCommand( CommandObject ): + """ + The command executed when the end-user simply types 'learn as + open' without specifying a target name. + """ + + def __init__( self, description, message ): + CommandObject.__init__( self ) + self.setDescription( description ) + self.message = message + + def run( self ): + # TODO: This single call couples this whole module to the + # enso.ui.messages module; any way to avoid this? + displayMessage( self.message ) + + +class GenericPrefixFactory( AbstractCommandFactory ): + """ + Uses a postfix-prefix system to generate autocompletions and + suggestions. + + This class can be used to avoid the necessity of implementing + autocompletion and suggestion functionality, for command factories + which simply correspond to a finite list of postfixes applied to a + single prefix. + """ + + # The portion of the command expression that is common to all + # the command names that this command factory produces. + PREFIX = "" + + # This class variable defines the "help text" displayed in + # place of a valid postfix; because *any* postfix is valid, + # we want to display something to indicate to the user + # that a postfix is required. + # LONGTERM TODO: Decide whether having a default is acceptable. + HELP_TEXT = "argument" + + # This class variable defines the "description text" displayed in + # the top line of the quasimode when a concrete parameter has + # not been displayed. + DESCRIPTION_TEXT = None + + # This class variable defines the "error message text" + # displayed as a primary message if the user fails to + # type an argument. + # LONGTERM TODO: Decide whether having a default is acceptable. + MESSAGE_TEXT = "<p>That command requires an argument.</p>" + + def __init__( self ): + """ + Instantiantes the command factory. + + Must be called by overriden constructors. + """ + + AbstractCommandFactory.__init__( self ) + + # Each postfix corresponds to one command that this command + # factory can produce; when combined with the prefix (above), + # the resulting string is a complete command name. + self.__postfixes = [] + self.__postfixesChanged = False + + self.__searchString = "" + + def getPostfixes( self ): + return self.__postfixes + + def setPostfixes( self, postfixes ): + self.__postfixesChanged = True + self.__postfixes = postfixes + + #A protected property; subclasses should maintain this and update + #it in the .update() method. + _postfixes = property( fget = getPostfixes, fset = setPostfixes, ) + + # Subclasses should use _addPostfix and _removePostfix instead of + # modifying the postfix list themselves, because modifying the list + # in place will not invoke the property set method, which means + # postfixesChanged won't get updated, which is bad. + def _addPostfix( self, cmdName ): + self._postfixes = self._postfixes[:] + [cmdName] + + def _removePostfix( self, cmdExpr ): + newPostfixes = self._postfixes[:] + newPostfixes.remove( cmdExpr ) + self._postfixes = newPostfixes + + def getCommandList( self ): + """ + Returns a list of all available command names based on the + most recently set list of postfixes. + """ + + self.update() + return [ self.PREFIX + post for post in self._postfixes ] + + + def __update( self ): + """ + Private method for maintaining a search-string structure. + """ + + self.update() + + if self.__postfixesChanged: + self.__postfixesChanged = False + self.__searchString = "\n".join( self.__postfixes ) + + + # LONGTERM TODO: This is not the greatest design. Perhaps in + # Mehitabel Core 2.0 this can be replaced with an Observer pattern. + def update( self ): + """ + Template Method - Designed to allow sub-classes to update the + class's interal command/postfix information. + + NOTE: BE CAREFUL! This function gets called on every keystroke + while the user has typed something that might match this + factory. If you don't need to update that often, then do + something to get out of this function quickly! + """ + + raise NotImplementedError + + + def retrieveSuggestions( self, userText ): + """ + Retrieves the VERY LATEST suggestions available that match + the userText string. + + NOTE: This method calls self.update() to update the internal + data structures of the CommandFactory to reflect any changes + in system state. + + This returns a list of Suggestion objects. + """ + + pattern = userText[len(self.PREFIX):] + pattern = _equivalizeChars( pattern ) + + # Match any command that contains the user postfix (i.e., + # any characters followed by the user postfix). + pattern = ".*" + pattern + + matches = self.__findMatches( pattern ) + + suggestions = [ Suggestion( userText, self.PREFIX + m ) + for m in matches ] + + if self.PREFIX.startswith( userText ): + # If seed text is all or part of the prefix, then + # autocomplete with help text. + suggestions.insert( + 0, + Suggestion( userText, self.PREFIX, self.HELP_TEXT ) + ) + + return suggestions + + + def autoComplete( self, userText ): + """ + If userText begins with this factory's prefix, and the + remainder of userText begins a word of one of this factory's + postfixes, then returns an Autocompletion object for the + match. Otherwise, returns None. + """ + + if self.PREFIX.startswith( userText ): + # If seed text is all or part of the prefix, then + # autocomplete with help text. + return AutoCompletion( userText, self.PREFIX, self.HELP_TEXT ) + + elif not userText.startswith( self.PREFIX ): + return None + + pattern = userText[len(self.PREFIX):] + pattern = _equivalizeChars( pattern ) + matches = self.__findMatches( pattern ) + if len( self.PREFIX ) > 0 and len( matches ) == 0: + # We have a real prefix; look for beginings of words. + matches = self.__findMatches( ".*\\b" + pattern ) + if len(matches) < 1: + return None + match = matches[0] + matchLocation = re.search( pattern, match ).start() + + newUserText = self.PREFIX + start = matchLocation + end = matchLocation + len(userText) - len(self.PREFIX) + newUserText += match[start:end] + completion = AutoCompletion( newUserText, self.PREFIX + match ) + return completion + + + def __findMatches( self, pattern ): + """ + Finds all command names that: + (1) start with the correct prefix, and + (2) match pattern. + """ + + self.__update() + + # This part works by using regular expressions to quickly grab + # all the new-line delimited substrings that contain + # the pattern. NOTE: This will allow us to modify the pattern + # into a more advanced regexp, allowing ( for example ) the + # user text "open boo 9temp0" to match to the command named + # "open boo (temp)". + + # ^ matches begining of line or begining of string + # $ matches end of line or end of string + # .* matches any number of any character, except newlines + + # re.M means that "multiline mode" is used, so "." does not + # match newlines. + matches = re.findall( pattern = "^" + pattern +".*$", + string = self.__searchString, + flags = re.M ) + matches = [ m for m in matches if len(m) > 0 ] + matches.sort() + return matches + + def getCommandObj( self, commandName ): + """ + Returns the command object that matches commandName, if any. + """ + + prefix = self.PREFIX + if ( len( commandName ) > len( prefix ) ) and \ + commandName.startswith( prefix ): + return self._generateCommandObj( commandName ) + elif commandName.startswith( prefix ) or \ + prefix.startswith( commandName ): + return self._generateFailedCommandObj() + else: + return None + + + def _generateCommandObj( self, commandName ): + """ + Virtual method for getting an actual command object. + + Must be overriden by subclasses. + """ + + # TODO: These assertions won't actually get called by + # subclasses that implement this method... Use the template + # method pattern here? + assert len( commandName ) > self.PREFIX + assert commandName.startswith( self.PREFIX ) + + raise NotImplementedError() + + + def _generateFailedCommandObj( self ): + """ + Method called when the requested command name + does not actually have a postfix. + + NOTE: A very good general behavior is built in, but + should a different behavior be required in the event + of no argument (e.g., "define "), then this method + may be overridden. + """ + + assert self.DESCRIPTION_TEXT != None + + return NoArgumentCommand( self.DESCRIPTION_TEXT, + self.MESSAGE_TEXT ) + + + + +class ArbitraryPostfixFactory( GenericPrefixFactory ): + """ + Abstract factory class for factories that produce "learn as" + commands, and other command families that can take any argument. + """ + + def autoComplete( self, seedText ): + """ + Returns an autocompletion for seedText if seedText begins + with all or part of the prefix for this command factory. + If the seedText did not have a postfix, then the returned + suggestion object will have help text. + + Returns None otherwise. + """ + + if self.PREFIX.startswith( seedText ): + # If seed text is all or part of the prefix, then + # autocomplete with help text. + return AutoCompletion( seedText, self.PREFIX, self.HELP_TEXT ) + elif seedText.startswith( self.PREFIX ): + # If the seed text begins with the prefix, but has + # more than just the seed text (i.e., made it beyond + # the previous condition), then there is a postfix; + # autocomplete without trailing help text. + return AutoCompletion( seedText, seedText ) + else: + return None + + + def retrieveSuggestions( self, userText ): + """ + Returns a suggestion if the userText is contained in the + prefix. + """ + + if userText in self.PREFIX: + return [ Suggestion( userText, + self.PREFIX, + self.HELP_TEXT ) ] + else: + return [] + + + def update( self ): + """ + Updates the available command list. + """ + + # Since this command factory accepts any postfix, don't do + # anything. + pass
--- a/enso/ui/messages/__init__.py Sat Feb 23 08:49:42 2008 -0600 +++ b/enso/ui/messages/__init__.py Sat Feb 23 09:05:11 2008 -0600 @@ -398,6 +398,24 @@ # ---------------------------------------------------------------------------- +# Convenience functions +# ---------------------------------------------------------------------------- + +def displayMessage( msgXml ): + """ + Displays a simple primary message that has no mini message. + """ + + msg = Message( + isPrimary = True, + isMini = False, + fullXml = msgXml, + ) + + messageManager.newMessage( msg ) + + +# ---------------------------------------------------------------------------- # Module Initilization # ----------------------------------------------------------------------------