1# ------------------------------------------------------------------------------
2# Copyright (c) 2007, Riverbank Computing Limited
3# All rights reserved.
4#
5# This software is provided without warranty under the terms of the BSD license.
6# However, when used with the GPL version of PyQt the additional terms described
7# in the PyQt GPL exception also apply.
8#
9# Author: Riverbank Computing Limited
10# ------------------------------------------------------------------------------
11
12""" Defines the concrete implementations of the traits Toolkit interface for
13the PyQt user interface toolkit.
14"""
15
16
17
18# Make sure that importing from this backend is OK:
19from traitsui.toolkit import assert_toolkit_import
20
21assert_toolkit_import(["qt4", "qt"])
22
23# Ensure that we can import Pyface backend.  This starts App as a side-effect.
24from pyface.toolkit import toolkit_object as pyface_toolkit
25
26_app = pyface_toolkit("init:_app")
27
28from traits.trait_notifiers import set_ui_handler
29from pyface.api import SystemMetrics
30from pyface.qt import QtCore, QtGui, qt_api
31
32from traitsui.toolkit import Toolkit
33
34
35# -------------------------------------------------------------------------
36#  Handles UI notification handler requests that occur on a thread other than
37#  the UI thread:
38# -------------------------------------------------------------------------
39
40_QT_TRAITS_EVENT = QtCore.QEvent.Type(QtCore.QEvent.registerEventType())
41
42
43class _CallAfter(QtCore.QObject):
44    """ This class dispatches a handler so that it executes in the main GUI
45        thread (similar to the wx function).
46    """
47
48    # The list of pending calls.
49    _calls = []
50
51    # The mutex around the list of pending calls.
52    _calls_mutex = QtCore.QMutex()
53
54    def __init__(self, handler, *args, **kwds):
55        """ Initialise the call.
56        """
57        QtCore.QObject.__init__(self)
58
59        # Save the details of the call.
60        self._handler = handler
61        self._args = args
62        self._kwds = kwds
63
64        # Add this to the list.
65        self._calls_mutex.lock()
66        self._calls.append(self)
67        self._calls_mutex.unlock()
68
69        # Move to the main GUI thread.
70        self.moveToThread(QtGui.QApplication.instance().thread())
71
72        # Post an event to be dispatched on the main GUI thread. Note that
73        # we do not call QTimer.singleShot, which would be simpler, because
74        # that only works on QThreads. We want regular Python threads to work.
75        event = QtCore.QEvent(_QT_TRAITS_EVENT)
76        QtGui.QApplication.instance().postEvent(self, event)
77
78    def event(self, event):
79        """ QObject event handler.
80        """
81        if event.type() == _QT_TRAITS_EVENT:
82            # Invoke the handler
83            self._handler(*self._args, **self._kwds)
84
85            # We cannot remove from self._calls here. QObjects don't like being
86            # garbage collected during event handlers (there are tracebacks,
87            # plus maybe a memory leak, I think).
88            QtCore.QTimer.singleShot(0, self._finished)
89
90            return True
91        else:
92            return QtCore.QObject.event(self, event)
93
94    def _finished(self):
95        """ Remove the call from the list, so it can be garbage collected.
96        """
97        self._calls_mutex.lock()
98        del self._calls[self._calls.index(self)]
99        self._calls_mutex.unlock()
100
101
102def ui_handler(handler, *args, **kwds):
103    """ Handles UI notification handler requests that occur on a thread other
104        than the UI thread.
105    """
106    _CallAfter(handler, *args, **kwds)
107
108
109# Tell the traits notification handlers to use this UI handler
110set_ui_handler(ui_handler)
111
112
113class _KeyEventHook(QtCore.QObject):
114    """
115    Event hook for handling Qt key events.
116    """
117
118    def __init__(self, handler):
119        super(_KeyEventHook, self).__init__()
120        self._handler = handler
121
122    def eventFilter(self, object, event):
123        if event.type() == QtCore.QEvent.KeyPress:
124            return self._handler(event)
125        else:
126            return QtCore.QObject.eventFilter(self, object, event)
127
128
129class GUIToolkit(Toolkit):
130    """ Implementation class for PyQt toolkit.
131    """
132
133    def ui_panel(self, ui, parent):
134        """ Creates a PyQt panel-based user interface using information
135            from the specified UI object.
136        """
137        from . import ui_panel
138
139        ui_panel.ui_panel(ui, parent)
140
141    def ui_subpanel(self, ui, parent):
142        """ Creates a PyQt subpanel-based user interface using information
143            from the specified UI object.
144        """
145        from . import ui_panel
146
147        ui_panel.ui_subpanel(ui, parent)
148
149    def ui_livemodal(self, ui, parent):
150        """ Creates a PyQt modal "live update" dialog user interface using
151            information from the specified UI object.
152        """
153        from . import ui_live
154
155        ui_live.ui_livemodal(ui, parent)
156
157    def ui_live(self, ui, parent):
158        """ Creates a PyQt non-modal "live update" window user interface
159            using information from the specified UI object.
160        """
161        from . import ui_live
162
163        ui_live.ui_live(ui, parent)
164
165    def ui_modal(self, ui, parent):
166        """ Creates a PyQt modal dialog user interface using information
167            from the specified UI object.
168        """
169        from . import ui_modal
170
171        ui_modal.ui_modal(ui, parent)
172
173    def ui_nonmodal(self, ui, parent):
174        """ Creates a PyQt non-modal dialog user interface using
175            information from the specified UI object.
176        """
177        from . import ui_modal
178
179        ui_modal.ui_nonmodal(ui, parent)
180
181    def ui_wizard(self, ui, parent):
182        """ Creates a PyQt wizard dialog user interface using information
183            from the specified UI object.
184        """
185        import ui_wizard
186
187        ui_wizard.ui_wizard(ui, parent)
188
189    def view_application(
190        self,
191        context,
192        view,
193        kind=None,
194        handler=None,
195        id="",
196        scrollable=None,
197        args=None,
198    ):
199        """ Creates a PyQt modal dialog user interface that
200            runs as a complete application, using information from the
201            specified View object.
202
203        Parameters
204        ----------
205        context : object or dictionary
206            A single object or a dictionary of string/object pairs, whose trait
207            attributes are to be edited. If not specified, the current object is
208            used.
209        view : view or string
210            A View object that defines a user interface for editing trait
211            attribute values.
212        kind : string
213            The type of user interface window to create. See the
214            **traitsui.view.kind_trait** trait for values and
215            their meanings. If *kind* is unspecified or None, the **kind**
216            attribute of the View object is used.
217        handler : Handler object
218            A handler object used for event handling in the dialog box. If
219            None, the default handler for Traits UI is used.
220        id : string
221            A unique ID for persisting preferences about this user interface,
222            such as size and position. If not specified, no user preferences
223            are saved.
224        scrollable : Boolean
225            Indicates whether the dialog box should be scrollable. When set to
226            True, scroll bars appear on the dialog box if it is not large enough
227            to display all of the items in the view at one time.
228
229        """
230        from . import view_application
231
232        return view_application.view_application(
233            context, view, kind, handler, id, scrollable, args
234        )
235
236    def position(self, ui):
237        """ Positions the associated dialog window on the display.
238        """
239        view = ui.view
240        window = ui.control
241
242        # Set up the default position of the window:
243        parent = window.parent()
244        if parent is None:
245            px = 0
246            py = 0
247            pdx = SystemMetrics().screen_width
248            pdy = SystemMetrics().screen_height
249        else:
250            pos = parent.pos()
251            if int(parent.windowFlags()) & QtCore.Qt.Window == 0:
252                pos = parent.mapToGlobal(pos)
253            px = pos.x()
254            py = pos.y()
255            pdx = parent.width()
256            pdy = parent.height()
257
258        # Get the window's prefered size.
259        size_hint = window.sizeHint()
260
261        # Calculate the correct width and height for the window:
262        cur_width = size_hint.width()
263        cur_height = size_hint.height()
264        width = view.width
265        height = view.height
266
267        if width < 0.0:
268            width = cur_width
269        elif width <= 1.0:
270            width = int(width * SystemMetrics().screen_width)
271        else:
272            width = int(width)
273
274        if height < 0.0:
275            height = cur_height
276        elif height <= 1.0:
277            height = int(height * SystemMetrics().screen_height)
278        else:
279            height = int(height)
280
281        # Calculate the correct position for the window:
282        x = view.x
283        y = view.y
284
285        if x < -99999.0:
286            x = px + ((pdx - width) // 2)
287        elif x <= -1.0:
288            x = px + pdx - width + int(x) + 1
289        elif x < 0.0:
290            x = px + pdx - width + int(x * pdx)
291        elif x <= 1.0:
292            x = px + int(x * pdx)
293        else:
294            x = int(x)
295
296        if y < -99999.0:
297            y = py + ((pdy - height) // 2)
298        elif y <= -1.0:
299            y = py + pdy - height + int(y) + 1
300        elif x < 0.0:
301            y = py + pdy - height + int(y * pdy)
302        elif y <= 1.0:
303            y = py + int(y * pdy)
304        else:
305            y = int(y)
306
307        # Position and size the window as requested:
308        layout = window.layout()
309        if layout.sizeConstraint() == QtGui.QLayout.SetFixedSize:
310            layout.setSizeConstraint(QtGui.QLayout.SetDefaultConstraint)
311            window.move(max(0, x), max(0, y))
312            window.setFixedSize(QtCore.QSize(width, height))
313        else:
314            window.setGeometry(max(0, x), max(0, y), width, height)
315
316    def show_help(self, ui, control):
317        """ Shows a help window for a specified UI and control.
318        """
319        from . import ui_panel
320
321        ui_panel.show_help(ui, control)
322
323    def save_window(self, ui):
324        """ Saves user preference information associated with a UI window.
325        """
326        from . import helper
327
328        helper.save_window(ui)
329
330    def rebuild_ui(self, ui):
331        """ Rebuilds a UI after a change to the content of the UI.
332        """
333        if ui.control is not None:
334            ui.recycle()
335            ui.info.ui = ui
336        ui.rebuild(ui, ui.parent)
337
338    def set_title(self, ui):
339        """ Sets the title for the UI window.
340        """
341        ui.control.setWindowTitle(ui.title)
342
343    def set_icon(self, ui):
344        """ Sets the icon for the UI window.
345        """
346        from pyface.image_resource import ImageResource
347
348        if isinstance(ui.icon, ImageResource):
349            ui.control.setWindowIcon(ui.icon.create_icon())
350
351    def key_event_to_name(self, event):
352        """ Converts a keystroke event into a corresponding key name.
353        """
354        from . import key_event_to_name
355
356        return key_event_to_name.key_event_to_name(event)
357
358    def hook_events(self, ui, control, events=None, handler=None):
359        """ Hooks all specified events for all controls in a UI so that they
360            can be routed to the correct event handler.
361        """
362        if events is None:
363            # FIXME: Implement drag and drop events ala toolkit.py for wx
364            return
365
366        elif events == "keys":
367            # It's unsafe to parent the event filter with the object it's
368            # filtering, so we store a reference to it here to ensure that it's
369            # not garbage collected prematurely.
370            ui._key_event_hook = _KeyEventHook(handler=handler)
371            control.installEventFilter(ui._key_event_hook)
372
373    def skip_event(self, event):
374        """ Indicates that an event should continue to be processed by the
375            toolkit.
376        """
377        event.ignore()
378
379    def destroy_control(self, control):
380        """ Destroys a specified GUI toolkit control.
381        """
382        # Block signals to prevent any editors from being updated (the control
383        # will not be deleted immediately).
384        control.blockSignals(True)
385
386        # This may be called from within the finished() signal handler so we
387        # need to do the delete after the handler has returned.
388        control.hide()
389        control.deleteLater()
390
391        # PyQt v4.3.1 and earlier deleteLater() didn't transfer ownership to
392        # C++, which is necessary for the QObject system to garbage collect it.
393        if qt_api == "pyqt":
394            if QtCore.PYQT_VERSION < 0x040302:
395                import sip
396
397                sip.transferto(control, None)
398
399    def destroy_children(self, control):
400        """ Destroys all of the child controls of a specified GUI toolkit
401            control.
402        """
403        for w in control.children():
404            # Only destroy widgets.
405            if isinstance(w, QtGui.QWidget):
406                # This may be called from within the finished() signal handler
407                # so we need to do the delete after the handler has returned.
408                w.deleteLater()
409
410    def image_size(self, image):
411        """ Returns a ( width, height ) tuple containing the size of a
412            specified toolkit image.
413        """
414        return (image.width(), image.height())
415
416    def constants(self):
417        """ Returns a dictionary of useful constants.
418
419            Currently, the dictionary should have the following key/value pairs:
420
421            - 'WindowColor': the standard window background color in the toolkit
422              specific color format.
423        """
424        return {
425            "WindowColor": QtGui.QApplication.palette().color(
426                QtGui.QPalette.Window
427            )
428        }
429
430    def color_trait(self, *args, **traits):
431        from . import color_trait as ct
432
433        return ct.PyQtColor(*args, **traits)
434
435    def rgb_color_trait(self, *args, **traits):
436        from . import rgb_color_trait as rgbct
437
438        return rgbct.RGBColor(*args, **traits)
439
440    def font_trait(self, *args, **traits):
441        from . import font_trait as ft
442
443        return ft.PyQtFont(*args, **traits)
444
445    # -------------------------------------------------------------------------
446    #  'Editor' class methods:
447    # -------------------------------------------------------------------------
448
449    # Generic UI-base editor:
450    def ui_editor(self):
451        from . import ui_editor
452
453        return ui_editor.UIEditor
454
455    # -------------------------------------------------------------------------
456    #  'EditorFactory' factory methods:
457    # -------------------------------------------------------------------------
458
459    # Array:
460    def array_editor(self, *args, **traits):
461        from . import array_editor as ae
462
463        return ae.ToolkitEditorFactory(*args, **traits)
464
465    # Boolean:
466    def boolean_editor(self, *args, **traits):
467        from . import boolean_editor as be
468
469        return be.ToolkitEditorFactory(*args, **traits)
470
471    # Button:
472    def button_editor(self, *args, **traits):
473        from . import button_editor as be
474
475        return be.ToolkitEditorFactory(*args, **traits)
476
477    # Check list:
478    def check_list_editor(self, *args, **traits):
479        from . import check_list_editor as cle
480
481        return cle.ToolkitEditorFactory(*args, **traits)
482
483    # Code:
484    def code_editor(self, *args, **traits):
485        from . import code_editor as ce
486
487        return ce.ToolkitEditorFactory(*args, **traits)
488
489    # Color:
490    def color_editor(self, *args, **traits):
491        from . import color_editor as ce
492
493        return ce.ToolkitEditorFactory(*args, **traits)
494
495    # Compound:
496    def compound_editor(self, *args, **traits):
497        from . import compound_editor as ce
498
499        return ce.ToolkitEditorFactory(*args, **traits)
500
501    def styled_date_editor(self, *args, **traits):
502        from . import styled_date_editor as sde
503
504        return sde.ToolkitEditorFactory(*args, **traits)
505
506    # Custom:
507    def custom_editor(self, *args, **traits):
508        from . import custom_editor as ce
509
510        return ce.ToolkitEditorFactory(*args, **traits)
511
512    # Directory:
513    def directory_editor(self, *args, **traits):
514        from . import directory_editor as de
515
516        return de.ToolkitEditorFactory(*args, **traits)
517
518    # Drop (drag and drop target):
519    def drop_editor(self, *args, **traits):
520        from . import drop_editor as de
521
522        return de.ToolkitEditorFactory(*args, **traits)
523
524    # Drag and drop:
525    def dnd_editor(self, *args, **traits):
526        import dnd_editor as dnd
527
528        return dnd.ToolkitEditorFactory(*args, **traits)
529
530    # Enum(eration):
531    def enum_editor(self, *args, **traits):
532        from . import enum_editor as ee
533
534        return ee.ToolkitEditorFactory(*args, **traits)
535
536    # File:
537    def file_editor(self, *args, **traits):
538        from . import file_editor as fe
539
540        return fe.ToolkitEditorFactory(*args, **traits)
541
542    # Font:
543    def font_editor(self, *args, **traits):
544        from . import font_editor as fe
545
546        return fe.ToolkitEditorFactory(*args, **traits)
547
548    # Key Binding:
549    def key_binding_editor(self, *args, **traits):
550        from . import key_binding_editor as kbe
551
552        return kbe.ToolkitEditorFactory(*args, **traits)
553
554    # History:
555    def history_editor(self, *args, **traits):
556        from . import history_editor as he
557
558        return he.HistoryEditor(*args, **traits)
559
560    # HTML:
561    def html_editor(self, *args, **traits):
562        from . import html_editor as he
563
564        return he.ToolkitEditorFactory(*args, **traits)
565
566    # Image:
567    def image_editor(self, *args, **traits):
568        from . import image_editor as ie
569
570        return ie.ImageEditor(*args, **traits)
571
572    # Image enum(eration):
573    def image_enum_editor(self, *args, **traits):
574        from . import image_enum_editor as iee
575
576        return iee.ToolkitEditorFactory(*args, **traits)
577
578    # Instance:
579    def instance_editor(self, *args, **traits):
580        from . import instance_editor as ie
581
582        return ie.ToolkitEditorFactory(*args, **traits)
583
584    # List:
585    def list_editor(self, *args, **traits):
586        from . import list_editor as le
587
588        return le.ToolkitEditorFactory(*args, **traits)
589
590    # ListStr:
591    def list_str_editor(self, *args, **traits):
592        from . import list_str_editor as lse
593
594        return lse.ListStrEditor(*args, **traits)
595
596    # Null:
597    def null_editor(self, *args, **traits):
598        from . import null_editor as ne
599
600        return ne.ToolkitEditorFactory(*args, **traits)
601
602    # Ordered set:
603    def ordered_set_editor(self, *args, **traits):
604        import ordered_set_editor as ose
605
606        return ose.ToolkitEditorFactory(*args, **traits)
607
608    # Plot:
609    def plot_editor(self, *args, **traits):
610        import plot_editor as pe
611
612        return pe.ToolkitEditorFactory(*args, **traits)
613
614    # Range:
615    def range_editor(self, *args, **traits):
616        from . import range_editor as re
617
618        return re.ToolkitEditorFactory(*args, **traits)
619
620    # RGB Color:
621    def rgb_color_editor(self, *args, **traits):
622        from . import rgb_color_editor as rgbce
623
624        return rgbce.ToolkitEditorFactory(*args, **traits)
625
626    # Set:
627    def set_editor(self, *args, **traits):
628        from . import set_editor as se
629
630        return se.ToolkitEditorFactory(*args, **traits)
631
632    # Shell:
633    def shell_editor(self, *args, **traits):
634        from . import shell_editor as se
635
636        return se.ToolkitEditorFactory(*args, **traits)
637
638    # Table:
639    def table_editor(self, *args, **traits):
640        from . import table_editor as te
641
642        return te.ToolkitEditorFactory(*args, **traits)
643
644    # Tabular:
645    def tabular_editor(self, *args, **traits):
646        from . import tabular_editor as te
647
648        return te.TabularEditor(*args, **traits)
649
650    # Text:
651    def text_editor(self, *args, **traits):
652        from . import text_editor as te
653
654        return te.ToolkitEditorFactory(*args, **traits)
655
656    # Title:
657    def title_editor(self, *args, **traits):
658        from . import title_editor
659
660        return title_editor.TitleEditor(*args, **traits)
661
662    # Tree:
663    def tree_editor(self, *args, **traits):
664        from . import tree_editor as te
665
666        return te.ToolkitEditorFactory(*args, **traits)
667
668    # Tuple:
669    def tuple_editor(self, *args, **traits):
670        from . import tuple_editor as te
671
672        return te.ToolkitEditorFactory(*args, **traits)
673
674    # Value:
675    def value_editor(self, *args, **traits):
676        from . import value_editor as ve
677
678        return ve.ToolkitEditorFactory(*args, **traits)
679