Reputation: 1380
The code below is part of a function that produces a simple dialog with caller selected text and buttons.
The problem lies in the handling of key inputs of 's', 'a', 'd', and 'c. The code operates correctly for mouse clicks and tabbing followed by the space or enter keys. The code is written in Python 3.4 and is being tested for compliance with Windows 7.
My understanding is that Tk handles refocusing for the end user's mouse clicks. The space and enter keys are used after the user has changed focus by tabbing. For all of these interactions the keys are bound to each button by the code:
for action in ('<1>', '<space>', '<Return>'):
b.bind(action, handler)
For 'underline' key input I believe my code needs to handle the refocusing prior to calling the handler. This is the purpose of the refocus routine. The print statement ("refocussing to...") is printed with the correct value for button.winfo_name() whenever 's', 'a', 's', or 'c' is pressed . This is why I believe button.focus_set()
is failing.
If it worked this would enable the handler to simply check which button was pressed by looking at event.widget.winfo_name()
. As it is, the failure to refocus means that the handler is called with the wrong button in event.widget.winfo_name()
If I move focus manually by tabbing then the focus gives the name returned by event.widget.winfo_name()
regardless of which of 's', 'a', 'd', or 'c' is pressed.
After reading other posts on Stack Overflow, I tried adding button.focus_force()
after button.focus_set()
. This had no effect on the problem.
I have tried passing the button name by changing the signature of the handler to def button_handler(event, *args)
and then changing the last line of refocus
to handler(event, button.winfo_name()) but *args
is empty when called.
def refocus_wrapper(button):
def refocus(event):
print("refocusing to '{}'".format(button.winfo_name()))
button.focus_set()
handler(event)
return refocus
for button_text, underline, button_name in buttons:
b = ttk.Button(button_inner_frame, text=button_text, name=button_name,
underline=underline)
b.pack(padx=2, side='left')
for action in ('<1>', '<space>', '<Return>'):
b.bind(action, handler)
action = '{}'.format(button_text[underline:underline + 1].lower())
dialog.bind(action, refocus_wrapper(b))
if not default or default == button_name:
default = button_name
b.focus_set()
Upvotes: 1
Views: 1650
Reputation: 1380
focus_set()
was, of course, working perfectly. My expectation that refocusing would rewrite the event object was at fault. The following code (which has been extensively revised to incorporate Brian Oakley's suggestions) produces the expected results.:
def _make_buttons(dialog, buttons, buttonbox, default, handler):
def bcommand_wrapper(button):
def bcommand():
name = button.winfo_name()
dialog.destroy()
handler(name)
return bcommand
for button_text, underline, button_name in buttons:
b = ttk.Button(buttonbox, text=button_text, underline=underline,
name=button_name)
b.configure(command=bcommand_wrapper(b))
b.pack(padx=2, side='left')
action = button_text[underline:underline + 1].lower()
try:
dialog.bind(action, lambda event, b=b: b.invoke())
except tk.TclError:
raise ValueError(
"Invalid underline of '{}' in position {}. Character '{}'.".
format(button_text, underline, action))
b.bind('<Return>', lambda event, b=b: b.invoke())
if not default or default == button_name:
default = button_name
b.focus_set()
I gave up my original idea of returning a Tk event to the caller. This is a dialog so the caller isn't going to need anything more than the name of the button that was clicked.
Note that I am not trapping accelerator keys with the 'Alt' modifier. The 'Alt' key, at least on MS Windows, is a functional key when used with accelerator keys: It causes the display of underlines on menus. Here the underlines are static and so the use of the 'Alt' key would be inappropriate.
Upvotes: 0
Reputation: 33223
Your initial assumption about needing to set the focus on the button is not correct. The usual method to handle this in Tk is to bind the accelerator key event on the dialog toplevel form and call the buttons invoke method for the event binding.
In Tcl/Tk thats:
toplevel .dlg
pack [button .dlg.b -text "Save" -underline 0 -command {puts "Save pressed"}]
bind .dlg <Alt-s> {.dlg.b invoke}
So bind the key events on whatever toplevel is the parent for your buttons. If that is a dialog then its parent should be the application toplevel widget.
A python equivalent is:
from tkinter import *
main = Tk()
dialog = Toplevel(main)
button = Button(dialog, text="Send", underline="0", command=lambda: print("hello"))
button.pack()
dialog.bind('<Alt-s>', lambda event: button.invoke())
main.mainloop()
The key binding appends an event object to the callback function which we can discard using a lambda to wrap the call the the button's invoke method.
Upvotes: 2
Reputation: 2383
Might I suggest using root.bind('c', ) to just have the shortcuts perform what you want?
Just make sure to only bind them when the window pops up, and unbind them when you're done.
Upvotes: -1