Coverage for src/robotide/action/actioninfo.py: 90%
119 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-06 10:40 +0100
1# Copyright 2008-2015 Nokia Networks
2# Copyright 2016- Robot Framework Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
16import builtins 1af
17import re 1af
19import wx 1af
21from ..widgets import ImageProvider 1af
22from .shortcut import Shortcut 1af
24_ = wx.GetTranslation # To keep linter/code analyser happy 1af
25builtins.__dict__['_'] = wx.GetTranslation 1af
28def action_info_collection(data, event_handler, data_nt=None, container=None): 1af
29 """Parses the ``data`` into a list of `ActionInfo` and `SeparatorInfo` objects.
31 The data is parsed based on the simple DSL documented below.
33 :Parameters:
34 data
35 The data to be parsed into `ActionInfo` and `SeparatorInfo` objects.
36 event_handler
37 The event handler that implements the actions. See `finding handlers`_
38 for more information.
39 data_nt
40 Since RIDE 2.1, this is the original English menudata, or None which will be a copy of data
41 This is due to the way menu is created for translations, which will not build the correct handler names.
42 container
43 the wxPython element containing the UI components associated with
44 the `ActionInfo`.
46 DSL syntax
47 ----------
48 ::
50 [menu]
51 name | documentation | shortcut | icon
53 Fields
54 ------
56 menu
57 The name of the menu under which the entries below it are inserted.
58 name
59 The name of the menu entry to be added. If name is ``---``, a
60 `SeparatorInfo` object is created instead of an `ActionInfo` object.
61 If name is post fixed with shortcuts between parenthesis and separated
62 with ' or ', these shortcuts are parsed to machine local presentation
63 and shown after the name. This can be used instead of shotrcut-element
64 if you want to add shortcuts that you want to bind yourself and/or add
65 several shortcuts.
66 documentation
67 Documentation for the action.
68 shortcut
69 Keyboard shortcut to invoke the action.
70 icon
71 Icon for the toolbar button.
72 position
73 Value for menu item ordering.
75 See the `ActionInfo` attributes with same/similar names for more
76 information about the fields and their possible values. Three
77 last fields are optional.
79 Finding handlers
80 ----------------
82 (Note: before v2.0.7)
83 The given ``event_handler`` must have handler methods that map to the
84 specified action names. The mapping is done by prefixing the name with
85 ``On``, removing spaces, and capitalizing all words. For example ``Save``
86 and ``My Action`` must have handler methods ``OnSave`` and ``OnMyAction``,
87 respectively. If name has content between parenthesis at the end, this
88 content is ignored when creating handler mapping.
90 (Note: since v2.0.7)
91 The given ``event_handler`` must have handler methods that map to the
92 specified action names. The mapping is done by prefixing the name with
93 ``on``, replacing spaces by ``_``, and lowercasing all words. For example ``Save``
94 and ``My Action`` must have handler methods ``on_save`` and ``on_my_action``,
95 respectively. If name has content between parenthesis at the end, this
96 content is ignored when creating handler mapping.
98 Specifying container
99 --------------------
101 By default, the given ``container`` is passed to the `ActionInfo.__init__`
102 method directly. This can be altered by prefixing the ``name`` with an
103 exclamation mark (e.g. ``!Save`` or ``!My Action``) to make that action
104 global. With these actions the container given to the `ActionInfo.__init__`
105 is always ``None``.
107 Example
108 -------
109 ::
111 [File]
112 !&Open | Open file containing tests | Ctrl-O | ART_FILE_OPEN
113 !Open &Resource | Open a resource file | Ctrl-R
114 ---
115 &Save | Save selected datafile | Ctrl-S | ART_FILE_SAVE
117 [Tools]
118 !Manage Plugins | | | | POSITION-80
120 [Content]
121 Content Assist (Ctrl-Space or Ctrl-Alt-Space) | Has two shortcuts.
122 """
124 if not data_nt: 1aecdb
125 data_nt = data 1ec
126 menu = None 1aecdb
127 actions = [] 1aecdb
128 for row, row_nt in zip(data.splitlines(), data_nt.splitlines()): 1aecdb
129 row = row.strip() 1aecdb
130 row_nt = row_nt.strip() 1aecdb
131 # print(f"DEBUG: actioninfo.py action_info_collection in loop row={row}\noriginal={row_nt} ")
132 if not row and not row_nt: 1aecdb
133 continue 1aecd
134 elif row.startswith('[') and row.endswith(']'): 1aecdb
135 menu = row[1:-1].strip() 1aecdb
136 # print(f"DEBUG: actioninfo.py action_info_collection menu={menu}")
137 else:
138 actions.append(_create_action_info(event_handler, menu, container, row, row_nt)) 1aecdb
139 return actions 1aecdb
142def _create_action_info(eventhandler, menu, container, row, row_nt): 1af
143 # print(f"DEBUG: actioninfo.py _create_action_info menu={menu} row={row} row_nt={row_nt}")
144 t_menu = _(menu) 1aecdb
145 # if t_menu.startswith('[') and t_menu.endswith(']'):
146 # t_menu = t_menu[1:-1].strip()
147 t_row = _(row) # .replace('&', '')) 1aecdb
148 # print(f"DEBUG: actioninfo.py _create_action_info menu={t_menu} t_row={t_row} row_nt={row_nt}")
149 if row_nt.startswith('---'): 1aecdb
150 return SeparatorInfo(menu) 1ab
151 tokens = [t.strip() for t in t_row.split('|')] 1aecdb
152 tokens += [''] * (5-len(tokens)) 1aecdb
153 name, doc, shortcut, icon, position = tokens 1aecdb
154 tokens_nt = [t.strip() for t in row_nt.split('|')] 1aecdb
155 tokens_nt += [''] * (5-len(tokens_nt)) 1aecdb
156 name_nt, __, __, __, __ = tokens_nt 1aecdb
157 if name_nt.startswith('!'): 1aecdb
158 name = name[1:] 1ad
159 name_nt = name_nt[1:] 1ad
160 container = None 1ad
161 eventhandler_name, name_nt = get_eventhandler_name_and_parsed_name(name_nt) 1aecdb
162 action = getattr(eventhandler, eventhandler_name) 1aecdb
163 # print(f"DEBUG: actioninfo.py _create_action_info menu={menu} eventhandler_name={eventhandler_name},"
164 # f" name_nt={name_nt}")
165 return ActionInfo(menu, name, action, container, shortcut, icon, doc, position) 1aecdb
168def get_eventhandler_name_and_parsed_name(name): 1af
169 eventhandler_name, name = _parse_shortcuts_from_name(name) 1aijklecdb
170 # DEBUG: before v2.0.7 return 'On%s' % eventhandler_name.replace(' ', '').replace('&', ''), name
171 return 'on_%s' % eventhandler_name.strip().replace(' ', '_').replace('&', '').lower(), name 1aijklecdb
174def _parse_shortcuts_from_name(name): 1af
175 if '(' in name: 1aijklecdb
176 eventhandler_name, shortcuts = name.split('(', 1) 1acb
177 shortcuts = shortcuts.split(')')[0] 1acb
178 elements = shortcuts.split(' or ') 1acb
179 name = '%s (%s)' % (eventhandler_name, ' or '.join(Shortcut(e).printable for e in elements)) 1acb
180 return eventhandler_name, name 1acb
181 return name, name 1aijkledb
184class MenuInfo(object): 1af
185 """Base class for `ActionInfo` and `SeparatorInfo`."""
187 def __init__(self): 1af
188 self.insertion_point = _InsertionPoint() 1aecgdbh
190 def is_separator(self): 1af
191 return False 1ab
193 def set_menu_position(self, before=None, after=None): 1af
194 """Sets the position of this menu entry.
196 :Parameters:
197 before
198 Place this menu entry before the specified entry.
199 after
200 Place this menu entry after the specified entry.
202 Use either ``before`` or ``after`` and give the name without the
203 possible shortcut.
204 """
205 self.insertion_point = _InsertionPoint(before, after)
208class ActionInfo(MenuInfo): 1af
209 """Used to create menu entries, keyboard shortcuts and/or toolbar buttons."""
211 def __init__(self, menu_name, name, action=None, container=None, 1af
212 shortcut=None, icon=None, doc='', position=-1):
213 """Initializes information needed to create actions...
215 :Parameters:
216 menu_name
217 The name of the menu where the new entry will be added. The menu is
218 created if it does not exist.
219 name
220 The name of the new menu entry. The name may contain an accelerator
221 key prefixed by an ampersand like ``New &Action``. If an accelerator
222 is not specified, or the one requested is already taken, the next
223 free key is selected.
224 action
225 The callable which will be called when a user does any of the
226 associated UI actions.
227 container
228 The wxPython element containing the UI components associated with
229 the ``action``. When any of the registered UI actions is executed,
230 the ``action`` is called only if the ``container`` or any of its
231 child components has focus. It is possible to make the ``action``
232 always active by using ``None`` as the ``container``.
233 shortcut
234 The keyboard shortcut associated to the ``action``. The ``shortcut``
235 must be a string constructed from optional modifiers (``Ctrl, Shift,
236 Alt``) and the actual shortcut key separating the parts with a hyphen.
237 The shortcut key can be either a single character or any of the
238 `wx keycodes`__ without the ``WXK_`` prefix. Examples: ``Ctrl-C``,
239 ``Shift-Ctrl-6``, ``Alt-Left``, ``F6``.
240 icon
241 The icon added to the toolbar as a toolbar button. It can be either
242 a 16x16 bitmap or a string presenting one of the icons provided by
243 `wxPython's ArtProvider`__ like ``ART_FILE_OPEN``.
244 doc
245 The documentation shown on the statusbar when selection is on
246 the associated menu entry or toolbar button.
247 position
248 The positional value of an item in the menu. Provided for ordering
249 Tools menu. Defaults to -1.
251 __ https://docs.wxwidgets.org/stable/wx_keycodes.html#keycodes
252 __ https://www.wxpython.org/docs/api/wx.ArtProvider-class.html
253 """
254 MenuInfo.__init__(self) 1aecgdb
255 self.menu_name = menu_name 1aecgdb
256 self.name = name 1aecgdb
257 self.action = action 1aecgdb
258 self.container = container 1aecgdb
259 self.shortcut = Shortcut(shortcut) 1aecgdb
260 self._icon = None 1aecgdb
261 self._icon_source = icon 1aecgdb
262 self.doc = doc 1aecgdb
263 self._position = position 1aecgdb
265 @property 1af
266 def icon(self): 1af
267 if not self._icon: 267 ↛ 269line 267 didn't jump to line 269 because the condition on line 267 was always true1ab
268 self._icon = self._get_icon() 1ab
269 return self._icon 1ab
271 def _get_icon(self): 1af
272 if not self._icon_source: 1ab
273 return None 1ab
274 if isinstance(self._icon_source, str):
275 if self._icon_source.startswith("CUSTOM_"): 275 ↛ 276line 275 didn't jump to line 276 because the condition on line 275 was never true
276 return ImageProvider().get_image_by_name(self._icon_source[len("CUSTOM_"):])
277 return wx.ArtProvider.GetBitmap(getattr(wx, self._icon_source),
278 wx.ART_TOOLBAR, (16, 16))
279 return self._icon_source
281 @property 1af
282 def position(self): 1af
283 if isinstance(self._position, int): 1ab
284 return self._position
285 elif isinstance(self._position, str) and len(self._position) > 0: 285 ↛ 287line 285 didn't jump to line 287 because the condition on line 285 was always true1ab
286 return int(self._position.split("POSITION-")[-1]) 1ab
287 return -1
290class SeparatorInfo(MenuInfo): 1af
291 """Used to create separators to menus."""
293 def __init__(self, menu_name): 1af
294 """Initializes information needed to add separators to menus.
296 :Parameters:
297 menu_name
298 The name of the menu where the separator will be added. If menu does
299 not exist, it is created automatically.
300 """
301 MenuInfo.__init__(self) 1abh
302 self.menu_name = menu_name 1abh
304 def is_separator(self): 1af
305 return True 1abh
308class _InsertionPoint(object): 1af
309 _shortcut_remover = re.compile(r" {2,}\([^()]+\)$") 1af
311 def __init__(self, before=None, after=None): 1af
312 self._item = before or after 1aecgdbh
313 self._insert_before = before is not None 1aecgdbh
315 def get_index(self, menu): 1af
316 if not self._item: 1abh
317 return menu.GetMenuItemCount() 1abh
318 index = self._find_position_in_menu(menu)
319 if not index: 319 ↛ 321line 319 didn't jump to line 321 because the condition on line 319 was always true
320 return menu.GetMenuItemCount()
321 if not self._insert_before:
322 index += 1
323 return index
325 def _find_position_in_menu(self, menu): 1af
326 for index in range(0, menu.GetMenuItemCount()):
327 item = menu.FindItemByPosition(index)
328 if self._get_menu_item_name(item).lower() == self._item.lower(): 328 ↛ 329line 328 didn't jump to line 329 because the condition on line 328 was never true
329 return index
330 return None
332 def _get_menu_item_name(self, item): 1af
333 if wx.VERSION < (4, 1, 0): 333 ↛ 334line 333 didn't jump to line 334 because the condition on line 333 was never true
334 return self._shortcut_remover.split(item.GetLabel())[0]
335 return self._shortcut_remover.split(item.GetItemLabel())[0]