Source code for leo.core.leoMenu

#@+leo-ver=5-thin
#@+node:ekr.20031218072017.3749: * @file leoMenu.py
"""Gui-independent menu handling for Leo."""
import leo.core.leoGlobals as g
# import sys
#@+others
#@+node:ekr.20031218072017.3750: ** class LeoMenu
[docs]class LeoMenu(object): """The base class for all Leo menus.""" #@+others #@+node:ekr.20120124042346.12938: *3* LeoMenu.Birth def __init__(self, frame): self.c = frame.c self.enable_dict = {} # Created by finishCreate. self.frame = frame self.isNull = False self.menus = {} # Menu dictionary. self.menuShortcuts = {}
[docs] def finishCreate(self): self.define_enable_dict()
#@+node:ekr.20120124042346.12937: *4* LeoMenu.define_enable_table #@@nobeautify
[docs] def define_enable_dict (self): # pylint: disable=unnecessary-lambda # The lambdas *are* necessary. c = self.c if not c.commandsDict: return # This is not an error: it happens during init. self.enable_dict = d = { # File menu... # 'revert': True, # Revert is always enabled. # 'open-with': True, # Open-With is always enabled. # Edit menu... 'undo': c.undoer.canUndo, 'redo': c.undoer.canRedo, 'extract-names': c.canExtractSectionNames, 'extract': c.canExtract, 'match-brackets': c.canFindMatchingBracket, # Top-level Outline menu... 'cut-node': c.canCutOutline, 'delete-node': c.canDeleteHeadline, 'paste-node': c.canPasteOutline, 'paste-retaining-clones': c.canPasteOutline, 'clone-node': c.canClone, 'sort-siblings': c.canSortSiblings, 'hoist': c.canHoist, 'de-hoist': c.canDehoist, # Outline:Expand/Contract menu... 'contract-parent': c.canContractParent, 'contract-node': lambda: c.p.hasChildren() and c.p.isExpanded(), 'contract-or-go-left': lambda: c.p.hasChildren() and c.p.isExpanded() or c.p.hasParent(), 'expand-node': lambda: c.p.hasChildren() and not c.p.isExpanded(), 'expand-prev-level': lambda: c.p.hasChildren() and c.p.isExpanded(), 'expand-next-level': lambda: c.p.hasChildren(), 'expand-to-level-1': lambda: c.p.hasChildren() and c.p.isExpanded(), 'expand-or-go-right': lambda: c.p.hasChildren(), # Outline:Move menu... 'move-outline-down': lambda: c.canMoveOutlineDown(), 'move-outline-left': lambda: c.canMoveOutlineLeft(), 'move-outline-right': lambda: c.canMoveOutlineRight(), 'move-outline-up': lambda: c.canMoveOutlineUp(), 'promote': lambda: c.canPromote(), 'demote': lambda: c.canDemote(), # Outline:Go To menu... 'goto-prev-history-node': lambda: c.nodeHistory.canGoToPrevVisited(), 'goto-next-history-node': lambda: c.nodeHistory.canGoToNextVisited(), 'goto-prev-visible': lambda: c.canSelectVisBack(), 'goto-next-visible': lambda: c.canSelectVisNext(), # These are too slow... # 'go-to-next-marked': c.canGoToNextMarkedHeadline, # 'go-to-next-changed': c.canGoToNextDirtyHeadline, 'goto-next-clone': lambda: c.p.isCloned(), 'goto-prev-node': lambda: c.canSelectThreadBack(), 'goto-next-node': lambda: c.canSelectThreadNext(), 'goto-parent': lambda: c.p.hasParent(), 'goto-prev-sibling': lambda: c.p.hasBack(), 'goto-next-sibling': lambda: c.p.hasNext(), # Outline:Mark menu... 'mark-subheads': lambda: c.p.hasChildren(), # too slow... # 'mark-changed-items': c.canMarkChangedHeadlines, } for i in range(1,9): d ['expand-to-level-%s' % (i)] = lambda: c.p.hasChildren() if 0: # Initial testing. commandKeys = list(c.commandsDict.keys()) for key in sorted(d.keys()): if key not in commandKeys: g.trace('*** bad entry for %s' % (key))
#@+node:ekr.20031218072017.3775: *3* LeoMenu.error and oops
[docs] def oops(self): g.pr("LeoMenu oops:", g.callers(4), "should be overridden in subclass")
[docs] def error(self, s): g.error('', s)
#@+node:ekr.20031218072017.3781: *3* LeoMenu.Gui-independent menu routines #@+node:ekr.20060926213642: *4* LeoMenu.capitalizeMinibufferMenuName #@@nobeautify
[docs] def capitalizeMinibufferMenuName(self, s, removeHyphens): result = [] for i, ch in enumerate(s): prev = s[i - 1] if i > 0 else '' prevprev = s[i - 2] if i > 1 else '' if ( i == 0 or i == 1 and prev == '&' or prev == '-' or prev == '&' and prevprev == '-' ): result.append(ch.capitalize()) elif removeHyphens and ch == '-': result.append(' ') else: result.append(ch) return ''.join(result)
#@+node:ekr.20031218072017.3785: *4* LeoMenu.createMenusFromTables & helpers
[docs] def createMenusFromTables(self): '''(leoMenu) Usually over-ridden.''' c = self.c aList = c.config.getMenusList() if aList: self.createMenusFromConfigList(aList) else: g.es_print('No @menu setting found')
#@+node:ekr.20070926135612: *5* LeoMenu.createMenusFromConfigList & helpers
[docs] def createMenusFromConfigList(self, aList): ''' Create menus from aList. The 'top' menu has already been created. ''' # Called from createMenuBar. c = self.c for z in aList: kind, val, val2 = z if kind.startswith('@menu'): name = kind[len('@menu'):].strip() if not self.handleSpecialMenus(name, parentName=None): # Fix #528: Don't create duplicate menu items. menu = self.createNewMenu(name) # Create top-level menu. if menu: self.createMenuFromConfigList(name, val, level=0) else: g.trace('no menu', name) else: self.error('%s %s not valid outside @menu tree' % (kind, val)) aList = c.config.getOpenWith() if aList: # a list of dicts. self.createOpenWithMenuFromTable(aList)
#@+node:ekr.20070927082205: *6* LeoMenu.createMenuFromConfigList
[docs] def createMenuFromConfigList(self, parentName, aList, level=0): """Build menu based on nested list List entries are either: ['@item', 'command-name', 'optional-view-name'] or: ['@menu Submenu name', <nested list>, None] :param str parentName: name of menu under which to place this one :param list aList: list of entries as described above """ parentMenu = self.getMenu(parentName) table = [] for z in aList: kind, val, val2 = z if kind.startswith('@menu'): # Menu names can be unicode without any problem. name = kind[5:].strip() if table: self.createMenuEntries(parentMenu, table) if not self.handleSpecialMenus(name, parentName, alt_name=val2, #848. table=table, ): menu = self.createNewMenu(name, parentName) # Create submenu of parent menu. if menu: # Partial fix for #528. self.createMenuFromConfigList(name, val, level + 1) table = [] elif kind == '@item': name = str(val) # Item names must always be ascii. if val2: # Translated names can be unicode. table.append((val2, name),) else: table.append(name) else: g.trace('can not happen: bad kind:', kind) if table: self.createMenuEntries(parentMenu, table)
#@+node:ekr.20070927172712: *6* LeoMenu.handleSpecialMenus
[docs] def handleSpecialMenus(self, name, parentName, alt_name=None, table=None): ''' Handle a special menu if name is the name of a special menu. return True if this method handles the menu. ''' c = self.c if table is None: table = [] name2 = name.replace('&', '').replace(' ', '').lower() if name2 == 'plugins': # Create the plugins menu using a hook. g.doHook("create-optional-menus", c=c) return True elif name2.startswith('recentfiles'): # Just create the menu. # createRecentFilesMenuItems will create the contents later. g.app.recentFilesManager.recentFilesMenuName = alt_name or name #848 self.createNewMenu(alt_name or name, parentName) return True elif name2 == 'help' and g.isMac: helpMenu = self.getMacHelpMenu(table) return helpMenu is not None else: return False
#@+node:ekr.20031218072017.3780: *4* LeoMenu.hasSelection # Returns True if text in the outline or body text is selected.
[docs] def hasSelection(self): c = self.c; w = c.frame.body.wrapper if c.frame.body: first, last = w.getSelectionRange() return first != last else: return False
#@+node:ekr.20051022053758.1: *3* LeoMenu.Helpers #@+node:ekr.20031218072017.3783: *4* LeoMenu.canonicalizeMenuName & cononicalizeTranslatedMenuName
[docs] def canonicalizeMenuName(self, name): return ''.join([ch for ch in name.lower() if ch.isalnum()])
[docs] def canonicalizeTranslatedMenuName(self, name): return ''.join([ch for ch in name.lower() if ch not in '& \t\n\r'])
#@+node:ekr.20051022044950: *4* LeoMenu.computeOldStyleShortcutKey
[docs] def computeOldStyleShortcutKey(self, s): '''Compute the old-style shortcut key for @shortcuts entries.''' return ''.join([ch for ch in s.strip().lower() if ch.isalnum()])
#@+node:ekr.20031218072017.1723: *4* LeoMenu.createMenuEntries & helpers
[docs] def createMenuEntries(self, menu, table, dynamicMenu=False): '''Create a menu entry from the table. New in 4.4: this method shows the shortcut in the menu, but this method **never** binds any shortcuts.''' c = self.c if g.app.unitTesting: return if not menu: return self.traceMenuTable(table) for data in table: label, command, done = self.getMenuEntryInfo(data, menu) if done: continue commandName = self.getMenuEntryBindings(command, dynamicMenu, label) if not commandName: continue masterMenuCallback = self.createMasterMenuCallback( dynamicMenu, command, commandName) realLabel = self.getRealMenuName(label) amp_index = realLabel.find("&") realLabel = realLabel.replace("&", "") # c.add_command ensures that c.outerUpdate is called. c.add_command(menu, label=realLabel, accelerator='', # The accelerator is now computed dynamically. command=masterMenuCallback, commandName=commandName, underline=amp_index)
#@+node:ekr.20111102072143.10016: *5* LeoMenu.createMasterMenuCallback
[docs] def createMasterMenuCallback(self, dynamicMenu, command, commandName): c = self.c def setWidget(): w = c.frame.getFocus() if w and g.isMac: # 2012/01/11: redirect (MacOS only). wname = c.widget_name(w) if wname.startswith('head'): w = c.frame.tree.edit_widget(c.p) # 2015/05/14: return a wrapper if possible. if not g.isTextWrapper(w): w = getattr(w, 'wrapper', w) return w if dynamicMenu: if command: def masterDynamicMenuCallback(c=c, command=command): # 2012/01/07: set w here. w = setWidget() event = g.app.gui.create_key_event(c, w=w) return c.k.masterCommand(func=command, event=event) return masterDynamicMenuCallback else: g.internalError('no callback for dynamic menu item.') def dummyMasterMenuCallback(): pass return dummyMasterMenuCallback else: def masterStaticMenuCallback(c=c, commandName=commandName): # 2011/10/28: Use only the command name to dispatch the command. # 2012/01/07: Bug fix: set w here. w = setWidget() event = g.app.gui.create_key_event(c, w=w) return c.k.masterCommand(commandName=commandName, event=event) return masterStaticMenuCallback
#@+node:ekr.20111028060955.16568: *5* LeoMenu.getMenuEntryBindings
[docs] def getMenuEntryBindings(self, command, dynamicMenu, label): '''Compute commandName from command.''' c = self.c if g.isString(command): # Command is really a command name. commandName = command else: # First, get the old-style name. commandName = self.computeOldStyleShortcutKey(label) command = c.commandsDict.get(commandName) return commandName
#@+node:ekr.20111028060955.16565: *5* LeoMenu.getMenuEntryInfo
[docs] def getMenuEntryInfo(self, data, menu): done = False if g.isString(data): # A single string is both the label and the command. s = data removeHyphens = s and s[0] == '*' if removeHyphens: s = s[1:] label = self.capitalizeMinibufferMenuName(s, removeHyphens) command = s.replace('&', '').lower() if label == '-': self.add_separator(menu) done = True # That's all. else: ok = isinstance(data, (list, tuple)) and len(data) in (2, 3) if ok: if len(data) == 2: # Command can be a minibuffer-command name. label, command = data else: # Ignore shortcuts bound in menu tables. label, junk, command = data if label in (None, '-'): self.add_separator(menu) done = True # That's all. else: g.trace('bad data in menu table: %s' % repr(data)) done = True # Ignore bad data return label, command, done
#@+node:ekr.20111028060955.16563: *5* LeoMenu.traceMenuTable
[docs] def traceMenuTable(self, table): trace = False and not g.unitTesting if not trace: return format = '%40s %s' g.trace('*' * 40) for data in table: if isinstance(data, (list, tuple)): n = len(data) if n == 2: print(format % (data[0], data[1])) elif n == 3: name, junk, func = data print(format % (name, func and func.__name__ or '<NO FUNC>')) else: print(format % (data, ''))
#@+node:ekr.20031218072017.3784: *4* LeoMenu.createMenuItemsFromTable
[docs] def createMenuItemsFromTable(self, menuName, table, dynamicMenu=False): if g.app.gui.isNullGui: return try: menu = self.getMenu(menuName) if menu is None: return self.createMenuEntries(menu, table, dynamicMenu=dynamicMenu) except Exception: g.es_print("exception creating items for", menuName, "menu") g.es_exception() g.app.menuWarningsGiven = True
#@+node:ekr.20031218072017.3804: *4* LeoMenu.createNewMenu
[docs] def createNewMenu(self, menuName, parentName="top", before=None): try: parent = self.getMenu(parentName) # parent may be None. menu = self.getMenu(menuName) if menu: # Not an error. # g.error("menu already exists:", menuName) return None # Fix #528. else: menu = self.new_menu(parent, tearoff=0, label=menuName) self.setMenu(menuName, menu) label = self.getRealMenuName(menuName) amp_index = label.find("&") label = label.replace("&", "") if before: # Insert the menu before the "before" menu. index_label = self.getRealMenuName(before) amp_index = index_label.find("&") index_label = index_label.replace("&", "") index = parent.index(index_label) self.insert_cascade(parent, index=index, label=label, menu=menu, underline=amp_index) else: self.add_cascade(parent, label=label, menu=menu, underline=amp_index) return menu except Exception: g.es("exception creating", menuName, "menu") g.es_exception() return None
#@+node:ekr.20031218072017.4116: *4* LeoMenu.createOpenWithMenuFromTable & helpers
[docs] def createOpenWithMenuFromTable(self, table): ''' Table is a list of dictionaries, created from @openwith settings nodes. This menu code uses these keys: 'name': menu label. 'shortcut': optional menu shortcut. efc.open_temp_file uses these keys: 'args': the command-line arguments to be used to open the file. 'ext': the file extension. 'kind': the method used to open the file, such as subprocess.Popen. ''' k = self.c.k if not table: return g.app.openWithTable = table # Override any previous table. # Delete the previous entry. parent = self.getMenu("File") if not parent: if not g.app.batchMode: g.error('', 'createOpenWithMenuFromTable:', 'no File menu') return label = self.getRealMenuName("Open &With...") amp_index = label.find("&") label = label.replace("&", "") try: index = parent.index(label) parent.delete(index) except Exception: try: index = parent.index("Open With...") parent.delete(index) except Exception: g.trace('unexpected exception') g.es_exception() return # Create the Open With menu. openWithMenu = self.createOpenWithMenu(parent, label, index, amp_index) if not openWithMenu: g.trace('openWithMenu returns None') return self.setMenu("Open With...", openWithMenu) # Create the menu items in of the Open With menu. self.createOpenWithMenuItemsFromTable(openWithMenu, table) for d in table: k.bindOpenWith(d)
#@+node:ekr.20051022043608.1: *5* LeoMenu.createOpenWithMenuItemsFromTable & callback
[docs] def createOpenWithMenuItemsFromTable(self, menu, table): ''' Create an entry in the Open with Menu from the table, a list of dictionaries. Each dictionary d has the following keys: 'args': the command-line arguments used to open the file. 'ext': not used here: used by efc.open_temp_file. 'kind': not used here: used by efc.open_temp_file. 'name': menu label. 'shortcut': optional menu shortcut. ''' c = self.c if g.app.unitTesting: return for d in table: label = d.get('name') args = d.get('args', []) accel = d.get('shortcut') or '' if label and args: realLabel = self.getRealMenuName(label) underline = realLabel.find("&") realLabel = realLabel.replace("&", "") callback = self.defineOpenWithMenuCallback(d) c.add_command(menu, label=realLabel, accelerator=accel, command=callback, underline=underline)
#@+node:ekr.20031218072017.4118: *6* LeoMenu.defineOpenWithMenuCallback
[docs] def defineOpenWithMenuCallback(self, d): # The first parameter must be event, and it must default to None. def openWithMenuCallback(event=None, self=self, d=d): return self.c.openWith(d=d) return openWithMenuCallback
#@+node:tbrown.20080509212202.7: *4* LeoMenu.deleteRecentFilesMenuItems
[docs] def deleteRecentFilesMenuItems(self, menu): """Delete recent file menu entries""" rf = g.app.recentFilesManager # Why not just delete all the entries? recentFiles = rf.getRecentFiles() toDrop = len(recentFiles) + len(rf.getRecentFilesTable()) self.delete_range(menu, 0, toDrop) for i in rf.groupedMenus: menu = self.getMenu(i) if menu: self.destroy(menu) self.destroyMenu(i)
#@+node:ekr.20031218072017.4117: *4* LeoMenu.defineMenuCallback
[docs] def defineMenuCallback(self, command, name, minibufferCommand): c = self.c if minibufferCommand: # Create a dummy event as a signal to doCommand. event = g.app.gui.create_key_event(c) # The first parameter must be event, and it must default to None. def minibufferMenuCallback(event=event, self=self, command=command, label=name): c = self.c return c.doCommand(command, label, event) return minibufferMenuCallback else: # The first parameter must be event, and it must default to None. def legacyMenuCallback(event=None, self=self, command=command, label=name): c = self.c # 2012/03/04. c.check_event(event) return c.doCommand(command, label) return legacyMenuCallback
#@+node:ekr.20031218072017.3805: *4* LeoMenu.deleteMenu
[docs] def deleteMenu(self, menuName): try: menu = self.getMenu(menuName) if menu: self.destroy(menu) self.destroyMenu(menuName) else: g.es("can't delete menu:", menuName) except Exception: g.es("exception deleting", menuName, "menu") g.es_exception()
#@+node:ekr.20031218072017.3806: *4* LeoMenu.deleteMenuItem
[docs] def deleteMenuItem(self, itemName, menuName="top"): """Delete itemName from the menu whose name is menuName.""" try: menu = self.getMenu(menuName) if menu: realItemName = self.getRealMenuName(itemName) self.delete(menu, realItemName) else: g.es("menu not found:", menuName) except Exception: g.es("exception deleting", itemName, "from", menuName, "menu") g.es_exception()
#@+node:ekr.20031218072017.3782: *4* LeoMenu.get/setRealMenuName & setRealMenuNamesFromTable # Returns the translation of a menu name or an item name.
[docs] def getRealMenuName(self, menuName): cmn = self.canonicalizeTranslatedMenuName(menuName) return g.app.realMenuNameDict.get(cmn, menuName)
[docs] def setRealMenuName(self, untrans, trans): cmn = self.canonicalizeTranslatedMenuName(untrans) g.app.realMenuNameDict[cmn] = trans
[docs] def setRealMenuNamesFromTable(self, table): try: for untrans, trans in table: self.setRealMenuName(untrans, trans) except Exception: g.es("exception in", "setRealMenuNamesFromTable") g.es_exception()
#@+node:ekr.20031218072017.3807: *4* LeoMenu.getMenu, setMenu, destroyMenu
[docs] def getMenu(self, menuName): cmn = self.canonicalizeMenuName(menuName) return self.menus.get(cmn)
[docs] def setMenu(self, menuName, menu): cmn = self.canonicalizeMenuName(menuName) self.menus[cmn] = menu
[docs] def destroyMenu(self, menuName): cmn = self.canonicalizeMenuName(menuName) del self.menus[cmn]
#@+node:ekr.20031218072017.3808: *3* LeoMenu.Must be overridden in menu subclasses #@+node:ekr.20031218072017.3809: *4* LeoMenu.9 Routines with Tk spellings
[docs] def add_cascade(self, parent, label, menu, underline): self.oops()
[docs] def add_command(self, menu, **keys): self.oops()
[docs] def add_separator(self, menu): self.oops()
# def bind (self,bind_shortcut,callback): # self.oops()
[docs] def delete(self, menu, realItemName): self.oops()
[docs] def delete_range(self, menu, n1, n2): self.oops()
[docs] def destroy(self, menu): self.oops()
[docs] def insert(self, menuName, position, label, command, underline=None): # New in Leo 4.4.3 a1 self.oops()
[docs] def insert_cascade(self, parent, index, label, menu, underline): self.oops()
[docs] def new_menu(self, parent, tearoff=0, label=''): # 2010: added label arg for pylint. self.oops(); return None
#@+node:ekr.20031218072017.3810: *4* LeoMenu.9 Routines with new spellings
[docs] def activateMenu(self, menuName): # New in Leo 4.4b2. self.oops()
[docs] def clearAccel(self, menu, name): self.oops()
[docs] def createMenuBar(self, frame): self.oops()
[docs] def createOpenWithMenu(self, parent, label, index, amp_index): self.oops(); return None
[docs] def disableMenu(self, menu, name): self.oops()
[docs] def enableMenu(self, menu, name, val): self.oops()
[docs] def getMacHelpMenu(self, table): return None
[docs] def getMenuLabel(self, menu, name): self.oops()
[docs] def setMenuLabel(self, menu, name, label, underline=-1): self.oops()
#@-others #@+node:ekr.20031218072017.3811: ** class NullMenu
[docs]class NullMenu(LeoMenu): """A null menu class for testing and batch execution.""" #@+others #@+node:ekr.20050104094308: *3* ctor (NullMenu) def __init__(self, frame): # Init the base class. LeoMenu.__init__(self, frame) self.isNull = True #@+node:ekr.20050104094029: *3* oops
[docs] def oops(self): pass
#@-others #@-others #@@language python #@@tabwidth -4 #@@pagewidth 70 #@-leo