Event dependent change of precedence between conflicting default and user-defined bindings in Tkinter

Question:

I have a Python GUI with Tkinter. In a class, I have some Text widgets. These have some default bindings some of which I’d like to replace preferably with bind_class(). I managed to do it with all, except the ones with a capitalized hotkey, like Ctrl-E/A/B/F (i.e. Ctrl-Shift-e/a/b/f). These I can only change by applying the bind() method on every widget separately. I am wondering why is this and if there is a smarter way to do this with bind_class(), which otherwise seems to have no effect on the above mentioned default bindings.

Please consider the code below. After clicking on each text widget and trying all four hotkeys (Ctrl-b/B/y/Y), one can see that all of them work in both widgets as expected, except for Ctrl-B (i.e. Ctrl-Shift-b), which works only if it is bound to the widget with bind().

As I see this issue now: for a built-in function with a capitalized Key, the order of precedence is apparently the following: bind() > built-in function with a return 'break' at the end probably > bind_class(). This means, that bind_class() won’t ever work and the built-in function only gets called, if there’s no return 'break' at the end of the function referenced in bind(). At the same time, built-in hotkeys with lowercase keys (e.g. Ctrl-a/e/b/f) have somehow lower priority and even bind_class() can take precedence over them.

So my questions are:

  1. Is my above conclusion correct, or there’s something else to this?
  2. Is this the intended functionality of bind() and bind_class()?
  3. Is it somehow still possible to overwrite a built-in function like Ctrl-B using only bind_class() for all the widgets at once instead of bind()-ing them all one-by-one?

Any insightful comment is welcome. Thank you!

from tkinter import *

class Test:
    def __init__(self):
        self.window = Toplevel(root)
        self.window.lift()
        
        self.t1 = Text(self.window)
        self.t1.insert(1.0, 'Initial text in Text widget #1')
        #self.t1.bind('<Control-B>', self.func)
        self.t1.pack()
        
        self.t2 = Text(self.window)
        self.t2.insert(1.0, 'Initial text in Text widget #2')
        self.t2.bind('<Control-B>', self.func)
        self.t2.pack()
        
        self.t1.bind_class('Text', '<Control-b>', self.func)
        self.t1.bind_class('Text', '<Control-B>', self.func)   # This line has no effect.
        
        self.t1.bind_class('Text', '<Control-y>', self.func)
        self.t1.bind_class('Text', '<Control-Y>', self.func)
    
    def func(self, event):
        obj = root.focus_get()
        obj.insert(END, 'n'+str(event))
        return 'break'

root = Tk()
t1 = Test()
root.lower()
root.mainloop()
Asked By: Babber

||

Answers:

The order of precedence doesn’t change, it is the same for all widgets under all circumstances, and is always defined by the bindtags for the widget. So, to answer your first couple of questions: no, your conclusion isn’t correct, and yes, this is working as designed.

The problem in this case is that at least on some platforms, <Control-Shift-Key-B> is associated with the virtual event <<SelectPrevChar>> (and <Control-B> is the same as <Control-Shift-Key-B>). When you press control+B, the event gets translated into the <<SelectPrevChar>> virtual event before being passed to the widget. The class binding is on that event rather than the raw keypress. In other words, the widget never sees <Control-Shift-Key-B>, it only sees <<SelectPrevChar>>.

You can solve this problem in one of two ways:

  • you can remove the virtual event, in which case your class binding will work
  • you can change the class binding for the virtual event

To remove that specific raw event from the virtual event and add an explicit binding to the raw event sequence you can do this:

self.window.event_delete("<<SelectPrevChar>>", "<Control-Shift-Key-B>")
self.t1.bind_class("Text", "<Control-Shift-Key-B>", self.func)

To change the class binding for the virtual event you can use bind_class on the virtual event:

self.t1.bind_class("Text", "<<SelectPrevChar>>", self.func)

To see a list of all virtual events and the keys they are bound to you can run this little bit of code which uses the event_info command with no arguments to get a list of virtual events, and then uses the same method to get information about each event.

for event in self.window.event_info():
    info = self.window.event_info(event)
    print(f"{event:<20s} => {', '.join(info)}")

Answered By: Bryan Oakley