Mercurial > traipse_dev
diff orpg/tools/predTextCtrl.py @ 0:4385a7d0efd1 grumpy-goblin
Deleted and repushed it with the 'grumpy-goblin' branch. I forgot a y
author | sirebral |
---|---|
date | Tue, 14 Jul 2009 16:41:58 -0500 |
parents | |
children | 5aff3ef1ae46 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/orpg/tools/predTextCtrl.py Tue Jul 14 16:41:58 2009 -0500 @@ -0,0 +1,613 @@ +# Copyright (C) 2001 The OpenRPG Project +# +# openrpg-dev@lists.sourceforge.net +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# -- +# +# File: predTextCtrl.py +# Author: Andrew Bennett +# Maintainer: Andrew Bennett +# Version: $id:$ +# +# Description: This file contains an extension to the wxPython wx.TextCtrl that provides predictive word completion +# based on a word file loaded at instantiation. Also, it learns new words as you type, dynamically +# adjusting a weight for each word based on how often you type it. +# + +## +## Module Loading +## + +import string +from orpg.orpg_windows import * + +# This line added to test CVS commit + +## +## Class Definitions +## + +# This class implements a tree node that represents a letter +# +# Defines: +# __init__(self,filename) +# +class Letter: + def __init__(self,asciiCharIn,parentNodeIn): + self.asciiChar = asciiCharIn # should be an ASCII char + self.parentNode = parentNodeIn # should be ref to a class Letter + self.priority = 0 # should be an int + self.mostCommon = self # should be a ref to a class Letter + self.children = {} # should be a ref to a dictionary of class Letter + + def __str__(self): + return self.asciiChar + + +# This class implements the tree structure to hold the words +# +# Defines: +# __init__(self,filename) +# updateMostCommon(self,target) +# setWord(self,wordText,priority,sumFlag) +# addWord(self,wordText) +# setWord(self,wordText) +# setWordPriority(self,wordText,priority) +# findWordNode(self,wordText) returns class Letter +# findWordPriority(self,wordText) returns int +# getPredition(self,k,cur) returns string +class LetterTreeClass(object): + + # Initialization subroutine. + # + # self : instance of self + # filename : name of word file to use + # + # returns None + # + # Purpose: Constructor for LetterTree. Basically, it initializes itself with a word file, if present. + def __init__(self, singletonKey): + if not isinstance(singletonKey, _SingletonKey): + raise invalid_argument(_("Use LetterTree() to get access to singleton")) + + self.rootNode = Letter("",None) # rootNode is a class Letter + self.rootNode.children = {} # initialize the children list + + + # updateMostCommon subroutine. + # + # self : instance of self + # target : class Letter that was updated + # + # Returns None + # + # Purpose: Updates all of the parent nodes of the target, such that their mostCommon member + # points to the proper class Letter, based on the newly updated priorities. + def updateMostCommon(self, target): + # cur is a class Letter + prev = target.parentNode # prev is a class Letter + while prev: + if prev.mostCommon is None: + prev.mostCommon = target + else: + if target.priority > prev.mostCommon.priority: + prev.mostCommon = target + prev = prev.parentNode + + + + # setWord subroutine. + # + # self : instance of self + # wordText : string representing word to add + # priority : integer priority to set the word + # sumFlag : if True, add the priority to the existing, else assign the priority + # + # Returns: None + # + # Purpose: Sets or increments the priority of a word, adding the word if necessary + def setWord(self,wordText,priority = 1,sumFlag = 0): + + cur = self.rootNode # start from the root + + for ch in wordText: # for each character in the word + if cur.children.has_key(ch): # check to see if we've found a new word + + cur = cur.children[ch] # if we haven't found a new word, move to the next letter and try again + + else: # in this clause, we're creating a new branch, as the word is new + newLetter = Letter(ch,cur) # create a new class Letter using this ascii code and the current letter as a parent + + if cur is self.rootNode: # special case: Others expect the top level letters to point to None, not self.rootNode + newLetter.parentNode = None + + cur.children[ch] = newLetter # add the new letter to the list of children of the current letter + cur = newLetter # make the new letter the current one for the next time through + + # at this point, cur is pointing to the last letter of either a new or existing word. + + if sumFlag: # if the caller wants to add to the existing (0 if new) + cur.priority += priority + else: # else, the set the priority directly + cur.priority = priority + + self.updateMostCommon(cur) # this will run back through the tree to fix up the mostCommon members + + + # addWord subroutine. + # + # self : instance of self + # wordText : string representing word to add + # + # Returns: None + # + # Purpose: Convenience method that wraps setWord. Used to add words known not to exist. + def addWord(self,wordText): + self.setWord(wordText,priority = 1) + + + # incWord subroutine. + # + # self : instance of self + # wordText : string representing word to add + # + # Returns: None + # + # Purpose: Convenience method that wraps setWord. Used to increment the priority of existing words and add new words. + # Note: Generally, this method can be used instead of addWord. + def incWord(self,wordText): + self.setWord(wordText,priority = 1, sumFlag = 1) + + + # setWordPriority subroutine. + # + # self : instance of self + # wordText : string representing word to add + # priority: int that is the new priority + # + # Returns: None + # + # Purpose: Convenience method that wraps setWord. Sets existing words to priority or adds new words with priority = priority + def setWordPriority(self,wordText,priority): + self.setWord(wordText,priority = priority) + + + # findWordNode subroutine. + # + # self : instance of self + # wordText : string representing word to add + # + # Returns: class Letter or None if word isn't found. + # + # Purpose: Given a word, it returns the class Letter node that corresponds to the word. Used mostly in prep for a call to + # getPrediction() + def findWordNode(self,wordText): #returns class Letter that represents the last letter in the word + + cur = self.rootNode # start at the root + + for ch in wordText: # move through each letter in the word + if cur.children.has_key(ch): # if the next letter exists, make cur equal that letter and loop + cur = cur.children[ch] + else: + return None # return None if letter not found + + return cur # return cur, as this points to the last letter if we got this far + + + # findWordPriority subroutine. + # + # self : instance of self + # wordText : string representing word to add + # + # Returns: Int representing the word's priority or 0 if not found. + # + # Purpose: Returns the priority of the given word + def findWordPriority(self,wordText): + + cur = self.findWordNode(wordText) # find the class Letter node that corresponds to this word + if cur: + return cur.priority # if it was found, return it's priority + else: + return 0 # else, return 0, meaning word not found + + + def printTree(self, current=None): + letters = [] + if current is None: + current = self.rootNode + + for l, letter in current.children.iteritems(): + letters.append(str(letter)) + if letter.children != {}: + m = self.printTree(letter) + letters.append(m) + + return letters + + + + # getPrediction subroutine. + # + # self : instance of self + # k : ASCII char that was typed + # cur : class Letter that points to the current node in LetterTree + # + # Returns: The predicted text or "" if none found + # + # Purpose: This is the meat and potatoes of data structure. It takes the "current" Letter node and the next key typed + # and returns it's guess of the rest of the word, based on the highest priority letter in the rest of the branch. + def getPrediction(self,k,cur): + + if cur.children.has_key(k) : # check to see if the key typed is a sub branch + # If so, make a prediction. Otherwise, do the else at the bottom of + # the method (see below). + + cur = cur.children[k] # set the cur to the typed key's class Letter in the sub-branch + + backtrace = cur.mostCommon # backtrace is a class Letter. It's used as a placeholder to back trace + # from the last letter of the mostCommon word in the + # sub-tree up through the tree until we meet ourself at cur. We'll + # build the guess text this way + + returnText = "" # returnText is a string. This will act as a buffer to hold the string + # we build. + + while cur is not backtrace: # Here's the loop. We loop until we've snaked our way back to cur + + returnText = backtrace.asciiChar + returnText # Create a new string that is the character at backtrace + everything + # so far. So, for "tion" we'll build "n","on","ion","tion" as we .. + + backtrace = backtrace.parentNode # ... climb back towards cur + + return returnText # And, having reached here, we've met up with cur, and returnText holds + # the string we built. Return it. + + else: # This is the else to the original if. + # If the letter typed isn't in a sub branch, then + # the letter being typed isn't in our tree, so + return "" # return the empty string + + +# End of class LetterTree! + + + +class _SingletonKey(object): + def __new__(cls, *args, **kwargs): + if not hasattr(cls, '_inst'): + cls._inst = super(_SingletonKey, cls).__new__(cls, *args, **kwargs) + return cls._inst + +__key = _SingletonKey() +LetterTree = LetterTreeClass(__key) + +# This class extends wx.TextCtrl +# +# Extends: wx.TextCtrl +# +# Overrides: +# wx.TextCtrl.__init__(self,parent,id,value,size,style,name) +# wx.TextCtrl.OnChar(self,Event) +# +# Defines: +# findWord(self,insert,st) +class predTextCtrl(wx.TextCtrl): + + # Initialization subroutine. + # + # self : instance of self + # parent: reference to parent window (wxWindow, me-thinks) + # id: new Window Id, default to -1 to create new (I think: see docs for wxPython) + # value: String that is the initial value the control holds, defaulting to "" + # size: defaults to wx.DefaultSize + # style: defaults to 0 + # name: defaults to "text" + # keyHook: must be a function pointer that takes self and a GetKeyCode object + # validator: defaults to None + # + # Note: These parameters are essentially just passed back to the native wx.TextCtrl. + # I basically just included (stole) enough of them from chatutils.py to make + # it work. Known missing args are pos and validator, which aren't used by + # chatutils.py. + # + # Returns: None + # + # Purpose: Constructor for predTextCtrl. Calls wx.TextCtrl.__init__ to get default init + # behavior and then inits a LetterTree and captures the parent for later use in + # passing events up the chain. + def __init__(self, parent, id = -1, value = "" , size = wx.DefaultSize, style = 0, name = "text",keyHook = None, validator=None): + + # Call super() for default behavior + if validator: + wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name, validator=validator ) + else: + wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name) + + self.tree = LetterTree # Instantiate a new LetterTree. + # TODO: make name of word file an argument. + + + + + self.parent = parent # Save parent for later use in passing KeyEvents + + self.cur = self.tree.rootNode # self.cur is a short cut placeholder for typing consecutive chars + # It may be vestigal + + self.keyHook = keyHook # Save the keyHook passed in + + + # findWord subroutine. + # + # self : instance of self + # insert: index of last char in st + # st : string from insert to the left + # + # Note: This implementation is about the third one for this method. Originally, + # st was an arbitrary string and insert was the point within + # this string to begin looking left. Since, I finally got it + # to work right as it is around 2 in the morning, I'm not touching it, for now. + # + # Returns: String that is the word or "" if none found. This generally doesn't + # happen, as st usually ends in a letter, which will be returned. + # + # Purpose: This function is generally used to figure out the beginning of the + # current word being typed, for later use in a LetterTree.getPrediction() + def findWord(self,insert,st): + + # Good luck reading this one. Basically, I started out with an idea, and fiddled with the + # constants as best I could until it worked. It's not a piece of work of which I'm too + # proud. Basically, it's intent is to check each character to the left until it finds one + # that isn't a letter. If it finds such a character, it stops and returns the slice + # from that point to insert. Otherwise, it returns the whole thing, due to begin being + # initialized to 0 + + begin = 0 + for offset in range(insert - 1): + if st[-(offset + 2)] not in string.letters: + begin = insert - (offset + 1) + break + return st[begin:insert] + + + # OnChar subroutine. + # + # self : instance of self + # event: a GetKeyCode object + # + # Returns: None + # + # Purpose: This function is the key event handler for predTextCtrl. It handles what it + # needs to and passes the event on to it's parent's OnChar method. + def OnChar(self,event): + + # Before we do anything, call the keyHook handler, if not None + # This is currently used to implement the typing/not_typing messages in a manner that + # doesn't place the code here. Maybe I should reconsider that. :) + if(self.keyHook): + if self.keyHook(event) == 1: # if the passed in keyHook routine returns a one, it wants no predictive behavior + self.parent.OnChar(event) + return + + + + # This bit converts the GetKeyCode() return (int) to a char if it's in a certain range + asciiKey = "" + if (event.GetKeyCode() < 256) and (event.GetKeyCode() > 19): + asciiKey = chr(event.GetKeyCode()) + + + if asciiKey == "": # If we didn't convert it to a char, then process based on the int GetKeyCodes + + if event.GetKeyCode() == wx.WXK_TAB: # We want to hook tabs to allow the user to signify acceptance of a + # predicted word. + # Handle Tab key + + fromPos = toPos = 0 # get the current selection range + (fromPos,toPos) = self.GetSelection() + + if (toPos - fromPos) == 0: # if there is no selection, pass tab on + self.parent.OnChar(event) + return + + else: # This means at least one char is selected + + self.SetInsertionPoint(toPos) # move the insertion point to the end of the selection + self.SetSelection(toPos,toPos) # and set the selection to no chars + # The prediction, if any, had been inserted into the text earlier, so + # moving the insertion point to the spot directly afterwards is + # equivalent to acceptance. Without this, the next typed key would + # clobber the prediction. + + return # Don't pass tab on in this case + + elif event.GetKeyCode() == wx.WXK_RETURN: # We want to hook returns, so that we can update the word list + + st = self.GetValue() # Grab the text from the control + newSt = "" # Init a buffer + + # This block of code, by popular demand, changes the behavior of the control to ignore any prediction that + # hasn't been "accepted" when the enter key is struck. + (startSel,endSel) = self.GetSelection() # get the curren selection + + # + # Start update + # Changed the following to allow for more friendly behavior in + # a multilined predTextCtrl. + # + # front = st[:startSel] # Slice off the text to the front of where we are + # back = st[endSel:] # Slice off the text to the end from where we are + # st = front + back # This expression creates a string that get rid of any selected text. + # self.SetValue(st) + + self.Remove( startSel, endSel ) + st = string.strip( self.GetValue() ) + # + # End update + # + + # this loop will walk through every character in st and add it to + # newSt if it's a letter. If it's not a letter, (e.g. a comma or + # hyphen) a space is added to newSt in it's place. + for ch in st: + if ch not in string.letters: + newSt += " " + else: + newSt += ch + + # Now that we've got a string of just letter sequences (words) and spaces + # split it and to a LetterTree.incWord on the lowercase version of it. + # Reminder: incWord will increment the priority of existing words and add + # new ones + for aWord in string.split(newSt): + self.tree.incWord(string.lower(aWord)) + + self.parent.OnChar(event) # Now that all of the words are added, pass the event and return + return + + # We want to capture the right arrow key to fix a slight UI bug that occurs when one right arrows + # out of a selection. I set the InsertionPoint to the beginning of the selection. When the default + # right arrow event occurs, the selection goes away, but the cursor is in an unexpected location. + # This snippet fixes this behavior and then passes on the event. + elif event.GetKeyCode() == wx.WXK_RIGHT: + (startSel,endSel) = self.GetSelection() + self.SetInsertionPoint(endSel) + self.parent.OnChar(event) + return + + # Ditto as wx.WXK_RIGHT, but for completeness sake + elif event.GetKeyCode() == wx.WXK_LEFT: + (startSel,endSel) = self.GetSelection() + self.SetInsertionPoint(startSel) + self.parent.OnChar(event) + return + + + else: + # Handle any other non-ascii events by calling parent's OnChar() + self.parent.OnChar(event) #Call super.OnChar to get default behavior + return + + + elif asciiKey in string.letters: + # This is the real meat and potatoes of predTextCtrl. This is where most of the + # wx.TextCtrl logic is changed. + + (startSel,endSel) = self.GetSelection() # get the curren selection + st = self.GetValue() # and the text in the control + front = st[:startSel] # Slice off the text to the front of where we are + back = st[endSel:] # Slice off the text to the end from where we are + + st = front + asciiKey + back # This expression creates a string that will insert the + # typed character (asciiKey is generated at the + # beginning of OnChar()) into the text. If there + # was text selected, that text will not be part + # of the new string, due to the way front and back + # were sliced. + + insert = startSel + 1 # creates an int that denotes where the new InsertionPoint + # should be. + + curWord = "" # Assume there's a problem with finding the curWord + + if (len(back) == 0) or (back[0] not in string.letters): # We should only insert a prediction if we are typing + # at the end of a word, not in the middle. There are + # three cases: we are typing at the end of the string or + # we are typing in the middle of the string and the next + # char is NOT a letter or we are typing in the middle of the + # string and the next char IS a letter. Only the former two + # cases denote that we should make a prediction + # Note: The order of the two logical clauses here is important! + # If len(back) == 0, then the expression back[0] will + # blow up with an out of bounds array subscript. Luckily + # the or operator is a short-circuit operator and in this + # case will only evaluate back[0] if len(back) != 0, in + # which we're safely in bounds. + + curWord = self.findWord(insert,front + asciiKey) # Now that we know we're supposed to make a prediction, + # let's find what word root to use in our prediction. + # Note: This is using the confusing findWord method. I + # send it insert and the text from the beginning + # of the text through the key just entered. This is + # NOT the original usage, but it does work. See + # findWord() for more details. + + else: # Here, we've found we're in the middle of a word, so we're + # going to call the parent's event handler. + self.parent.OnChar(event) + return + + if curWord == "": # Here, we do a quick check to make sure we have a good root + # word. If not, allow the default thing to happen. Of course, + # now that I'm documenting this, it occurs to me to wonder why + # I didn't do the same thing I just talked about. Hmmmmmm. + + self.parent.OnChar(event) # we're done here + return + + self.cur = self.tree.findWordNode(string.lower(curWord[:-1])) # Still with me? We're almost done. At this point, we + # need to convert our word string to a Letter node, + # because that's what getPrediction expects. Notice + # that we're feeding in the string with the last + # char sliced off. For developmentally historical + # reasons, getPrediction wants the node just before + # the typed character and the typed char separately. + + + if self.cur is None: + self.parent.OnChar(event) # if there's no word or no match, we're done + return + + + # get the prediction + predictText = self.tree.getPrediction(string.lower(asciiKey),self.cur) # This is the big prediction, as noted above + # Note the use of string.lower() because we + # keep the word list in all lower case,but we + # want to be able to match any capitalization + + if predictText == "": + self.parent.OnChar(event) # if there's no prediction, we're done + return + + # And now for the big finale. We're going to take the string st + # we created earlier and insert the prediction right after the + # newly typed character. + front = st[:insert] # Grab a new front from st + back = st[insert:] # Grab a new back + + st = front + predictText + back # Insert the prediction + + self.SetValue(st) # Now, overwrite the controls text with the new text + self.SetInsertionPoint(insert) # Set the proper insertion point, directly behind the + # newly typed character and directly in front of the + # predicted text. + + self.SetSelection(insert,insert+len(predictText)) # Very important! Set the selection to encompass the predicted + # text. This way, the user can ignore the prediction by simply + # continuing to type. Remember, if the user wants the prediction + # s/he must strike the tab key at this point. Of course, one could + # just use the right arrow key as well, but that's not as easy to + # reach. + + return # Done! Do NOT pass the event on at this point, because it's all done. + + + else: + # Handle every other non-letter ascii (e.g. semicolon) by passing the event on. + self.parent.OnChar(event) #Call super.OnChar to get default behavior + return + +# End of class predTextCtrl!