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

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. 

15 

16import builtins 1af

17import re 1af

18 

19import wx 1af

20 

21from ..widgets import ImageProvider 1af

22from .shortcut import Shortcut 1af

23 

24_ = wx.GetTranslation # To keep linter/code analyser happy 1af

25builtins.__dict__['_'] = wx.GetTranslation 1af

26 

27 

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. 

30 

31 The data is parsed based on the simple DSL documented below. 

32 

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`. 

45 

46 DSL syntax 

47 ---------- 

48 :: 

49 

50 [menu] 

51 name | documentation | shortcut | icon 

52 

53 Fields 

54 ------ 

55 

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. 

74 

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. 

78 

79 Finding handlers 

80 ---------------- 

81 

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. 

89 

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. 

97 

98 Specifying container 

99 -------------------- 

100 

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``. 

106 

107 Example 

108 ------- 

109 :: 

110 

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 

116 

117 [Tools] 

118 !Manage Plugins | | | | POSITION-80 

119 

120 [Content] 

121 Content Assist (Ctrl-Space or Ctrl-Alt-Space) | Has two shortcuts. 

122 """ 

123 

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

140 

141 

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

166 

167 

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

172 

173 

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

182 

183 

184class MenuInfo(object): 1af

185 """Base class for `ActionInfo` and `SeparatorInfo`.""" 

186 

187 def __init__(self): 1af

188 self.insertion_point = _InsertionPoint() 1aecgdbh

189 

190 def is_separator(self): 1af

191 return False 1ab

192 

193 def set_menu_position(self, before=None, after=None): 1af

194 """Sets the position of this menu entry. 

195 

196 :Parameters: 

197 before 

198 Place this menu entry before the specified entry. 

199 after 

200 Place this menu entry after the specified entry. 

201 

202 Use either ``before`` or ``after`` and give the name without the 

203 possible shortcut. 

204 """ 

205 self.insertion_point = _InsertionPoint(before, after) 

206 

207 

208class ActionInfo(MenuInfo): 1af

209 """Used to create menu entries, keyboard shortcuts and/or toolbar buttons.""" 

210 

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... 

214 

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. 

250 

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

264 

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

270 

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 

280 

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 

288 

289 

290class SeparatorInfo(MenuInfo): 1af

291 """Used to create separators to menus.""" 

292 

293 def __init__(self, menu_name): 1af

294 """Initializes information needed to add separators to menus. 

295 

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

303 

304 def is_separator(self): 1af

305 return True 1abh

306 

307 

308class _InsertionPoint(object): 1af

309 _shortcut_remover = re.compile(r" {2,}\([^()]+\)$") 1af

310 

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

314 

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 

324 

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 

331 

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]