1# ------------------------------------------------------------------------------
2# Copyright (c) 2007, Riverbank Computing Limited
3# Copyright (c) 2019, Enthought Inc.
4# All rights reserved.
5#
6# This software is provided without warranty under the terms of the BSD license.
7# However, when used with the GPL version of PyQt the additional terms
8# described in the PyQt GPL exception also apply
9
10#
11# Author: Riverbank Computing Limited
12# ------------------------------------------------------------------------------
13
14""" Defines the tree editor for the PyQt user interface toolkit.
15"""
16
17
18
19import copy
20import collections.abc
21from itertools import zip_longest
22import logging
23
24from pyface.qt import QtCore, QtGui
25
26from pyface.api import ImageResource
27from pyface.ui_traits import convert_image
28from pyface.timer.api import do_later
29from traits.api import Any, Event, Int
30from traitsui.editors.tree_editor import (
31    CopyAction,
32    CutAction,
33    DeleteAction,
34    NewAction,
35    PasteAction,
36    RenameAction,
37)
38from traitsui.tree_node import (
39    ITreeNodeAdapterBridge,
40    MultiTreeNode,
41    ObjectTreeNode,
42    TreeNode,
43)
44from traitsui.menu import Menu, Action, Separator
45from traitsui.ui_traits import SequenceTypes
46from traitsui.undo import ListUndoItem
47
48from .clipboard import clipboard, PyMimeData
49from .editor import Editor
50from .helper import pixmap_cache
51from .tree_node_renderers import WordWrapRenderer
52
53
54
55logger = logging.getLogger(__name__)
56
57
58# The renderer to use when word_wrap is True.
59DEFAULT_WRAP_RENDERER = WordWrapRenderer()
60
61
62class SimpleEditor(Editor):
63    """ Simple style of tree editor.
64    """
65
66    # -------------------------------------------------------------------------
67    #  Trait definitions:
68    # -------------------------------------------------------------------------
69
70    #: Is the tree editor is scrollable? This value overrides the default.
71    scrollable = True
72
73    #: Allows an external agent to set the tree selection
74    selection = Event()
75
76    #: The currently selected object
77    selected = Any()
78
79    #: The event fired when a tree node is activated by double clicking or
80    #: pressing the Enter key on a node.
81    activated = Event()
82
83    #: The event fired when a tree node is clicked on:
84    click = Event()
85
86    #: The event fired when a tree node is double-clicked on:
87    dclick = Event()
88
89    #: The event fired when the application wants to veto an operation:
90    veto = Event()
91
92    #: The vent fired when the application wants to refresh the viewport.
93    refresh = Event()
94
95    def init(self, parent):
96        """ Finishes initializing the editor by creating the underlying toolkit
97            widget.
98        """
99        factory = self.factory
100
101        self._editor = None
102
103        if factory.editable:
104
105            # Check to see if the tree view is based on a shared trait editor:
106            if factory.shared_editor:
107                factory_editor = factory.editor
108
109                # If this is the editor that defines the trait editor panel:
110                if factory_editor is None:
111
112                    # Remember which editor has the trait editor in the
113                    # factory:
114                    factory._editor = self
115
116                    # Create the trait editor panel:
117                    self.control = sa = QtGui.QScrollArea()
118                    sa.setFrameShape(QtGui.QFrame.NoFrame)
119                    sa.setWidgetResizable(True)
120                    self.control._node_ui = self.control._editor_nid = None
121
122                    # Check to see if there are any existing editors that are
123                    # waiting to be bound to the trait editor panel:
124                    editors = factory._shared_editors
125                    if editors is not None:
126                        for editor in factory._shared_editors:
127
128                            # If the editor is part of this UI:
129                            if editor.ui is self.ui:
130
131                                # Then bind it to the trait editor panel:
132                                editor._editor = self.control
133
134                        # Indicate all pending editors have been processed:
135                        factory._shared_editors = None
136
137                    # We only needed to build the trait editor panel, so exit:
138                    return
139
140                # Check to see if the matching trait editor panel has been
141                # created yet:
142                editor = factory_editor._editor
143                if (editor is None) or (editor.ui is not self.ui):
144                    # If not, add ourselves to the list of pending editors:
145                    shared_editors = factory_editor._shared_editors
146                    if shared_editors is None:
147                        factory_editor._shared_editors = shared_editors = []
148                    shared_editors.append(self)
149                else:
150                    # Otherwise, bind our trait editor panel to the shared one:
151                    self._editor = editor.control
152
153                # Finally, create only the tree control:
154                self.control = self._tree = _TreeWidget(self)
155            else:
156                # If editable, create a tree control and an editor panel:
157                self._tree = _TreeWidget(self)
158
159                self._editor = sa = QtGui.QScrollArea()
160                sa.setFrameShape(QtGui.QFrame.NoFrame)
161                sa.setWidgetResizable(True)
162                sa._node_ui = sa._editor_nid = None
163
164                if factory.orientation == "horizontal":
165                    orient = QtCore.Qt.Horizontal
166                else:
167                    orient = QtCore.Qt.Vertical
168
169                self.control = splitter = QtGui.QSplitter(orient)
170                splitter.setSizePolicy(
171                    QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding
172                )
173                splitter.addWidget(self._tree)
174                splitter.addWidget(sa)
175        else:
176            # Otherwise, just create the tree control:
177            self.control = self._tree = _TreeWidget(self)
178
179        # Create our item delegate
180        delegate = TreeItemDelegate()
181        delegate.editor = self
182        self._tree.setItemDelegate(delegate)
183        # From Qt Docs: QAbstractItemView does not take ownership of `delegate`
184        self._item_delegate = delegate
185
186        # Set up the mapping between objects and tree id's:
187        self._map = {}
188
189        # Initialize the 'undo state' stack:
190        self._undoable = []
191
192        # Synchronize external object traits with the editor:
193        self.sync_value(factory.refresh, "refresh")
194        self.sync_value(factory.selected, "selected")
195        self.sync_value(factory.activated, "activated", "to")
196        self.sync_value(factory.click, "click", "to")
197        self.sync_value(factory.dclick, "dclick", "to")
198        self.sync_value(factory.veto, "veto", "from")
199
200    def _selection_changed(self, selection):
201        """ Handles the **selection** event.
202        """
203        try:
204            tree = self._tree
205            if not isinstance(selection, str) and isinstance(
206                selection, collections.abc.Iterable
207            ):
208
209                item_selection = QtGui.QItemSelection()
210                for sel in selection:
211                    item = self._object_info(sel)[2]
212                    idx = tree.indexFromItem(item)
213                    item_selection.append(QtGui.QItemSelectionRange(idx))
214
215                tree.selectionModel().select(
216                    item_selection, QtGui.QItemSelectionModel.ClearAndSelect
217                )
218            else:
219                tree.setCurrentItem(self._object_info(selection)[2])
220        except:
221            from traitsui.api import raise_to_debug
222
223            raise_to_debug()
224
225    def _selected_changed(self, selected):
226        """ Handles the **selected** trait being changed.
227        """
228        if not self._no_update_selected:
229            self._selection_changed(selected)
230
231    def _veto_changed(self):
232        """ Handles the 'veto' event being fired.
233        """
234        self._veto = True
235
236    def _refresh_changed(self):
237        """ Update the viewport.
238        """
239        self._tree.viewport().update()
240
241    def dispose(self):
242        """ Disposes of the contents of an editor.
243        """
244        if self._tree is not None:
245            # Stop the chatter (specifically about the changing selection).
246            self._tree.blockSignals(True)
247
248            self._delete_node(self._tree.invisibleRootItem())
249
250            self._tree = None
251
252        super(SimpleEditor, self).dispose()
253
254    def expand_levels(self, nid, levels, expand=True):
255        """ Expands from the specified node the specified number of sub-levels.
256        """
257        if levels > 0:
258            expanded, node, object = self._get_node_data(nid)
259            if self._has_children(node, object):
260                self._expand_node(nid)
261                if expand:
262                    nid.setExpanded(True)
263                for cnid in self._nodes_for(nid):
264                    self.expand_levels(cnid, levels - 1)
265
266    def update_editor(self):
267        """ Updates the editor when the object trait changes externally to the
268            editor.
269        """
270        tree = self._tree
271        if tree is None:
272            return
273        saved_state = {}
274
275        object, node = self._node_for(self.old_value)
276        old_nid = self._get_object_nid(object, node.get_children_id(object))
277        if old_nid:
278            self._delete_node(old_nid)
279
280        object, node = self._node_for(self.old_value)
281        old_nid = self._get_object_nid(object, node.get_children_id(object))
282        if old_nid:
283            self._delete_node(old_nid)
284
285        tree.clear()
286        self._map = {}
287
288        object, node = self._node_for(self.value)
289        if node is not None:
290            if self.factory.hide_root:
291                nid = tree.invisibleRootItem()
292            else:
293                nid = self._create_item(tree, node, object)
294
295            self._map[id(object)] = [(node.get_children_id(object), nid)]
296            self._add_listeners(node, object)
297            self._set_node_data(nid, (False, node, object))
298            if self.factory.hide_root or self._has_children(node, object):
299                self._expand_node(nid)
300                if not self.factory.hide_root:
301                    nid.setExpanded(True)
302                    tree.setCurrentItem(nid)
303
304            self.expand_levels(nid, self.factory.auto_open, False)
305        ncolumns = self._tree.columnCount()
306        if ncolumns > 1:
307            for i in range(ncolumns):
308                self._tree.resizeColumnToContents(i)
309        # FIXME: Clear the current editor (if any)...
310
311    def get_error_control(self):
312        """ Returns the editor's control for indicating error status.
313        """
314        return self._tree
315
316    def _get_brush(self, color):
317        if isinstance(color, SequenceTypes):
318            q_color = QtGui.QColor(*color)
319        else:
320            q_color = QtGui.QColor(color)
321        return QtGui.QBrush(q_color)
322
323    def _set_column_labels(self, nid, node, object):
324        """ Set the column labels. """
325        column_labels = node.get_column_labels(object)
326
327        for i, (header, label) in enumerate(
328            zip_longest(self.factory.column_headers[1:], column_labels), 1
329        ):
330            renderer = node.get_renderer(object, i)
331            handles_text = renderer.handles_text if renderer else False
332            if header is None or label is None or handles_text:
333                nid.setText(i, "")
334            else:
335                nid.setText(i, label)
336
337    def _create_item(self, nid, node, object, index=None):
338        """ Create  a new TreeWidgetItem as per word_wrap policy.
339
340        Index is the index of the new node in the parent:
341            None implies append the child to the end. """
342        if index is None:
343            cnid = QtGui.QTreeWidgetItem(nid)
344        else:
345            cnid = QtGui.QTreeWidgetItem()
346            nid.insertChild(index, cnid)
347
348        renderer = node.get_renderer(object)
349        handles_text = getattr(renderer, "handles_text", False)
350        handles_icon = getattr(renderer, "handles_icon", False)
351        if not (self.factory.word_wrap or handles_text):
352            cnid.setText(0, node.get_label(object))
353        if not handles_icon:
354            cnid.setIcon(0, self._get_icon(node, object))
355        cnid.setToolTip(0, node.get_tooltip(object))
356
357        self._set_column_labels(cnid, node, object)
358
359        color = node.get_background(object)
360        if color:
361            cnid.setBackground(0, self._get_brush(color))
362        color = node.get_foreground(object)
363        if color:
364            cnid.setForeground(0, self._get_brush(color))
365
366        return cnid
367
368    def _set_label(self, nid, col=0):
369        """ Set the label of the specified item """
370        if col != 0:
371            # these are handled by _set_column_labels
372            return
373        expanded, node, object = self._get_node_data(nid)
374        renderer = node.get_renderer(object)
375        handles_text = getattr(renderer, "handles_text", False)
376        if self.factory.word_wrap or handles_text:
377            nid.setText(col, "")
378        else:
379            nid.setText(col, node.get_label(object))
380
381    def _append_node(self, nid, node, object):
382        """ Appends a new node to the specified node.
383        """
384        return self._insert_node(nid, None, node, object)
385
386    def _insert_node(self, nid, index, node, object):
387        """ Inserts a new node before a specified index into the children of the
388            specified node.
389        """
390
391        cnid = self._create_item(nid, node, object, index)
392
393        has_children = self._has_children(node, object)
394        self._set_node_data(cnid, (False, node, object))
395        self._map.setdefault(id(object), []).append(
396            (node.get_children_id(object), cnid)
397        )
398        self._add_listeners(node, object)
399
400        # Automatically expand the new node (if requested):
401        if has_children:
402            if node.can_auto_open(object):
403                cnid.setExpanded(True)
404            else:
405                # Qt only draws the control that expands the tree if there is a
406                # child.  As the tree is being populated lazily we create a
407                # dummy that will be removed when the node is expanded for the
408                # first time.
409                cnid._dummy = QtGui.QTreeWidgetItem(cnid)
410
411        # Return the newly created node:
412        return cnid
413
414    def _delete_node(self, nid):
415        """ Deletes a specified tree node and all its children.
416        """
417
418        for cnid in self._nodes_for(nid):
419            self._delete_node(cnid)
420
421        # See if it is a dummy.
422        pnid = nid.parent()
423        if pnid is not None and getattr(pnid, "_dummy", None) is nid:
424            pnid.removeChild(nid)
425            del pnid._dummy
426            return
427
428        try:
429            expanded, node, object = self._get_node_data(nid)
430        except AttributeError:
431            # The node has already been deleted.
432            pass
433        else:
434            id_object = id(object)
435            object_info = self._map[id_object]
436            for i, info in enumerate(object_info):
437                # QTreeWidgetItem does not have an equal operator, so use id()
438                if id(nid) == id(info[1]):
439                    del object_info[i]
440                    break
441
442            if len(object_info) == 0:
443                self._remove_listeners(node, object)
444                del self._map[id_object]
445
446        if pnid is None:
447            self._tree.takeTopLevelItem(self._tree.indexOfTopLevelItem(nid))
448        else:
449            pnid.removeChild(nid)
450
451        # If the deleted node had an active editor panel showing, remove it:
452        # Note: QTreeWidgetItem does not have an equal operator, so use id()
453        if (self._editor is not None) and (
454            id(nid) == id(self._editor._editor_nid)
455        ):
456            self._clear_editor()
457
458    def _expand_node(self, nid):
459        """ Expands the contents of a specified node (if required).
460        """
461        expanded, node, object = self._get_node_data(nid)
462
463        # Lazily populate the item's children:
464        if not expanded:
465            # Remove any dummy node.
466            dummy = getattr(nid, "_dummy", None)
467            if dummy is not None:
468                nid.removeChild(dummy)
469                del nid._dummy
470
471            for child in node.get_children(object):
472                child, child_node = self._node_for(child)
473                if child_node is not None:
474                    self._append_node(nid, child_node, child)
475
476            # Indicate the item is now populated:
477            self._set_node_data(nid, (True, node, object))
478
479    def _nodes_for(self, nid):
480        """ Returns all child node ids of a specified node id.
481        """
482        return [nid.child(i) for i in range(nid.childCount())]
483
484    def _node_index(self, nid):
485        pnid = nid.parent()
486        if pnid is None:
487            if self.factory.hide_root:
488                pnid = self._tree.invisibleRootItem()
489            if pnid is None:
490                return (None, None, None)
491
492        for i in range(pnid.childCount()):
493            if pnid.child(i) is nid:
494                _, pnode, pobject = self._get_node_data(pnid)
495                return (pnode, pobject, i)
496        else:
497            # doesn't match any node, so return None
498            return (None, None, None)
499
500    def _has_children(self, node, object):
501        """ Returns whether a specified object has any children.
502        """
503        return node.allows_children(object) and node.has_children(object)
504
505    # -------------------------------------------------------------------------
506    #  Returns the icon index for the specified object:
507    # -------------------------------------------------------------------------
508
509    STD_ICON_MAP = {
510        "<item>": QtGui.QStyle.SP_FileIcon,
511        "<group>": QtGui.QStyle.SP_DirClosedIcon,
512        "<open>": QtGui.QStyle.SP_DirOpenIcon,
513    }
514
515    def _get_icon(self, node, object, is_expanded=False):
516        """ Returns the index of the specified object icon.
517        """
518        if not self.factory.show_icons:
519            return QtGui.QIcon()
520
521        icon_name = node.get_icon(object, is_expanded)
522        if isinstance(icon_name, str):
523            if icon_name.startswith("@"):
524                image_resource = convert_image(icon_name, 4)
525                return image_resource.create_icon()
526            elif icon_name in self.STD_ICON_MAP:
527                icon = self.STD_ICON_MAP[icon_name]
528                return self._tree.style().standardIcon(icon)
529
530            path = node.get_icon_path(object)
531            if isinstance(path, str):
532                path = [path, node]
533            else:
534                path = path + [node]
535
536            image_resource = ImageResource(icon_name, path)
537
538        elif isinstance(icon_name, ImageResource):
539            image_resource = icon_name
540
541        elif isinstance(icon_name, tuple):
542            if max(icon_name) <= 1.0:
543                # rgb(a) color tuple between 0.0 and 1.0
544                color = QtGui.QColor.fromRgbF(*icon_name)
545            else:
546                # rgb(a) color tuple between 0 and 255
547                color = QtGui.QColor.fromRgb(*icon_name)
548            return self._icon_from_color(color)
549
550        elif isinstance(icon_name, QtGui.QColor):
551            return self._icon_from_color(icon_name)
552
553        else:
554            raise ValueError(
555                "Icon value must be a string or color or color tuple or "
556                + "IImageResource instance: "
557                + "given {!r}".format(icon_name)
558            )
559
560        file_name = image_resource.absolute_path
561        return QtGui.QIcon(pixmap_cache(file_name))
562
563    def _icon_from_color(self, color):
564        """ Create a square icon filled with the given color. """
565        pixmap = QtGui.QPixmap(self._tree.iconSize())
566        pixmap.fill(color)
567        return QtGui.QIcon(pixmap)
568
569    def _add_listeners(self, node, object):
570        """ Adds the event listeners for a specified object.
571        """
572
573        if node.allows_children(object):
574            node.when_children_replaced(object, self._children_replaced, False)
575            node.when_children_changed(object, self._children_updated, False)
576
577        node.when_label_changed(object, self._label_updated, False)
578        node.when_column_labels_change(
579            object, self._column_labels_updated, False
580        )
581
582    def _remove_listeners(self, node, object):
583        """ Removes any event listeners from a specified object.
584        """
585
586        if node.allows_children(object):
587            node.when_children_replaced(object, self._children_replaced, True)
588            node.when_children_changed(object, self._children_updated, True)
589
590        node.when_label_changed(object, self._label_updated, True)
591        node.when_column_labels_change(
592            object, self._column_labels_updated, True
593        )
594
595    def _object_info(self, object, name=""):
596        """ Returns the tree node data for a specified object in the form
597            ( expanded, node, nid ).
598        """
599        info = self._map[id(object)]
600        for name2, nid in info:
601            if name == name2:
602                break
603        else:
604            nid = info[0][1]
605
606        expanded, node, ignore = self._get_node_data(nid)
607
608        return (expanded, node, nid)
609
610    def _object_info_for(self, object, name=""):
611        """ Returns the tree node data for a specified object as a list of the
612            form: [ ( expanded, node, nid ), ... ].
613        """
614        result = []
615        for name2, nid in self._map[id(object)]:
616            if name == name2:
617                expanded, node, ignore = self._get_node_data(nid)
618                result.append((expanded, node, nid))
619
620        return result
621
622    def _node_for(self, object):
623        """ Returns the TreeNode associated with a specified object.
624        """
625        if (
626            (isinstance(object, tuple))
627            and (len(object) == 2)
628            and isinstance(object[1], TreeNode)
629        ):
630            return object
631
632        # Select all nodes which understand this object:
633        factory = self.factory
634        nodes = [node for node in factory.nodes if node.is_node_for(object)]
635
636        # If only one found, we're done, return it:
637        if len(nodes) == 1:
638            return (object, nodes[0])
639
640        # If none found, give up:
641        if len(nodes) == 0:
642            return (object, ITreeNodeAdapterBridge(adapter=object))
643
644        # Use all selected nodes that have the same 'node_for' list as the
645        # first selected node:
646        base = nodes[0].node_for
647        nodes = [node for node in nodes if base == node.node_for]
648
649        # If only one left, then return that node:
650        if len(nodes) == 1:
651            return (object, nodes[0])
652
653        # Otherwise, return a MultiTreeNode based on all selected nodes...
654
655        # Use the node with no specified children as the root node. If not
656        # found, just use the first selected node as the 'root node':
657        root_node = None
658        for i, node in enumerate(nodes):
659            if node.children == "":
660                root_node = node
661                del nodes[i]
662                break
663        else:
664            root_node = nodes[0]
665
666        # If we have a matching MultiTreeNode already cached, return it:
667        key = (root_node,) + tuple(nodes)
668        if key in factory.multi_nodes:
669            return (object, factory.multi_nodes[key])
670
671        # Otherwise create one, cache it, and return it:
672        factory.multi_nodes[key] = multi_node = MultiTreeNode(
673            root_node=root_node, nodes=nodes
674        )
675
676        return (object, multi_node)
677
678    def _node_for_class(self, klass):
679        """ Returns the TreeNode associated with a specified class.
680        """
681        for node in self.factory.nodes:
682            if issubclass(klass, tuple(node.node_for)):
683                return node
684        return None
685
686    def _node_for_class_name(self, class_name):
687        """ Returns the node and class associated with a specified class name.
688        """
689        for node in self.factory.nodes:
690            for klass in node.node_for:
691                if class_name == klass.__name__:
692                    return (node, klass)
693        return (None, None)
694
695    def _update_icon(self, nid):
696        """ Updates the icon for a specified node.
697        """
698        expanded, node, object = self._get_node_data(nid)
699        renderer = node.get_renderer(object)
700        if renderer is None or not renderer.handles_icon:
701            nid.setIcon(0, self._get_icon(node, object, expanded))
702        else:
703            nid.setIcon(0, QtGui.QIcon())
704
705    def _begin_undo(self):
706        """ Begins an "undoable" transaction.
707        """
708        ui = self.ui
709        self._undoable.append(ui._undoable)
710        if (ui._undoable == -1) and (ui.history is not None):
711            ui._undoable = ui.history.now
712
713    def _end_undo(self):
714        if self._undoable.pop() == -1:
715            self.ui._undoable = -1
716
717    def _get_undo_item(self, object, name, event):
718        return ListUndoItem(
719            object=object,
720            name=name,
721            index=event.index,
722            added=event.added,
723            removed=event.removed,
724        )
725
726    def _undoable_append(self, node, object, data, make_copy=True):
727        """ Performs an undoable append operation.
728        """
729        try:
730            self._begin_undo()
731            if make_copy:
732                data = copy.deepcopy(data)
733            node.append_child(object, data)
734        finally:
735            self._end_undo()
736
737    def _undoable_insert(self, node, object, index, data, make_copy=True):
738        """ Performs an undoable insert operation.
739        """
740        try:
741            self._begin_undo()
742            if make_copy:
743                data = copy.deepcopy(data)
744            node.insert_child(object, index, data)
745        finally:
746            self._end_undo()
747
748    def _undoable_delete(self, node, object, index):
749        """ Performs an undoable delete operation.
750        """
751        try:
752            self._begin_undo()
753            node.delete_child(object, index)
754        finally:
755            self._end_undo()
756
757    def _get_object_nid(self, object, name=""):
758        """ Gets the ID associated with a specified object (if any).
759        """
760        info = self._map.get(id(object))
761        if info is None:
762            return None
763        for name2, nid in info:
764            if name == name2:
765                return nid
766        else:
767            return info[0][1]
768
769    def _clear_editor(self):
770        """ Clears the current editor pane (if any).
771        """
772        editor = self._editor
773        if editor._node_ui is not None:
774            editor.setWidget(None)
775            editor._node_ui.dispose()
776            editor._node_ui = editor._editor_nid = None
777
778    # -------------------------------------------------------------------------
779    #  Gets/Sets the node specific data:
780    # -------------------------------------------------------------------------
781
782    @staticmethod
783    def _get_node_data(nid):
784        """ Gets the node specific data. """
785        return nid._py_data
786
787    @staticmethod
788    def _set_node_data(nid, data):
789        """ Sets the node specific data. """
790        nid._py_data = data
791
792    # ----- User callable methods: --------------------------------------------
793
794    def get_object(self, nid):
795        """ Gets the object associated with a specified node.
796        """
797        return self._get_node_data(nid)[2]
798
799    def get_parent(self, object, name=""):
800        """ Returns the object that is the immmediate parent of a specified
801            object in the tree.
802        """
803        nid = self._get_object_nid(object, name)
804        if nid is not None:
805            pnid = nid.parent()
806            if pnid is not self._tree.invisibleRootItem():
807                return self.get_object(pnid)
808        return None
809
810    def get_node(self, object, name=""):
811        """ Returns the node associated with a specified object.
812        """
813        nid = self._get_object_nid(object, name)
814        if nid is not None:
815            return self._get_node_data(nid)[1]
816        return None
817
818    # ----- Tree event handlers: ----------------------------------------------
819
820    def _on_item_expanded(self, nid):
821        """ Handles a tree node being expanded.
822        """
823        expanded, node, object = self._get_node_data(nid)
824
825        # If 'auto_close' requested for this node type, close all of the node's
826        # siblings:
827        if node.can_auto_close(object):
828            parent = nid.parent()
829
830            if parent is not None:
831                for snid in self._nodes_for(parent):
832                    if snid is not nid:
833                        snid.setExpanded(False)
834
835        # Expand the node (i.e. populate its children if they are not there
836        # yet):
837        self._expand_node(nid)
838
839        self._update_icon(nid)
840
841    def _on_item_collapsed(self, nid):
842        """ Handles a tree node being collapsed.
843        """
844        self._update_icon(nid)
845
846    def _on_item_clicked(self, nid, col):
847        """ Handles a tree item being clicked.
848        """
849        _, node, object = self._get_node_data(nid)
850
851        if node.click(object) is True and self.factory.on_click is not None:
852            self.ui.evaluate(self.factory.on_click, object)
853
854        # Fire the 'click' event with the object as its value:
855        self.click = object
856
857    def _on_item_dclicked(self, nid, col):
858        """ Handles a tree item being double-clicked.
859        """
860        _, node, object = self._get_node_data(nid)
861
862        if node.dclick(object) is True:
863            if self.factory.on_dclick is not None:
864                self.ui.evaluate(self.factory.on_dclick, object)
865                self._veto = True
866        else:
867            self._veto = True
868
869        # Fire the 'dclick' event with the clicked on object as value:
870        self.dclick = object
871
872    def _on_item_activated(self, nid, col):
873        """ Handles a tree item being activated.
874        """
875        _, node, object = self._get_node_data(nid)
876
877        if node.activated(object) is True:
878            if self.factory.on_activated is not None:
879                self.ui.evaluate(self.factory.on_activated, object)
880                self._veto = True
881        else:
882            self._veto = True
883
884        # Fire the 'activated' event with the clicked on object as value:
885        self.activated = object
886
887    def _on_tree_sel_changed(self):
888        """ Handles a tree node being selected.
889        """
890        # Get the new selection:
891        nids = self._tree.selectedItems()
892
893        selected = []
894        if len(nids) > 0:
895            for nid in nids:
896                # If there is a real selection, get the associated object:
897                expanded, node, sel_object = self._get_node_data(nid)
898                selected.append(sel_object)
899
900                # Try to inform the node specific handler of the selection, if
901                # there are multiple selections, we only care about the first
902                # (or maybe the last makes more sense?)
903
904                # QTreeWidgetItem does not have an equal operator, so use id()
905                if id(nid) == id(nids[0]):
906                    object = sel_object
907                    not_handled = node.select(sel_object)
908        else:
909            nid = None
910            object = None
911            not_handled = True
912
913        # Set the value of the new selection:
914        if self.factory.selection_mode == "single":
915            self._no_update_selected = True
916            self.selected = object
917            self._no_update_selected = False
918        else:
919            self._no_update_selected = True
920            self.selected = selected
921            self._no_update_selected = False
922
923        # If no one has been notified of the selection yet, inform the editor's
924        # select handler (if any) of the new selection:
925        if not_handled is True:
926            self.ui.evaluate(self.factory.on_select, object)
927
928        # Check to see if there is an associated node editor pane:
929        editor = self._editor
930        if editor is not None:
931            # If we already had a node editor, destroy it:
932            editor.setUpdatesEnabled(False)
933            self._clear_editor()
934
935            # If there is a selected object, create a new editor for it:
936            if object is not None:
937                # Try to chain the undo history to the main undo history:
938                view = node.get_view(object)
939                if view is None or isinstance(view, str):
940                    view = object.trait_view(view)
941
942                if (self.ui.history is not None) or (view.kind == "subpanel"):
943                    ui = object.edit_traits(
944                        parent=editor, view=view, kind="subpanel"
945                    )
946                else:
947                    # Otherwise, just set up our own new one:
948                    ui = object.edit_traits(
949                        parent=editor, view=view, kind="panel"
950                    )
951
952                # Make our UI the parent of the new UI:
953                ui.parent = self.ui
954
955                # Remember the new editor's UI and node info:
956                editor._node_ui = ui
957                editor._editor_nid = nid
958
959                # Finish setting up the editor:
960                if ui.control.layout() is not None:
961                    ui.control.layout().setContentsMargins(0, 0, 0, 0)
962                editor.setWidget(ui.control)
963
964            # Allow the editor view to show any changes that have occurred:
965            editor.setUpdatesEnabled(True)
966
967    def _on_context_menu(self, pos):
968        """ Handles the user requesting a context menuright clicking on a tree node.
969        """
970        nid = self._tree.itemAt(pos)
971
972        if nid is None:
973            return
974
975        _, node, object = self._get_node_data(nid)
976
977        self._data = (node, object, nid)
978        self._context = {
979            "object": object,
980            "editor": self,
981            "node": node,
982            "info": self.ui.info,
983            "handler": self.ui.handler,
984        }
985
986        # Try to get the parent node of the node clicked on:
987        pnid = nid.parent()
988        if pnid is None or pnid is self._tree.invisibleRootItem():
989            parent_node = parent_object = None
990        else:
991            _, parent_node, parent_object = self._get_node_data(pnid)
992
993        self._menu_node = node
994        self._menu_parent_node = parent_node
995        self._menu_parent_object = parent_object
996
997        menu = node.get_menu(object)
998
999        if menu is None:
1000            # Use the standard, default menu:
1001            menu = self._standard_menu(node, object)
1002
1003        elif isinstance(menu, Menu):
1004            # Use the menu specified by the node:
1005            group = menu.find_group(NewAction)
1006            if group is not None:
1007                # Only set it the first time:
1008                group.id = ""
1009                actions = self._new_actions(node, object)
1010                if len(actions) > 0:
1011                    group.insert(0, Menu(name="New", *actions))
1012
1013        else:
1014            # All other values mean no menu should be displayed:
1015            menu = None
1016
1017        # Only display the menu if a valid menu is defined:
1018        if menu is not None:
1019            qmenu = menu.create_menu(self._tree, self)
1020            qmenu.exec_(self._tree.mapToGlobal(pos))
1021
1022        # Reset all menu related cached values:
1023        self._data = (
1024            self._context
1025        ) = (
1026            self._menu_node
1027        ) = self._menu_parent_node = self._menu_parent_object = None
1028
1029    def _standard_menu(self, node, object):
1030        """ Returns the standard contextual pop-up menu.
1031        """
1032        actions = [
1033            CutAction,
1034            CopyAction,
1035            PasteAction,
1036            Separator(),
1037            DeleteAction,
1038            Separator(),
1039            RenameAction,
1040        ]
1041
1042        # See if the 'New' menu section should be added:
1043        items = self._new_actions(node, object)
1044        if len(items) > 0:
1045            actions[0:0] = [Menu(name="New", *items), Separator()]
1046
1047        return Menu(*actions)
1048
1049    def _new_actions(self, node, object):
1050        """ Returns a list of Actions that will create new objects.
1051        """
1052        object = self._data[1]
1053        items = []
1054        add = node.get_add(object)
1055        if len(add) > 0:
1056            for klass in add:
1057                prompt = False
1058                if isinstance(klass, tuple):
1059                    klass, prompt = klass
1060                add_node = self._node_for_class(klass)
1061                if add_node is not None:
1062                    class_name = klass.__name__
1063                    name = add_node.get_name(object)
1064                    if name == "":
1065                        name = class_name
1066                    items.append(
1067                        Action(
1068                            name=name,
1069                            action="editor._menu_new_node('%s',%s)"
1070                            % (class_name, prompt),
1071                        )
1072                    )
1073        return items
1074
1075    # -------------------------------------------------------------------------
1076    #  Menu action helper methods:
1077    # -------------------------------------------------------------------------
1078
1079    def _is_copyable(self, object):
1080        parent = self._menu_parent_node
1081        if isinstance(parent, ObjectTreeNode):
1082            return parent.can_copy(self._menu_parent_object)
1083        return (parent is not None) and parent.can_copy(object)
1084
1085    def _is_cutable(self, object):
1086        parent = self._menu_parent_node
1087        if isinstance(parent, ObjectTreeNode):
1088            can_cut = parent.can_copy(
1089                self._menu_parent_object
1090            ) and parent.can_delete(self._menu_parent_object)
1091        else:
1092            can_cut = (
1093                (parent is not None)
1094                and parent.can_copy(object)
1095                and parent.can_delete(object)
1096            )
1097        return can_cut and self._menu_node.can_delete_me(object)
1098
1099    def _is_pasteable(self, object):
1100        return self._menu_node.can_add(object, clipboard.instance_type)
1101
1102    def _is_deletable(self, object):
1103        parent = self._menu_parent_node
1104        if isinstance(parent, ObjectTreeNode):
1105            can_delete = parent.can_delete(self._menu_parent_object)
1106        else:
1107            can_delete = (parent is not None) and parent.can_delete(object)
1108        return can_delete and self._menu_node.can_delete_me(object)
1109
1110    def _is_renameable(self, object):
1111        parent = self._menu_parent_node
1112        if isinstance(parent, ObjectTreeNode):
1113            can_rename = parent.can_rename(self._menu_parent_object)
1114        else:
1115            can_rename = (parent is not None) and parent.can_rename(object)
1116
1117        can_rename = can_rename and self._menu_node.can_rename_me(object)
1118
1119        # Set the widget item's editable flag appropriately.
1120        nid = self._get_object_nid(object)
1121        flags = nid.flags()
1122        if can_rename:
1123            flags |= QtCore.Qt.ItemIsEditable
1124        else:
1125            flags &= ~QtCore.Qt.ItemIsEditable
1126        nid.setFlags(flags)
1127
1128        return can_rename
1129
1130    def _is_droppable(self, node, object, add_object, for_insert):
1131        """ Returns whether a given object is droppable on the node.
1132        """
1133        if for_insert and (not node.can_insert(object)):
1134            return False
1135
1136        return node.can_add(object, add_object)
1137
1138    def _drop_object(self, node, object, dropped_object, make_copy=True):
1139        """ Returns a droppable version of a specified object.
1140        """
1141        new_object = node.drop_object(object, dropped_object)
1142        if (new_object is not dropped_object) or (not make_copy):
1143            return new_object
1144
1145        return copy.deepcopy(new_object)
1146
1147    # ----- pyface.action 'controller' interface implementation: --------------
1148
1149    def add_to_menu(self, menu_item):
1150        """ Adds a menu item to the menu bar being constructed.
1151        """
1152        action = menu_item.item.action
1153        self.eval_when(action.enabled_when, menu_item, "enabled")
1154        self.eval_when(action.checked_when, menu_item, "checked")
1155
1156    def add_to_toolbar(self, toolbar_item):
1157        """ Adds a toolbar item to the toolbar being constructed.
1158        """
1159        self.add_to_menu(toolbar_item)
1160
1161    def can_add_to_menu(self, action):
1162        """ Returns whether the action should be defined in the user interface.
1163        """
1164        if action.defined_when != "":
1165            if not eval(action.defined_when, globals(), self._context):
1166                return False
1167
1168        if action.visible_when != "":
1169            if not eval(action.visible_when, globals(), self._context):
1170                return False
1171
1172        return True
1173
1174    def can_add_to_toolbar(self, action):
1175        """ Returns whether the toolbar action should be defined in the user
1176            interface.
1177        """
1178        return self.can_add_to_menu(action)
1179
1180    def perform(self, action, action_event=None):
1181        """ Performs the action described by a specified Action object.
1182        """
1183        self.ui.do_undoable(self._perform, action)
1184
1185    def _perform(self, action):
1186        node, object, nid = self._data
1187        method_name = action.action
1188        info = self.ui.info
1189        handler = self.ui.handler
1190
1191        if method_name.find(".") >= 0:
1192            if method_name.find("(") < 0:
1193                method_name += "()"
1194            try:
1195                eval(
1196                    method_name,
1197                    globals(),
1198                    {
1199                        "object": object,
1200                        "editor": self,
1201                        "node": node,
1202                        "info": info,
1203                        "handler": handler,
1204                    },
1205                )
1206            except:
1207                from traitsui.api import raise_to_debug
1208
1209                raise_to_debug()
1210            return
1211
1212        method = getattr(handler, method_name, None)
1213        if method is not None:
1214            method(info, object)
1215            return
1216
1217        if action.on_perform is not None:
1218            action.on_perform(object)
1219
1220    # ----- Menu support methods: ---------------------------------------------
1221
1222    def eval_when(self, condition, object, trait):
1223        """ Evaluates a condition within a defined context, and sets a
1224        specified object trait based on the result, which is assumed to be a
1225        Boolean.
1226        """
1227        if condition != "":
1228            value = True
1229            try:
1230                if not eval(condition, globals(), self._context):
1231                    value = False
1232            except Exception as e:
1233                logger.warning(
1234                    "Exception (%s) raised when evaluating the "
1235                    "condition %s. Returning True." % (e, condition)
1236                )
1237            setattr(object, trait, value)
1238
1239    # ----- Menu event handlers: ----------------------------------------------
1240
1241    def _menu_copy_node(self):
1242        """ Copies the current tree node object to the paste buffer.
1243        """
1244        clipboard.instance = self._data[1]
1245        self._data = None
1246
1247    def _menu_cut_node(self):
1248        """ Cuts the current tree node object into the paste buffer.
1249        """
1250        node, object, nid = self._data
1251        clipboard.instance = object
1252        self._data = None
1253        self._undoable_delete(*self._node_index(nid))
1254
1255    def _menu_paste_node(self):
1256        """ Pastes the current contents of the paste buffer into the current
1257            node.
1258        """
1259        node, object, nid = self._data
1260        self._data = None
1261        self._undoable_append(node, object, clipboard.instance, True)
1262
1263    def _menu_delete_node(self):
1264        """ Deletes the current node from the tree.
1265        """
1266        node, object, nid = self._data
1267        self._data = None
1268        rc = node.confirm_delete(object)
1269        if rc is not False:
1270            if rc is not True:
1271                if self.ui.history is None:
1272                    # If no undo history, ask user to confirm the delete:
1273                    butn = QtGui.QMessageBox.question(
1274                        self._tree,
1275                        "Confirm Deletion",
1276                        "Are you sure you want to delete %s?"
1277                        % node.get_label(object),
1278                        QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
1279                    )
1280                    if butn != QtGui.QMessageBox.Yes:
1281                        return
1282
1283            self._undoable_delete(*self._node_index(nid))
1284
1285    def _menu_rename_node(self):
1286        """ Rename the current node.
1287        """
1288        _, _, nid = self._data
1289        self._data = None
1290        self._tree.editItem(nid)
1291
1292    def _on_nid_changed(self, nid, col):
1293        """ Handle changes to a widget item.
1294        """
1295        # The node data may not have been set up for the nid yet.  Ignore it if
1296        # it hasn't.
1297        try:
1298            _, node, object = self._get_node_data(nid)
1299        except:
1300            return
1301
1302        new_label = str(nid.text(col))
1303        old_label = node.get_label(object)
1304
1305        if new_label != old_label:
1306            if new_label != "":
1307                node.set_label(object, new_label)
1308            else:
1309                self._set_label(nid, col)
1310
1311    def _menu_new_node(self, class_name, prompt=False):
1312        """ Adds a new object to the current node.
1313        """
1314        node, object, nid = self._data
1315        self._data = None
1316        new_node, new_class = self._node_for_class_name(class_name)
1317        new_object = new_class()
1318        if (not prompt) or new_object.edit_traits(
1319            parent=self.control, kind="livemodal"
1320        ).result:
1321            self._undoable_append(node, object, new_object, False)
1322
1323            # Automatically select the new object if editing is being
1324            # performed:
1325            if self.factory.editable:
1326                self._tree.setCurrentItem(nid.child(nid.childCount() - 1))
1327
1328    # ----- Model event handlers: ---------------------------------------------
1329
1330    def _children_replaced(self, object, name="", new=None):
1331        """ Handles the children of a node being completely replaced.
1332        """
1333        tree = self._tree
1334        for expanded, node, nid in self._object_info_for(object, name):
1335            children = node.get_children(object)
1336
1337            # Only add/remove the changes if the node has already been
1338            # expanded:
1339            if expanded:
1340                # Delete all current child nodes:
1341                for cnid in self._nodes_for(nid):
1342                    self._delete_node(cnid)
1343
1344                # Add all of the children back in as new nodes:
1345                for child in children:
1346                    child, child_node = self._node_for(child)
1347                    if child_node is not None:
1348                        self._append_node(nid, child_node, child)
1349            else:
1350                dummy = getattr(nid, "_dummy", None)
1351                if dummy is None and len(children) > 0:
1352                    # if model now has children add dummy child
1353                    nid._dummy = QtGui.QTreeWidgetItem(nid)
1354                elif dummy is not None and len(children) == 0:
1355                    # if model no longer has children remove dummy child
1356                    nid.removeChild(dummy)
1357                    del nid._dummy
1358
1359            # Try to expand the node (if requested):
1360            if node.can_auto_open(object):
1361                nid.setExpanded(True)
1362
1363    def _children_updated(self, object, name, event):
1364        """ Handles the children of a node being changed.
1365        """
1366        # Log the change that was made made (removing '_items' from the end of
1367        # the name):
1368        name = name[:-6]
1369        self.log_change(self._get_undo_item, object, name, event)
1370
1371        # Get information about the node that was changed:
1372        start = event.index
1373        n = len(event.added)
1374        end = start + len(event.removed)
1375        tree = self._tree
1376
1377        for expanded, node, nid in self._object_info_for(object, name):
1378            children = node.get_children(object)
1379
1380            # If the new children aren't all at the end, remove/add them all:
1381            # if (n > 0) and ((start + n) != len( children )):
1382            #    self._children_replaced( object, name, event )
1383            #    return
1384
1385            # Only add/remove the changes if the node has already been
1386            # expanded:
1387            if expanded:
1388                # Remove all of the children that were deleted:
1389                for cnid in self._nodes_for(nid)[start:end]:
1390                    self._delete_node(cnid)
1391
1392                remaining = len(children) - len(event.removed)
1393                child_index = 0
1394                # Add all of the children that were added:
1395                for child in event.added:
1396                    child, child_node = self._node_for(child)
1397                    if child_node is not None:
1398                        insert_index = (
1399                            (start + child_index)
1400                            if (start <= remaining)
1401                            else None
1402                        )
1403                        self._insert_node(nid, insert_index, child_node, child)
1404                        child_index += 1
1405            else:
1406                dummy = getattr(nid, "_dummy", None)
1407                if dummy is None and len(children) > 0:
1408                    # if model now has children add dummy child
1409                    nid._dummy = QtGui.QTreeWidgetItem(nid)
1410                elif dummy is not None and len(children) == 0:
1411                    # if model no longer has children remove dummy child
1412                    nid.removeChild(dummy)
1413                    del nid._dummy
1414
1415            # Try to expand the node (if requested):
1416            if node.can_auto_open(object):
1417                nid.setExpanded(True)
1418
1419    def _label_updated(self, object, name, label):
1420        """  Handles the label of an object being changed.
1421        """
1422        # Prevent the itemChanged() signal from being emitted.
1423        blk = self._tree.blockSignals(True)
1424        try:
1425            # Have to use a list rather than a set because nids for PyQt
1426            # on Python 3 (QTreeWidgetItem instances) aren't hashable.
1427            # This means potentially quadratic behaviour, but the number of
1428            # nodes for a particular object shouldn't be high
1429            nids = []
1430            for name2, nid in self._map[id(object)]:
1431                if nid not in nids:
1432                    nids.append(nid)
1433                    node = self._get_node_data(nid)[1]
1434                    self._set_label(nid, 0)
1435                    self._update_icon(nid)
1436        finally:
1437            self._tree.blockSignals(blk)
1438
1439    def _column_labels_updated(self, object, name, new):
1440        """  Handles the column labels of an object being changed.
1441        """
1442        # Prevent the itemChanged() signal from being emitted.
1443        blk = self._tree.blockSignals(True)
1444        try:
1445            nids = []
1446            for name2, nid in self._map[id(object)]:
1447                if nid not in nids:
1448                    nids.append(nid)
1449                    node = self._get_node_data(nid)[1]
1450                    self._set_column_labels(nid, node, object)
1451        finally:
1452            self._tree.blockSignals(blk)
1453
1454    # -- UI preference save/restore interface ---------------------------------
1455
1456    def restore_prefs(self, prefs):
1457        """ Restores any saved user preference information associated with the
1458            editor.
1459        """
1460        if isinstance(self.control, QtGui.QSplitter):
1461            if isinstance(prefs, dict):
1462                structure = prefs.get("structure")
1463            else:
1464                structure = prefs
1465
1466            self.control.restoreState(structure)
1467        header = self._tree.header()
1468        self.control.setExpandsOnDoubleClick(self.factory.expands_on_dclick)
1469
1470        if header is not None and "column_state" in prefs:
1471            header.restoreState(prefs["column_state"])
1472
1473    def save_prefs(self):
1474        """ Returns any user preference information associated with the editor.
1475        """
1476        prefs = {}
1477        if isinstance(self.control, QtGui.QSplitter):
1478            prefs["structure"] = self.control.saveState().data()
1479        header = self._tree.header()
1480        if header is not None:
1481            prefs["column_state"] = header.saveState().data()
1482
1483        return prefs
1484
1485
1486# -- End UI preference save/restore interface -----------------------------
1487
1488
1489class _TreeWidget(QtGui.QTreeWidget):
1490    """ The _TreeWidget class is a specialised QTreeWidget that reimplements
1491        the drag'n'drop support so that it hooks into the provided Traits
1492        support.
1493    """
1494
1495    def __init__(self, editor, parent=None):
1496        """ Initialise the tree widget.
1497        """
1498        QtGui.QTreeWidget.__init__(self, parent)
1499
1500        self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1501        self.setDragEnabled(True)
1502        self.setAcceptDrops(True)
1503        self.setIconSize(QtCore.QSize(*editor.factory.icon_size))
1504
1505        # Set up headers if necessary.
1506        column_count = len(editor.factory.column_headers)
1507        if column_count > 0:
1508            self.setHeaderHidden(False)
1509            self.setColumnCount(column_count)
1510            self.setHeaderLabels(editor.factory.column_headers)
1511        else:
1512            self.setHeaderHidden(True)
1513
1514        self.setAlternatingRowColors(editor.factory.alternating_row_colors)
1515        padding = editor.factory.vertical_padding
1516        if padding > 0:
1517            self.setStyleSheet(
1518                """
1519            QTreeView::item {
1520                padding-top: %spx;
1521                padding-bottom: %spx;
1522            }
1523            """
1524                % (padding, padding)
1525            )
1526
1527        if editor.factory.selection_mode == "extended":
1528            self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
1529
1530        self.itemExpanded.connect(editor._on_item_expanded)
1531        self.itemCollapsed.connect(editor._on_item_collapsed)
1532        self.itemClicked.connect(editor._on_item_clicked)
1533        self.itemDoubleClicked.connect(editor._on_item_dclicked)
1534        self.itemActivated.connect(editor._on_item_activated)
1535        self.itemSelectionChanged.connect(editor._on_tree_sel_changed)
1536        self.customContextMenuRequested.connect(editor._on_context_menu)
1537        self.itemChanged.connect(editor._on_nid_changed)
1538
1539        self._editor = editor
1540        self._dragging = None
1541
1542    def resizeEvent(self, event):
1543        """ Overridden to emit sizeHintChanged() of items for word wrapping """
1544        if self._editor.factory.word_wrap:
1545            for i in range(self.topLevelItemCount()):
1546                mi = self.indexFromItem(self.topLevelItem(i))
1547                id = self.itemDelegate(mi)
1548                id.sizeHintChanged.emit(mi)
1549        super(self.__class__, self).resizeEvent(event)
1550
1551    def startDrag(self, actions):
1552        """ Reimplemented to start the drag of a tree widget item.
1553        """
1554        nid = self.currentItem()
1555        if nid is None:
1556            return
1557
1558        self._dragging = nid
1559
1560        # Calculate the hotspot so that the pixmap appears on top of the
1561        # original item.
1562        nid_rect = self.visualItemRect(nid)
1563        hspos = (
1564            self.viewport().mapFromGlobal(QtGui.QCursor.pos())
1565            - nid_rect.topLeft()
1566        )
1567
1568        _, node, object = self._editor._get_node_data(nid)
1569
1570        # Convert the item being dragged to MIME data.
1571        drag_object = node.get_drag_object(object)
1572        md = PyMimeData.coerce(drag_object)
1573
1574        # Render the item being dragged as a pixmap.
1575        rect = nid_rect.intersected(self.viewport().rect())
1576        pm = QtGui.QPixmap(rect.size())
1577        pm.fill(self.palette().base().color())
1578        painter = QtGui.QPainter(pm)
1579
1580        option = self.viewOptions()
1581        option.state |= QtGui.QStyle.State_Selected
1582        option.rect = QtCore.QRect(
1583            nid_rect.topLeft() - rect.topLeft(), nid_rect.size()
1584        )
1585        self.itemDelegate().paint(painter, option, self.indexFromItem(nid))
1586
1587        painter.end()
1588
1589        # Start the drag.
1590        drag = QtGui.QDrag(self)
1591        drag.setMimeData(md)
1592        drag.setPixmap(pm)
1593        drag.setHotSpot(hspos)
1594        drag.exec_(actions)
1595
1596    def dragEnterEvent(self, e):
1597        """ Reimplemented to see if the current drag can be handled by the
1598            tree.
1599        """
1600        # Assume the drag is invalid.
1601        e.ignore()
1602
1603        # Check if we have a python object instance, we might be interested
1604        data = PyMimeData.coerce(e.mimeData()).instance()
1605        if data is None:
1606            return
1607
1608        # We might be able to handle it (but it depends on what the final
1609        # target is).
1610        e.acceptProposedAction()
1611
1612    def dragMoveEvent(self, e):
1613        """ Reimplemented to see if the current drag can be handled by the
1614            particular tree widget item underneath the cursor.
1615        """
1616        # Assume the drag is invalid.
1617        e.ignore()
1618
1619        action, to_node, to_object, to_index, data = self._get_action(e)
1620
1621        if action is not None:
1622            e.acceptProposedAction()
1623
1624    def dropEvent(self, e):
1625        """ Reimplemented to update the model and tree.
1626        """
1627        # Assume the drop is invalid.
1628        e.ignore()
1629
1630        editor = self._editor
1631
1632        dragging = self._dragging
1633        self._dragging = None
1634
1635        action, to_node, to_object, to_index, data = self._get_action(e)
1636
1637        if action == "append":
1638            if dragging is not None:
1639                data = self._editor._drop_object(
1640                    to_node, to_object, data, False
1641                )
1642                if data is not None:
1643                    try:
1644                        editor._begin_undo()
1645                        editor._undoable_delete(*editor._node_index(dragging))
1646                        editor._undoable_append(
1647                            to_node, to_object, data, False
1648                        )
1649                    finally:
1650                        editor._end_undo()
1651            else:
1652                data = editor._drop_object(to_node, to_object, data, True)
1653                if data is not None:
1654                    editor._undoable_append(to_node, to_object, data, False)
1655        elif action == "insert":
1656            if dragging is not None:
1657                data = editor._drop_object(to_node, to_object, data, False)
1658                if data is not None:
1659                    from_node, from_object, from_index = editor._node_index(
1660                        dragging
1661                    )
1662                    if (to_object is from_object) and (to_index > from_index):
1663                        to_index -= 1
1664                    try:
1665                        editor._begin_undo()
1666                        editor._undoable_delete(
1667                            from_node, from_object, from_index
1668                        )
1669                        editor._undoable_insert(
1670                            to_node, to_object, to_index, data, False
1671                        )
1672                    finally:
1673                        editor._end_undo()
1674            else:
1675                data = self._editor._drop_object(
1676                    to_node, to_object, data, True
1677                )
1678                if data is not None:
1679                    editor._undoable_insert(
1680                        to_node, to_object, to_index, data, False
1681                    )
1682        else:
1683            return
1684
1685        e.acceptProposedAction()
1686
1687    def _get_action(self, event):
1688        """ Work out what action on what object to perform for a drop event
1689        """
1690        # default values to return
1691        action = None
1692        to_node = None
1693        to_object = None
1694        to_index = None
1695        data = None
1696
1697        editor = self._editor
1698
1699        # Get the tree widget item under the cursor.
1700        nid = self.itemAt(event.pos())
1701        if nid is None:
1702            if editor.factory.hide_root:
1703                nid = self.invisibleRootItem()
1704            else:
1705                return (action, to_node, to_object, to_index, data)
1706
1707        # Check that the target is not the source of a child of the source.
1708        if self._dragging is not None:
1709            pnid = nid
1710            while pnid is not None:
1711                if pnid is self._dragging:
1712                    return (action, to_node, to_object, to_index, data)
1713
1714                pnid = pnid.parent()
1715
1716        data = PyMimeData.coerce(event.mimeData()).instance()
1717        _, node, object = editor._get_node_data(nid)
1718
1719        if event.proposedAction() == QtCore.Qt.MoveAction and editor._is_droppable(
1720            node, object, data, False
1721        ):
1722            # append to node being dropped on
1723            action = "append"
1724            to_node = node
1725            to_object = object
1726            to_index = None
1727        else:
1728            # get parent of node being dropped on
1729            to_node, to_object, to_index = editor._node_index(nid)
1730            if to_node is None:
1731                # no parent, can't do anything
1732                action = None
1733            elif editor._is_droppable(to_node, to_object, data, True):
1734                # insert into the parent of the node being dropped on
1735                action = "insert"
1736            elif editor._is_droppable(to_node, to_object, data, False):
1737                # append to the parent of the node being dropped on
1738                action = "append"
1739            else:
1740                # parent can't be modified, can't do anything
1741                action = None
1742
1743        return (action, to_node, to_object, to_index, data)
1744
1745
1746class TreeItemDelegate(QtGui.QStyledItemDelegate):
1747    """ A delegate class to draw wrapped text labels """
1748
1749    # FIXME: sizeHint() should return the size required by the label,
1750    # which is dependent on the width available, which is different for
1751    # each item due to the nested tree structure. However the option.rect
1752    # argument available to the sizeHint() is invalid (width=-1) so as a
1753    # hack sizeHintChanged is emitted in paint() and the size of drawn
1754    # text is returned, as paint() gets a valid option.rect argument.
1755
1756    # TODO: add ability to override editor in item delegate
1757
1758    def sizeHint(self, option, index):
1759        """ returns area taken by the text. """
1760        column = index.column()
1761        item = self.editor._tree.itemFromIndex(index)
1762        expanded, node, instance = self.editor._get_node_data(item)
1763        column = index.column()
1764
1765        renderer = node.get_renderer(object, column=column)
1766        if renderer is None:
1767            return super(TreeItemDelegate, self).sizeHint(option, index)
1768
1769        size_context = (option, index)
1770        size = renderer.size(self.editor, node, column, instance, size_context)
1771        if size is None:
1772            return QtCore.QSize(1, 21)
1773        else:
1774            return QtCore.QSize(*size)
1775
1776    def updateEditorGeometry(self, editor, option, index):
1777        """ Update the editor's geometry.
1778        """
1779        editor.setGeometry(option.rect)
1780
1781    def paint(self, painter, option, index):
1782        """ Render the contents of the item. """
1783        item = self.editor._tree.itemFromIndex(index)
1784        expanded, node, instance = self.editor._get_node_data(item)
1785        column = index.column()
1786
1787        renderer = node.get_renderer(object, column=column)
1788        if renderer is None and self.editor.factory.word_wrap:
1789            renderer = DEFAULT_WRAP_RENDERER
1790        if renderer is None:
1791            super(TreeItemDelegate, self).paint(painter, option, index)
1792        else:
1793            if not renderer.handles_all:
1794                # renderers background and selection highlights
1795                # will also render icon and text if flags are set
1796                super(TreeItemDelegate, self).paint(painter, option, index)
1797            paint_context = (painter, option, index)
1798            size = renderer.paint(
1799                self.editor, node, column, instance, paint_context
1800            )
1801            if size is not None:
1802                do_later(self.sizeHintChanged.emit, index)
1803