Question: How to Prevent Tkinter Menu from Taking Keyboard Focus

G

galyle

Hello, I'm trying to build a menu which provides suggestions to a user
based on input to an entry. I have done something like this before
using Tcl/Tk, so I expected that it would work without much difficulty
with Tkinter. I was wrong.

The problem I have is that, as soon as the menu is posted, it appears
to take the keyboard focus until the user either selects an option or
clicks an area outside of the menu. The behavior I would like is for
keyboard input to go to the entry, and for the menu to update based on
that input. I have tried different bindings (both to the entry and
menu) and I have tried different focus/grab combinations (even
force_focus and grab_set_global to the entry) when the menu is posted,
but nothing seems to work. Are there any suggestions on how to get
the behavior I'm seeking? The following code demonstrates the issue
I'm having:


import Tkinter

class demo:
def __init__(self, parent):
self._entry = Tkinter.Entry(width = 60)
self._suggestions = Tkinter.Menu(parent, tearoff = 0,
takefocus = 0)
self._entry.pack(padx = 20, pady = 20)
self._entry.bind('<Key>', self._suggest_text, add = '+')

def _suggest_text(self, event):
curr = self._entry.get() + repr(event.char)[1]
x = self._entry.winfo_rootx()
y = self._entry.winfo_rooty()
y += self._entry.winfo_height()
try:
self._suggestions.delete(0, 'end')
self._suggestions.add_command(label = curr + '_1')
self._suggestions.add_command(label = curr + '_2')
self._suggestions.add_command(label = curr + '_3')
self._suggestions.add_command(label = curr + '_4')
self._suggestions.post(x, y)
finally:
self._suggestions.grab_release()

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 
R

rantingrick

Hello, I'm trying to build a menu which provides suggestions to a user
based on input to an entry.  I have done something like this before
using Tcl/Tk, so I expected that it would work without much difficulty
with Tkinter.  I was wrong.


Why not just use the Tix.ComboBox instead? I believe it's editable.
 
G

galyle

Why not just use the Tix.ComboBox instead? I believe it's editable.

Thanks for the suggestion. I tried using a Pmw.ComboBox, but I've run
into essentially the same problem. If I bring up the selection list
on a keypress, then the selection takes the focus and further input no
longer goes to the entry of the ComboBox. However, if I set the focus
to the entry after bringing up the selection list, I can at least
continue inputting to the entry, even though the selection list menu
gets placed behind the entry. This is not desirable behavior, but at
least it is closer. Any idea how to bring the menu to the forefront
without the entry losing focus? The following demo demonstrates the
issue:


import Tkinter
import Pmw

class demo:
def __init__(self, parent):
self._search_bar = Pmw.ComboBox(parent,
label_text = 'Register Mnemonic',
labelpos = 'w', entry_width = 60)
self._search_bar.pack(padx = 20, pady = 20)
self._search_bar._entryfield.component('entry').bind('<Key>',
self._suggest_text, add = '+')

def _suggest_text(self, event):
curr = self._search_bar._entryfield.getvalue() +
repr(event.char)[1]
self._search_bar._list.setlist([curr + '_0', curr + '_1', curr
+ '_2'])
self._search_bar._postList(event)
self._search_bar._entryfield.component('entry').focus_set()
print curr

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 
W

woooee

Adding focus_set seems to work for me. What do want to do
differently?

import Tkinter

class demo:
def __init__(self, parent):
self._entry = Tkinter.Entry(width = 60)
self._suggestions = Tkinter.Menu(parent, tearoff = 0,
takefocus = 0)
self._entry.pack(padx = 20, pady = 20)
self._entry.bind('<Key>', self._suggest_text, add = '+')
self._entry.focus_set()

def _suggest_text(self, event):
curr = self._entry.get() + repr(event.char)[1]
x = self._entry.winfo_rootx()
y = self._entry.winfo_rooty()
y += self._entry.winfo_height()
try:
self._suggestions.delete(0, 'end')
self._suggestions.add_command(label = curr + '_1')
self._suggestions.add_command(label = curr + '_2')
self._suggestions.add_command(label = curr + '_3')
self._suggestions.add_command(label = curr + '_4')
self._suggestions.post(x, y)
finally:
self._suggestions.grab_release()

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 
W

