Reputation: 3964
I have a small module that pops up a Toplevel
widget when an Entry
widget gains focus. The Toplevel window is a keyboard, so it expects button clicks that trigger a method which inserts that button click into the Entry widget. The Toplevel should be destroyed on 2 conditions: 1) the user presses a key on their actual keyboard, 2) the parent of the Entry widget is moved or resized.
Everything works, except for one bug: if the user clicks on the Toplevel, it becomes active, and if one of the destroy events occur, I get unintended results (like the popup coming back when the Entry gets focus again).
My thought is that if I can make the Entry retain focus throughout the process, everything will work, but I haven't found a way to make that happen.
Here's an example, it's about as stripped down as I can make it while retaining the structure of the module. Note: Python 2.7
from Tkinter import *
class Top(Toplevel):
def __init__(self, attach):
Toplevel.__init__(self)
self.attach = attach
Button(self, text='Button A', command=self.callback).pack()
self.bind('<Key>', self.destroyPopup)
def callback(self):
self.attach.insert(END, 'A')
def destroyPopup(self, event):
self.destroy()
class EntryBox(Frame):
def __init__(self, parent, *args, **kwargs):
Frame.__init__(self, parent)
self.parent = parent
self.entry = Entry(self, *args, **kwargs)
self.entry.pack()
self.entry.bind('<FocusIn>', self.createPopup)
self.entry.bind('<Key>', self.destroyPopup)
self.parent.bind('<Configure>', self.destroyPopup)
def createPopup(self, event):
self.top = Top(self.entry)
def destroyPopup(self, event):
try:
self.top.destroy()
except AttributeError:
pass
root = Tk()
e1 = EntryBox(root).pack()
root.mainloop()
So, is there some kind of never_get_focus()
method I haven't found that I can apply to the Toplevel widget, or am I approaching this problem from the wrong way, or what? Any help is appreciated.
EDIT: I found a band-aid type solution that seems to work, but I feel like there's still a better way to handle this that I haven't found yet.
Here's what I've added to the Frame subclass popup methods:
def createPopup(self, event):
try: #When focus moves in to the entry widget,
self.top.destroy() #try to destroy the Toplevel
del self.top #and remove the reference to it
except AttributeError: #unless it doesn't exist,
self.top = Top(self.entry) #then, create it
def destroyPopup(self, event):
try:
self.top.destroy()
del self.top
except AttributeError:
pass
I'm adding a bounty because I want to see if there's another, cleaner way of doing this. The steps I want are:
Upvotes: 2
Views: 2260
Reputation: 4604
You can use a state machine to handle the behavior you describe. State machines are quite common to implement behaviors in graphical user interfaces. Here is a brief example to give you an idea of what it might look like.
First design the fsm, here is a simple one that perform almost what you want (skip the configure part for the sake of brevity).
For the implementation, you might pick an existing library, build your own framework, or go for a good old nested if . Following my quick and dirty implementation.
Adapt the subscription to create state and redirect event to fsm:
self.state = "idle"
self.entry.bind('<FocusIn>', lambda e:self.fsm("focus_entry"))
self.entry.bind('<FocusOut>', lambda e:self.fsm("focus_out_entry"))
self.entry.bind('<Key>', lambda e:self.fsm("key_entry"))
self.parent.bind('<Configure>', lambda e:self.fsm("configure_parent"))
Select the combination of state / event that you want to address and put appropriate actions. You might discover that you get trapped in a certain state and adapt your FSM accordingly.
def fsm(self, event):
old_state = self.state #only for logging
if self.state == "idle":
if event == "focus_entry":
self.createPopup()
self.state = "virtualkeyboard"
elif self.state == "virtualkeyboard":
if event == "focus_entry":
self.destroyPopup()
self.state = "typing"
if event == "focus_out_entry":
self.destroyPopup()
self.state = "idle"
elif event == "key_entry":
self.destroyPopup()
self.state = "typing"
elif self.state == "typing":
if event == "focus_out_entry":
self.state = "idle"
print "{} --{}--> {}".format(old_state, event, self.state)
Upvotes: 2
Reputation: 1
This might help you out: Making a tkinter widget take focus
If you are determined to just change focus then you may be able to write it in to do just that.
I may not fully understand the scope of what you are doing, I believe the reason it may be acting oddly is due to the binding you have on the toplevel.
def destroyPopup(self, event):
self.destroy()
working with:
self.entry.bind('<FocusIn>', self.createPopup)
Not sure if this helps, but possibly look into adding some:
print "____ is triggered"
To each method to see what exactly is happening when you switch focus, might help identify what is going on.
Upvotes: 0