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