comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:4385a7d0efd1
1 # Copyright (C) 2001 The OpenRPG Project
2 #
3 # openrpg-dev@lists.sourceforge.net
4 #
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 # --
19 #
20 # File: predTextCtrl.py
21 # Author: Andrew Bennett
22 # Maintainer: Andrew Bennett
23 # Version: $id:$
24 #
25 # Description: This file contains an extension to the wxPython wx.TextCtrl that provides predictive word completion
26 # based on a word file loaded at instantiation. Also, it learns new words as you type, dynamically
27 # adjusting a weight for each word based on how often you type it.
28 #
29
30 ##
31 ## Module Loading
32 ##
33
34 import string
35 from orpg.orpg_windows import *
36
37 # This line added to test CVS commit
38
39 ##
40 ## Class Definitions
41 ##
42
43 # This class implements a tree node that represents a letter
44 #
45 # Defines:
46 # __init__(self,filename)
47 #
48 class Letter:
49 def __init__(self,asciiCharIn,parentNodeIn):
50 self.asciiChar = asciiCharIn # should be an ASCII char
51 self.parentNode = parentNodeIn # should be ref to a class Letter
52 self.priority = 0 # should be an int
53 self.mostCommon = self # should be a ref to a class Letter
54 self.children = {} # should be a ref to a dictionary of class Letter
55
56 def __str__(self):
57 return self.asciiChar
58
59
60 # This class implements the tree structure to hold the words
61 #
62 # Defines:
63 # __init__(self,filename)
64 # updateMostCommon(self,target)
65 # setWord(self,wordText,priority,sumFlag)
66 # addWord(self,wordText)
67 # setWord(self,wordText)
68 # setWordPriority(self,wordText,priority)
69 # findWordNode(self,wordText) returns class Letter
70 # findWordPriority(self,wordText) returns int
71 # getPredition(self,k,cur) returns string
72 class LetterTreeClass(object):
73
74 # Initialization subroutine.
75 #
76 # self : instance of self
77 # filename : name of word file to use
78 #
79 # returns None
80 #
81 # Purpose: Constructor for LetterTree. Basically, it initializes itself with a word file, if present.
82 def __init__(self, singletonKey):
83 if not isinstance(singletonKey, _SingletonKey):
84 raise invalid_argument(_("Use LetterTree() to get access to singleton"))
85
86 self.rootNode = Letter("",None) # rootNode is a class Letter
87 self.rootNode.children = {} # initialize the children list
88
89
90 # updateMostCommon subroutine.
91 #
92 # self : instance of self
93 # target : class Letter that was updated
94 #
95 # Returns None
96 #
97 # Purpose: Updates all of the parent nodes of the target, such that their mostCommon member
98 # points to the proper class Letter, based on the newly updated priorities.
99 def updateMostCommon(self, target):
100 # cur is a class Letter
101 prev = target.parentNode # prev is a class Letter
102 while prev:
103 if prev.mostCommon is None:
104 prev.mostCommon = target
105 else:
106 if target.priority > prev.mostCommon.priority:
107 prev.mostCommon = target
108 prev = prev.parentNode
109
110
111
112 # setWord subroutine.
113 #
114 # self : instance of self
115 # wordText : string representing word to add
116 # priority : integer priority to set the word
117 # sumFlag : if True, add the priority to the existing, else assign the priority
118 #
119 # Returns: None
120 #
121 # Purpose: Sets or increments the priority of a word, adding the word if necessary
122 def setWord(self,wordText,priority = 1,sumFlag = 0):
123
124 cur = self.rootNode # start from the root
125
126 for ch in wordText: # for each character in the word
127 if cur.children.has_key(ch): # check to see if we've found a new word
128
129 cur = cur.children[ch] # if we haven't found a new word, move to the next letter and try again
130
131 else: # in this clause, we're creating a new branch, as the word is new
132 newLetter = Letter(ch,cur) # create a new class Letter using this ascii code and the current letter as a parent
133
134 if cur is self.rootNode: # special case: Others expect the top level letters to point to None, not self.rootNode
135 newLetter.parentNode = None
136
137 cur.children[ch] = newLetter # add the new letter to the list of children of the current letter
138 cur = newLetter # make the new letter the current one for the next time through
139
140 # at this point, cur is pointing to the last letter of either a new or existing word.
141
142 if sumFlag: # if the caller wants to add to the existing (0 if new)
143 cur.priority += priority
144 else: # else, the set the priority directly
145 cur.priority = priority
146
147 self.updateMostCommon(cur) # this will run back through the tree to fix up the mostCommon members
148
149
150 # addWord subroutine.
151 #
152 # self : instance of self
153 # wordText : string representing word to add
154 #
155 # Returns: None
156 #
157 # Purpose: Convenience method that wraps setWord. Used to add words known not to exist.
158 def addWord(self,wordText):
159 self.setWord(wordText,priority = 1)
160
161
162 # incWord subroutine.
163 #
164 # self : instance of self
165 # wordText : string representing word to add
166 #
167 # Returns: None
168 #
169 # Purpose: Convenience method that wraps setWord. Used to increment the priority of existing words and add new words.
170 # Note: Generally, this method can be used instead of addWord.
171 def incWord(self,wordText):
172 self.setWord(wordText,priority = 1, sumFlag = 1)
173
174
175 # setWordPriority subroutine.
176 #
177 # self : instance of self
178 # wordText : string representing word to add
179 # priority: int that is the new priority
180 #
181 # Returns: None
182 #
183 # Purpose: Convenience method that wraps setWord. Sets existing words to priority or adds new words with priority = priority
184 def setWordPriority(self,wordText,priority):
185 self.setWord(wordText,priority = priority)
186
187
188 # findWordNode subroutine.
189 #
190 # self : instance of self
191 # wordText : string representing word to add
192 #
193 # Returns: class Letter or None if word isn't found.
194 #
195 # Purpose: Given a word, it returns the class Letter node that corresponds to the word. Used mostly in prep for a call to
196 # getPrediction()
197 def findWordNode(self,wordText): #returns class Letter that represents the last letter in the word
198
199 cur = self.rootNode # start at the root
200
201 for ch in wordText: # move through each letter in the word
202 if cur.children.has_key(ch): # if the next letter exists, make cur equal that letter and loop
203 cur = cur.children[ch]
204 else:
205 return None # return None if letter not found
206
207 return cur # return cur, as this points to the last letter if we got this far
208
209
210 # findWordPriority subroutine.
211 #
212 # self : instance of self
213 # wordText : string representing word to add
214 #
215 # Returns: Int representing the word's priority or 0 if not found.
216 #
217 # Purpose: Returns the priority of the given word
218 def findWordPriority(self,wordText):
219
220 cur = self.findWordNode(wordText) # find the class Letter node that corresponds to this word
221 if cur:
222 return cur.priority # if it was found, return it's priority
223 else:
224 return 0 # else, return 0, meaning word not found
225
226
227 def printTree(self, current=None):
228 letters = []
229 if current is None:
230 current = self.rootNode
231
232 for l, letter in current.children.iteritems():
233 letters.append(str(letter))
234 if letter.children != {}:
235 m = self.printTree(letter)
236 letters.append(m)
237
238 return letters
239
240
241
242 # getPrediction subroutine.
243 #
244 # self : instance of self
245 # k : ASCII char that was typed
246 # cur : class Letter that points to the current node in LetterTree
247 #
248 # Returns: The predicted text or "" if none found
249 #
250 # Purpose: This is the meat and potatoes of data structure. It takes the "current" Letter node and the next key typed
251 # and returns it's guess of the rest of the word, based on the highest priority letter in the rest of the branch.
252 def getPrediction(self,k,cur):
253
254 if cur.children.has_key(k) : # check to see if the key typed is a sub branch
255 # If so, make a prediction. Otherwise, do the else at the bottom of
256 # the method (see below).
257
258 cur = cur.children[k] # set the cur to the typed key's class Letter in the sub-branch
259
260 backtrace = cur.mostCommon # backtrace is a class Letter. It's used as a placeholder to back trace
261 # from the last letter of the mostCommon word in the
262 # sub-tree up through the tree until we meet ourself at cur. We'll
263 # build the guess text this way
264
265 returnText = "" # returnText is a string. This will act as a buffer to hold the string
266 # we build.
267
268 while cur is not backtrace: # Here's the loop. We loop until we've snaked our way back to cur
269
270 returnText = backtrace.asciiChar + returnText # Create a new string that is the character at backtrace + everything
271 # so far. So, for "tion" we'll build "n","on","ion","tion" as we ..
272
273 backtrace = backtrace.parentNode # ... climb back towards cur
274
275 return returnText # And, having reached here, we've met up with cur, and returnText holds
276 # the string we built. Return it.
277
278 else: # This is the else to the original if.
279 # If the letter typed isn't in a sub branch, then
280 # the letter being typed isn't in our tree, so
281 return "" # return the empty string
282
283
284 # End of class LetterTree!
285
286
287
288 class _SingletonKey(object):
289 def __new__(cls, *args, **kwargs):
290 if not hasattr(cls, '_inst'):
291 cls._inst = super(_SingletonKey, cls).__new__(cls, *args, **kwargs)
292 return cls._inst
293
294 __key = _SingletonKey()
295 LetterTree = LetterTreeClass(__key)
296
297 # This class extends wx.TextCtrl
298 #
299 # Extends: wx.TextCtrl
300 #
301 # Overrides:
302 # wx.TextCtrl.__init__(self,parent,id,value,size,style,name)
303 # wx.TextCtrl.OnChar(self,Event)
304 #
305 # Defines:
306 # findWord(self,insert,st)
307 class predTextCtrl(wx.TextCtrl):
308
309 # Initialization subroutine.
310 #
311 # self : instance of self
312 # parent: reference to parent window (wxWindow, me-thinks)
313 # id: new Window Id, default to -1 to create new (I think: see docs for wxPython)
314 # value: String that is the initial value the control holds, defaulting to ""
315 # size: defaults to wx.DefaultSize
316 # style: defaults to 0
317 # name: defaults to "text"
318 # keyHook: must be a function pointer that takes self and a GetKeyCode object
319 # validator: defaults to None
320 #
321 # Note: These parameters are essentially just passed back to the native wx.TextCtrl.
322 # I basically just included (stole) enough of them from chatutils.py to make
323 # it work. Known missing args are pos and validator, which aren't used by
324 # chatutils.py.
325 #
326 # Returns: None
327 #
328 # Purpose: Constructor for predTextCtrl. Calls wx.TextCtrl.__init__ to get default init
329 # behavior and then inits a LetterTree and captures the parent for later use in
330 # passing events up the chain.
331 def __init__(self, parent, id = -1, value = "" , size = wx.DefaultSize, style = 0, name = "text",keyHook = None, validator=None):
332
333 # Call super() for default behavior
334 if validator:
335 wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name, validator=validator )
336 else:
337 wx.TextCtrl.__init__(self, parent, id=id, value=value, size=size, style=style, name=name)
338
339 self.tree = LetterTree # Instantiate a new LetterTree.
340 # TODO: make name of word file an argument.
341
342
343
344
345 self.parent = parent # Save parent for later use in passing KeyEvents
346
347 self.cur = self.tree.rootNode # self.cur is a short cut placeholder for typing consecutive chars
348 # It may be vestigal
349
350 self.keyHook = keyHook # Save the keyHook passed in
351
352
353 # findWord subroutine.
354 #
355 # self : instance of self
356 # insert: index of last char in st
357 # st : string from insert to the left
358 #
359 # Note: This implementation is about the third one for this method. Originally,
360 # st was an arbitrary string and insert was the point within
361 # this string to begin looking left. Since, I finally got it
362 # to work right as it is around 2 in the morning, I'm not touching it, for now.
363 #
364 # Returns: String that is the word or "" if none found. This generally doesn't
365 # happen, as st usually ends in a letter, which will be returned.
366 #
367 # Purpose: This function is generally used to figure out the beginning of the
368 # current word being typed, for later use in a LetterTree.getPrediction()
369 def findWord(self,insert,st):
370
371 # Good luck reading this one. Basically, I started out with an idea, and fiddled with the
372 # constants as best I could until it worked. It's not a piece of work of which I'm too
373 # proud. Basically, it's intent is to check each character to the left until it finds one
374 # that isn't a letter. If it finds such a character, it stops and returns the slice
375 # from that point to insert. Otherwise, it returns the whole thing, due to begin being
376 # initialized to 0
377
378 begin = 0
379 for offset in range(insert - 1):
380 if st[-(offset + 2)] not in string.letters:
381 begin = insert - (offset + 1)
382 break
383 return st[begin:insert]
384
385
386 # OnChar subroutine.
387 #
388 # self : instance of self
389 # event: a GetKeyCode object
390 #
391 # Returns: None
392 #
393 # Purpose: This function is the key event handler for predTextCtrl. It handles what it
394 # needs to and passes the event on to it's parent's OnChar method.
395 def OnChar(self,event):
396
397 # Before we do anything, call the keyHook handler, if not None
398 # This is currently used to implement the typing/not_typing messages in a manner that
399 # doesn't place the code here. Maybe I should reconsider that. :)
400 if(self.keyHook):
401 if self.keyHook(event) == 1: # if the passed in keyHook routine returns a one, it wants no predictive behavior
402 self.parent.OnChar(event)
403 return
404
405
406
407 # This bit converts the GetKeyCode() return (int) to a char if it's in a certain range
408 asciiKey = ""
409 if (event.GetKeyCode() < 256) and (event.GetKeyCode() > 19):
410 asciiKey = chr(event.GetKeyCode())
411
412
413 if asciiKey == "": # If we didn't convert it to a char, then process based on the int GetKeyCodes
414
415 if event.GetKeyCode() == wx.WXK_TAB: # We want to hook tabs to allow the user to signify acceptance of a
416 # predicted word.
417 # Handle Tab key
418
419 fromPos = toPos = 0 # get the current selection range
420 (fromPos,toPos) = self.GetSelection()
421
422 if (toPos - fromPos) == 0: # if there is no selection, pass tab on
423 self.parent.OnChar(event)
424 return
425
426 else: # This means at least one char is selected
427
428 self.SetInsertionPoint(toPos) # move the insertion point to the end of the selection
429 self.SetSelection(toPos,toPos) # and set the selection to no chars
430 # The prediction, if any, had been inserted into the text earlier, so
431 # moving the insertion point to the spot directly afterwards is
432 # equivalent to acceptance. Without this, the next typed key would
433 # clobber the prediction.
434
435 return # Don't pass tab on in this case
436
437 elif event.GetKeyCode() == wx.WXK_RETURN: # We want to hook returns, so that we can update the word list
438
439 st = self.GetValue() # Grab the text from the control
440 newSt = "" # Init a buffer
441
442 # This block of code, by popular demand, changes the behavior of the control to ignore any prediction that
443 # hasn't been "accepted" when the enter key is struck.
444 (startSel,endSel) = self.GetSelection() # get the curren selection
445
446 #
447 # Start update
448 # Changed the following to allow for more friendly behavior in
449 # a multilined predTextCtrl.
450 #
451 # front = st[:startSel] # Slice off the text to the front of where we are
452 # back = st[endSel:] # Slice off the text to the end from where we are
453 # st = front + back # This expression creates a string that get rid of any selected text.
454 # self.SetValue(st)
455
456 self.Remove( startSel, endSel )
457 st = string.strip( self.GetValue() )
458 #
459 # End update
460 #
461
462 # this loop will walk through every character in st and add it to
463 # newSt if it's a letter. If it's not a letter, (e.g. a comma or
464 # hyphen) a space is added to newSt in it's place.
465 for ch in st:
466 if ch not in string.letters:
467 newSt += " "
468 else:
469 newSt += ch
470
471 # Now that we've got a string of just letter sequences (words) and spaces
472 # split it and to a LetterTree.incWord on the lowercase version of it.
473 # Reminder: incWord will increment the priority of existing words and add
474 # new ones
475 for aWord in string.split(newSt):
476 self.tree.incWord(string.lower(aWord))
477
478 self.parent.OnChar(event) # Now that all of the words are added, pass the event and return
479 return
480
481 # We want to capture the right arrow key to fix a slight UI bug that occurs when one right arrows
482 # out of a selection. I set the InsertionPoint to the beginning of the selection. When the default
483 # right arrow event occurs, the selection goes away, but the cursor is in an unexpected location.
484 # This snippet fixes this behavior and then passes on the event.
485 elif event.GetKeyCode() == wx.WXK_RIGHT:
486 (startSel,endSel) = self.GetSelection()
487 self.SetInsertionPoint(endSel)
488 self.parent.OnChar(event)
489 return
490
491 # Ditto as wx.WXK_RIGHT, but for completeness sake
492 elif event.GetKeyCode() == wx.WXK_LEFT:
493 (startSel,endSel) = self.GetSelection()
494 self.SetInsertionPoint(startSel)
495 self.parent.OnChar(event)
496 return
497
498
499 else:
500 # Handle any other non-ascii events by calling parent's OnChar()
501 self.parent.OnChar(event) #Call super.OnChar to get default behavior
502 return
503
504
505 elif asciiKey in string.letters:
506 # This is the real meat and potatoes of predTextCtrl. This is where most of the
507 # wx.TextCtrl logic is changed.
508
509 (startSel,endSel) = self.GetSelection() # get the curren selection
510 st = self.GetValue() # and the text in the control
511 front = st[:startSel] # Slice off the text to the front of where we are
512 back = st[endSel:] # Slice off the text to the end from where we are
513
514 st = front + asciiKey + back # This expression creates a string that will insert the
515 # typed character (asciiKey is generated at the
516 # beginning of OnChar()) into the text. If there
517 # was text selected, that text will not be part
518 # of the new string, due to the way front and back
519 # were sliced.
520
521 insert = startSel + 1 # creates an int that denotes where the new InsertionPoint
522 # should be.
523
524 curWord = "" # Assume there's a problem with finding the curWord
525
526 if (len(back) == 0) or (back[0] not in string.letters): # We should only insert a prediction if we are typing
527 # at the end of a word, not in the middle. There are
528 # three cases: we are typing at the end of the string or
529 # we are typing in the middle of the string and the next
530 # char is NOT a letter or we are typing in the middle of the
531 # string and the next char IS a letter. Only the former two
532 # cases denote that we should make a prediction
533 # Note: The order of the two logical clauses here is important!
534 # If len(back) == 0, then the expression back[0] will
535 # blow up with an out of bounds array subscript. Luckily
536 # the or operator is a short-circuit operator and in this
537 # case will only evaluate back[0] if len(back) != 0, in
538 # which we're safely in bounds.
539
540 curWord = self.findWord(insert,front + asciiKey) # Now that we know we're supposed to make a prediction,
541 # let's find what word root to use in our prediction.
542 # Note: This is using the confusing findWord method. I
543 # send it insert and the text from the beginning
544 # of the text through the key just entered. This is
545 # NOT the original usage, but it does work. See
546 # findWord() for more details.
547
548 else: # Here, we've found we're in the middle of a word, so we're
549 # going to call the parent's event handler.
550 self.parent.OnChar(event)
551 return
552
553 if curWord == "": # Here, we do a quick check to make sure we have a good root
554 # word. If not, allow the default thing to happen. Of course,
555 # now that I'm documenting this, it occurs to me to wonder why
556 # I didn't do the same thing I just talked about. Hmmmmmm.
557
558 self.parent.OnChar(event) # we're done here
559 return
560
561 self.cur = self.tree.findWordNode(string.lower(curWord[:-1])) # Still with me? We're almost done. At this point, we
562 # need to convert our word string to a Letter node,
563 # because that's what getPrediction expects. Notice
564 # that we're feeding in the string with the last
565 # char sliced off. For developmentally historical
566 # reasons, getPrediction wants the node just before
567 # the typed character and the typed char separately.
568
569
570 if self.cur is None:
571 self.parent.OnChar(event) # if there's no word or no match, we're done
572 return
573
574
575 # get the prediction
576 predictText = self.tree.getPrediction(string.lower(asciiKey),self.cur) # This is the big prediction, as noted above
577 # Note the use of string.lower() because we
578 # keep the word list in all lower case,but we
579 # want to be able to match any capitalization
580
581 if predictText == "":
582 self.parent.OnChar(event) # if there's no prediction, we're done
583 return
584
585 # And now for the big finale. We're going to take the string st
586 # we created earlier and insert the prediction right after the
587 # newly typed character.
588 front = st[:insert] # Grab a new front from st
589 back = st[insert:] # Grab a new back
590
591 st = front + predictText + back # Insert the prediction
592
593 self.SetValue(st) # Now, overwrite the controls text with the new text
594 self.SetInsertionPoint(insert) # Set the proper insertion point, directly behind the
595 # newly typed character and directly in front of the
596 # predicted text.
597
598 self.SetSelection(insert,insert+len(predictText)) # Very important! Set the selection to encompass the predicted
599 # text. This way, the user can ignore the prediction by simply
600 # continuing to type. Remember, if the user wants the prediction
601 # s/he must strike the tab key at this point. Of course, one could
602 # just use the right arrow key as well, but that's not as easy to
603 # reach.
604
605 return # Done! Do NOT pass the event on at this point, because it's all done.
606
607
608 else:
609 # Handle every other non-letter ascii (e.g. semicolon) by passing the event on.
610 self.parent.OnChar(event) #Call super.OnChar to get default behavior
611 return
612
613 # End of class predTextCtrl!