woooee

Sorry, I did not understand the question correctly, and so have added
another focus_set for the entry after the menu's creation. You can
still enter after the menu comes up, even though you can't see where
you are entering.

import Tkinter

class demo:
def __init__(self, parent):
self._entry = Tkinter.Entry(width = 60)
self._suggestions = Tkinter.Menu(parent, tearoff = 0,
takefocus = 0)
self._entry.pack(padx = 20, pady = 20)
self._entry.bind('<Key>', self._suggest_text, add = '+')
self._entry.focus_set()

def _suggest_text(self, event):
curr = self._entry.get() + repr(event.char)[1]
x = self._entry.winfo_rootx()
y = self._entry.winfo_rooty()
y += self._entry.winfo_height()
try:
self._suggestions.delete(0, 'end')
self._suggestions.add_command(label = curr + '_1')
self._suggestions.add_command(label = curr + '_2')
self._suggestions.add_command(label = curr + '_3')
self._suggestions.add_command(label = curr + '_4')
self._suggestions.post(x, y)
finally:
self._suggestions.grab_release()

self._entry.focus_set()

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 
G

galyle

Sorry, I did not understand the question correctly, and so have added
another focus_set for the entry after the menu's creation.  You can
still enter after the menu comes up, even though you can't see where
you are entering.

import Tkinter

class demo:
    def __init__(self, parent):
        self._entry = Tkinter.Entry(width = 60)
        self._suggestions = Tkinter.Menu(parent, tearoff = 0,
takefocus = 0)
        self._entry.pack(padx = 20, pady = 20)
        self._entry.bind('<Key>', self._suggest_text, add = '+')
        self._entry.focus_set()

    def _suggest_text(self, event):
        curr = self._entry.get() + repr(event.char)[1]
        x = self._entry.winfo_rootx()
        y = self._entry.winfo_rooty()
        y += self._entry.winfo_height()
        try:
            self._suggestions.delete(0, 'end')
            self._suggestions.add_command(label = curr + '_1')
            self._suggestions.add_command(label = curr + '_2')
            self._suggestions.add_command(label = curr + '_3')
            self._suggestions.add_command(label = curr + '_4')
            self._suggestions.post(x, y)
        finally:
            self._suggestions.grab_release()

        self._entry.focus_set()

if __name__ == '__main__':
    root = Tkinter.Tk()
    root.title('A Problem')
    demo(root)
    root.mainloop()

Perhaps it is because I'm trying this on Windows, but the above code
does not work for me. After the menu is posted, no further keyboard
input gets directed to the entry until the menu is unposted.

It looks like my best bet is to use the ComboBox suggested above. The
ComboBox method is very close to working, but I've got a problem now
where all focus on the app is directed to the ComboBox entry (I need
to set it that way to redirect keyboard input from the popup menu),
making it impossible to close the app without killing it. I've tried
to return focus to the entry parent by binding the entry to <FocusOut>
and <Leave>, but this has the effect of making the popup take the
focus after every keystroke, which is not what I want.

The (almost working) demo below should demonstrate the issue fairly
well:

import Tkinter
import Pmw

class demo:
def __init__(self, parent):
self._search_bar = Pmw.ComboBox(parent,
label_text = 'Register Mnemonic',
labelpos = 'w', entry_width = 60)
self._search_bar.pack(padx = 20, pady = 20)
self._search_bar._entryfield.component('entry').bind('<Key>',
self._suggest_text, add = '+')

def _suggest_text(self, event):
curr = self._search_bar._entryfield.getvalue()
curr += repr(event.char)[1]
self._search_bar._list.setlist([curr + '_0', curr + '_1',
curr + '_2'])
self._search_bar._postList(event)
self._search_bar._entryfield.component('entry').focus_set()
self._search_bar._popup.lift()

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 
G

galyle

