Tuesday, August 21, 2012

Tkinter Menu Demo

This code is based on the Tcl menu.tcl demo. It demonstrates creating top level, cascading menus some of which include icons, radio and check buttons, as well as short-cut keys and accelerator keys. Only the Colors menu is implemented as a tear-off; a menu that can be opened as a separate window.





# File: menu.py
#    http://infohost.nmt.edu/tcc/help/pubs/tkinter//menu.html
#    http://tkinter.unpythonic.net/wiki/A_tour_of_Tkinter_widgets (menu.py)
#    http://stackoverflow.com/questions/3485397/python-tkinter-dropdown-menu-w-keyboard-shortcuts
#
# Notes:
#    - under Windows, the underlines for the menu shortcut keys
#      don't appear until 'Alt' is pressed

from tkinter import *
from tkinter import ttk
from demopanels import MsgPanel, SeeDismissPanel

class MenuDemo(ttk.Frame):
    
    def __init__(self, isapp=True, name='menudemo'):
        ttk.Frame.__init__(self, name=name)
        self.pack(expand=Y, fill=BOTH)
        self.master.title('Menu Demo')
        self.isapp = isapp
        self._create_widgets()
        
    def _create_widgets(self):
        if self.isapp:             
            SeeDismissPanel(self)
        
        self._create_demo_panel()
        
    def _create_demo_panel(self):
        demoPanel = Frame(self, name='demo')
        demoPanel.pack(side=TOP, fill=BOTH, expand=Y)
        
        msg = ["This window contains a menubar with cascaded menus.  You can post ",
              "a menu from the keyboard by typing Alt+x, where \"x\" is the ",
              "character underlined on the menu.\n\tYou can then traverse among the ",
              "menus using the arrow keys.\n\tWhen a menu is posted, you can invoke ",
              "the current entry by typing space, or you can invoke any entry by ",
              "typing its underlined character.\n\tIf a menu entry has an accelerator, ",
              "you can invoke the entry without posting the menu just by typing ",
              "the accelerator.\n\tThe rightmost menu can be torn off into a palette ",
              "by selecting the first item in the menu."]
        
        lbl = ttk.Label(demoPanel, text=''.join(msg), wraplength='4i', justify=LEFT)
        lbl.pack(side=TOP, padx=5, pady=5)
        
        # create statusbar 
        statusBar = ttk.Frame()
        self.__status = ttk.Label(self.master, text=' ', relief=SUNKEN, borderwidth=1,
                              font=('Helv 10'), anchor=W, name='status')
        
        self.__status.pack(side=LEFT, padx=2, expand=Y, fill=BOTH)
        statusBar.pack(side=BOTTOM, fill=X, pady=2)
                
        # create the main menu (only displays if child of the 'root' window)
        self.master.option_add('*tearOff', False)  # disable all tearoff's
        self._menu = Menu(self.master, name='menu')
        self._build_submenus()
        self.master.config(menu=self._menu)
        
        # set up standard bindings for the Menu class
        # (essentially to capture mouse enter/leave events)
        self._menu.bind_class('Menu', '<<MenuSelect>>', self._update_status)

    def _build_submenus(self):
        # create the submenus
        # the routines are essentially the same:
        #    1. create the submenu, passing the main menu as parent
        #    2. add the submenu to the main menu as a 'cascade'
        #    3. add the submenu's individual items
        
        self._add_file_menu()
        self._add_basic_menu()
        self._add_cascades_menu()
        self._add_icons_menu()
        self._add_more_menu()
        self._add_colors_menu()

    # ================================================================================
    # Submenu routines
    # ================================================================================ 

    # File menu ------------------------------------------------------------------                
    def _add_file_menu(self):
        fmenu = Menu(self._menu, name='fmenu')
        self._menu.add_cascade(label='File', menu=fmenu, underline=0)
        labels = ('Open...', 'New', 'Save', 'Save As...', 'Print Setup...',
                   'Print...', )

        for item in labels:
            fmenu.add_command(label=item,
                              command=lambda m=item: self._demo_only(m))

        fmenu.add_separator()
        fmenu.add_command(label='Exit Demo', 
                          command=lambda: self.master.destroy()) # kill toplevel wnd
        
    # Basic menu ------------------------------------------------------------------        
    def _add_basic_menu(self):
        bmenu = Menu(self._menu)
        self._menu.add_cascade(menu=bmenu, label='Basic', underline=0)
        bmenu.add_command(label='Long entry that does nothing')
        labels = ['A', 'B', 'C', 'D', 'E', 'F']
        for item in labels:
            bmenu.add_command(label='Print letter "{}"'.format(item),
                              underline=14, 
                              accelerator='Control+{}'.format(item),
                              command=lambda i=item: self._print_it(None, i))
            
            # bind accelerator key to a method; the bind is on ALL the
            # applications widgets
            self.bind_all('<Control-{}>'.format(item.lower()), 
                            lambda e, i=item: self._print_it(e, i))

    # Cascades menu ------------------------------------------------------------------            
    def _add_cascades_menu(self):
        cascades = Menu(self._menu)
        self._menu.add_cascade(label='Cascades', menu=cascades, underline=0)
        
        cascades.add_command(label='Print Hello', underline=6,
                             accelerator='Control+H',
                             command=lambda: self._print_it(None, 'Hello'))

        cascades.add_command(label='Print Goodbye', underline=6,
                             accelerator='Control+G',
                             command=lambda: self._print_it(None, 'Goodbye'))

        # add submenus        
        self._add_casc_cbs(cascades)    # check buttons
        self._add_casc_rbs(cascades)    # radio buttons

        # bind accelerator key to a method; the bind is on ALL the
        # applications widgets
        self.bind_all('<Control-h>', 
                        lambda e: self._print_it(e, 'Hello'))
        self.bind_all('<Control-g>', 
                        lambda e: self._print_it(e, 'Goodbye'))

    def _add_casc_cbs(self, cascades):
        # build the Cascades->Check Buttons submenu
        check = Menu(cascades)
        cascades.add_cascade(label='Check Buttons', underline=0,
                               menu=check)
        
        self.__vars = {}
        labels = ('Oil checked', 'Transmission checked',
                  'Brakes checked', 'Lights checked' )

        for item in labels:
            self.__vars[item] = IntVar()
            check.add_checkbutton(label=item, variable=self.__vars[item])
            
        # set items 1 and 3 to 'selected' state
        check.invoke(1)
        check.invoke(3)
            
        check.add_separator()
        check.add_command(label='Show values',
                          command=lambda lbls=labels: self._show_vars(lbls))
                    
    def _add_casc_rbs(self, cascades):
        # build Cascades->Radio Buttuns subment
        submenu = Menu(cascades)
        cascades.add_cascade(label='Radio Buttons', underline=0,
                               menu=submenu)
        
        self.__vars['size'] = StringVar()
        self.__vars['font'] = StringVar()
        
        for item in (10,14,18,24,32):
            submenu.add_radiobutton(label='{} points'.format(item), 
                                    variable=self.__vars['size'])
            
        submenu.add_separator()
        for item in ('Roman', 'Bold', 'Italic'):
            submenu.add_radiobutton(label=item, 
                                    variable=self.__vars['font'])
    
        # set items 1 and 7 to 'selected' state
        submenu.invoke(1)
        submenu.invoke(7)
            
        submenu.add_separator()
        submenu.add_command(label='Show values',
                            command=lambda: self._show_vars(('size','font')))
        

    # Icons menu ------------------------------------------------------------------        
    def _add_icons_menu(self):       
        menu = Menu(self._menu)
        self._menu.add_cascade(label='Icons', menu=menu, underline=0)
        
        # 'info', 'questhead', and 'error' are Tk built-in icons
        for bm in ('@images/pattern.xbm', 'info', 'questhead', 'error'):
            menu.add_command(bitmap=bm, hidemargin=1,
                             command=lambda b=bm: self._you_invoked((b, 'bitmap')))
            
        menu.entryconfigure(2, columnbreak=1)      
    
    # More menu ------------------------------------------------------------------    
    def _add_more_menu(self):
        menu = Menu(self._menu)
        self._menu.add_cascade(label='More', menu=menu, underline=0)
        
        labels = ('An entry', 'Another entry', 'Does nothing',
                'Does almost nothing', 'Make life meaningful')
        
        for item in labels:
            menu.add_command(label=item,
                             command=lambda i=item: self._you_invoked((i, 'entry')))
            
        menu.entryconfig(3, bitmap='questhead', compound=LEFT,
                         command=lambda i=labels[3]: 
                                    self._you_invoked((i, 
                                                      'entry; a bitmap and a text string')))            
          
    # Colors menu ------------------------------------------------------------------           
    def _add_colors_menu(self):
        menu = Menu(self._menu, tearoff=True)
        self._menu.add_cascade(label='Colors', menu=menu, underline=1)
        
        for c in ('red', 'orange', 'yellow', 'green', 'blue'):
            menu.add_command(label=c, background=c,
                             command=lambda c=c: self._you_invoked((c, 'color')))    
                    
    # ================================================================================
    # Bound and Command methods
    # ================================================================================                
    def _print_it(self, e, txt):
        # triggered by multiple menu items that print letters or greetings
        # or by an accelerator keypress (Ctrl+a, Ctrl+b, etc).
        print(txt)
        
    def _demo_only(self, menuItem):
        # triggered when unimplemented submenu items are clicked
        self.bell()
        self.__status.configure(background='red', foreground='white',
                                text="No action has been defined for menu item '" + menuItem + "'")        
        
    def _update_status(self, evt):
        # triggered on mouse entry if a menu item has focus 
        # (focus occurs when user clicks on a top level menu item)
        try:
            item = self.tk.eval('%s entrycget active -label' % evt.widget )
            self.__status.configure(background='gray90', foreground='black',
                                    text=item)
        except TclError:
            # no label available, ignore
            pass
        
    def _show_vars(self, values):
        # called when Cascades->Check Buttons or Radio Buttons
        # 'Show Values' item is selected
        # displayf variable values in the status bar
        v = []
        for e in values:
            t = self.__vars[e].get()
            s = '{}: {}  '.format(e, t)
            v.append(s)
        
        self.__status.configure(background='white', foreground='black',
                                text=''.join(v))
    
    def _you_invoked(self, value):
        # triggered when an entry in the Icons, More or Colors menu is selected
        self.bell()
        self.__status.configure(background='SeaGreen1', foreground='black',
                                text="You invoked the '{}' {}.".format(value[0], 
                                                                       value[1]))
        
if __name__ == '__main__':
    MenuDemo().mainloop()