Discussion:
Tkinter Text widget reading input (how to do overwrite mode?)
Jeff Epler
2002-04-29 15:00:39 UTC
Permalink
I have implemented an 'overwrite mode' in the Text widget in tcl.
Conversion to Python is left as an exercise to the reader.
Make your Text widget an OverwriteText by calling 'MakeOverwriteText'
on it, which inserts OverwriteText in the list of bindtags.
Tested (minimally) on tk8.4 CVS, not on earlier versions.

proc TextOverwrite {w s} {
set len [string length $s]
$w delete insert insert+${len}c
}

# The overwrite flag is stored as a tag, if it's present
# overwrite mode is active
proc TextIsOverwrite {w} {
expr {[lsearch [$w tag names] overwrite_flag] != -1}
}

proc TextSetOverwrite {w} {
$w tag add overwrite_flag 0.0 end
}

proc TextSetInsert {w} {
$w tag delete overwrite_flag
}

proc TextToggleOverwrite {w} {
if {[TextIsOverwrite $w]} {
TextSetInsert $w
} else {
TextSetOverwrite $w
}
}

bind OverwriteText <KeyPress> {
if {[TextIsOverwrite %W]} { TextOverwrite %W %A }
# fall through to Text's <KeyPress> binding
}
bind OverwriteText <Insert> {
TextToggleOverwrite %W
break; # apparently <Insert> pastes selection, don't fall through
}

proc MakeOverwriteText { w } {
set bt [bindtags $w]
set t [lsearch $w Text]
bindtags $w [linsert $bt $t OverwriteText]
}

#text .t
#pack .t
#MakeOverwriteText .t
J.Jacob
2002-04-30 11:03:12 UTC
Permalink
Thank you !
Frankly i do not understand all of the code, maybe you could put
comments in for Tk newbies? :-), but a quick test here did show it
worked. With this i think i can build a readxy() function as i
described in the earlier post with 'overwrite-mode' as i intended.
Jeff Epler
2002-04-30 16:09:21 UTC
Permalink
Here's another version of the code, with a small enhancement plus
a number of comments.


import Tkinter, sys

def comparable_index(index):
""" Convert a Text index like '37.15' to a tuple that can be compared
with Python's normal comparison operators """
return map(int, index.split("."))

class OverwriteText(Tkinter.Text):
def __init__(self, *args, **kw):
Tkinter.Text.__init__(self, *args, **kw) # Create the widget

# Insert the OverwriteText bindings in the widget tags.
# When an event happens, Tk starts with the most specific tag
# and unless processing is stopped, it moves to the next
# less specific tag. At each step, the best match for the
# event is determined, and that binding is executed.

# Usually a widget's tags look like
# .full.path WidgetClass .toplevel all
# where .full.path is the Tk name for the widget, and .toplevel
# is the name for the toplevel window containing it (often ".").
# Because we want to modify the behavior of the Text widget,
# we insert this new tag, OverwriteText just before Text.
# It *should* always be between indices 0 and 1, but we go through
# this activity anyway.
tags = list(self.bindtags())
idx = tags.index("Text")
tags[idx:idx] = ["OverwriteText"]
self.bindtags(tuple(tags))
self.is_overwrite = 0

# bind_class() with one argument returns all the bindings for that
# class. If none exist, then create them. This notion of the "class"
# is distinct from Python classes
if not self.bind_class("OverwriteText"):
self._init_bindings()

def _init_bindings(self):
# Create two bindings, one that will change the way <KeyPress> works,
# and one that will replace the old behavior of the Insert key with
# a new one (toggle overwrite mode)

# Just writing self.bind_class(..., OverwriteText.KeyPress) would not
# work, because the first (only) arg would be "evt" not an instance
# of OverwriteText

# The following bindings are necessary due to the Tk event matching
# machinery. We don't want to treat Alt-X the same as X, and the way
# Tk does this is by creating a more specific binding which performs
# no action. The whole thing is complicated, and explained in the
# Tk manual page for "bind" under the "MULTIPLE MATCHES" section.
# http://tcl.activestate.com/man/tcl8.2.3/TkCmd/bind.htm#M41
nop = "# nothing"
self.bind_class("OverwriteText", "<Alt-KeyPress>", nop)
self.bind_class("OverwriteText", "<Meta-KeyPress>", nop)
self.bind_class("OverwriteText", "<Control-KeyPress>", nop)
self.bind_class("OverwriteText", "<Escape>", nop)
self.bind_class("OverwriteText", "<KP_Enter>", nop)
if sys.platform == "mac":
self.bind_class("OverwriteText", "<Command-KeyPress>", nop)

self.bind_class("OverwriteText", "<KeyPress>",
lambda evt: OverwriteText.KeyPress(evt.widget, evt))
self.bind_class("OverwriteText", "<Insert>",
lambda evt: OverwriteText.Insert(evt.widget, evt))

def toggle_overwrite(self):
self.is_overwrite = not self.is_overwrite

