3

I used the following solution from this earlier question to track whether a tkinter text widget has been modified:

import tkinter as tk


class CustomText(tk.Text):
    def __init__(self, *args, **kwargs):
        """A text widget that report on internal widget commands"""
        tk.Text.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)

    def _proxy(self, command, *args):
        cmd = (self._orig, command) + args
        result = self.tk.call(cmd)

        if command in ("insert", "delete", "replace"):
            self.event_generate("<<TextModified>>")

        return result


root = tk.Tk()
label = tk.Label(root, anchor="w")
text = CustomText(root, width=40, height=4)

label.pack(side="bottom", fill="x")
text.pack(side="top", fill="both", expand=True)


def onModification(event):
    chars = len(event.widget.get("1.0", "end-1c"))
    label.configure(text="%s chars" % chars)


text.bind("<<TextModified>>", onModification)

root.mainloop()

However, after integrating it into my own code, I discovered a problem in both my code and the bare solution above. If you attempt to paste into the text widget, the entire program will crash. The terminal gives the following error:

Traceback (most recent call last):
  File "Test.py", line 39, in <module>
    root.mainloop()
  File "AppData\Local\Programs\Python\Python36-32\lib\tkinter\__init__.py", line 1277, in mainloop
    self.tk.mainloop(n)
  File "Test.py", line 16, in _proxy
    result = self.tk.call(cmd)
_tkinter.TclError: text doesn't contain any characters tagged with "sel"

(I removed identifying information in the file paths above, but otherwise that's copied straight from the console.)

After a bit of testing, it turns out that you can paste without crashing the program as long as you have some text selected/highlighted when you paste.

This behavior does not occur in the unmodified text widget; you can paste without selecting text beforehand as normal, no crashing.

So, my question is, how can I modify the above solution such that pasting does not crash it? I am not familiar with Tcl/Tk, so I don't know how to start investigating this. This is in Python 3.6.3.

(I also would have contacted the original author of this code directly, but it turns out there's no private messaging feature here and I can't leave a comment as a new user.)

Edit: I have working code now, though the solution feels like it's held together with duct tape instead of actually addressing the underlying problem. I altered the CustomText class as follows:

class CustomText(tk.Text):
    def __init__(self, *args, **kwargs):
        """A text widget that report on internal widget commands"""
        tk.Text.__init__(self, *args, **kwargs)

        # create a proxy for the underlying widget
        self._orig = self._w + "_orig"
        self.tk.call("rename", self._w, self._orig)
        self.tk.createcommand(self._w, self._proxy)
        self.bind("<<Paste>>", self.Paste)

    def _proxy(self, command, *args):
        cmd = (self._orig, command) + args
        result = self.tk.call(cmd)

        if command in ("insert", "delete", "replace"):
            self.event_generate("<<TextModified>>")

        return result

    def Paste(self, event):
        tagranges = self.tag_ranges("sel")
        if tagranges:
            selectionstart = self.index(tk.SEL_FIRST)
            selectionend = self.index(tk.SEL_LAST)
            self.delete(selectionstart, selectionend)
            self.mark_set(tk.INSERT, selectionstart)
        self.insert(tk.INSERT, root.clipboard_get())
        self.see(tk.INSERT)
        return "break"

By binding "<<Paste>>" to a function that ends in return "break", I can stop the widget from passing the event along to whatever is causing the crash, and the modification event still fires as expected. Amusingly, I can code my own paste function that precedes the return "break" line, and it functions as I believe it should have from the start.

Still no idea what causes this issue, except that it's apparently a Windows problem (thanks for checking it out, Bryan Oakley).

Snackhole
  • 75
  • 7
  • 1
    your code works fine for me. I don't get a crash, with or without anything in the text widget or selected in the text widget. Unfortunately, I don't have a windows machine to test on. – Bryan Oakley Nov 08 '17 at 18:51
  • I edited in a slapped-together solution, though I still have no idea what causes the problem in the first place beyond it apparently being OS-dependent. Thanks for taking a look! – Snackhole Nov 08 '17 at 20:14
  • 1
    I could replicate it on Windows 10 on python 3.6 – Ethan Field Nov 08 '17 at 22:41

1 Answers1

0

You can bind on the event "<Control-v>" and "<Control-c>".

This means that you could setup a special condition to handle it differently if a user uses either of those.

from tkinter import *

root = Tk()

text = Text(root)
text.pack()

def callback(*args):
    print(True)

text.bind("<Control-v>", callback)

root.mainloop()

This is more of a workaround rather than a solution, as this leaves you open to potential crashes from other keyboard combinations which we don't know about.

I'll leave my answer here unless anyone knows of a better solution.

Ethan Field
  • 4,646
  • 3
  • 22
  • 43
  • Unfortunately, that didn't work. It just adds a callback that fires in addition to the crashing. For example, `text.bind("", lambda event: print("Paste intercepted"))` will print "Paste intercepted" to the console right before it crashes. I also tried this with the `"<>"` event, with the same result. – Snackhole Nov 08 '17 at 16:56
  • 1
    Sorry, I didn't make myself clear enough. You'd need to somehow integrate a way to prevent the calling of your custom event when these events are triggered. This is why my solution is more of a workaround than a solution. – Ethan Field Nov 08 '17 at 16:58
  • I'll accept this answer for now because it lead me to working code, even if it's a bit hacky and doesn't actually address the bug. Just have to bind `""` to a callback that ends in `return "break"` so the event doesn't propagate past the customized widget. You can then code your own paste functionality before the return to simulate it working as it was supposed to all along... If someone comes up with a better answer, I'd be happy, though. – Snackhole Nov 08 '17 at 20:00
  • If someone has a better answer, I'll happily remove mine – Ethan Field Nov 08 '17 at 22:40