Coverage for C:\leo.repo\leo-editor\leo\core\leoMenu.py: 22%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#@+leo-ver=5-thin
2#@+node:ekr.20031218072017.3749: * @file leoMenu.py
3"""Gui-independent menu handling for Leo."""
4from typing import Any, List
5from leo.core import leoGlobals as g
6#@+others
7#@+node:ekr.20031218072017.3750: ** class LeoMenu
8class LeoMenu:
9 """The base class for all Leo menus."""
10 #@+others
11 #@+node:ekr.20120124042346.12938: *3* LeoMenu.Birth
12 def __init__(self, frame):
13 self.c = frame.c
14 self.enable_dict = {} # Created by finishCreate.
15 self.frame = frame
16 self.isNull = False
17 self.menus = {} # Menu dictionary.
18 self.menuShortcuts = {}
20 def finishCreate(self):
21 self.define_enable_dict()
22 #@+node:ekr.20120124042346.12937: *4* LeoMenu.define_enable_table
23 #@@nobeautify
25 def define_enable_dict (self):
27 # pylint: disable=unnecessary-lambda
28 # The lambdas *are* necessary.
29 c = self.c
30 if not c.commandsDict:
31 return # This is not an error: it happens during init.
32 self.enable_dict = d = {
34 # File menu...
35 # 'revert': True, # Revert is always enabled.
36 # 'open-with': True, # Open-With is always enabled.
38 # Edit menu...
39 'undo': c.undoer.canUndo,
40 'redo': c.undoer.canRedo,
41 'extract-names': c.canExtractSectionNames,
42 'extract': c.canExtract,
43 'match-brackets': c.canFindMatchingBracket,
45 # Top-level Outline menu...
46 'cut-node': c.canCutOutline,
47 'delete-node': c.canDeleteHeadline,
48 'paste-node': c.canPasteOutline,
49 'paste-retaining-clones': c.canPasteOutline,
50 'clone-node': c.canClone,
51 'sort-siblings': c.canSortSiblings,
52 'hoist': c.canHoist,
53 'de-hoist': c.canDehoist,
55 # Outline:Expand/Contract menu...
56 'contract-parent': c.canContractParent,
57 'contract-node': lambda: c.p.hasChildren() and c.p.isExpanded(),
58 'contract-or-go-left': lambda: c.p.hasChildren() and c.p.isExpanded() or c.p.hasParent(),
59 'expand-node': lambda: c.p.hasChildren() and not c.p.isExpanded(),
60 'expand-prev-level': lambda: c.p.hasChildren() and c.p.isExpanded(),
61 'expand-next-level': lambda: c.p.hasChildren(),
62 'expand-to-level-1': lambda: c.p.hasChildren() and c.p.isExpanded(),
63 'expand-or-go-right': lambda: c.p.hasChildren(),
65 # Outline:Move menu...
66 'move-outline-down': lambda: c.canMoveOutlineDown(),
67 'move-outline-left': lambda: c.canMoveOutlineLeft(),
68 'move-outline-right': lambda: c.canMoveOutlineRight(),
69 'move-outline-up': lambda: c.canMoveOutlineUp(),
70 'promote': lambda: c.canPromote(),
71 'demote': lambda: c.canDemote(),
73 # Outline:Go To menu...
74 'goto-prev-history-node': lambda: c.nodeHistory.canGoToPrevVisited(),
75 'goto-next-history-node': lambda: c.nodeHistory.canGoToNextVisited(),
76 'goto-prev-visible': lambda: c.canSelectVisBack(),
77 'goto-next-visible': lambda: c.canSelectVisNext(),
78 # These are too slow...
79 # 'go-to-next-marked': c.canGoToNextMarkedHeadline,
80 # 'go-to-next-changed': c.canGoToNextDirtyHeadline,
81 'goto-next-clone': lambda: c.p.isCloned(),
82 'goto-prev-node': lambda: c.canSelectThreadBack(),
83 'goto-next-node': lambda: c.canSelectThreadNext(),
84 'goto-parent': lambda: c.p.hasParent(),
85 'goto-prev-sibling': lambda: c.p.hasBack(),
86 'goto-next-sibling': lambda: c.p.hasNext(),
88 # Outline:Mark menu...
89 'mark-subheads': lambda: c.p.hasChildren(),
90 # too slow...
91 # 'mark-changed-items': c.canMarkChangedHeadlines,
92 }
94 for i in range(1,9):
95 d [f"expand-to-level-{i}"] = lambda: c.p.hasChildren()
97 if 0: # Initial testing.
98 commandKeys = list(c.commandsDict.keys())
99 for key in sorted(d.keys()):
100 if key not in commandKeys:
101 g.trace(f"*** bad entry for {key}")
102 #@+node:ekr.20031218072017.3775: *3* LeoMenu.error and oops
103 def oops(self):
104 g.pr("LeoMenu oops:", g.callers(4), "should be overridden in subclass")
106 def error(self, s):
107 g.error('', s)
108 #@+node:ekr.20031218072017.3781: *3* LeoMenu.Gui-independent menu routines
109 #@+node:ekr.20060926213642: *4* LeoMenu.capitalizeMinibufferMenuName
110 #@@nobeautify
112 def capitalizeMinibufferMenuName(self, s, removeHyphens):
113 result = []
114 for i, ch in enumerate(s):
115 prev = s[i - 1] if i > 0 else ''
116 prevprev = s[i - 2] if i > 1 else ''
117 if (
118 i == 0 or
119 i == 1 and prev == '&' or
120 prev == '-' or
121 prev == '&' and prevprev == '-'
122 ):
123 result.append(ch.capitalize())
124 elif removeHyphens and ch == '-':
125 result.append(' ')
126 else:
127 result.append(ch)
128 return ''.join(result)
129 #@+node:ekr.20031218072017.3785: *4* LeoMenu.createMenusFromTables & helpers
130 def createMenusFromTables(self):
131 """(leoMenu) Usually over-ridden."""
132 c = self.c
133 aList = c.config.getMenusList()
134 if aList:
135 self.createMenusFromConfigList(aList)
136 else:
137 g.es_print('No @menu setting found')
138 #@+node:ekr.20070926135612: *5* LeoMenu.createMenusFromConfigList & helpers
139 def createMenusFromConfigList(self, aList):
140 """
141 Create menus from aList.
142 The 'top' menu has already been created.
143 """
144 # Called from createMenuBar.
145 c = self.c
146 for z in aList:
147 kind, val, val2 = z
148 if kind.startswith('@menu'):
149 name = kind[len('@menu') :].strip()
150 if not self.handleSpecialMenus(name, parentName=None):
151 # #528: Don't create duplicate menu items.
152 # Create top-level menu.
153 menu = self.createNewMenu(name)
154 if menu:
155 self.createMenuFromConfigList(name, val, level=0)
156 else:
157 g.trace('no menu', name)
158 else:
159 self.error(f"{kind} {val} not valid outside @menu tree")
160 aList = c.config.getOpenWith()
161 if aList:
162 # a list of dicts.
163 self.createOpenWithMenuFromTable(aList)
164 #@+node:ekr.20070927082205: *6* LeoMenu.createMenuFromConfigList
165 def createMenuFromConfigList(self, parentName, aList, level=0):
166 """Build menu based on nested list
168 List entries are either:
170 ['@item', 'command-name', 'optional-view-name']
172 or:
174 ['@menu Submenu name', <nested list>, None]
176 :param str parentName: name of menu under which to place this one
177 :param list aList: list of entries as described above
178 """
179 parentMenu = self.getMenu(parentName)
180 if not parentMenu:
181 g.trace('NO PARENT', parentName, g.callers())
182 return # #2030
183 table: List[Any] = []
184 for z in aList:
185 kind, val, val2 = z
186 if kind.startswith('@menu'):
187 # Menu names can be unicode without any problem.
188 name = kind[5:].strip()
189 if table:
190 self.createMenuEntries(parentMenu, table)
191 if not self.handleSpecialMenus(name, parentName,
192 alt_name=val2, #848.
193 table=table,
194 ):
195 # Create submenu of parent menu.
196 menu = self.createNewMenu(name, parentName)
197 if menu:
198 # Partial fix for #528.
199 self.createMenuFromConfigList(name, val, level + 1)
200 table = []
201 elif kind == '@item':
202 name = str(val) # Item names must always be ascii.
203 if val2:
204 # Translated names can be unicode.
205 table.append((val2, name),)
206 else:
207 table.append(name)
208 else:
209 g.trace('can not happen: bad kind:', kind)
210 if table:
211 self.createMenuEntries(parentMenu, table)
212 #@+node:ekr.20070927172712: *6* LeoMenu.handleSpecialMenus
213 def handleSpecialMenus(self, name, parentName, alt_name=None, table=None):
214 """
215 Handle a special menu if name is the name of a special menu.
216 return True if this method handles the menu.
217 """
218 c = self.c
219 if table is None:
220 table = []
221 name2 = name.replace('&', '').replace(' ', '').lower()
222 if name2 == 'plugins':
223 # Create the plugins menu using a hook.
224 g.doHook("create-optional-menus", c=c, menu_name=name)
225 return True
226 if name2.startswith('recentfiles'):
227 # Just create the menu.
228 # createRecentFilesMenuItems will create the contents later.
229 g.app.recentFilesManager.recentFilesMenuName = alt_name or name # #848
230 self.createNewMenu(alt_name or name, parentName)
231 return True
232 if name2 == 'help' and g.isMac:
233 helpMenu = self.getMacHelpMenu(table)
234 return helpMenu is not None
235 return False
236 #@+node:ekr.20031218072017.3780: *4* LeoMenu.hasSelection
237 # Returns True if text in the outline or body text is selected.
239 def hasSelection(self):
240 c = self.c
241 w = c.frame.body.wrapper
242 if c.frame.body:
243 first, last = w.getSelectionRange()
244 return first != last
245 return False
246 #@+node:ekr.20051022053758.1: *3* LeoMenu.Helpers
247 #@+node:ekr.20031218072017.3783: *4* LeoMenu.canonicalize*
248 def canonicalizeMenuName(self, name):
250 # #1121 & #1188. Allow Chinese characters in command names
251 if g.isascii(name):
252 return ''.join([ch for ch in name.lower() if ch.isalnum()])
253 return name
255 def canonicalizeTranslatedMenuName(self, name):
257 # #1121 & #1188. Allow Chinese characters in command names
258 if g.isascii(name):
259 return ''.join([ch for ch in name.lower() if ch not in '& \t\n\r'])
260 return ''.join([ch for ch in name if ch not in '& \t\n\r'])
261 #@+node:ekr.20031218072017.1723: *4* LeoMenu.createMenuEntries & helpers
262 def createMenuEntries(self, menu, table):
263 """
264 Create a menu entry from the table.
266 This method shows the shortcut in the menu, but **never** binds any shortcuts.
267 """
268 c = self.c
269 if g.unitTesting:
270 return
271 if not menu:
272 return
273 self.traceMenuTable(table)
274 for data in table:
275 label, command, done = self.getMenuEntryInfo(data, menu)
276 if done:
277 continue
278 commandName = self.getMenuEntryBindings(command, label)
279 if not commandName:
280 continue
281 masterMenuCallback = self.createMasterMenuCallback(command, commandName)
282 realLabel = self.getRealMenuName(label)
283 amp_index = realLabel.find("&")
284 realLabel = realLabel.replace("&", "")
285 # c.add_command ensures that c.outerUpdate is called.
286 c.add_command(menu, label=realLabel,
287 accelerator='', # The accelerator is now computed dynamically.
288 command=masterMenuCallback,
289 commandName=commandName,
290 underline=amp_index)
291 #@+node:ekr.20111102072143.10016: *5* LeoMenu.createMasterMenuCallback
292 def createMasterMenuCallback(self, command, commandName):
293 """
294 Create a callback for the given args.
296 - If command is a string, it is treated as a command name.
297 - Otherwise, it should be a callable representing the actual command.
298 """
299 c = self.c
301 def getWidget():
302 """Carefully return the widget that has focus."""
303 w = c.frame.getFocus()
304 if w and g.isMac:
305 # Redirect (MacOS only).
306 wname = c.widget_name(w)
307 if wname.startswith('head'):
308 w = c.frame.tree.edit_widget(c.p)
309 # Return a wrapper if possible.
310 if not g.isTextWrapper(w):
311 w = getattr(w, 'wrapper', w)
312 return w
314 if isinstance(command, str):
316 def static_menu_callback():
317 event = g.app.gui.create_key_event(c, w=getWidget())
318 c.doCommandByName(commandName, event)
320 return static_menu_callback
322 # The command must be a callable.
323 if not callable(command):
325 def dummy_menu_callback(event=None):
326 pass
328 g.trace(f"bad command: {command!r}", color='red')
329 return dummy_menu_callback
331 # Create a command dynamically.
333 def dynamic_menu_callback():
334 event = g.app.gui.create_key_event(c, w=getWidget())
335 return c.doCommand(command, commandName, event) # #1595
337 return dynamic_menu_callback
338 #@+node:ekr.20111028060955.16568: *5* LeoMenu.getMenuEntryBindings
339 def getMenuEntryBindings(self, command, label):
340 """Compute commandName from command."""
341 c = self.c
342 if isinstance(command, str):
343 # Command is really a command name.
344 commandName = command
345 else:
346 # First, get the old-style name.
347 # #1121: Allow Chinese characters in command names
348 commandName = label.strip()
349 command = c.commandsDict.get(commandName)
350 return commandName
351 #@+node:ekr.20111028060955.16565: *5* LeoMenu.getMenuEntryInfo
352 def getMenuEntryInfo(self, data, menu):
353 """
354 Parse a single entry in the table passed to createMenuEntries.
356 Table entries have the following formats:
358 1. A string, used as the command name.
359 2. A 2-tuple: (command_name, command_func)
360 3. A 3-tuple: (command_name, menu_shortcut, command_func)
362 Special case: If command_name is None or "-" it represents a menu separator.
363 """
364 done = False
365 if isinstance(data, str):
366 # A single string is both the label and the command.
367 s = data
368 removeHyphens = s and s[0] == '*'
369 if removeHyphens:
370 s = s[1:]
371 label = self.capitalizeMinibufferMenuName(s, removeHyphens)
372 command = s.replace('&', '').lower()
373 if label == '-':
374 self.add_separator(menu)
375 done = True # That's all.
376 else:
377 ok = isinstance(data, (list, tuple)) and len(data) in (2, 3)
378 if ok:
379 if len(data) == 2:
380 # Command can be a minibuffer-command name.
381 label, command = data
382 else:
383 # Ignore shortcuts bound in menu tables.
384 label, junk, command = data
385 if label in (None, '-'):
386 self.add_separator(menu)
387 done = True # That's all.
388 else:
389 g.trace(f"bad data in menu table: {repr(data)}")
390 done = True # Ignore bad data
391 return label, command, done
392 #@+node:ekr.20111028060955.16563: *5* LeoMenu.traceMenuTable
393 def traceMenuTable(self, table):
395 trace = False and not g.unitTesting
396 if not trace:
397 return
398 format = '%40s %s'
399 g.trace('*' * 40)
400 for data in table:
401 if isinstance(data, (list, tuple)):
402 n = len(data)
403 if n == 2:
404 print(format % (data[0], data[1]))
405 elif n == 3:
406 name, junk, func = data
407 print(format % (name, func and func.__name__ or '<NO FUNC>'))
408 else:
409 print(format % (data, ''))
410 #@+node:ekr.20031218072017.3784: *4* LeoMenu.createMenuItemsFromTable
411 def createMenuItemsFromTable(self, menuName, table):
413 if g.app.gui.isNullGui:
414 return
415 try:
416 menu = self.getMenu(menuName)
417 if menu is None:
418 return
419 self.createMenuEntries(menu, table)
420 except Exception:
421 g.es_print("exception creating items for", menuName, "menu")
422 g.es_exception()
423 g.app.menuWarningsGiven = True
424 #@+node:ekr.20031218072017.3804: *4* LeoMenu.createNewMenu
425 def createNewMenu(self, menuName, parentName="top", before=None):
426 try:
427 parent = self.getMenu(parentName) # parent may be None.
428 menu = self.getMenu(menuName)
429 if menu:
430 # Not an error.
431 # g.error("menu already exists:", menuName)
432 return None # Fix #528.
433 menu = self.new_menu(parent, tearoff=0, label=menuName)
434 self.setMenu(menuName, menu)
435 label = self.getRealMenuName(menuName)
436 amp_index = label.find("&")
437 label = label.replace("&", "")
438 if before: # Insert the menu before the "before" menu.
439 index_label = self.getRealMenuName(before)
440 amp_index = index_label.find("&")
441 index_label = index_label.replace("&", "")
442 index = parent.index(index_label)
443 self.insert_cascade(
444 parent, index=index, label=label, menu=menu, underline=amp_index)
445 else:
446 self.add_cascade(parent, label=label, menu=menu, underline=amp_index)
447 return menu
448 except Exception:
449 g.es("exception creating", menuName, "menu")
450 g.es_exception()
451 return None
452 #@+node:ekr.20031218072017.4116: *4* LeoMenu.createOpenWithMenuFromTable & helpers
453 def createOpenWithMenuFromTable(self, table):
454 """
455 Table is a list of dictionaries, created from @openwith settings nodes.
457 This menu code uses these keys:
459 'name': menu label.
460 'shortcut': optional menu shortcut.
462 efc.open_temp_file uses these keys:
464 'args': the command-line arguments to be used to open the file.
465 'ext': the file extension.
466 'kind': the method used to open the file, such as subprocess.Popen.
467 """
468 k = self.c.k
469 if not table:
470 return
471 g.app.openWithTable = table # Override any previous table.
472 # Delete the previous entry.
473 parent = self.getMenu("File")
474 if not parent:
475 if not g.app.batchMode:
476 g.error('', 'createOpenWithMenuFromTable:', 'no File menu')
477 return
478 label = self.getRealMenuName("Open &With...")
479 amp_index = label.find("&")
480 label = label.replace("&", "")
481 try:
482 index = parent.index(label)
483 parent.delete(index)
484 except Exception:
485 try:
486 index = parent.index("Open With...")
487 parent.delete(index)
488 except Exception:
489 g.trace('unexpected exception')
490 g.es_exception()
491 return
492 # Create the Open With menu.
493 openWithMenu = self.createOpenWithMenu(parent, label, index, amp_index)
494 if not openWithMenu:
495 g.trace('openWithMenu returns None')
496 return
497 self.setMenu("Open With...", openWithMenu)
498 # Create the menu items in of the Open With menu.
499 self.createOpenWithMenuItemsFromTable(openWithMenu, table)
500 for d in table:
501 k.bindOpenWith(d)
502 #@+node:ekr.20051022043608.1: *5* LeoMenu.createOpenWithMenuItemsFromTable & callback
503 def createOpenWithMenuItemsFromTable(self, menu, table):
504 """
505 Create an entry in the Open with Menu from the table, a list of dictionaries.
507 Each dictionary d has the following keys:
509 'args': the command-line arguments used to open the file.
510 'ext': not used here: used by efc.open_temp_file.
511 'kind': not used here: used by efc.open_temp_file.
512 'name': menu label.
513 'shortcut': optional menu shortcut.
514 """
515 c = self.c
516 if g.unitTesting:
517 return
518 for d in table:
519 label = d.get('name')
520 args = d.get('args', [])
521 accel = d.get('shortcut') or ''
522 if label and args:
523 realLabel = self.getRealMenuName(label)
524 underline = realLabel.find("&")
525 realLabel = realLabel.replace("&", "")
526 callback = self.defineOpenWithMenuCallback(d)
527 c.add_command(menu,
528 label=realLabel,
529 accelerator=accel,
530 command=callback,
531 underline=underline)
532 #@+node:ekr.20031218072017.4118: *6* LeoMenu.defineOpenWithMenuCallback
533 def defineOpenWithMenuCallback(self, d=None):
534 # The first parameter must be event, and it must default to None.
536 def openWithMenuCallback(event=None, self=self, d=d):
537 d1 = d.copy() if d else {}
538 return self.c.openWith(d=d1)
540 return openWithMenuCallback
541 #@+node:tbrown.20080509212202.7: *4* LeoMenu.deleteRecentFilesMenuItems
542 def deleteRecentFilesMenuItems(self, menu):
543 """Delete recent file menu entries"""
544 rf = g.app.recentFilesManager
545 # Why not just delete all the entries?
546 recentFiles = rf.getRecentFiles()
547 toDrop = len(recentFiles) + len(rf.getRecentFilesTable())
548 self.delete_range(menu, 0, toDrop)
549 for i in rf.groupedMenus:
550 menu = self.getMenu(i)
551 if menu:
552 self.destroy(menu)
553 self.destroyMenu(i)
554 #@+node:ekr.20031218072017.3805: *4* LeoMenu.deleteMenu
555 def deleteMenu(self, menuName):
556 try:
557 menu = self.getMenu(menuName)
558 if menu:
559 self.destroy(menu)
560 self.destroyMenu(menuName)
561 else:
562 g.es("can't delete menu:", menuName)
563 except Exception:
564 g.es("exception deleting", menuName, "menu")
565 g.es_exception()
566 #@+node:ekr.20031218072017.3806: *4* LeoMenu.deleteMenuItem
567 def deleteMenuItem(self, itemName, menuName="top"):
568 """Delete itemName from the menu whose name is menuName."""
569 try:
570 menu = self.getMenu(menuName)
571 if menu:
572 realItemName = self.getRealMenuName(itemName)
573 self.delete(menu, realItemName)
574 else:
575 g.es("menu not found:", menuName)
576 except Exception:
577 g.es("exception deleting", itemName, "from", menuName, "menu")
578 g.es_exception()
579 #@+node:ekr.20031218072017.3782: *4* LeoMenu.get/setRealMenuName & setRealMenuNamesFromTable
580 # Returns the translation of a menu name or an item name.
582 def getRealMenuName(self, menuName):
583 cmn = self.canonicalizeTranslatedMenuName(menuName)
584 return g.app.realMenuNameDict.get(cmn, menuName)
586 def setRealMenuName(self, untrans, trans):
587 cmn = self.canonicalizeTranslatedMenuName(untrans)
588 g.app.realMenuNameDict[cmn] = trans
590 def setRealMenuNamesFromTable(self, table):
591 try:
592 for untrans, trans in table:
593 self.setRealMenuName(untrans, trans)
594 except Exception:
595 g.es("exception in", "setRealMenuNamesFromTable")
596 g.es_exception()
597 #@+node:ekr.20031218072017.3807: *4* LeoMenu.getMenu, setMenu, destroyMenu
598 def getMenu(self, menuName):
599 cmn = self.canonicalizeMenuName(menuName)
600 return self.menus.get(cmn)
602 def setMenu(self, menuName, menu):
603 cmn = self.canonicalizeMenuName(menuName)
604 self.menus[cmn] = menu
606 def destroyMenu(self, menuName):
607 cmn = self.canonicalizeMenuName(menuName)
608 del self.menus[cmn]
609 #@+node:ekr.20031218072017.3808: *3* LeoMenu.Must be overridden in menu subclasses
610 #@+node:ekr.20031218072017.3809: *4* LeoMenu.9 Routines with Tk spellings
611 def add_cascade(self, parent, label, menu, underline):
612 self.oops()
614 def add_command(self, menu, **keys):
615 self.oops()
617 def add_separator(self, menu):
618 self.oops()
620 # def bind (self,bind_shortcut,callback):
621 # self.oops()
623 def delete(self, menu, realItemName):
624 self.oops()
626 def delete_range(self, menu, n1, n2):
627 self.oops()
629 def destroy(self, menu):
630 self.oops()
632 def insert(
633 self, menuName, position, label, command, underline=None): # New in Leo 4.4.3 a1
634 self.oops()
636 def insert_cascade(self, parent, index, label, menu, underline):
637 self.oops()
639 def new_menu(self, parent, tearoff=0, label=''):
640 # 2010: added label arg for pylint.
641 self.oops()
642 #@+node:ekr.20031218072017.3810: *4* LeoMenu.9 Routines with new spellings
643 def activateMenu(self, menuName): # New in Leo 4.4b2.
644 self.oops()
646 def clearAccel(self, menu, name):
647 self.oops()
649 def createMenuBar(self, frame):
650 self.oops()
652 def createOpenWithMenu(self, parent, label, index, amp_index):
653 self.oops()
655 def disableMenu(self, menu, name):
656 self.oops()
658 def enableMenu(self, menu, name, val):
659 self.oops()
661 def getMacHelpMenu(self, table):
662 return None
664 def getMenuLabel(self, menu, name):
665 self.oops()
667 def setMenuLabel(self, menu, name, label, underline=-1):
668 self.oops()
669 #@-others
670#@+node:ekr.20031218072017.3811: ** class NullMenu
671class NullMenu(LeoMenu):
672 """A null menu class for testing and batch execution."""
673 #@+others
674 #@+node:ekr.20050104094308: *3* ctor (NullMenu)
675 def __init__(self, frame):
676 super().__init__(frame)
677 self.isNull = True
678 #@+node:ekr.20050104094029: *3* oops
679 def oops(self):
680 pass
681 #@-others
682#@-others
683#@@language python
684#@@tabwidth -4
685#@@pagewidth 70
686#@-leo