atlasologist
atlasologist

Reputation: 3964

Making a pop-up keyboard in Tkinter with Toplevel

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:

  1. Focus moves to the Entry widget
  2. A popup Toplevel is created (this is a keyboard)
  3. The Toplevel is destroyed when a) a Key press event occurs from the actual keyboard, b) focus moves out of the Entry widget to another widget or off the GUI, c) the main window is moved
  4. This process is repeatable if focus moves back into the Entry widget later

Upvotes: 2

Views: 2260

Answers (2)

FabienAndre
FabienAndre

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).

fsm picture

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

ndsu_jurf
ndsu_jurf

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

Related Questions