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!