1# ------------------------------------------------------------------------------
2# Copyright (c) 2008, 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
7# described in the PyQt GPL exception also apply
8
9#
10# Author: Riverbank Computing Limited
11# ------------------------------------------------------------------------------
12
13""" Defines the various instance editors and the instance editor factory for
14    the PyQt user interface toolkit..
15"""
16
17
18from pyface.qt import QtCore, QtGui
19
20from traits.api import HasTraits, Instance, Property
21
22# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
23# compatibility. The class has been moved to the
24# traitsui.editors.instance_editor file.
25from traitsui.editors.instance_editor import ToolkitEditorFactory
26from traitsui.ui_traits import AView
27from traitsui.helper import user_name_for
28from traitsui.handler import Handler
29from traitsui.instance_choice import InstanceChoiceItem
30from .editor import Editor
31from .drop_editor import _DropEventFilter
32from .constants import DropColor
33from .helper import position_window
34
35
36OrientationMap = {
37    "default": None,
38    "horizontal": QtGui.QBoxLayout.LeftToRight,
39    "vertical": QtGui.QBoxLayout.TopToBottom,
40}
41
42
43class CustomEditor(Editor):
44    """ Custom style of editor for instances. If selection among instances is
45    allowed, the editor displays a combo box listing instances that can be
46    selected. If the current instance is editable, the editor displays a panel
47    containing trait editors for all the instance's traits.
48    """
49
50    #: Background color when an item can be dropped on the editor:
51    ok_color = DropColor
52
53    #: The orientation of the instance editor relative to the instance selector:
54    orientation = QtGui.QBoxLayout.TopToBottom
55
56    #: Class constant:
57    extra = 0
58
59    # -------------------------------------------------------------------------
60    #  Trait definitions:
61    # -------------------------------------------------------------------------
62
63    #: List of InstanceChoiceItem objects used by the editor
64    items = Property()
65
66    #: The view to use for displaying the instance
67    view = AView
68
69    def init(self, parent):
70        """ Finishes initializing the editor by creating the underlying toolkit
71            widget.
72        """
73        factory = self.factory
74        if factory.name != "":
75            self._object, self._name, self._value = self.parse_extended_name(
76                factory.name
77            )
78
79        # Create a panel to hold the object trait's view:
80        if factory.editable:
81            self.control = self._panel = parent = QtGui.QWidget()
82
83        # Build the instance selector if needed:
84        selectable = factory.selectable
85        droppable = factory.droppable
86        items = self.items
87        for item in items:
88            droppable |= item.is_droppable()
89            selectable |= item.is_selectable()
90
91        if selectable:
92            self._object_cache = {}
93            item = self.item_for(self.value)
94            if item is not None:
95                self._object_cache[id(item)] = self.value
96
97            self._choice = QtGui.QComboBox()
98            self._choice.activated.connect(self.update_object)
99
100            self.set_tooltip(self._choice)
101
102            if factory.name != "":
103                self._object.on_trait_change(
104                    self.rebuild_items, self._name, dispatch="ui"
105                )
106                self._object.on_trait_change(
107                    self.rebuild_items, self._name + "_items", dispatch="ui"
108                )
109
110            factory.on_trait_change(
111                self.rebuild_items, "values", dispatch="ui"
112            )
113            factory.on_trait_change(
114                self.rebuild_items, "values_items", dispatch="ui"
115            )
116
117            self.rebuild_items()
118
119        elif droppable:
120            self._choice = QtGui.QLineEdit()
121            self._choice.setReadOnly(True)
122            self.set_tooltip(self._choice)
123
124        if droppable:
125            # Install EventFilter on control to handle DND events.
126            drop_event_filter = _DropEventFilter(self.control)
127            self.control.installEventFilter(drop_event_filter)
128
129        orientation = OrientationMap[factory.orientation]
130        if orientation is None:
131            orientation = self.orientation
132
133        if (selectable or droppable) and factory.editable:
134            layout = QtGui.QBoxLayout(orientation, parent)
135            layout.setContentsMargins(0, 0, 0, 0)
136            layout.addWidget(self._choice)
137
138            if orientation == QtGui.QBoxLayout.TopToBottom:
139                hline = QtGui.QFrame()
140                hline.setFrameShape(QtGui.QFrame.HLine)
141                hline.setFrameShadow(QtGui.QFrame.Sunken)
142
143                layout.addWidget(hline)
144
145            self.create_editor(parent, layout)
146        elif self.control is None:
147            if self._choice is None:
148                self._choice = QtGui.QComboBox()
149                self._choice.activated[int].connect(self.update_object)
150
151            self.control = self._choice
152        else:
153            layout = QtGui.QBoxLayout(orientation, parent)
154            layout.setContentsMargins(0, 0, 0, 0)
155            self.create_editor(parent, layout)
156
157        # Synchronize the 'view' to use:
158        # fixme: A normal assignment can cause a crash (for unknown reasons) in
159        # some cases, so we make sure that no notifications are generated:
160        self.trait_setq(view=factory.view)
161        self.sync_value(factory.view_name, "view", "from")
162
163    def create_editor(self, parent, layout):
164        """ Creates the editor control.
165        """
166        self._panel = QtGui.QWidget()
167        layout.addWidget(self._panel)
168
169    def _get_items(self):
170        """ Gets the current list of InstanceChoiceItem items.
171        """
172        if self._items is not None:
173            return self._items
174
175        factory = self.factory
176        if self._value is not None:
177            values = self._value() + factory.values
178        else:
179            values = factory.values
180
181        items = []
182        adapter = factory.adapter
183        for value in values:
184            if not isinstance(value, InstanceChoiceItem):
185                value = adapter(object=value)
186            items.append(value)
187
188        self._items = items
189
190        return items
191
192    def rebuild_items(self):
193        """ Rebuilds the object selector list.
194        """
195        # Clear the current cached values:
196        self._items = None
197
198        # Rebuild the contents of the selector list:
199        name = -1
200        value = self.value
201        choice = self._choice
202        choice.clear()
203        for i, item in enumerate(self.items):
204            if item.is_selectable():
205                choice.addItem(item.get_name())
206                if item.is_compatible(value):
207                    name = i
208
209        # Reselect the current item if possible:
210        if name >= 0:
211            choice.setCurrentIndex(name)
212        else:
213            # Otherwise, current value is no longer valid, try to discard it:
214            try:
215                self.value = None
216            except:
217                pass
218
219    def item_for(self, object):
220        """ Returns the InstanceChoiceItem for a specified object.
221        """
222        for item in self.items:
223            if item.is_compatible(object):
224                return item
225
226        return None
227
228    def view_for(self, object, item):
229        """ Returns the view to use for a specified object.
230        """
231        view = ""
232        if item is not None:
233            view = item.get_view()
234
235        if view == "":
236            view = self.view
237
238        return self.ui.handler.trait_view_for(
239            self.ui.info, view, object, self.object_name, self.name
240        )
241
242    def update_object(self, index):
243        """ Handles the user selecting a new value from the combo box.
244        """
245        item = self.items[index]
246        id_item = id(item)
247        object = self._object_cache.get(id_item)
248        if object is None:
249            object = item.get_object()
250            if (not self.factory.editable) and item.is_factory:
251                view = self.view_for(object, self.item_for(object))
252                view.ui(object, self.control, "modal")
253
254            if self.factory.cachable:
255                self._object_cache[id_item] = object
256
257        self.value = object
258        self.resynch_editor()
259
260    def update_editor(self):
261        """ Updates the editor when the object trait changes externally to the
262            editor.
263        """
264        # Synchronize the editor contents:
265        self.resynch_editor()
266
267        # Update the selector (if any):
268        choice = self._choice
269        item = self.item_for(self.value)
270        if (choice is not None) and (item is not None):
271            name = item.get_name(self.value)
272            if self._object_cache is not None:
273                idx = choice.findText(name)
274                if idx < 0:
275                    idx = choice.count()
276                    choice.addItem(name)
277
278                choice.setCurrentIndex(idx)
279            else:
280                choice.setText(name)
281
282    def resynch_editor(self):
283        """ Resynchronizes the contents of the editor when the object trait
284        changes externally to the editor.
285        """
286        panel = self._panel
287        if panel is not None:
288            # Dispose of the previous contents of the panel:
289            layout = panel.layout()
290            if layout is None:
291                layout = QtGui.QVBoxLayout(panel)
292                layout.setContentsMargins(0, 0, 0, 0)
293            elif self._ui is not None:
294                self._ui.dispose()
295                self._ui = None
296            else:
297                child = layout.takeAt(0)
298                while child is not None:
299                    child = layout.takeAt(0)
300
301                del child
302
303            # Create the new content for the panel:
304            stretch = 0
305            value = self.value
306            if not isinstance(value, HasTraits):
307                str_value = ""
308                if value is not None:
309                    str_value = self.str_value
310                control = QtGui.QLabel(str_value)
311            else:
312                view = self.view_for(value, self.item_for(value))
313                context = value.trait_context()
314                handler = None
315                if isinstance(value, Handler):
316                    handler = value
317                context.setdefault("context", self.object)
318                context.setdefault("context_handler", self.ui.handler)
319                self._ui = ui = view.ui(
320                    context,
321                    panel,
322                    "subpanel",
323                    value.trait_view_elements(),
324                    handler,
325                    self.factory.id,
326                )
327                control = ui.control
328                self.scrollable = ui._scrollable
329                ui.parent = self.ui
330
331                if view.resizable or view.scrollable or ui._scrollable:
332                    stretch = 1
333
334            # FIXME: Handle stretch.
335            layout.addWidget(control)
336
337    def dispose(self):
338        """ Disposes of the contents of an editor.
339        """
340        # Make sure we aren't hanging on to any object refs:
341        self._object_cache = None
342
343        if self._ui is not None:
344            self._ui.dispose()
345
346        if self._choice is not None:
347            if self._object is not None:
348                self._object.on_trait_change(
349                    self.rebuild_items, self._name, remove=True
350                )
351                self._object.on_trait_change(
352                    self.rebuild_items, self._name + "_items", remove=True
353                )
354
355            self.factory.on_trait_change(
356                self.rebuild_items, "values", remove=True
357            )
358            self.factory.on_trait_change(
359                self.rebuild_items, "values_items", remove=True
360            )
361
362        super(CustomEditor, self).dispose()
363
364    def error(self, excp):
365        """ Handles an error that occurs while setting the object's trait value.
366        """
367        pass
368
369    def get_error_control(self):
370        """ Returns the editor's control for indicating error status.
371        """
372        return self._choice or self.control
373
374    # -- UI preference save/restore interface ---------------------------------
375
376    def restore_prefs(self, prefs):
377        """ Restores any saved user preference information associated with the
378            editor.
379        """
380        ui = self._ui
381        if (ui is not None) and (prefs.get("id") == ui.id):
382            ui.set_prefs(prefs.get("prefs"))
383
384    def save_prefs(self):
385        """ Returns any user preference information associated with the editor.
386        """
387        ui = self._ui
388        if (ui is not None) and (ui.id != ""):
389            return {"id": ui.id, "prefs": ui.get_prefs()}
390
391        return None
392
393    # -- Traits event handlers ------------------------------------------------
394
395    def _view_changed(self, view):
396        self.resynch_editor()
397
398
399class SimpleEditor(CustomEditor):
400    """ Simple style of editor for instances, which displays a button. Clicking
401    the button displays a dialog box in which the instance can be edited.
402    """
403
404    #: The ui instance for the currently open editor dialog
405    _dialog_ui = Instance("traitsui.ui.UI")
406
407    #: Class constants:
408    orientation = QtGui.QBoxLayout.LeftToRight
409    extra = 2
410
411    def create_editor(self, parent, layout):
412        """ Creates the editor control (a button).
413        """
414        self._button = QtGui.QPushButton()
415        layout.addWidget(self._button)
416        self._button.clicked.connect(self.edit_instance)
417        # Make sure the editor is properly disposed if parent UI is closed
418        self._button.destroyed.connect(self._parent_closed)
419
420    def edit_instance(self):
421        """ Edit the contents of the object trait when the user clicks the
422            button.
423        """
424        # Create the user interface:
425        factory = self.factory
426        view = self.ui.handler.trait_view_for(
427            self.ui.info, factory.view, self.value, self.object_name, self.name
428        )
429        self._dialog_ui = self.value.edit_traits(
430            view, kind=factory.kind, id=factory.id
431        )
432
433        # Check to see if the view was 'modal', in which case it will already
434        # have been closed (i.e. is None) by the time we get control back:
435        if self._dialog_ui.control is not None:
436            # Position the window on the display:
437            position_window(self._dialog_ui.control)
438
439            # Chain our undo history to the new user interface if it does not
440            # have its own:
441            if self._dialog_ui.history is None:
442                self._dialog_ui.history = self.ui.history
443
444        else:
445            self._dialog_ui = None
446
447    def resynch_editor(self):
448        """ Resynchronizes the contents of the editor when the object trait
449            changes externally to the editor.
450        """
451        button = self._button
452        if button is not None:
453            label = self.factory.label
454            if label == "":
455                label = user_name_for(self.name)
456
457            button.setText(label)
458            button.setEnabled(isinstance(self.value, HasTraits))
459
460    def _parent_closed(self):
461        if self._dialog_ui is not None:
462            if self._dialog_ui.control is not None:
463                self._dialog_ui.control.close()
464            self._dialog_ui.dispose()
465            self._dialog_ui = None
466