import Tkinter PREFIX = "tkController" class Controller(object): def __init__(self, master=None): if master is None: master = Tkinter._default_root assert master is not None self.tag = PREFIX + str(id(self)) def bind(event, handler): master.bind_class(self.tag, event, handler) self.create(bind) def install(self, widget): widgetclass = widget.winfo_class() # remove widget class bindings and other controllers tags = [self.tag] for tag in widget.bindtags(): if tag != widgetclass and tag[:len(PREFIX)] != PREFIX: tags.append(tag) widget.bindtags(tuple(tags)) def create(self, handle): # override if necessary # the default implementation looks for decorated methods for key in dir(self): method = getattr(self, key) if hasattr(method, "tkevent") and callable(method): for eventSequence in method.tkevent: handle(eventSequence, method) def bind(*events): def decorator(func): func.tkevent = events return func return decorator class KBController( Controller ): def __init__( self ): Controller.__init__( self ) self._alt = False self._control = False self._lock = False self._meta = False self._shift = False @bind("") # type 2 def KeyPress(self, event): if event.keysym in ('Alt_L', 'Alt_R'): self._alt = True elif event.keysym in ('Control_L', 'Control_R'): self._control = True elif event.keysym == 'Caps_Lock': self._lock = True elif event.keysym in ('Meta_L', 'Meta_R'): self._meta = True elif event.keysym in ( 'Shift_L', 'Shift_R'): self._shift = True elif (len(event.char) > 0) and (32 <= ord(event.char) <= 127): self.onTypedCharacterKey( event ) else: self.onTypedSpecialKey( event ) @bind("") # type 3 def KeyRelease( self, event ): if event.keysym in ('Alt_L', 'Alt_R'): self._alt = False elif event.keysym in ('Control_L', 'Control_R'): self._control = False elif event.keysym == 'Caps_Lock': self._lock = False elif event.keysym in ('Meta_L', 'Meta_R'): self._meta = False elif event.keysym in ('Shift_L', 'Shift_R'): self._shift = False def onTypedCharacterKey( self, event ): '''Override to handle typing of any printable keyboard character, Typed character is in event.char (which accounts for shift).''' pass def onTypedSpecialKey( self, event ): '''Override to handle typing of any special characters (tab, \n, backspace, delete, home, prior, insert, etc. Any any key combinations involving Alt or Control.''' pass class EnhancedTextController( KBController ): def __init__( self ): KBController.__init__( self ) self._insert_x_pos = None def onTypedCharacterKey( self, event ): try: event.widget.sel_delete( ) except: pass event.widget.insert( 'insert', event.char ) event.widget.sel_clear( ) self._insert_x_pos = event.widget.bbox( 'insert' )[0] def moveCarrot( self, event ): widget = event.widget if self._shift: if not widget.sel_isAnchorSet(): widget.sel_setAnchor( 'insert' ) else: widget.sel_clear( ) if event.keysym in ( 'Home', 'KP_Home' ): if self._control: # Move to beginning of text widget.mark_set( 'insert', '1.0' ) else: # Move to front of line widget.mark_set( 'insert', widget.dline_start('insert') ) elif event.keysym in ( 'End', 'KP_End' ): if self._control: # Move to end of text widget.mark_set( 'insert', 'end' ) else: # Move to end of line widget.mark_set( 'insert', widget.dline_end('insert') ) elif event.keysym == 'Right': if self._control: # Move by word currentPos = widget.index( 'insert' ) maxPos = widget.index( 'end wordstart' ) if currentPos == maxPos: return offset = 1 while widget.compare( currentPos, '==', widget.index('insert') ): widget.mark_set( 'insert', 'insert wordend +%dc wordstart' % offset ) offset += 1 else: # Move by character widget.mark_set( 'insert', 'insert +1 chars' ) elif event.keysym == 'Left': if self._control: # Move by word currentPos = widget.index( 'insert' ) minPos = widget.index( '1.0 wordstart' ) if currentPos == minPos: return offset = 2 widget.mark_set( 'insert', 'insert wordstart' ) while widget.compare( currentPos, '==', widget.index('insert') ): widget.mark_set( 'insert', 'insert -%dc wordstart' % offset ) offset += 1 else: # Move by character widget.mark_set( 'insert', 'insert -1 chars' ) elif event.keysym == 'Down': if self._control: # Move by Paragraph widget.mark_set( 'insert', 'insert +1 lines' ) else: # Move by line widget.mark_set( 'insert', widget.dline_next( 'insert', useThisX=self._insert_x_pos ) ) elif event.keysym == 'Up': if self._control: # Move by Paragraph widget.mark_set( 'insert', 'insert -1 lines' ) pass else: # Move by line widget.mark_set( 'insert', widget.dline_prev( 'insert', useThisX=self._insert_x_pos ) ) elif event.keysym == 'Prior': if self._control: pass else: # Move by page event.widget.yview_scroll( -1, 'pages' ) elif event.keysym == 'Next': if self._control: pass else: # Move by page event.widget.yview_scroll( 1, 'pages' ) widget.see( 'insert' ) if event.keysym not in ('Up','Down'): self._insert_x_pos = event.widget.bbox( 'insert' )[0] def typeSpecial( self, event ): widget = event.widget if event.keysym in ( 'Return', 'Enter', 'KP_Enter' ): try: widget.sel_delete( ) finally: widget.insert( 'insert', '\n' ) elif event.keysym == 'Tab': widget.insert( 'insert', '\t' ) elif event.keysym == 'BackSpace': try: widget.sel_delete( ) except: widget.delete( 'insert -1 char', 'insert' ) elif event.keysym in ( 'Delete', 'KP_Delete' ): try: widget.sel_delete( ) except: widget.delete( 'insert', 'insert +1 char' ) widget.sel_clear() self._insert_x_pos = event.widget.bbox( 'insert' )[0] def onTypedSpecialKey( self, event ): widget = event.widget if event.keysym in ( 'Return','Enter','KP_Enter','Tab','BackSpace','Delete','Insert' ): self.typeSpecial( event ) elif event.keysym in ( 'Up', 'Down', 'Left', 'Right', 'Home', 'End', 'Prior', 'Next' 'KP_Up', 'KP_Down', 'KP_Left', 'KP_Right', 'KP_Home', 'KP_End', 'KP_Prior', 'KP_Next' ): self.moveCarrot( event ) elif self._control: if event.keysym == 'a': # select all self._selectionAnchor = '1.0' widget.mark_set( 'insert', 'end' ) elif event.keysym == 'c': # copy try: widget.clipboard_append( widget.get( 'sel.first', 'sel.last' ) ) except: pass elif event.keysym == 'r': # Redo try: widget.edit_redo( ) except: pass widget.sel_clear( ) elif event.keysym == 'v': # paste try: widget.mark_set( 'insert', 'sel.first' ) widget.delete( 'sel.first', 'sel.last' ) except: pass widget.insert( 'insert', widget.clipboard_get( ) ) self.sel_clear( ) elif event.keysym == 'x': # cut try: widget.clipboard_append( widget.get( 'sel.first', 'sel.last' ) ) widget.delete( 'sel.first', 'sel.last' ) widget.ins_updateTags( ) except: pass widget.sel_clear( ) elif event.keysym == 'z': # Undo try: widget.edit_undo( ) except: pass self.sel_clear( ) self._insert_x_pos = event.widget.bbox( 'insert' )[0] @bind( '' ) def click( self, event ): event.widget.focus_set( ) if not self._shift and not self._control: event.widget.sel_clear( ) event.widget.sel_setAnchor( 'current' ) self._insert_x_pos = event.x @bind( '', '' ) def dragSelection( self, event ): widget = event.widget if event.y < 0: widget.yview_scroll( -1, 'units' ) elif event.y >= widget.winfo_height(): widget.yview_scroll( 1, 'units' ) if not widget.sel_isAnchorSet( ): widget.self_setAnchor( '@%d,%d' % (event.x+2, event.y) ) widget.mark_set( 'insert', '@%d,%d' % (event.x+2, event.y) ) self._insert_x_pos = event.x @bind( '' ) def moveCarrot_deselect( self, event ): widget = event.widget widget.grab_release() widget.mark_set( 'insert', 'current' ) self._insert_x_pos = event.x @bind( '' ) def selectWord( self, event ): event.widget.sel_setAnchor( 'insert wordstart' ) event.widget.mark_set( 'insert', 'insert wordend' ) self._insert_x_pos = event.x @bind( '' ) def selectLine( self, event ): event.widget.sel_setAnchor( 'insert linestart' ) event.widget.mark_set( 'insert', 'insert lineend' ) self._insert_x_pos = event.x @bind( '' ) def scrollView( self, event ): widget = event.widget if event.y < 0: widget.yview_scroll( -1, 'units' ) elif event.y >= widget.winfo_height(): widget.yview_scroll( 1, 'units' ) widget.grab_set( ) @bind( '' ) def wheelScroll( self, event ): widget = event.widget if event.delta < 0: widget.yview_scroll( 1, 'units' ) else: widget.yview_scroll( -1, 'units' ) class EnhancedText( Tkinter.Text ): def __init__( self, parent, **options ): Tkinter.Text.__init__( self, parent, **options ) controller = EnhancedTextController( ) controller.install( self ) # Selection Operations def sel_clear( self ): try: self.tag_remove( 'sel', '1.0', 'end' ) except: pass try: self.mark_unset( 'sel.anchor', 'sel.first', 'sel.last' ) except: pass def sel_setAnchor( self, index ): self.mark_set( 'sel.anchor', index ) def sel_isAnchorSet( self ): try: self.index( 'sel.anchor' ) return True except: return False def sel_isSelection( self ): try: self.index( 'sel.first' ) return True except: return False def sel_update( self ): if widget.compare( 'sel.anchor', '<', 'insert' ): widget.mark_set( 'sel.first', 'sel.anchor' ) widget.mark_set( 'sel.last', 'insert' ) elif widget.compare( 'sel.anchor', '>', 'insert' ): widget.mark_set( 'sel.first', 'insert' ) widget.mark_set( 'sel.last', 'sel.anchor' ) else: return widget.tag_remove( 'sel', '1.0', 'end' ) widget.tag_add( 'sel', 'sel.first', 'sel.last' ) def sel_delete( self ): try: Tkinter.Text.delete( self, 'sel.first', 'sel.last' ) except: pass self.sel_clear( ) # Display Lines def dline_prev( self, index, useThisX=None ): x,y,width,height = self.bbox( index ) if (y-height) < 0: self.yview_scroll( -1, 'units' ) x,y,width,height = self.bbox( index ) if useThisX: x = useThisX insertIndex = self.index( index ) newY = y - height newIndex = self.index( '@%d,%d' % ( x, newY ) ) while (self.compare( newIndex, '==', insertIndex)) and (self.compare( newIndex, '>', '1.0' )): newY -= height newIndex = self.index( '@%d,%d' % ( x, newY ) ) return newIndex def dline_next( self, index, useThisX=None ): x,y,width,height = self.bbox( index ) insertIndex = self.index( index ) if useThisX: x = useThisX newY = y + height newIndex = self.index( '@%d,%d' % ( x, newY ) ) while (self.compare( newIndex, '==', insertIndex)) and (self.compare( newIndex, '<', 'end -1 chars' )): newY += height newIndex = self.index( '@%d,%d' % ( x, newY ) ) return newIndex def dline_start( self, index ): x,y,width,height = self.bbox( index ) return self.index( '@%d,%d' % (0, y) ) def dline_end( self, index ): x,y,width,height = self.bbox( index ) return self.index( '@%d,%d' % ( self.winfo_width(), y ) ) # Overloads def mark_set( self, name, index ): Tkinter.Text.mark_set( self, name, index ) if name == 'insert': try: if self.compare( 'sel.anchor', '<', 'insert' ): self.mark_set( 'sel.first', 'sel.anchor' ) self.mark_set( 'sel.last', 'insert' ) elif self.compare( 'sel.anchor', '>', 'insert' ): self.mark_set( 'sel.first', 'insert' ) self.mark_set( 'sel.last', 'sel.anchor' ) else: return self.tag_remove( 'sel', '1.0', 'end' ) self.tag_add( 'sel', 'sel.first', 'sel.last' ) except: pass sample = '''zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen twenty. twenty-one twenty-two twenty-three twenty-four twenty-five twenty-six twenty-seven. thirty-one thirty-two thirty-three thirty-four thirty-five thirty-six thirty-seven thirty-eight.''' root = Tkinter.Tk() text = EnhancedText( root ) text.pack() text.insert( 'end', sample ) root.mainloop()