1# ------------------------------------------------------------------------------ 2# Copyright (c) 2005, Enthought, Inc. 3# All rights reserved. 4# 5# This software is provided without warranty under the terms of the BSD 6# license included in LICENSE.txt and may be redistributed only 7# under the conditions described in the aforementioned license. The license 8# is also available online at http://www.enthought.com/licenses/BSD.txt 9# 10# Thanks for using Enthought open source! 11# 12# Author: David C. Morrill 13# Date: 01/24/2002 14# 15# ------------------------------------------------------------------------------ 16 17""" Dynamically construct wxPython Menus or MenuBars from a supplied string 18 description of the menu. 19 20Menu Description Syntax:: 21 22 submenu_label {help_string} 23 menuitem_label | accelerator {help_string} [~/-name]: code 24 25*submenu_label* 26 Label of a sub menu 27*menuitem_label* 28 Label of a menu item 29{*help_string*} 30 Help string to display on the status line (optional) 31*accelerator* 32 Accelerator key (e.g., Ctrl-C) (The '|' and keyname are optional, but must 33 be used together.) 34[~] 35 The menu item is checkable, but is not checked initially (optional) 36[/] 37 The menu item is checkable, and is checked initially (optional) 38[-] 39 The menu item disabled initially (optional) 40[*name*] 41 Symbolic name used to refer to menu item (optional) 42*code* 43 Python code invoked when menu item is selected 44 45A line beginning with a hyphen (-) is interpreted as a menu separator. 46""" 47 48# ========================================================================= 49# Imports: 50# ========================================================================= 51 52 53import re 54import logging 55 56 57import wx 58 59 60logger = logging.getLogger(__name__) 61 62 63# ========================================================================= 64# Constants: 65# ========================================================================= 66 67help_pat = re.compile(r"(.*){(.*)}(.*)") 68options_pat = re.compile(r"(.*)\[(.*)\](.*)") 69 70# Mapping of key name strings to wxPython key codes 71key_map = { 72 "F1": wx.WXK_F1, 73 "F2": wx.WXK_F2, 74 "F3": wx.WXK_F3, 75 "F4": wx.WXK_F4, 76 "F5": wx.WXK_F5, 77 "F6": wx.WXK_F6, 78 "F7": wx.WXK_F7, 79 "F8": wx.WXK_F8, 80 "F9": wx.WXK_F9, 81 "F10": wx.WXK_F10, 82 "F11": wx.WXK_F11, 83 "F12": wx.WXK_F12, 84} 85 86 87class MakeMenu: 88 """ Manages creation of menus. 89 """ 90 91 #: Initialize the globally unique menu ID: 92 cur_id = 1000 93 94 def __init__(self, desc, owner, popup=False, window=None): 95 """ Initializes the object. 96 """ 97 self.owner = owner 98 if window is None: 99 window = owner 100 self.window = window 101 self.indirect = getattr(owner, "call_menu", None) 102 self.names = {} 103 self.desc = desc.split("\n") 104 self.index = 0 105 self.keys = [] 106 if popup: 107 self.menu = menu = wx.Menu() 108 self.parse(menu, -1) 109 else: 110 self.menu = menu = wx.MenuBar() 111 self.parse(menu, -1) 112 window.SetMenuBar(menu) 113 if len(self.keys) > 0: 114 window.SetAcceleratorTable(wx.AcceleratorTable(self.keys)) 115 116 def parse(self, menu, indent): 117 """ Recursively parses menu items from the description. 118 """ 119 120 while True: 121 122 # Make sure we have not reached the end of the menu description 123 # yet: 124 if self.index >= len(self.desc): 125 return 126 127 # Get the next menu description line and check its indentation: 128 dline = self.desc[self.index] 129 line = dline.lstrip() 130 indented = len(dline) - len(line) 131 if indented <= indent: 132 return 133 134 # Indicate that the current line has been processed: 135 self.index += 1 136 137 # Check for a blank or comment line: 138 if (line == "") or (line[0:1] == "#"): 139 continue 140 141 # Check for a menu separator: 142 if line[0:1] == "-": 143 menu.AppendSeparator() 144 continue 145 146 # Allocate a new menu ID: 147 MakeMenu.cur_id += 1 148 cur_id = MakeMenu.cur_id 149 150 # Extract the help string (if any): 151 help = "" 152 match = help_pat.search(line) 153 if match: 154 help = " " + match.group(2).strip() 155 line = match.group(1) + match.group(3) 156 157 # Check for a menu item: 158 col = line.find(":") 159 if col >= 0: 160 handler = line[col + 1 :].strip() 161 if handler != "": 162 if self.indirect: 163 self.indirect(cur_id, handler) 164 handler = self.indirect 165 else: 166 try: 167 _locl = dict(self=self) 168 exec( 169 "def handler(event, self=self.owner):\n %s\n" 170 % handler, 171 globals(), 172 _locl, 173 ) 174 handler = _locl["handler"] 175 except Exception: 176 logger.exception( 177 "Invalid menu handler {:r}".format(handler) 178 ) 179 handler = null_handler 180 else: 181 try: 182 _locl = dict(self=self) 183 exec( 184 "def handler(event, self=self.owner):\n%s\n" 185 % (self.get_body(indented),), 186 globals(), 187 _locl, 188 ) 189 handler = _locl["handler"] 190 except Exception: 191 logger.exception( 192 "Invalid menu handler {:r}".format(handler) 193 ) 194 handler = null_handler 195 self.window.Bind(wx.EVT_MENU, handler, id=cur_id) 196 not_checked = checked = disabled = False 197 line = line[:col] 198 match = options_pat.search(line) 199 if match: 200 line = match.group(1) + match.group(3) 201 not_checked, checked, disabled, name = option_check( 202 "~/-", match.group(2).strip() 203 ) 204 if name != "": 205 self.names[name] = cur_id 206 setattr(self.owner, name, MakeMenuItem(self, cur_id)) 207 label = line.strip() 208 col = label.find("|") 209 if col >= 0: 210 key = label[col + 1 :].strip() 211 label = "%s%s%s" % (label[:col].strip(), "\t", key) 212 key = key.upper() 213 flag = wx.ACCEL_NORMAL 214 col = key.find("-") 215 if col >= 0: 216 flag = { 217 "CTRL": wx.ACCEL_CTRL, 218 "SHIFT": wx.ACCEL_SHIFT, 219 "ALT": wx.ACCEL_ALT, 220 }.get(key[:col].strip(), wx.ACCEL_CTRL) 221 key = key[col + 1 :].strip() 222 code = key_map.get(key, None) 223 try: 224 if code is None: 225 code = ord(key) 226 self.keys.append( 227 wx.AcceleratorEntry(flag, code, cur_id) 228 ) 229 except: 230 pass 231 menu.Append(cur_id, label, help, not_checked or checked) 232 if checked: 233 menu.Check(cur_id, True) 234 if disabled: 235 menu.Enable(cur_id, False) 236 continue 237 238 # Else must be the start of a sub menu: 239 submenu = wx.Menu() 240 label = line.strip() 241 242 # Recursively parse the sub-menu: 243 self.parse(submenu, indented) 244 245 # Add the menu to its parent: 246 try: 247 menu.AppendMenu(cur_id, label, submenu, help) 248 except: 249 # Handle the case where 'menu' is really a 'MenuBar' (which does 250 # not understand 'MenuAppend'): 251 menu.Append(submenu, label) 252 253 def get_body(self, indent): 254 """ Returns the body of an inline method. 255 """ 256 result = [] 257 while self.index < len(self.desc): 258 line = self.desc[self.index] 259 if (len(line) - len(line.lstrip())) <= indent: 260 break 261 result.append(line) 262 self.index += 1 263 result = "\n".join(result).rstrip() 264 if result != "": 265 return result 266 return " pass" 267 268 def get_id(self, name): 269 """ Returns the ID associated with a specified name. 270 """ 271 if isinstance(name, str): 272 return self.names[name] 273 return name 274 275 def checked(self, name, check=None): 276 """ Checks (or unchecks) a menu item specified by name. 277 """ 278 if check is None: 279 return self.menu.IsChecked(self.get_id(name)) 280 self.menu.Check(self.get_id(name), check) 281 282 def enabled(self, name, enable=None): 283 """ Enables (or disables) a menu item specified by name. 284 """ 285 if enable is None: 286 return self.menu.IsEnabled(self.get_id(name)) 287 self.menu.Enable(self.get_id(name), enable) 288 289 def label(self, name, label=None): 290 """ Gets or sets the label for a menu item. 291 """ 292 if label is None: 293 return self.menu.GetLabel(self.get_id(name)) 294 self.menu.SetLabel(self.get_id(name), label) 295 296 297class MakeMenuItem: 298 """ A menu item for a menu managed by MakeMenu. 299 """ 300 301 def __init__(self, menu, id): 302 self.menu = menu 303 self.id = id 304 305 def checked(self, check=None): 306 return self.menu.checked(self.id, check) 307 308 def toggle(self): 309 checked = not self.checked() 310 self.checked(checked) 311 return checked 312 313 def enabled(self, enable=None): 314 return self.menu.enabled(self.id, enable) 315 316 def label(self, label=None): 317 return self.menu.label(self.id, label) 318 319 320# ------------------------------------------------------------------------- 321# Determine whether a string contains any specified option characters, and 322# remove them if it does: 323# ------------------------------------------------------------------------- 324 325 326def option_check(test, string): 327 """ Determines whether a string contains any specified option characters, 328 and removes them if it does. 329 """ 330 result = [] 331 for char in test: 332 col = string.find(char) 333 result.append(col >= 0) 334 if col >= 0: 335 string = string[:col] + string[col + 1 :] 336 return result + [string.strip()] 337 338 339# ------------------------------------------------------------------------- 340# Null menu option selection handler: 341# ------------------------------------------------------------------------- 342 343 344def null_handler(event): 345 print("null_handler invoked") 346