1# (C) Copyright 2005-2020 Enthought, Inc., Austin, TX
2# All rights reserved.
3#
4# This software is provided without warranty under the terms of the BSD
5# license included in LICENSE.txt and may be redistributed only under
6# the conditions described in the aforementioned license. The license
7# is also available online at http://www.enthought.com/licenses/BSD.txt
8#
9# Thanks for using Enthought open source!
10
11""" The wx specific implementations the action manager internal classes.
12"""
13
14
15from inspect import getfullargspec
16
17
18import wx
19
20
21from traits.api import Any, Bool, HasTraits
22
23
24from pyface.action.action_event import ActionEvent
25
26
27_STYLE_TO_KIND_MAP = {
28    "push": wx.ITEM_NORMAL,
29    "radio": wx.ITEM_RADIO,
30    "toggle": wx.ITEM_CHECK,
31    "widget": None,
32}
33
34
35class _MenuItem(HasTraits):
36    """ A menu item representation of an action item. """
37
38    # '_MenuItem' interface ------------------------------------------------
39
40    # Is the item checked?
41    checked = Bool(False)
42
43    # A controller object we delegate taking actions through (if any).
44    controller = Any()
45
46    # Is the item enabled?
47    enabled = Bool(True)
48
49    # Is the item visible?
50    visible = Bool(True)
51
52    # The radio group we are part of (None if the menu item is not part of such
53    # a group).
54    group = Any()
55
56    # ------------------------------------------------------------------------
57    # 'object' interface.
58    # ------------------------------------------------------------------------
59
60    def __init__(self, parent, menu, item, controller):
61        """ Creates a new menu item for an action item. """
62
63        self.item = item
64
65        # Create an appropriate menu item depending on the style of the action.
66        #
67        # N.B. Don't try to use -1 as the Id for the menu item... wx does not
68        # like it!
69        action = item.action
70        label = action.name
71        kind = _STYLE_TO_KIND_MAP[action.style]
72        longtip = action.description or action.tooltip
73
74        if action.style == "widget":
75            raise NotImplementedError(
76                "WxPython does not support widgets in menus"
77            )
78
79        if len(action.accelerator) > 0:
80            label = label + "\t" + action.accelerator
81
82        # This just helps with debugging when people forget to specify a name
83        # for their action (without this wx just barfs which is not very
84        # helpful!).
85        if len(label) == 0:
86            label = item.action.__class__.__name__
87
88        if getattr(action, "menu_role", False):
89            if action.menu_role == "About":
90                self.control_id = wx.ID_ABOUT
91            elif action.menu_role == "Preferences":
92                self.control_id = wx.ID_PREFERENCES
93            elif action.menu_role == "Quit":
94                self.control_id = wx.ID_EXIT
95        else:
96            self.control_id = wx.ID_ANY
97        self.control = wx.MenuItem(menu, self.control_id, label, longtip, kind)
98
99        # If the action has an image then display it.
100        if action.image is not None:
101            try:
102                self.control.SetBitmap(action.image.create_bitmap())
103            except Exception:
104                # Some platforms don't allow radio buttons to have
105                # bitmaps, so just ignore the exception if it happens
106                pass
107
108        menu.Append(self.control)
109        menu.menu_items.append(self)
110
111        # Set the initial enabled/disabled state of the action.
112        self.control.Enable(action.enabled and action.visible)
113
114        # Set the initial checked state.
115        if action.style in ["radio", "toggle"]:
116            self.control.Check(action.checked)
117
118        # Wire it up...create an ugly flag since some platforms dont skip the
119        # event when we thought they would
120        self._skip_menu_event = False
121        parent.Bind(wx.EVT_MENU, self._on_menu, self.control)
122
123        # Listen for trait changes on the action (so that we can update its
124        # enabled/disabled/checked state etc).
125        action.on_trait_change(self._on_action_enabled_changed, "enabled")
126        action.on_trait_change(self._on_action_visible_changed, "visible")
127        action.on_trait_change(self._on_action_checked_changed, "checked")
128        action.on_trait_change(self._on_action_name_changed, "name")
129        action.on_trait_change(self._on_action_image_changed, "image")
130
131        if controller is not None:
132            self.controller = controller
133            controller.add_to_menu(self)
134
135    def dispose(self):
136        action = self.item.action
137        action.on_trait_change(
138            self._on_action_enabled_changed, "enabled", remove=True
139        )
140        action.on_trait_change(
141            self._on_action_visible_changed, "visible", remove=True
142        )
143        action.on_trait_change(
144            self._on_action_checked_changed, "checked", remove=True
145        )
146        action.on_trait_change(
147            self._on_action_name_changed, "name", remove=True
148        )
149
150    # ------------------------------------------------------------------------
151    # Private interface.
152    # ------------------------------------------------------------------------
153
154    # Trait event handlers -------------------------------------------------
155
156    def _enabled_changed(self):
157        """ Called when our 'enabled' trait is changed. """
158
159        self.control.Enable(self.enabled and self.visible)
160
161    def _visible_changed(self):
162        """ Called when our 'visible' trait is changed. """
163
164        self.control.Enable(self.visible and self.enabled)
165
166    def _checked_changed(self):
167        """ Called when our 'checked' trait is changed. """
168
169        if self.item.action.style == "radio":
170            # fixme: Not sure why this is even here, we had to guard it to
171            # make it work? Must take a look at svn blame!
172            # FIXME v3: Note that menu_checked() doesn't seem to exist, so we
173            # comment it out and do the following instead.
174            # if self.group is not None:
175            #    self.group.menu_checked(self)
176
177            # If we're turning this one on, then we need to turn all the others
178            # off.  But if we're turning this one off, don't worry about the
179            # others.
180            if self.checked:
181                for item in self.item.parent.items:
182                    if item is not self.item:
183                        item.action.checked = False
184
185        self.control.Check(self.checked)
186
187    def _on_action_enabled_changed(self, action, trait_name, old, new):
188        """ Called when the enabled trait is changed on an action. """
189
190        self.control.Enable(action.enabled and action.visible)
191
192    def _on_action_visible_changed(self, action, trait_name, old, new):
193        """ Called when the visible trait is changed on an action. """
194
195        self.control.Enable(action.visible and action.enabled)
196
197    def _on_action_checked_changed(self, action, trait_name, old, new):
198        """ Called when the checked trait is changed on an action. """
199
200        if self.item.action.style == "radio":
201            # fixme: Not sure why this is even here, we had to guard it to
202            # make it work? Must take a look at svn blame!
203            # FIXME v3: Note that menu_checked() doesn't seem to exist, so we
204            # comment it out and do the following instead.
205            # if self.group is not None:
206            #    self.group.menu_checked(self)
207
208            # If we're turning this one on, then we need to turn all the others
209            # off.  But if we're turning this one off, don't worry about the
210            # others.
211            if action.checked:
212                for item in self.item.parent.items:
213                    if item is not self.item:
214                        item.action.checked = False
215
216        # This will *not* emit a menu event because of this ugly flag
217        self._skip_menu_event = True
218        self.control.Check(action.checked)
219        self._skip_menu_event = False
220
221    def _on_action_name_changed(self, action, trait_name, old, new):
222        """ Called when the name trait is changed on an action. """
223
224        label = action.name
225        if len(action.accelerator) > 0:
226            label = label + "\t" + action.accelerator
227        self.control.SetText(label)
228
229    def _on_action_image_changed(self, action, trait_name, old, new):
230        """ Called when the name trait is changed on an action. """
231
232        if self.control is not None:
233            self.control.SetIcon(action.image.create_icon())
234
235        return
236
237    # wx event handlers ----------------------------------------------------
238
239    def _on_menu(self, event):
240        """ Called when the menu item is clicked. """
241
242        # if the ugly flag is set, do not perform the menu event
243        if self._skip_menu_event:
244            return
245
246        action = self.item.action
247        action_event = ActionEvent()
248
249        is_checkable = action.style in ["radio", "toggle"]
250
251        # Perform the action!
252        if self.controller is not None:
253            if is_checkable:
254                # fixme: There is a difference here between having a controller
255                # and not in that in this case we do not set the checked state
256                # of the action! This is confusing if you start off without a
257                # controller and then set one as the action now behaves
258                # differently!
259                self.checked = self.control.IsChecked() == 1
260
261            # Most of the time, action's do no care about the event (it
262            # contains information about the time the event occurred etc), so
263            # we only pass it if the perform method requires it. This is also
264            # useful as Traits UI controllers *never* require the event.
265            argspec = getfullargspec(self.controller.perform)
266
267            # If the only arguments are 'self' and 'action' then don't pass
268            # the event!
269            if len(argspec.args) == 2:
270                self.controller.perform(action)
271
272            else:
273                self.controller.perform(action, action_event)
274
275        else:
276            if is_checkable:
277                action.checked = self.control.IsChecked() == 1
278
279            # Most of the time, action's do no care about the event (it
280            # contains information about the time the event occurred etc), so
281            # we only pass it if the perform method requires it.
282            argspec = getfullargspec(action.perform)
283
284            # If the only argument is 'self' then don't pass the event!
285            if len(argspec.args) == 1:
286                action.perform()
287
288            else:
289                action.perform(action_event)
290
291
292class _Tool(HasTraits):
293    """ A tool bar tool representation of an action item. """
294
295    # '_Tool' interface ----------------------------------------------------
296
297    # Is the item checked?
298    checked = Bool(False)
299
300    # A controller object we delegate taking actions through (if any).
301    controller = Any()
302
303    # Is the item enabled?
304    enabled = Bool(True)
305
306    # Is the item visible?
307    visible = Bool(True)
308
309    # The radio group we are part of (None if the tool is not part of such a
310    # group).
311    group = Any()
312
313    # ------------------------------------------------------------------------
314    # 'object' interface.
315    # ------------------------------------------------------------------------
316
317    def __init__(
318        self, parent, tool_bar, image_cache, item, controller, show_labels
319    ):
320        """ Creates a new tool bar tool for an action item. """
321
322        self.item = item
323        self.tool_bar = tool_bar
324
325        # Create an appropriate tool depending on the style of the action.
326        action = self.item.action
327        label = action.name
328
329        # Tool bar tools never have '...' at the end!
330        if label.endswith("..."):
331            label = label[:-3]
332
333        # And they never contain shortcuts.
334        label = label.replace("&", "")
335
336        # If the action has an image then convert it to a bitmap (as required
337        # by the toolbar).
338        if action.image is not None:
339            image = action.image.create_image(
340                self.tool_bar.GetToolBitmapSize()
341            )
342            path = action.image.absolute_path
343            bmp = image_cache.get_bitmap(path)
344
345        else:
346            from pyface.api import ImageResource
347
348            image = ImageResource("image_not_found")
349            bmp = image.create_bitmap()
350
351        kind = _STYLE_TO_KIND_MAP[action.style]
352        tooltip = action.tooltip
353        longtip = action.description
354
355        if not show_labels:
356            label = ""
357
358        else:
359            self.tool_bar.SetSize((-1, 50))
360
361        if action.style == "widget":
362            widget = action.create_control(self.tool_bar)
363            self.control = tool_bar.AddControl(widget, label)
364            self.control_id = self.control.GetId()
365        else:
366            self.control = tool_bar.AddTool(
367                wx.ID_ANY,
368                label,
369                bmp,
370                wx.NullBitmap,
371                kind,
372                tooltip,
373                longtip,
374                None,
375            )
376            self.control_id = self.control.GetId()
377
378            # Set the initial checked state.
379            tool_bar.ToggleTool(self.control_id, action.checked)
380
381            if hasattr(tool_bar, "ShowTool"):
382                # Set the initial enabled/disabled state of the action.
383                tool_bar.EnableTool(self.control_id, action.enabled)
384
385                # Set the initial visibility
386                tool_bar.ShowTool(self.control_id, action.visible)
387            else:
388                # Set the initial enabled/disabled state of the action.
389                tool_bar.EnableTool(
390                    self.control_id, action.enabled and action.visible
391                )
392
393            # Wire it up.
394            parent.Bind(wx.EVT_TOOL, self._on_tool, self.control)
395
396        # Listen for trait changes on the action (so that we can update its
397        # enabled/disabled/checked state etc).
398        action.on_trait_change(self._on_action_enabled_changed, "enabled")
399        action.on_trait_change(self._on_action_visible_changed, "visible")
400        action.on_trait_change(self._on_action_checked_changed, "checked")
401
402        if controller is not None:
403            self.controller = controller
404            controller.add_to_toolbar(self)
405
406    # ------------------------------------------------------------------------
407    # Private interface.
408    # ------------------------------------------------------------------------
409
410    # Trait event handlers -------------------------------------------------
411
412    def _enabled_changed(self):
413        """ Called when our 'enabled' trait is changed. """
414
415        if hasattr(self.tool_bar, "ShowTool"):
416            self.tool_bar.EnableTool(self.control_id, self.enabled)
417        else:
418            self.tool_bar.EnableTool(
419                self.control_id, self.enabled and self.visible
420            )
421
422    def _visible_changed(self):
423        """ Called when our 'visible' trait is changed. """
424
425        if hasattr(self.tool_bar, "ShowTool"):
426            self.tool_bar.ShowTool(self.control_id, self.visible)
427        else:
428            self.tool_bar.EnableTool(
429                self.control_id, self.enabled and self.visible
430            )
431
432    def _checked_changed(self):
433        """ Called when our 'checked' trait is changed. """
434
435        if self.item.action.style == "radio":
436            # FIXME v3: Note that toolbar_checked() doesn't seem to exist, so
437            # we comment it out and do the following instead.
438            # self.group.toolbar_checked(self)
439
440            # If we're turning this one on, then we need to turn all the others
441            # off.  But if we're turning this one off, don't worry about the
442            # others.
443            if self.checked:
444                for item in self.item.parent.items:
445                    if item is not self.item:
446                        item.action.checked = False
447
448        self.tool_bar.ToggleTool(self.control_id, self.checked)
449
450    def _on_action_enabled_changed(self, action, trait_name, old, new):
451        """ Called when the enabled trait is changed on an action. """
452
453        if hasattr(self.tool_bar, "ShowTool"):
454            self.tool_bar.EnableTool(self.control_id, action.enabled)
455        else:
456            self.tool_bar.EnableTool(
457                self.control_id, action.enabled and action.visible
458            )
459
460    def _on_action_visible_changed(self, action, trait_name, old, new):
461        """ Called when the visible trait is changed on an action. """
462
463        if hasattr(self.tool_bar, "ShowTool"):
464            self.tool_bar.ShowTool(self.control_id, action.visible)
465        else:
466            self.tool_bar.EnableTool(
467                self.control_id, self.enabled and action.visible
468            )
469
470    def _on_action_checked_changed(self, action, trait_name, old, new):
471        """ Called when the checked trait is changed on an action. """
472
473        if action.style == "radio":
474            # If we're turning this one on, then we need to turn all the others
475            # off.  But if we're turning this one off, don't worry about the
476            # others.
477            if new:
478                for item in self.item.parent.items:
479                    if item is not self.item:
480                        item.action.checked = False
481
482        # This will *not* emit a tool event.
483        self.tool_bar.ToggleTool(self.control_id, new)
484
485        return
486
487    # wx event handlers ----------------------------------------------------
488
489    def _on_tool(self, event):
490        """ Called when the tool bar tool is clicked. """
491
492        action = self.item.action
493        action_event = ActionEvent()
494
495        is_checkable = action.style == "radio" or action.style == "check"
496
497        # Perform the action!
498        if self.controller is not None:
499            # fixme: There is a difference here between having a controller
500            # and not in that in this case we do not set the checked state
501            # of the action! This is confusing if you start off without a
502            # controller and then set one as the action now behaves
503            # differently!
504            self.checked = self.tool_bar.GetToolState(self.control_id) == 1
505
506            # Most of the time, action's do no care about the event (it
507            # contains information about the time the event occurred etc), so
508            # we only pass it if the perform method requires it. This is also
509            # useful as Traits UI controllers *never* require the event.
510            argspec = getfullargspec(self.controller.perform)
511
512            # If the only arguments are 'self' and 'action' then don't pass
513            # the event!
514            if len(argspec.args) == 2:
515                self.controller.perform(action)
516
517            else:
518                self.controller.perform(action, action_event)
519
520        else:
521            action.checked = self.tool_bar.GetToolState(self.control_id) == 1
522
523            # Most of the time, action's do no care about the event (it
524            # contains information about the time the event occurred etc), so
525            # we only pass it if the perform method requires it.
526            argspec = getfullargspec(action.perform)
527
528            # If the only argument is 'self' then don't pass the event!
529            if len(argspec.args) == 1:
530                action.perform()
531
532            else:
533                action.perform(action_event)
534
535
536class _PaletteTool(HasTraits):
537    """ A tool palette representation of an action item. """
538
539    # '_PaletteTool' interface ---------------------------------------------
540
541    # The radio group we are part of (None if the tool is not part of such a
542    # group).
543    group = Any()
544
545    # ------------------------------------------------------------------------
546    # 'object' interface.
547    # ------------------------------------------------------------------------
548
549    def __init__(self, tool_palette, image_cache, item, show_labels):
550        """ Creates a new tool palette tool for an action item. """
551
552        self.item = item
553        self.tool_palette = tool_palette
554
555        action = self.item.action
556        label = action.name
557
558        if action.style == "widget":
559            raise NotImplementedError(
560                "WxPython does not support widgets in palettes"
561            )
562
563        # Tool palette tools never have '...' at the end.
564        if label.endswith("..."):
565            label = label[:-3]
566
567        # And they never contain shortcuts.
568        label = label.replace("&", "")
569
570        image = action.image.create_image()
571        path = action.image.absolute_path
572        bmp = image_cache.get_bitmap(path)
573
574        kind = action.style
575        tooltip = action.tooltip
576        longtip = action.description
577
578        if not show_labels:
579            label = ""
580
581        # Add the tool to the tool palette.
582        self.tool_id = tool_palette.add_tool(
583            label, bmp, kind, tooltip, longtip
584        )
585        tool_palette.toggle_tool(self.tool_id, action.checked)
586        tool_palette.enable_tool(self.tool_id, action.enabled)
587        tool_palette.on_tool_event(self.tool_id, self._on_tool)
588
589        # Listen to the trait changes on the action (so that we can update its
590        # enabled/disabled/checked state etc).
591        action.on_trait_change(self._on_action_enabled_changed, "enabled")
592        action.on_trait_change(self._on_action_checked_changed, "checked")
593
594        return
595
596    # ------------------------------------------------------------------------
597    # Private interface.
598    # ------------------------------------------------------------------------
599
600    # Trait event handlers -------------------------------------------------
601
602    def _on_action_enabled_changed(self, action, trait_name, old, new):
603        """ Called when the enabled trait is changed on an action. """
604
605        self.tool_palette.enable_tool(self.tool_id, action.enabled)
606
607    def _on_action_checked_changed(self, action, trait_name, old, new):
608        """ Called when the checked trait is changed on an action. """
609
610        if action.style == "radio":
611            # If we're turning this one on, then we need to turn all the others
612            # off.  But if we're turning this one off, don't worry about the
613            # others.
614            if new:
615                for item in self.item.parent.items:
616                    if item is not self.item:
617                        item.action.checked = False
618
619        # This will *not* emit a tool event.
620        self.tool_palette.toggle_tool(self.tool_id, new)
621
622        return
623
624    # Tool palette event handlers -----------------------------------------#
625
626    def _on_tool(self, event):
627        """ Called when the tool palette button is clicked. """
628
629        action = self.item.action
630        action_event = ActionEvent()
631
632        is_checkable = action.style == "radio" or action.style == "check"
633
634        # Perform the action!
635        action.checked = self.tool_palette.get_tool_state(self.tool_id) == 1
636        action.perform(action_event)
637
638        return
639