def KeyPress(self, evt):
# This is called when a key has been pressed, whether or not
# overwrite mode is enabled
if self.is_overwrite:
# We find where the end-of-line is
eol = self.index("insert lineend")
# And we also find the index for the characters we would ovwrrite
nextchar = self.index("insert + %dc" % len(evt.char))
eol_num = comparable_index(eol)
nextchar_num = comparable_index(nextchar)
if eol_num < nextchar_num:
# If the EOL is closer to the insert point, then delete only
# that far
index = eol
else:
# Otherwise, delete as many chars as we'll insert
index = nextchar
# Actually perform the deletion, from the insertion point to
# the index determined above
self.delete("insert", index)

def Insert(self, evt):
self.toggle_overwrite()
# Returning "break" from the binding function means that
# Tk will not pass this event on to the next tag. Otherwise,
# pressing Insert would toggle between insert and overwrite modes,
# and then insert the text from the clipboard
return "break"

def _test():
import sys
t = OverwriteText()
t.pack()
t.insert(0.0, open(sys.argv[0]).read())
t.mainloop()

# If you run this as a script (as opposed to importing it), create a sample
if __name__ == '__main__': _test()
Jeff Epler
2002-04-29 18:20:06 UTC
Permalink
Post by Jeff Epler
I have implemented an 'overwrite mode' in the Text widget in tcl.
Conversion to Python is left as an exercise to the reader.
Okay, I can't stand an unfinished exercise. In my "testing", I decided
that it could stand to be a little smarter, in particular when the
insertion point is at the end of a line and I'm in overwrite mode, I don't
expect the newline to be removed to make room for the new character.

Since arbitrary new attributes can be hung on a Python class, there's no
fooling around with a fake tag on the text widget .. yay Python!

Things still behave slightly oddly when the widget contains the
selection. If the overlined characters are selected and | shows the
insertion point, here's what happens when you overwrite "x":
____
abc|defghijk
becomes
abcx|ijk
This suggests that a test should be made for whether "nextchar" lies
inside the selection -- if so, the step inside Text's KeyPress binding
which deletes the selected text will delete it for us.

# --- overwritetext.py
import Tkinter

def comparable_index(index):
return map(int, index.split("."))

class OverwriteText(Tkinter.Text):
def __init__(self, *args, **kw):
Tkinter.Text.__init__(self, *args, **kw)
tags = list(self.bindtags())
idx = tags.index("Text")
tags[idx:idx] = ["OverwriteText"]
self.bindtags(tuple(tags))
self.is_overwrite = 0

if not self.bind_class("OverwriteText"):
self._init_bindings()

def _init_bindings(self):
self.bind_class("OverwriteText", "<KeyPress>",
lambda evt: OverwriteText.KeyPress(evt.widget, evt))
self.bind_class("OverwriteText", "<Insert>",
lambda evt: OverwriteText.Insert(evt.widget, evt))

def toggle_overwrite(self):
self.is_overwrite = not self.is_overwrite

def KeyPress(self, evt):
if self.is_overwrite:
eol = self.index("insert lineend")
nextchar = self.index("insert + %dc" % len(evt.char))
eol_num = comparable_index(eol)
nextchar_num = comparable_index(nextchar)
if eol_num < nextchar_num:
index = eol
else:
index = nextchar
self.delete("insert", index)

def Insert(self, evt):
self.toggle_overwrite()
return "break" # Don't insert clipboard text

def _test():
import sys
t = OverwriteText()
t.pack()
t.insert(0.0, open(sys.argv[0]).read())
t.mainloop()

if __name__ == '__main__': _test()
J.Jacob
2002-04-29 14:24:41 UTC
Permalink
Here is a question for somebody with experience with Tkinter Text widgets.
I have a Tkinter widget that inherits from Frame and it has a self.text
attribute of type Text.
To read user input in my widget (until the user presses RETURN) i add
the following two methods (this is part of my code):

def readxy(self, x=0, y=1):
where = `y`+'.'+`x`
self.startUserInput = where
self.userInput = StringVar()
self.userInput.set('')
self.text.mark_set(INSERT, where)
self.text.bind('<Return>', lambda event, self=self: self._onEnter(event))
self.text.focus()
self.text.update()
self.text.wait_variable(self.userInput)
return self.userInput.get()
def _onEnter(self, event):
self.userInput.set(self.text.get(self.startUserInput, INSERT))
self.text.mark_unset(INSERT)
self.text.unbind('<Return>', '')

This works fine, but the user is typing in 'INSERT' mode. That means that any
text on the line after the cursor is shifted right with each typed character.
My question is: how can i get an 'OVERWRITE' mode ?
Do i have to program that manually or is there a simple way to 'put a Text
widget in overwrite mode'. Did somebody do this before? Of course i searched
the net for an answer before asking it here on c.l.p. I noticed that IDLE
does not have an 'OVERWRITE' mode either, nor does any other editor
implemented with Tkinter i could find on the net.

Joost Jacob
www.liacs.nl/home/jjacob/

I have a very small mind and must live with it.
-- E. Dijkstra

Loading...