Sorry, I did not understand the question correctly, and so have added
another focus_set for the entry after the menu's creation.  You can
still enter after the menu comes up, even though you can't see where
you are entering.
import Tkinter
class demo:
    def __init__(self, parent):
        self._entry = Tkinter.Entry(width = 60)
        self._suggestions = Tkinter.Menu(parent, tearoff = 0,
takefocus = 0)
        self._entry.pack(padx = 20, pady = 20)
        self._entry.bind('<Key>', self._suggest_text, add = '+')
        self._entry.focus_set()
    def _suggest_text(self, event):
        curr = self._entry.get() + repr(event.char)[1]
        x = self._entry.winfo_rootx()
        y = self._entry.winfo_rooty()
        y += self._entry.winfo_height()
        try:
            self._suggestions.delete(0, 'end')
            self._suggestions.add_command(label = curr + '_1')
            self._suggestions.add_command(label = curr + '_2')
            self._suggestions.add_command(label = curr + '_3')
            self._suggestions.add_command(label = curr + '_4')
            self._suggestions.post(x, y)
        finally:
            self._suggestions.grab_release()
        self._entry.focus_set()
if __name__ == '__main__':
    root = Tkinter.Tk()
    root.title('A Problem')
    demo(root)
    root.mainloop()

Perhaps it is because I'm trying this on Windows, but the above code
does not work for me.  After the menu is posted, no further keyboard
input gets directed to the entry until the menu is unposted.

It looks like my best bet is to use the ComboBox suggested above.  The
ComboBox method is very close to working, but I've got a problem now
where all focus on the app is directed to the ComboBox entry (I need
to set it that way to redirect keyboard input from the popup menu),
making it impossible to close the app without killing it.  I've tried
to return focus to the entry parent by binding the entry to <FocusOut>
and <Leave>, but this has the effect of making the popup take the
focus after every keystroke, which is not what I want.

The (almost working) demo below should demonstrate the issue fairly
well:

import Tkinter
import Pmw

class demo:
    def __init__(self, parent):
        self._search_bar = Pmw.ComboBox(parent,
            label_text = 'Register Mnemonic',
            labelpos = 'w', entry_width = 60)
        self._search_bar.pack(padx = 20, pady = 20)
        self._search_bar._entryfield.component('entry').bind('<Key>',
            self._suggest_text, add = '+')

    def _suggest_text(self, event):
        curr = self._search_bar._entryfield.getvalue()
        curr += repr(event.char)[1]
        self._search_bar._list.setlist([curr + '_0', curr + '_1',
            curr + '_2'])
        self._search_bar._postList(event)
        self._search_bar._entryfield.component('entry').focus_set()
        self._search_bar._popup.lift()

if __name__ == '__main__':
    root = Tkinter.Tk()
    root.title('A Problem')
    demo(root)
    root.mainloop()

Well, it required quite a bit of digging, but I finally have what I
want. For those who are curious, the following demo should provide
some help:


import Tkinter
import Pmw

class demo:
def __init__(self, parent):
self._parent = parent
self._search_bar = Pmw.ComboBox(parent,
label_text = 'Register Mnemonic',
labelpos = 'w', entry_width = 60)
self._search_bar.pack(padx = 20, pady = 20)
self._search_bar._entryfield.component('entry').bind('<Key>',
self._suggest_text, add = '+')

self._search_bar._entryfield.component('entry').bind('<Escape>',
self._remove_list, add = '+')

self._search_bar._entryfield.component('entry').bind('<space>',
self._remove_list, add = '+')

self._search_bar._entryfield.component('entry').bind('<Return>',
self._remove_list, add = '+')
self._search_bar._popup.bind('<ButtonRelease-1>',
self._remove_list, add = '+')

def _suggest_text(self, event):
x = self._search_bar._entryfield.winfo_rootx()
y = self._search_bar._entryfield.winfo_rooty() + \
self._search_bar._entryfield.winfo_height()
w = self._search_bar._entryfield.winfo_width() + \
self._search_bar._arrowBtn.winfo_width()

curr = self._search_bar._entryfield.getvalue()
curr += repr(event.char)[1]
self._search_bar._list.setlist([curr + '_0', curr + '_1',
curr + '_2'])

self._search_bar._list.configure(hull_width=w)
Pmw.setgeometryanddeiconify(self._search_bar._popup,
'+%d+%d' % (x, y))

self._search_bar._popup.lift()

def _remove_list(self, event):
self._search_bar._popup.withdraw()

if __name__ == '__main__':
root = Tkinter.Tk()
root.title('A Problem')
demo(root)
root.mainloop()
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,057
Latest member
KetoBeezACVGummies

Latest Threads

Top