Coverage for src/robotide/ui/actiontriggers.py: 79%
260 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 1ad
17import wx 1ad
19from ..context import IS_WINDOWS, IS_MAC 1ad
20_ = wx.GetTranslation # To keep linter/code analyser happy 1ad
21builtins.__dict__['_'] = wx.GetTranslation 1ad
23ID_CustomizeToolbar = wx.ID_HIGHEST + 1 1ad
26def accel_index(menu: list, item: str) -> int: 1ad
27 """ Gets the index from a list ignoring the accelarator marker, &
28 :param menu: list - list to get index
29 :param item: str - item to find in menu list
30 :returns index: int
31 """
32 for idx, m in enumerate(menu): 32 ↛ 35line 32 didn't jump to line 35 because the loop on line 32 didn't complete
33 if m.replace('&', '') == item.replace('&', ''):
34 return idx
35 raise ValueError(f"{item} is not in list")
38class MenuBar(object): 1ad
40 def __init__(self, frame): 1ad
41 self._mb = wx.MenuBar()
42 self._name_builder = _NameBuilder()
43 self.m_frame = frame
44 self._accelerators = []
45 self._menus = []
46 self._create_default_menus()
48 def take_menu_bar_into_use(self): 1ad
49 """This should be called after fully populating menus,
50 Otherwise help menu will not be functional in osx."""
51 self.m_frame.SetMenuBar(self._mb)
53 def _create_default_menus(self): 1ad
54 for name in [_('File'), _('Edit'), _('Tools'), _('Help')]:
55 self._create_menu(name, before_help=False)
57 def _create_menu(self, name, before_help=True): 1ad
58 # print(f"DEBUG: actiontriggers.py _create_menu ENTER name={name}")
59 menu = _Menu(self._name_builder.get_name(name), self.m_frame)
60 self._insert_menu(menu, before_help)
61 return menu
63 def _insert_menu(self, menu, before_help): 1ad
64 if before_help:
65 index = accel_index([m.name for m in self._menus], _('Help'))
66 else:
67 index = len(self._menus)
68 self._menus.insert(index, menu)
69 self._mb.Insert(index, menu.wx_menu, menu.name)
71 def register(self, action): 1ad
72 menu = self._find_menu(action.menu_name) 1abc
73 if not menu: 1abc
74 menu = self._create_menu(action.menu_name)
75 menu.add_menu_item(action) 1abc
77 def _find_menu(self, name): 1ad
78 t_name = name.replace('&', '') 1abc
79 t_registered = self._name_builder.get_registered_name(_(t_name)) 1abc
80 registered = self._name_builder.get_registered_name(name) 1abc
81 if not t_registered and not registered: 1abc
82 return None
83 if t_registered: 83 ↛ 85line 83 didn't jump to line 85 because the condition on line 83 was always true1abc
84 registered = t_registered 1abc
85 for menu in self._menus: 85 ↛ exitline 85 didn't return from function '_find_menu' because the loop on line 85 didn't complete1abc
86 if menu.name == registered: 1abc
87 return menu 1abc
90class _Menu(object): 1ad
92 def __init__(self, name, frame): 1ad
93 self.name = name
94 # print(f"DEBUG: actiontriggers.py _Menu name={name}")
95 self._frame = frame
96 self.wx_menu = wx.Menu()
97 self._menu_items = {}
98 self._name_builder = _NameBuilder()
99 self._open = False
100 self._frame.Bind(wx.EVT_MENU_OPEN, self.on_menu_open)
101 self._frame.Bind(wx.EVT_MENU_CLOSE, self.on_menu_close)
103 def on_menu_open(self, event): 1ad
104 if self.wx_menu == event.GetMenu() and not self._open:
105 self._open = True
106 for menu_item in self._menu_items.values():
107 menu_item.refresh_availability()
108 event.Skip()
110 def on_menu_close(self, event): 1ad
111 if self._open:
112 self._open = False
113 for menu_item in self._menu_items.values():
114 menu_item.set_enabled()
115 event.Skip()
117 def add_menu_item(self, action): 1ad
118 menu_item = self._construct_menu_item(action) 1abc
119 self._menu_items[menu_item.id] = menu_item 1abc
120 menu_item.register(action) 1abc
122 def _construct_menu_item(self, action): 1ad
123 if action.is_separator(): 1abc
124 return self._create_separator(action) 1abc
125 menu_item = self._get_menu_item(action) 1abc
126 if not menu_item: 1abc
127 menu_item = self._create_menu_item(action) 1abc
128 return menu_item 1abc
130 def _create_separator(self, action): 1ad
131 menu_item = SeparatorMenuItem(self._frame, self, action) 1abc
132 pos = action.get_insertion_index(self.wx_menu) 1abc
133 menu_item.set_wx_menu_item(self.wx_menu.InsertSeparator(pos)) 1abc
134 return menu_item 1abc
136 def _get_menu_item(self, action): 1ad
137 for menu_item in self._menu_items.values(): 1abc
138 if self._names_equal(menu_item, action): 1abc
139 return menu_item
140 return None 1abc
142 def _names_equal(self, menu_item, action): 1ad
143 return menu_item.name == self._get_name(action, build_new=False) 1abc
145 def _get_name(self, action, build_new): 1ad
146 get_name = build_new and self._name_builder.get_name or \ 1abc
147 self._name_builder.get_registered_name
148 if not action.shortcut: # DEBUG not action.shortcut: 148 ↛ 149line 148 didn't jump to line 149 because the condition on line 148 was never true1abc
149 return get_name(action.name)
150 sht = action.get_shortcut() 1abc
151 if sht: 1abc
152 return '%s (%s)' % (get_name(action.name), sht) 1abc
153 return '%s' % get_name(action.name) 1ac
155 def _create_menu_item(self, action): 1ad
156 name_with_accelerator = self._get_name(action, build_new=True) 1abc
157 menu_item = MenuItem(self._frame, self, name_with_accelerator) 1abc
158 pos = action.get_insertion_index(self.wx_menu) 1abc
159 wx_menu_item = wx.MenuItem(self.wx_menu, menu_item.id, menu_item.name, action.doc) 1abc
160 if action.icon: 1abc
161 wx_menu_item.SetBitmap(action.icon)
162 wx_menu_item = self.wx_menu.Insert(pos, wx_menu_item) 1abc
163 menu_item.set_wx_menu_item(wx_menu_item) 1abc
164 return menu_item 1abc
166 def remove_menu_item(self, idd): 1ad
167 self.wx_menu.Delete(idd)
168 del(self._menu_items[idd])
171class _NameBuilder(object): 1ad
173 def __init__(self): 1ad
174 self._names = {}
175 self._accelerators = []
177 def get_name(self, name): 1ad
178 registered = self.get_registered_name(name) 1abcfjglmkehi
179 if registered: 1abcfjglmkehi
180 return registered 1k
181 try: 1abcfjglmkehi
182 name = self._use_given_accelerator(name) 1abcfjglmkehi
183 except ValueError: 1abcfjgehi
184 name = self._generate_accelerator(name) 1abcfjgehi
185 self._register(name) 1abcfjglmkehi
186 # print(f"DEBUG: actiontriggers.py get_name RETURN name={name}")
187 return name 1abcfjglmkehi
189 def get_registered_name(self, name): 1ad
190 try: 1abcfjglmkehi
191 return self._names[name.replace('&', '').upper()] 1abcfjglmkehi
192 except KeyError: 1abcfjglmkehi
193 return None 1abcfjglmkehi
195 def _register(self, name): 1ad
196 self._names[name.replace('&', '').upper()] = name 1abcfjglmkehi
198 def _use_given_accelerator(self, name): 1ad
199 index = name.find('&') + 1 1abcfjglmkehi
200 if 0 < index < len(name) and self._accelerator_is_free(name[index]): 1abcfjglmkehi
201 return name 1ablmke
202 raise ValueError 1abcfjgehi
204 def _generate_accelerator(self, name): 1ad
205 name = name.replace('&', '') 1abcfjgehi
206 for pos, char in enumerate(name): 1abcfjgehi
207 if self._accelerator_is_free(char): 1abcfjgehi
208 return '%s&%s' % (name[:pos], name[pos:]) 1abcfjgehi
209 return name 1abfe
211 def _accelerator_is_free(self, char): 1ad
212 char = char.upper() 1abcfjglmkehi
213 if char not in self._accelerators and char != u' ': 1abcfjglmkehi
214 self._accelerators.append(char) 1abcfjglmkehi
215 return True 1abcfjglmkehi
216 return False 1abfgehi
219class _MenuItem(object): 1ad
221 def __init__(self, frame, menu, name): 1ad
222 self._frame = frame 1abc
223 self._menu = menu 1abc
224 self.name = name 1abc
225 self._wx_menu_item = None 1abc
226 self._action_delegator = ActionDelegator(self._frame) 1abc
227 self.id = self._action_delegator.id 1abc
229 def set_wx_menu_item(self, wx_menu_item): 1ad
230 self._wx_menu_item = wx_menu_item 1abc
232 def register(self, action): 1ad
233 self._action_delegator.add(action) 1abc
234 action.register(self) 1abc
236 def unregister(self, action): 1ad
237 if self._action_delegator.remove(action):
238 self._menu.remove_menu_item(self.id)
240 def refresh_availability(self): 1ad
241 self._wx_menu_item.Enable(self._is_enabled())
243 def set_enabled(self): 1ad
244 self._wx_menu_item.Enable(True)
246 @staticmethod 1ad
247 def _is_enabled(): 1ad
248 return NotImplemented
251class MenuItem(_MenuItem): 1ad
253 def _is_enabled(self): 1ad
254 return self._action_delegator.is_active()
257class SeparatorMenuItem(_MenuItem): 1ad
259 def set_wx_menu_item(self, wx_menu_item): 1ad
260 _MenuItem.set_wx_menu_item(self, wx_menu_item) 1abc
261 # Should get ITEM_SEPARATOR
262 self.id = wx.ID_SEPARATOR 1abc
264 @staticmethod 1ad
265 def _is_enabled(): 1ad
266 return False
268 def set_enabled(self): 1ad
269 """ Just ignore it """
270 pass
273class _RideSearchMenuItem(object): 1ad
275 def __init__(self, handler, icon): 1ad
276 self._handler = handler
277 self.icon = icon
279 def __call__(self, *args, **kwargs): 1ad
280 self._handler(*args, **kwargs)
283class ToolBarButton(object): 1ad
285 def __init__(self, frame, toolbar, action): 1ad
286 self._toolbar = toolbar
287 self.icon = action.icon
288 self._action_delegator = ActionDelegator(frame)
289 self.id = self._action_delegator.id
291 def register(self, action): 1ad
292 self._action_delegator.add(action)
293 action.register(self)
294 action.inform_changes_in_enabled_status(self)
296 def unregister(self, action): 1ad
297 if self._action_delegator.remove(action):
298 self._toolbar.remove_toolbar_button(self)
300 def enabled_status_changed(self, action): 1ad
301 self._toolbar.enabled_status_changed(self.id, action)
304class ShortcutRegistry(object): 1ad
306 def __init__(self, frame): 1ad
307 self._frame = frame
308 self._actions = {}
310 def register(self, action): 1ad
311 if action.has_shortcut() and action.has_action(): 1ab
312 delegator = self._actions.setdefault(action.get_shortcut(), 1ab
313 ActionDelegator(self._frame,
314 action.shortcut))
315 delegator.add(action) 1ab
316 action.register(self) 1ab
317 self._update_accelerator_table() 1ab
319 def unregister(self, action): 1ad
320 key = action.get_shortcut()
321 if self._actions[key].remove(action):
322 del(self._actions[key])
323 self._update_accelerator_table()
325 def _update_accelerator_table(self): 1ad
326 accelerators = [] 1ab
327 for delegator in self._actions.values(): 1ab
328 try: 1ab
329 flags, key_code = delegator.shortcut.parse() 1ab
330 except TypeError: 1ab
331 continue 1ab
332 accelerators.append(wx.AcceleratorEntry(flags, key_code, 1ab
333 delegator.id))
334 self._frame.SetAcceleratorTable(wx.AcceleratorTable(accelerators)) 1ab
337class ActionDelegator(object): 1ad
339 def __init__(self, frame, shortcut=None): 1ad
340 self._frame = frame 1abc
341 self.shortcut = shortcut 1abc
342 self.id = wx.NewIdRef() 1abc
343 self._actions = [] 1abc
345 def add(self, action): 1ad
346 self._actions.append(action) 1abc
347 if len(self._actions) == 1: 1abc
348 self._frame.Bind(wx.EVT_MENU, self, id=self.id) 1abc
350 def remove(self, action): 1ad
351 """Removes action and returns True if delegator is empty."""
352 self._actions.remove(action)
353 if len(self._actions) == 0:
354 self._frame.Unbind(wx.EVT_MENU, id=self.id)
355 return True
356 return False
358 def is_active(self): 1ad
359 for action in self._actions:
360 if action.is_active():
361 return True
362 return False
364 def __call__(self, event): 1ad
365 for action in self._actions:
366 action.act(event)
367 if not (IS_WINDOWS or IS_MAC):
368 event.Skip()