1# ------------------------------------------------------------------------------
2#
3#  Copyright (c) 2005-19, Enthought, Inc.
4#  All rights reserved.
5#
6#  This software is provided without warranty under the terms of the BSD
7#  license included in LICENSE.txt and may be redistributed only
8#  under the conditions described in the aforementioned license.  The license
9#  is also available online at http://www.enthought.com/licenses/BSD.txt
10#
11#  Thanks for using Enthought open source!
12#
13#  Author: David C. Morrill
14#  Date:   10/13/2004
15#
16# ------------------------------------------------------------------------------
17
18""" Defines the concrete implementations of the traits Toolkit interface for
19    the wxPython user interface toolkit.
20"""
21
22# Make sure that importimg from this backend is OK:
23import logging
24
25from traitsui.toolkit import assert_toolkit_import
26
27assert_toolkit_import(["wx"])
28
29import wx
30
31# Ensure that we can import Pyface backend.  This starts App as a side-effect.
32from pyface.toolkit import toolkit_object as pyface_toolkit
33
34_app = pyface_toolkit("init:_app")
35
36from traits.api import HasPrivateTraits, Instance
37from traits.trait_notifiers import set_ui_handler
38from pyface.api import SystemMetrics
39from pyface.wx.drag_and_drop import PythonDropTarget
40
41from traitsui.theme import Theme
42from traitsui.ui import UI
43from traitsui.toolkit import Toolkit
44from .constants import WindowColor
45from .helper import position_window
46
47logger = logging.getLogger(__name__)
48
49#: Mapping from wx events to method suffixes.
50EventSuffix = {
51    wx.wxEVT_LEFT_DOWN: "left_down",
52    wx.wxEVT_LEFT_DCLICK: "left_dclick",
53    wx.wxEVT_LEFT_UP: "left_up",
54    wx.wxEVT_MIDDLE_DOWN: "middle_down",
55    wx.wxEVT_MIDDLE_DCLICK: "middle_dclick",
56    wx.wxEVT_MIDDLE_UP: "middle_up",
57    wx.wxEVT_RIGHT_DOWN: "right_down",
58    wx.wxEVT_RIGHT_DCLICK: "right_dclick",
59    wx.wxEVT_RIGHT_UP: "right_up",
60    wx.wxEVT_MOTION: "mouse_move",
61    wx.wxEVT_ENTER_WINDOW: "enter",
62    wx.wxEVT_LEAVE_WINDOW: "leave",
63    wx.wxEVT_MOUSEWHEEL: "mouse_wheel",
64    wx.wxEVT_PAINT: "paint",
65}
66
67#: Types of popup views:
68Popups = {"popup", "popover", "info"}
69
70
71# -------------------------------------------------------------------------
72# Traits UI dispatch infrastructure
73# -------------------------------------------------------------------------
74
75def ui_handler(handler, *args):
76    """ Handles UI notification handler requests that occur on a thread other
77        than the UI thread.
78    """
79    wx.CallAfter(handler, *args)
80
81
82# Tell the traits notification handlers to use this UI handler
83set_ui_handler(ui_handler)
84
85
86# -------------------------------------------------------------------------
87# Wx Toolkit Implementation
88# -------------------------------------------------------------------------
89
90class GUIToolkit(Toolkit):
91    """ Implementation class for wxPython toolkit.
92    """
93
94    def ui_panel(self, ui, parent):
95        """ Creates a wxPython panel-based user interface using information
96            from the specified UI object.
97        """
98        from . import ui_panel
99
100        ui_panel.ui_panel(ui, parent)
101
102    def ui_subpanel(self, ui, parent):
103        """ Creates a wxPython subpanel-based user interface using information
104            from the specified UI object.
105        """
106        from . import ui_panel
107
108        ui_panel.ui_subpanel(ui, parent)
109
110    def ui_livemodal(self, ui, parent):
111        """ Creates a wxPython modal "live update" dialog user interface using
112            information from the specified UI object.
113        """
114        from . import ui_live
115
116        ui_live.ui_livemodal(ui, parent)
117
118    def ui_live(self, ui, parent):
119        """ Creates a wxPython non-modal "live update" window user interface
120            using information from the specified UI object.
121        """
122        from . import ui_live
123
124        ui_live.ui_live(ui, parent)
125
126    def ui_modal(self, ui, parent):
127        """ Creates a wxPython modal dialog user interface using information
128            from the specified UI object.
129        """
130        from . import ui_modal
131
132        ui_modal.ui_modal(ui, parent)
133
134    def ui_nonmodal(self, ui, parent):
135        """ Creates a wxPython non-modal dialog user interface using
136            information from the specified UI object.
137        """
138        from . import ui_modal
139
140        ui_modal.ui_nonmodal(ui, parent)
141
142    def ui_popup(self, ui, parent):
143        """ Creates a wxPython temporary "live update" popup dialog user
144            interface using information from the specified UI object.
145        """
146        from . import ui_live
147
148        ui_live.ui_popup(ui, parent)
149
150    def ui_popover(self, ui, parent):
151        """ Creates a wxPython temporary "live update" popup dialog user
152            interface using information from the specified UI object.
153        """
154        from . import ui_live
155
156        ui_live.ui_popover(ui, parent)
157
158    def ui_info(self, ui, parent):
159        """ Creates a wxPython temporary "live update" popup dialog user
160            interface using information from the specified UI object.
161        """
162        from . import ui_live
163
164        ui_live.ui_info(ui, parent)
165
166    def ui_wizard(self, ui, parent):
167        """ Creates a wxPython wizard dialog user interface using information
168            from the specified UI object.
169        """
170        from . import ui_wizard
171
172        ui_wizard.ui_wizard(ui, parent)
173
174    def view_application(
175        self,
176        context,
177        view,
178        kind=None,
179        handler=None,
180        id="",
181        scrollable=None,
182        args=None,
183    ):
184        """ Creates a wxPython modal dialog user interface that
185            runs as a complete application, using information from the
186            specified View object.
187
188        Parameters
189        ----------
190        context : object or dictionary
191            A single object or a dictionary of string/object pairs, whose trait
192            attributes are to be edited. If not specified, the current object is
193            used.
194        view : view or string
195            A View object that defines a user interface for editing trait
196            attribute values.
197        kind : string
198            The type of user interface window to create. See the
199            **traitsui.view.kind_trait** trait for values and
200            their meanings. If *kind* is unspecified or None, the **kind**
201            attribute of the View object is used.
202        handler : Handler object
203            A handler object used for event handling in the dialog box. If
204            None, the default handler for Traits UI is used.
205        id : string
206            A unique ID for persisting preferences about this user interface,
207            such as size and position. If not specified, no user preferences
208            are saved.
209        scrollable : Boolean
210            Indicates whether the dialog box should be scrollable. When set to
211            True, scroll bars appear on the dialog box if it is not large enough
212            to display all of the items in the view at one time.
213
214        """
215        from . import view_application
216
217        return view_application.view_application(
218            context, view, kind, handler, id, scrollable, args
219        )
220
221    def position(self, ui):
222        """ Positions the associated dialog window on the display.
223        """
224        view = ui.view
225        window = ui.control
226
227        # Set up the default position of the window:
228        parent = window.GetParent()
229        if parent is None:
230            px, py = 0, 0
231            pdx = SystemMetrics().screen_width
232            pdy = SystemMetrics().screen_height
233        else:
234            px, py = parent.GetPosition()
235            pdx, pdy = parent.GetSize()
236
237        # Calculate the correct width and height for the window:
238        cur_width, cur_height = window.GetSize()
239        width = view.width
240        height = view.height
241
242        if width < 0.0:
243            width = cur_width
244        elif width <= 1.0:
245            width = int(width * SystemMetrics().screen_width)
246        else:
247            width = int(width)
248
249        if height < 0.0:
250            height = cur_height
251        elif height <= 1.0:
252            height = int(height * SystemMetrics().screen_height)
253        else:
254            height = int(height)
255
256        if view.kind in Popups:
257            position_window(window, width, height)
258            return
259
260        # Calculate the correct position for the window:
261        x = view.x
262        y = view.y
263
264        if x < -99999.0:
265            # BH- I think this is the case when there is a parent
266            # so this logic tries to place it in the middle of the parent
267            # if possible, otherwise tries an offset from the parent
268            x = px + (pdx - width) // 2
269            if x < 0:
270                x = px + 20
271        elif x <= -1.0:
272            x = px + pdx - width + int(x) + 1
273        elif x < 0.0:
274            x = px + pdx - width + int(x * pdx)
275        elif x <= 1.0:
276            x = px + int(x * pdx)
277        else:
278            x = int(x)
279
280        if y < -99999.0:
281            # BH- I think this is the case when there is a parent
282            # so this logic tries to place it in the middle of the parent
283            # if possible, otherwise tries an offset from the parent
284            y = py + (pdy - height) // 2
285            if y < 0:
286                y = py + 20
287        elif y <= -1.0:
288            y = py + pdy - height + int(y) + 1
289        elif x < 0.0:
290            y = py + pdy - height + int(y * pdy)
291        elif y <= 1.0:
292            y = py + int(y * pdy)
293        else:
294            y = int(y)
295
296        # make sure the position is on the visible screen, maybe
297        # the desktop had been resized?
298        x = min(x, wx.DisplaySize()[0])
299        y = min(y, wx.DisplaySize()[1])
300
301        # Position and size the window as requested:
302        window.SetSize(max(0, x), max(0, y), width, height)
303
304    def show_help(self, ui, control):
305        """ Shows a help window for a specified UI and control.
306        """
307        from . import ui_panel
308
309        ui_panel.show_help(ui, control)
310
311    def save_window(self, ui):
312        """ Saves user preference information associated with a UI window.
313        """
314        from . import helper
315
316        helper.save_window(ui)
317
318    def rebuild_ui(self, ui):
319        """ Rebuilds a UI after a change to the content of the UI.
320        """
321        parent = size = None
322
323        if ui.control is not None:
324            size = ui.control.GetSize()
325            parent = ui.control._parent
326            info = ui.info
327            ui.recycle()
328            ui.info = info
329            info.ui = ui
330
331        ui.rebuild(ui, parent)
332
333        if parent is not None:
334            ui.control.SetSize(size)
335            sizer = parent.GetSizer()
336            if sizer is not None:
337                sizer.Add(ui.control, 1, wx.EXPAND)
338
339    def set_title(self, ui):
340        """ Sets the title for the UI window.
341        """
342        ui.control.SetTitle(ui.title)
343
344    def set_icon(self, ui):
345        """ Sets the icon for the UI window.
346        """
347        from pyface.image_resource import ImageResource
348
349        if isinstance(ui.icon, ImageResource):
350            ui.control.SetIcon(ui.icon.create_icon())
351
352    def key_event_to_name(self, event):
353        """ Converts a keystroke event into a corresponding key name.
354        """
355        from . import key_event_to_name
356
357        return key_event_to_name.key_event_to_name(event)
358
359    def hook_events(self, ui, control, events=None, handler=None):
360        """ Hooks all specified events for all controls in a UI so that they
361            can be routed to the correct event handler.
362        """
363        if events is None:
364            events = (
365                wx.wxEVT_LEFT_DOWN,
366                wx.wxEVT_LEFT_DCLICK,
367                wx.wxEVT_LEFT_UP,
368                wx.wxEVT_MIDDLE_DOWN,
369                wx.wxEVT_MIDDLE_DCLICK,
370                wx.wxEVT_MIDDLE_UP,
371                wx.wxEVT_RIGHT_DOWN,
372                wx.wxEVT_RIGHT_DCLICK,
373                wx.wxEVT_RIGHT_UP,
374                wx.wxEVT_MOTION,
375                wx.wxEVT_ENTER_WINDOW,
376                wx.wxEVT_LEAVE_WINDOW,
377                wx.wxEVT_MOUSEWHEEL,
378                wx.wxEVT_PAINT,
379            )
380            control.SetDropTarget(
381                PythonDropTarget(DragHandler(ui=ui, control=control))
382            )
383        elif events == "keys":
384            events = (wx.wxEVT_CHAR,)
385
386        if handler is None:
387            handler = ui.route_event
388
389        id = control.GetId()
390        event_handler = EventHandlerWrapper()
391        connect = event_handler.Connect
392
393        for event in events:
394            connect(id, id, event, handler)
395
396        control.PushEventHandler(event_handler)
397
398        for child in control.GetChildren():
399            self.hook_events(ui, child, events, handler)
400
401    def route_event(self, ui, event):
402        """ Routes a hooked event to the correct handler method.
403        """
404        suffix = EventSuffix[event.GetEventType()]
405        control = event.GetEventObject()
406        handler = ui.handler
407        method = None
408
409        owner = getattr(control, "_owner", None)
410        if owner is not None:
411            method = getattr(
412                handler, "on_%s_%s" % (owner.get_id(), suffix), None
413            )
414
415        if method is None:
416            method = getattr(handler, "on_%s" % suffix, None) or getattr(
417                handler, "on_any_event", None
418            )
419
420        if (method is None) or (method(ui.info, owner, event) is False):
421            event.Skip()
422
423    def skip_event(self, event):
424        """ Indicates that an event should continue to be processed by the
425            toolkit.
426        """
427        event.Skip()
428
429    def destroy_control(self, control):
430        """ Destroys a specified GUI toolkit control.
431        """
432        _popEventHandlers(control)
433
434        def _destroy_control(control):
435            try:
436                control.Destroy()
437            except Exception:
438                logger.exception(
439                    "Wx control %r not destroyed cleanly", control)
440
441        wx.CallAfter(_destroy_control, control)
442
443    def destroy_children(self, control):
444        """ Destroys all of the child controls of a specified GUI toolkit
445            control.
446        """
447        for child in control.GetChildren():
448            _popEventHandlers(child)
449        wx.CallAfter(control.DestroyChildren)
450
451    def image_size(self, image):
452        """ Returns a ( width, height ) tuple containing the size of a
453            specified toolkit image.
454        """
455        return (image.GetWidth(), image.GetHeight())
456
457    def constants(self):
458        """ Returns a dictionary of useful constants.
459
460            Currently, the dictionary should have the following key/value pairs:
461
462            - WindowColor': the standard window background color in the toolkit
463              specific color format.
464        """
465        return {"WindowColor": WindowColor}
466
467    # -------------------------------------------------------------------------
468    #  GUI toolkit dependent trait definitions:
469    # -------------------------------------------------------------------------
470
471    def color_trait(self, *args, **traits):
472        from . import color_trait as ct
473
474        return ct.WxColor(*args, **traits)
475
476    def rgb_color_trait(self, *args, **traits):
477        from . import rgb_color_trait as rgbct
478
479        return rgbct.RGBColor(*args, **traits)
480
481    def font_trait(self, *args, **traits):
482        from . import font_trait as ft
483
484        return ft.WxFont(*args, **traits)
485
486    # -------------------------------------------------------------------------
487    #  'Editor' class methods:
488    # -------------------------------------------------------------------------
489
490    def ui_editor(self):
491        """ Generic base UI editor. """
492        from . import ui_editor
493
494        return ui_editor.UIEditor
495
496    def shell_editor(self, *args, **traits):
497        from . import shell_editor as se
498
499        return se.ToolkitEditorFactory(*args, **traits)
500
501
502class DragHandler(HasPrivateTraits):
503    """ Handler for drag events.
504    """
505
506    # -------------------------------------------------------------------------
507    #  Traits definitions:
508    # -------------------------------------------------------------------------
509
510    #: The UI associated with the drag handler
511    ui = Instance(UI)
512
513    #: The wx control associated with the drag handler
514    control = Instance(wx.Window)
515
516    # -- Drag and drop event handlers: ----------------------------------------
517
518    def wx_dropped_on(self, x, y, data, drag_result):
519        """ Handles a Python object being dropped on the window.
520        """
521        return self._drag_event("dropped_on", x, y, data, drag_result)
522
523    def wx_drag_over(self, x, y, data, drag_result):
524        """ Handles a Python object being dragged over the tree.
525        """
526        return self._drag_event("drag_over", x, y, data, drag_result)
527
528    def wx_drag_leave(self, data):
529        """ Handles a dragged Python object leaving the window.
530        """
531        return self._drag_event("drag_leave")
532
533    def _drag_event(self, suffix, x=None, y=None, data=None, drag_result=None):
534        """ Handles routing a drag event to the appropriate handler.
535        """
536        control = self.control
537        handler = self.ui.handler
538        method = None
539
540        owner = getattr(control, "_owner", None)
541        if owner is not None:
542            method = getattr(
543                handler, "on_%s_%s" % (owner.get_id(), suffix), None
544            )
545
546        if method is None:
547            method = getattr(handler, "on_%s" % suffix, None)
548
549        if method is None:
550            return wx.DragNone
551
552        if x is None:
553            result = method(self.ui.info, owner)
554        else:
555            result = method(self.ui.info, owner, x, y, data, drag_result)
556        if result is None:
557            result = drag_result
558        return result
559
560
561class EventHandlerWrapper(wx.EvtHandler):
562    """ Simple wrapper around wx.EvtHandler used to determine which event
563    handlers were added by traitui.
564    """
565    pass
566
567
568def _popEventHandlers(ctrl, handler_type=EventHandlerWrapper):
569    """ Pop any event handlers that have been pushed on to a window and its
570        children.
571    """
572    # FIXME: have to special case URLResolvingHtmlWindow because it doesn't
573    # want its EvtHandler cleaned up.  See issue #752.
574    from .html_editor import URLResolvingHtmlWindow
575
576    handler = ctrl.GetEventHandler()
577    while ctrl is not handler:
578        next_handler = handler.GetNextHandler()
579        if isinstance(handler, handler_type):
580            ctrl.PopEventHandler(True)
581        handler = next_handler
582    for child in ctrl.GetChildren():
583        _popEventHandlers(child, handler_type)
584