6
votes

I'm currently writing a color scheme editor. For the preview of the scheme, I use a text widget, where I insert text with the corresponding color tags (which I generate programmatically).

What I want is the following behaviour:

  • click anywhere on the text widget where no text is: change background color
  • click on text inserted with a tag: change tags corresponding foreground color

Now here's my problem:

When I click on a tagged text, the callback of the tag is called. So far so good. But then, the callback of the text widget is called as well, although I return "break" in the tags callback method (which should stop further event handling). How can I stop this?

To illustrate this specific problem, I wrote this working example (for Python 2 & 3):

#!/usr/bin/env python

try:
    from Tkinter import *
    from tkMessageBox import showinfo
except ImportError:
    from tkinter import *
    from tkinter.messagebox import showinfo

def on_click(event, widget_origin='?'):
    showinfo('Click', '"{}"" clicked'.format(widget_origin))
    return 'break'

root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
    tag_name = 'tag_{}'.format(i)
    text.tag_config(tag_name)
    text.tag_bind(tag_name, '<Button-1>',
        lambda e, w=tag_name: on_click(e, w))
    text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()

Any help is appreciated!

Edit: Tried Python 2 as well.

2

2 Answers

2
votes

Okay, after tkint tinkering around a bit, I was able to figure out a working solution. I think the problem is that tags are probably not subclassed from BaseWidget.

My workaround:

  • Make a seperate callback for the tags; set a variable there which keeps track of which tag was clicked
  • Let the event handler of the text widget decide what to do depending on the content of this variable

The workaround in code (sorry for using global here, but I just modified my questions simple example...):

#!/usr/bin/env python

try:
    from Tkinter import *
    from tkMessageBox import showinfo
except ImportError:
    from tkinter import *
    from tkinter.messagebox import showinfo

tag_to_handle = ''

def on_click(event, widget_origin='?'):
    global tag_to_handle
    if tag_to_handle:
        showinfo('Click', '"{}" clicked'.format(tag_to_handle))
        tag_to_handle = ''
    else:
        showinfo('Click', '"{}  " clicked'.format(widget_origin))

def on_tag_click(event, tag):
    global tag_to_handle
    tag_to_handle = tag

root = Tk()
text = Text(root)
text.pack()
text.insert(CURRENT, 'Some untagged text...\n')
text.bind('<Button-1>', lambda e, w='textwidget': on_click(e, w))
for i in range(5):
    tag_name = 'tag_{}'.format(i)
    text.tag_config(tag_name)
    text.tag_bind(tag_name, '<Button-1>',
        lambda e, w=tag_name: on_tag_click(e, w))
    text.insert(CURRENT, tag_name + ' ', tag_name)
root.mainloop()

I hope this is helpful for people having the same problem.

I'm still open to nicer solutions of course!

2
votes

Thanks for posting this question and for providing a solution. I can't count how many hours I lost trying to fix up the symptoms created by this behaviour. Weird Tk design decision that tag_bind is insenstive to return "break".

Following your idea to hijack the Text widget by binding it with the same event sequence as tag_bind, I have improved the solution, which enables now to simulate the expected return "break" behaviour of Tk's other bind+callback pairs. The idea is the following (full source below):

  1. create a wrapper around the wished callback, i.e. a callable class instance
  2. when the class instance is called, run callback and check its result.
    • if the result is "break", temporarily hijack the event propagation: bind the Text widget to the same event bound to tag_bind, with an empty callback. Then, after an idle time, unbind.
    • if the result is not "break": do nothing, the event will propagate to Text automatically

Here is a full working example. My specific problem was to get some sort of hyper text behaviour: ctrl-clicking on a hyper-text should not move the insertion point to the click's location. The example below shows that within the same callback wrapped in tag_bind, we can propagate or not the event to the Text widget, simply by returning "break" or another value.

try:
    # Python2
    import Tkinter as tk
except ImportError:
    # Python3
    import tkinter as tk

class TagBindWrapper:
    def __init__(self, sequence, callback):
        self.callback=callback
        self.sequence=sequence

    def __call__(self, event):
        if "break" == self.callback(event):
            global text
            self.bind_id=text.bind(self.sequence, self.break_tag_bind)
            return "break"
        else:
            return

    def break_tag_bind(self, event):
        global text
        # text.after(100, text.unbind(self.sequence, self.bind_id))
        text.after_idle(text.unbind, self.sequence, self.bind_id)
        return "break"



def callback_normal(event):
    print "normal text clicked"
    return "break"

def callback_hyper(event):
    print "hyper text clicked"
    if event.state & 0x004: # ctrl modifier
        return "break" # will not be passed on to text widget
    else:
        return # will be passed on to text widget

# setup Text widget
root=tk.Tk()
text = tk.Text(root)
text.pack()
text.tag_config("normal", foreground="black")
text.tag_config("hyper", foreground="blue")
text.tag_bind("hyper", "<Button-1>", TagBindWrapper("<Button-1>", callback_hyper))
text.tag_bind("normal", "<Button-1>", callback_normal)

# write some normal text and some hyper text
text.insert(tk.END, "normal text, ", "normal")
text.insert(tk.END, "hyper text (try normal-click and ctrl-click).", "hyper")

root.mainloop()

There is one simplification I couldn't find how to do: replace the wrapper call TagBindWrapper("<Button-1>", callback_hyper) by TagBindWrapper(callback_hyper), i.e. get the information of the event 'sequence' string ("<Button-1>") simply from the event object passed to __call__. Is it possible?