Reputation: 529
There are a lot of examples how to make a tkinter context menu. I have seen no one with the nested menus (.add_cascade were used). I attempted to make one and it exposed a problem (or peculiarity ) never mentioned. That's how .tk_popup or .post created menu window behaves when the sub menu (nested) item is invoked.
It is a complete example of a popup context menu (Python 3.9.2, Debian bullseye) which works fine until the "Insert" (sub menu) is gonna be selected. The example has two files: popup.py, canvas.py. I'm using classes in popup.py in other script.
class Popup(tk.Menu):
def __init__(self, *args, **kwargs):
super(Popup, self).__init__(*args, **kwargs)
self.bind('<FocusOut>', self.on_focus_out)
self.bind('<Escape>', self.on_focus_out)
def post(self, *args, **kwargs):
super(Popup, self).post(*args, **kwargs)
self.focus_set()
def on_focus_out(self, event=None):
self.unpost()
class PopupMixin:
def __init__(self, *args, **kwargs):
self.tk_focusFollowsMouse()
self.bind('<3>', self.on_click3)
self.popup = None
def on_click3(self, event):
try:
self.popup.post(event.x_root, event.y_root)
finally:
self.popup.grab_release()
canvas.py
class SPopupMixin(PopupMixin): # S[uper]PopupMixin
pass
class Canvas(tk.Canvas, SPopupMixin):
def __init__(self, *args, **kwargs):
super(Canvas, self).__init__(*args, **kwargs)
super(SPopupMixin, self).__init__(*args, **kwargs)
self.popup = Canvas.make_top_menu(self)
@staticmethod
def make_top_menu(parent):
popup = Popup(parent, tearoff=0)
popup.highlightthickness = 1
popup.add_command(label='Loop', command=lambda: print('Loop'))
popup.add_command(label='Single', command=lambda: print('Single'))
popup.add_cascade(label='Insert', menu=Canvas.make_sub_menu(popup))
return popup
@staticmethod
def make_sub_menu(parent):
sub = Popup(parent, tearoff=0)
sub.add_command(label='Glider', command=lambda: print('Glider'))
return sub
def main():
canv = Canvas(takefocus=1)
canv.pack()
canv.mainloop()
if __name__ == '__main__':
main()
The functionality I'd like to keep: Escape key pressed and moving the mouse outside the popup window (focus is lost) closes the popup menu so the Menu window never remains open if a user hasn't chosen an item.
When mouse button <3> is clicked on the Canvas the menu is shown. It is easy to invoke "Loop" or "Single"(see the example) (the popup remains visible until either is clicked). Invoking "Insert" is tricky. One way is to "click-and-drag" until sub menu "Glider" appears. Then release the mouse button over "Glider" and "Glider" is activated. This is inconvenient and differs from the way the 1st level items are selected. I'd like to understand the reason for that. Just clicking "Insert" gives nothing. The sub menu "Glider" disappears too soon to select it.
Let's call "clicking an item" the "natural" way of invoking the menu item. In the example we see "click-and-drag" way.
Is it possible to get the "natural way" of selecting the nested item ("Glider") in the example? In the week time I have not found how.
I started with .tk_popup. Have got better results with .post
Using Python 3.9.2, Debian bullseye, GNOME 3.38.5
UPDATE
answering Bryan Oakley question:
Sequence of events when "Glider" is selected the "click-and-drag" way:
focus_in id(event.widget) = 139798253649728
Glider
focus_out id(event.widget) = 139798253649728
Sequence of events when I attempted to select "Glider" by clicking "Insert":
focus_in id(event.widget) = 139760112942912
focus_out id(event.widget) = 139760112942912
focus_in id(event.widget) = 139760097572752
focus_out id(event.widget) = 139760097572752
"Glider" sub menu just blinks for a split of a second
When
# self.bind('<FocusOut>', self.on_focus_out)
menu works the "normal way" with exception that the popup window remains open (visible) until any choice is made, even when I switch apps (Alt-Tab).
UPDATE 10/25
When I try to invoke "Glider" menu choice in the normal way (click "Insert" , hover mouse over "Glider" and click it) one mouse click is lost after that. [for now "normal" way of selecting "Glider" doesn't work, the "Glider" sub menu disappears too soon.]
UPDATE 10/26
This is the next approximation to what I'd like to have.
$ git diff
diff --git a/popup.py b/popup.py
index 03e9a77..4630e7f 100644
--- a/popup.py
+++ b/popup.py
@@ -6,8 +6,11 @@ import tkinter as tk
class Popup(tk.Menu):
def __init__(self, *args, **kwargs):
super(Popup, self).__init__(*args, **kwargs)
- self.bind('<FocusOut>', self.on_focus_out)
self.bind('<Escape>', self.on_focus_out)
+ self.bind('<Enter>', self.on_enter)
+
+ def on_enter(self, event):
+ self.focus_set()
def post(self, *args, **kwargs):
super(Popup, self).post(*args, **kwargs)
<FocusOut> handler is removed , <Enter> is added instead. The "Glider" nested menu item is selected the normal way (clicking "Insert" , moving pointer, clicking "Glider"). Context menu can be closed any time by pressing the <Escape> key.
Upvotes: 0
Views: 302
Reputation: 529
The following Popup class satisfies the set "requirements":
Escape key pressed and/or moving the mouse outside the popup window (focus is lost) closes the popup menu so the Menu window never remains open if a user hasn't chosen an item.
class Popup(tk.Menu):
"""Two types of nested menu item invoking. 'click&drag' - click
and hold the menu item, move pointer to sub-menu item. Release the
button when it is over the sub-menu item. 'click&move' - click the
menu item (sub-menu appears), click the sub-menu item.
Controlled by *menumode* keyword arguemnt. 0 - click&move, 1 -
click&drag"""
menumode_default = 0
def __init__(self, *args, menumode=None, **kwargs):
super(Popup, self).__init__(*args, **kwargs)
menumode = self.menumode_default if menumode is None else menumode
if menumode:
self.bind('<FocusOut>', self.on_focus_out)
else:
self.bind('<Enter>', self.on_enter)
self.bind('<Escape>', self.on_focus_out)
def on_enter(self, event):
self.focus_set()
def post(self, *args, **kwargs):
super(Popup, self).post(*args, **kwargs)
self.focus_set()
def on_focus_out(self, event=None):
self.unpost()
It support two modes: click&drag, click&move which described in the class doc string. It is tested for Python 3.9.2, Debian bullseye. I haven't found the easy way to keep only one method (on_enter or on_focus_out) which has bound in __init__
Upvotes: 1