""" automenu.py, version 0.15, Sep 20 2005, by leonardo maffi This is an experimental version. Creates a menu automatically from a compact textual representation. Note: changing the already created menus isn't easy, they are contained in the self.menu[] dict, the index is the line number in the given menuDef string (empty lines don't count as lines). The root menu is self.menu[-1] TODO: - Extend the possibilities of the sintax, different kinds of menus, etc. - Improve the translation to Tkinter of key association - It can be added the underscore inside the first field, to show an underscore, ex: O_ptions """ import Tkinter as tk class Menu(object): """Menu(root, globalVars, menuDef, font=None, fieldSep=","): creates a menu automatically from a compact textual representation. globalVars has to be the globals() at the calling time.""" def __init__(self, root, globalVars, menuDef, font=None, fieldSep=",", itemSep="_"): self.parsedMenu = self._parseMenu(menuDef, fieldSep=fieldSep, itemSep=itemSep) self.createKeyAssociations(globalVars, self.parsedMenu) self._createMenu(root, globalVars, font) def _parseMenu(self, menuDef, fieldSep, itemSep): """ _parseMenu(menuDef, fieldSep, itemSep): given a multi-line string menuDef defining a menu, parses it giving a list of tuples containing a representation of the menu. Syntax of the menuDef: - Indentations are multiples of a fixed length (1,2,3,4,6,8..) - The field separator is the comma, but it can be changed - the first line of the string is meant as non indented, all the following ones are de-indented to the same extent - no indented lines are top level menus of the menu bar, one time indented are its items, two times indented are sub menus etc - Item separators are one or more underscores, but this can be changed. - menu items syntax: label|command[|accelerator] - command can be absent - self.keys contains a list of (key, cmd) pairs that can be used to associate keys. - the fields will be stripped from their spaces and tabs - the names of the menus and sub-menus are composed by just the label - Note: there are few consistency cheeks! - Note: allowed syntaxes for associated keys (more can be added): Ctrl-A Alt-B Ctrl-Shift-C Alt-D Ctrl-Shift-C F1 Ctrl+A Alt+B Ctrl+Shift+C Alt+D Ctrl+Shift+C F1 - Note: functions called by keys/menu items usually have an event=None parameter, because they receive a parameter if called by keyboard Example input of _parseMenu: File New|temp|Ctrl-N New Window|temp|Ctrl-Shift-N ____ Open|temp|Ctrl-O ____ Save|temp|Ctrl-S Save As|temp|F12 ____ Minimize|temp|Ctrl-M Edit Cut|temp|Ctrl-X Copy|temp|Ctrl-C Paste|temp|Ctrl-V ____ Align Left|temp Right|temp ____ Its output: [ (0, ['File']), (1, ['New', 'temp', 'Ctrl-N']), (1, ['New Window', 'temp', 'Ctrl-Shift-N']), (1, None), (1, ['Open', 'temp', 'Ctrl-O']), (1, None), (1, ['Save', 'temp', 'Ctrl-S']), (1, ['Save As', 'temp', 'F12']), (1, None), (1, ['Minimize', 'temp', 'Ctrl-M']), (0, ['Edit']), (1, ['Cut', 'temp', 'Ctrl-X']), (1, ['Copy', 'temp', 'Ctrl-C']), (1, ['Paste', 'temp', 'Ctrl-V']), (1, None), (1, ['Align']), (2, ['Left', 'temp', '']), (2, ['Right', 'temp', '']), (2, None) ] """ def gcd(a,b): while b: a,b = b, a % b return a def findIndent(menuList): indents = [ind for ind,line in menuList if ind] if indents == []: return 1 return reduce(gcd, indents) # Split lines, and remove empty lines menuDef2 = [line.rstrip() for line in menuDef.split("\n") if line.rstrip()] # Remove useless trailing spaces menuDef3 = [line.lstrip() for line in menuDef2] # Create a list of (indent,line), where indent is an integer and line is stripped menuDef4 = [(len(line2)-len(line3), line3) for line3,line2 in zip(menuDef3,menuDef2)] # Remove from all the lines the indent of the first line, if it's not zero if menuDef4[0][0]: deindent = menuDef4[0][0] menuDef4 = [(ni-deindent, line) for ni,line in menuDef4] indent = findIndent(menuDef4) # Intentations have a given lenght, so the first number is now the actual indentation menuDef5 = [(ni/indent, line) for ni,line in menuDef4] # Replace underscores (field separators) with None menuDef6 = [] setSep = set([itemSep]) # Usually itemSep="_" for pos,(ni,line) in enumerate(menuDef5): if set(line) == setSep: # usually setSep="," menuDef6.append( (ni, None) ) else: menuDef6.append( (ni, line) ) # Split&strip parameters, and add "" at the end if there are two parameters only menuDef7 = [] for ni,line in menuDef6: if line is not None: line = line.split(fieldSep) # Strip spaces from fields line = [element.strip() for element in line] if len(line) == 2: line.append("") menuDef7.append( (ni,line) ) return menuDef7 def createKeyAssociations(self, globalVars, parsedMenu): """createKeyAssociations(parsedMenu): assign to self.keys the (keys,cmd) pairs. It assigns only the meaningful (key,cmd) pairs. Allowed syntaxes for associated keys (more can be added): Ctrl-A Alt-B Ctrl-Shift-C Alt-D Ctrl-Shift-C F1 Ctrl+A Alt+B Ctrl+Shift+C Alt+D Ctrl+Shift+C F1 """ result = [] for ni,line in parsedMenu: if line is not None and len(line)>1 and line[1] and line[2]: #cmd = globalVars[ line[1] ] # More limited cmd = eval(line[1], globalVars) # Translate the associated keys for Tkinter procLine = line[2] # Take care of the shift if "Shift-" in procLine: procLine = procLine.replace("Shift-", "") else: procLine = procLine[:-1] + procLine[-1].lower() procLine = "KeyPress-" + procLine if "Alt-" in procLine: procLine = "Alt-" + procLine.replace("Alt-", "") if "Ctrl-" in procLine: procLine = "Control-" + procLine.replace("Ctrl-", "") procLine = "<" + procLine + ">" result.append( (procLine, cmd) ) # Sort the (key,cmd) pairs according to the key only result.sort(key = lambda item: item[0]) self.keys = result def _createMenu(self, root, globalVars, font): # menuStack is a stack used during the scan of self.parsedMenu, it contains the parent # menus seen so far. It's initialized with the top level menu. menuStack = [ tk.Menu(root, font=font, tearoff=False) ] # Configure the main menu of the root root.config(menu=menuStack[0]) # self.menu is a dict that contains all the menus created in the object, its index is # the line number in the given menuDef string (empty lines don't count as lines). # The root menu is self.menu[-1] self.menu = { -1: menuStack[0] } # Scan the parsed menu, nline is the line number, nind is the indentation level, and line # is a list of the fields of the line. for nline,(nind,line) in enumerate(self.parsedMenu): # If a menu or submenu is finished, then remove the last elements of the stack, # usually just the last one if nind < len(menuStack)-1: menuStack = menuStack[:-(len(menuStack)-1-nind)] # if line is none, then it's a separator if line is None: menuStack[-1].add_separator() # if line is just a label, then it's the header of a menu/submenu elif len(line) == 1: # Create a new menu/submenu, attached to the last menu of the stack self.menu[nline] = tk.Menu(menuStack[-1], font=font, tearoff=False) # Always create a submenu menuStack[-1].add_cascade(label=line[0], menu=self.menu[nline]) # Update the stack, pushing the just created menu menuStack.append( self.menu[nline] ) # the line isn't a label and it's not just a separator, so it's a menu item else: # Find the command for the meny entry. If it' "" then it's None. if not line[1]: cmd = None else: #cmd = globalVars[ line[1] ] # More limited cmd = eval(line[1], globalVars) # add the menu item, the accelerator is "" if it's empty menuStack[-1].add_command(label=line[0], accelerator=line[2], command=cmd, underline=None) if __name__ == '__main__': # Demos ----------------------------- root = tk.Tk() demo = 2 # This can be changed. 1 to 3 are possibile alternative demos if demo == 1: def para(n): print "Parametric called:", n def called(event=None): print "Generic called" def callNew(event=None): print "callNew (maybe Control-n pressed)" def callNewWindow(event=None): print "callNewWindow (maybe Control-shift-N pressed)" def CallSaveAs(event=None): print "CallSaveAs (maybe F12 pressed)" menudef = """ File New, callNew, Ctrl-N New Window, callNewWindow, Ctrl-Shift-N __ Open, lambda e=0:para(1), Ctrl-O __ Save, lambda e=0:para(2), Ctrl-S Save As, CallSaveAs, F12 __ Minimize, called, Ctrl-M Magic, called, Alt-X Edit Cut, called, Ctrl-X Copy, called, Ctrl-C Paste, called, Ctrl-V __ Align Left, Right, ___ """ m = Menu(root, globals(), menudef) elif demo == 2: menudef = """ A B, C, ____ D, E F, G, H I, ____ L X, Y, ____ M, N O, P Q, """ m = Menu(root, globals(), menudef) elif demo == 3: def load_new_image(): print "Load new image" def restart(): print "Restart" def info(): print "Info" menudef = """ Load new image |load_new_image Restart |restart Info |info """ m = Menu(root, globals(), menudef, fieldSep="|") print "Keys of (key,cmd) pairs that can be assigned:", [key for key,cmd in m.keys] print # Assign keys to the root for key,cmd in m.keys: root.bind(key, cmd) root.mainloop() """ A modified example from another system: Structure used to generate new menus: Field 1: The menu path. The letter after the underscore indicates an accelerator key once the menu is open. One or more underscores are a separator Field 2: The callback (or None) Field 3: parameters list for the callback (or None) Field 4: The accelerator key for the entry (or None) Field 5: The item type, used to define what kind of an item it is. Possible values: Item -> Item None or "" -> Item or separator "Title" -> create a title item "Item" -> create a simple item "CheckItem" -> create a check item "ToggleItem" -> create a toggle item "RadioItem" -> create a radio item path -> path of a radio item to link against "Left" -> create an item to hold sub items, right justified Notes: - Trailing Nones can be omitted. menuDef can be a string or a list/tuple, if it's a sequence this can be an example: menuDef = ( ("_File", None, None, None, "Branch" ), (" New", self.print_hello, None, "Ctrl-N" ), (" _Open", self.print_hello, None, "Ctrl-O" ), (" _Save", self.print_hello, None, "Ctrl-S" ), (" Save _As"), (" ____"), (" Quit", self.quit, None, "Ctrl-Q" ), ("Options", None, None, None, "Branch" ), (" Test"), ("_Help", None, None, None, "Left" ), (" About"), ) Alternative syntax1: """