1# ------------------------------------------------------------------------------
2#
3#  Copyright (c) 2005, Enthought, Inc.
4#  All rights reserved.
5#
6#  This software is provided without warranty under the terms of the BSD
7#  license included in LICENSE.txt and may be redistributed only
8#  under the conditions described in the aforementioned license.  The license
9#  is also available online at http://www.enthought.com/licenses/BSD.txt
10#
11#  Thanks for using Enthought open source!
12#
13#  Author: David C. Morrill
14#  Date:   12/03/2004
15#
16# ------------------------------------------------------------------------------
17
18""" Defines the tree editor for the wxPython user interface toolkit.
19"""
20
21
22import os
23import wx
24import copy
25
26
27try:
28    from pyface.wx.drag_and_drop import PythonDropSource, PythonDropTarget
29except:
30    PythonDropSource = PythonDropTarget = None
31
32from pyface.ui.wx.image_list import ImageList
33from traits.api import HasStrictTraits, Any, Str, Event, TraitError
34from traitsui.api import View, TreeNode, ObjectTreeNode, MultiTreeNode
35
36# FIXME: ToolkitEditorFactory is a proxy class defined here just for backward
37# compatibility. The class has been moved to the
38# traitsui.editors.tree_editor file.
39from traitsui.editors.tree_editor import (
40    CopyAction,
41    CutAction,
42    DeleteAction,
43    NewAction,
44    PasteAction,
45    RenameAction,
46    ToolkitEditorFactory,
47)
48from traitsui.undo import ListUndoItem
49from traitsui.tree_node import ITreeNodeAdapterBridge
50from traitsui.menu import Menu, Action, Separator
51
52from pyface.api import ImageResource
53from pyface.ui_traits import convert_image
54from pyface.dock.api import (
55    DockWindow,
56    DockSizer,
57    DockSection,
58    DockRegion,
59    DockControl,
60)
61
62from .constants import OKColor
63from .editor import Editor
64from .helper import TraitsUIPanel, TraitsUIScrolledPanel
65
66# -------------------------------------------------------------------------
67#  Global data:
68# -------------------------------------------------------------------------
69
70# Paste buffer for copy/cut/paste operations
71paste_buffer = None
72
73
74# -------------------------------------------------------------------------
75#  'SimpleEditor' class:
76# -------------------------------------------------------------------------
77
78
79class SimpleEditor(Editor):
80    """ Simple style of tree editor.
81    """
82
83    # -------------------------------------------------------------------------
84    #  Trait definitions:
85    # -------------------------------------------------------------------------
86
87    #: Is the tree editor is scrollable? This value overrides the default.
88    scrollable = True
89
90    #: Allows an external agent to set the tree selection
91    selection = Event()
92
93    #: The currently selected object
94    selected = Any()
95
96    #: The event fired when a tree node is activated by double clicking or
97    #: pressing the enter key on a node.
98    activated = Event()
99
100    #: The event fired when a tree node is clicked on:
101    click = Event()
102
103    #: The event fired when a tree node is double-clicked on:
104    dclick = Event()
105
106    #: The event fired when the application wants to veto an operation:
107    veto = Event()
108
109    def init(self, parent):
110        """ Finishes initializing the editor by creating the underlying toolkit
111            widget.
112        """
113        factory = self.factory
114        style = self._get_style()
115
116        if factory.editable:
117
118            # Check to see if the tree view is based on a shared trait editor:
119            if factory.shared_editor:
120                factory_editor = factory.editor
121
122                # If this is the editor that defines the trait editor panel:
123                if factory_editor is None:
124
125                    # Remember which editor has the trait editor in the
126                    # factory:
127                    factory._editor = self
128
129                    # Create the trait editor panel:
130                    self.control = TraitsUIPanel(parent, -1)
131                    self.control._node_ui = self.control._editor_nid = None
132
133                    # Check to see if there are any existing editors that are
134                    # waiting to be bound to the trait editor panel:
135                    editors = factory._shared_editors
136                    if editors is not None:
137                        for editor in factory._shared_editors:
138
139                            # If the editor is part of this UI:
140                            if editor.ui is self.ui:
141
142                                # Then bind it to the trait editor panel:
143                                editor._editor = self.control
144
145                        # Indicate all pending editors have been processed:
146                        factory._shared_editors = None
147
148                    # We only needed to build the trait editor panel, so exit:
149                    return
150
151                # Check to see if the matching trait editor panel has been
152                # created yet:
153                editor = factory_editor._editor
154                if (editor is None) or (editor.ui is not self.ui):
155                    # If not, add ourselves to the list of pending editors:
156                    shared_editors = factory_editor._shared_editors
157                    if shared_editors is None:
158                        factory_editor._shared_editors = shared_editors = []
159                    shared_editors.append(self)
160                else:
161                    # Otherwise, bind our trait editor panel to the shared one:
162                    self._editor = editor.control
163
164                # Finally, create only the tree control:
165                self.control = self._tree = tree = wx.TreeCtrl(
166                    parent, -1, style=style
167                )
168            else:
169                # If editable, create a tree control and an editor panel:
170                self._is_dock_window = True
171                theme = factory.dock_theme or self.item.container.dock_theme
172                self.control = splitter = DockWindow(
173                    parent, theme=theme
174                ).control
175                self._tree = tree = wx.TreeCtrl(splitter, -1, style=style)
176                self._editor = editor = TraitsUIScrolledPanel(splitter)
177                editor.SetSizer(wx.BoxSizer(wx.VERTICAL))
178                editor.SetScrollRate(16, 16)
179                editor.SetMinSize(wx.Size(100, 100))
180
181                self._editor._node_ui = self._editor._editor_nid = None
182                item = self.item
183                hierarchy_name = editor_name = ""
184                style = "fixed"
185                name = item.label
186                if name != "":
187                    hierarchy_name = name + " Hierarchy"
188                    editor_name = name + " Editor"
189                    style = item.dock
190
191                splitter.SetSizer(
192                    DockSizer(
193                        contents=DockSection(
194                            contents=[
195                                DockRegion(
196                                    contents=[
197                                        DockControl(
198                                            name=hierarchy_name,
199                                            id="tree",
200                                            control=tree,
201                                            style=style,
202                                        )
203                                    ]
204                                ),
205                                DockRegion(
206                                    contents=[
207                                        DockControl(
208                                            name=editor_name,
209                                            id="editor",
210                                            control=self._editor,
211                                            style=style,
212                                        )
213                                    ]
214                                ),
215                            ],
216                            is_row=(factory.orientation == "horizontal"),
217                        )
218                    )
219                )
220        else:
221            # Otherwise, just create the tree control:
222            self.control = self._tree = tree = wx.TreeCtrl(
223                parent, -1, style=style
224            )
225
226        # Set up to show tree node icon (if requested):
227        if factory.show_icons:
228            self._image_list = ImageList(*factory.icon_size)
229            tree.AssignImageList(self._image_list)
230
231        # Set up the mapping between objects and tree id's:
232        self._map = {}
233
234        # Initialize the 'undo state' stack:
235        self._undoable = []
236
237        # Set up the mouse event handlers:
238        tree.Bind(wx.EVT_LEFT_DOWN, self._on_left_down)
239        tree.Bind(wx.EVT_RIGHT_DOWN, self._on_right_down)
240        tree.Bind(wx.EVT_LEFT_DCLICK, self._on_left_dclick)
241
242        # Set up the tree event handlers:
243        tree.Bind(wx.EVT_TREE_ITEM_EXPANDING, self._on_tree_item_expanding)
244        tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self._on_tree_item_expanded)
245        tree.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self._on_tree_item_collapsing)
246        tree.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self._on_tree_item_collapse)
247        tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self._on_tree_item_activated)
248        tree.Bind(wx.EVT_TREE_SEL_CHANGED, self._on_tree_sel_changed)
249        tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self._on_tree_begin_drag)
250        tree.Bind(wx.EVT_TREE_BEGIN_LABEL_EDIT, self._on_tree_begin_label_edit)
251        tree.Bind(wx.EVT_TREE_END_LABEL_EDIT, self._on_tree_end_label_edit)
252        tree.Bind(wx.EVT_TREE_ITEM_GETTOOLTIP, self._on_tree_item_gettooltip)
253
254        # Set up general mouse events
255        tree.Bind(wx.EVT_MOTION, self._on_hover)
256
257        # Synchronize external object traits with the editor:
258        self.sync_value(factory.selected, "selected")
259        self.sync_value(factory.activated, "activated", "to")
260        self.sync_value(factory.click, "click", "to")
261        self.sync_value(factory.dclick, "dclick", "to")
262        self.sync_value(factory.veto, "veto", "from")
263
264        # Set up the drag and drop target:
265        if PythonDropTarget is not None:
266            tree.SetDropTarget(PythonDropTarget(self))
267
268    def dispose(self):
269        """ Disposes of the contents of an editor.
270        """
271        tree = self._tree
272        if tree is not None:
273            tree.Unbind(wx.EVT_LEFT_DOWN)
274            tree.Unbind(wx.EVT_RIGHT_DOWN)
275            tree.Unbind(wx.EVT_LEFT_DCLICK)
276
277            tree.Unbind(wx.EVT_TREE_ITEM_EXPANDING)
278            tree.Unbind(wx.EVT_TREE_ITEM_EXPANDED)
279            tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSING)
280            tree.Unbind(wx.EVT_TREE_ITEM_COLLAPSED)
281            tree.Unbind(wx.EVT_TREE_ITEM_ACTIVATED)
282            tree.Unbind(wx.EVT_TREE_SEL_CHANGED)
283            tree.Unbind(wx.EVT_TREE_BEGIN_DRAG)
284            tree.Unbind(wx.EVT_TREE_BEGIN_LABEL_EDIT)
285            tree.Unbind(wx.EVT_TREE_END_LABEL_EDIT)
286            tree.Unbind(wx.EVT_TREE_ITEM_GETTOOLTIP)
287
288            tree.Unbind(wx.EVT_MOTION)
289
290            nid = self._tree.GetRootItem()
291            if nid.IsOk():
292                self._delete_node(nid)
293
294        self._tree = None
295        super().dispose()
296
297    def _selection_changed(self, selection):
298        """ Handles the **selection** event.
299        """
300        try:
301            self._tree.SelectItem(self._object_info(selection)[2])
302        except Exception:
303            pass
304
305    def _selected_changed(self, selected):
306        """ Handles the **selected** trait being changed.
307        """
308        if not self._no_update_selected:
309            self._selection_changed(selected)
310
311    def _veto_changed(self):
312        """ Handles the 'veto' event being fired.
313        """
314        self._veto = True
315
316    def _get_style(self):
317        """ Returns the style settings used for displaying the wx tree.
318        """
319        factory = self.factory
320        style = wx.TR_EDIT_LABELS | wx.TR_HAS_BUTTONS | wx.CLIP_CHILDREN
321
322        # Turn lines off if explicit or for appearance on *nix:
323        if (factory.lines_mode == "off") or (
324            (factory.lines_mode == "appearance") and (os.name == "posix")
325        ):
326            style |= wx.TR_NO_LINES
327
328        if factory.hide_root:
329            style |= wx.TR_HIDE_ROOT | wx.TR_LINES_AT_ROOT
330
331        if factory.selection_mode != "single":
332            style |= wx.TR_MULTIPLE | wx.TR_EXTENDED
333
334        return style
335
336    def update_object(self, event):
337        """ Handles the user entering input data in the edit control.
338        """
339        try:
340            self.value = self._get_value()
341            self.control.SetBackgroundColour(OKColor)
342            self.control.Refresh()
343        except TraitError:
344            pass
345
346    def _save_state(self):
347        tree = self._tree
348        nid = tree.GetRootItem()
349        state = {}
350        if nid.IsOk():
351            nodes_to_do = [nid]
352            while nodes_to_do:
353                node = nodes_to_do.pop()
354                data = self._get_node_data(node)
355                try:
356                    is_expanded = tree.IsExpanded(node)
357                except:
358                    is_expanded = True
359                state[hash(data[-1])] = (data[0], is_expanded)
360                for cnid in self._nodes(node):
361                    nodes_to_do.append(cnid)
362        return state
363
364    def _restore_state(self, state):
365        if not state:
366            return
367        tree = self._tree
368        nid = tree.GetRootItem()
369        if nid.IsOk():
370            nodes_to_do = [nid]
371            while nodes_to_do:
372                node = nodes_to_do.pop()
373                for cnid in self._nodes(node):
374                    data = self._get_node_data(cnid)
375                    key = hash(data[-1])
376                    if key in state:
377                        was_expanded, current_state = state[key]
378                        if was_expanded:
379                            self._expand_node(cnid)
380                            if current_state:
381                                tree.Expand(cnid)
382                            nodes_to_do.append(cnid)
383
384    def expand_all(self):
385        """ Expands all nodes, starting from the selected node.
386        """
387        tree = self._tree
388
389        def _do_expand(nid):
390            expanded, node, object = self._get_node_data(nid)
391            if self._has_children(node, object):
392                tree.SetItemHasChildren(nid, True)
393                self._expand_node(nid)
394                tree.Expand(nid)
395
396        nid = tree.GetSelection()
397        if nid.IsOk():
398            nodes_to_do = [nid]
399            while nodes_to_do:
400                node = nodes_to_do.pop()
401                _do_expand(node)
402                for n in self._nodes(node):
403                    _do_expand(n)
404                    nodes_to_do.append(n)
405
406    def expand_levels(self, nid, levels, expand=True):
407        """ Expands from the specified node the specified number of sub-levels.
408        """
409        if levels > 0:
410            expanded, node, object = self._get_node_data(nid)
411            if self._has_children(node, object):
412                self._tree.SetItemHasChildren(nid, True)
413                self._expand_node(nid)
414                if expand:
415                    self._tree.Expand(nid)
416                for cnid in self._nodes(nid):
417                    self.expand_levels(cnid, levels - 1)
418
419    def update_editor(self):
420        """ Updates the editor when the object trait changes externally to the
421            editor.
422        """
423        tree = self._tree
424        saved_state = {}
425        if tree is not None:
426            nid = tree.GetRootItem()
427            if nid.IsOk():
428                self._delete_node(nid)
429            object, node = self._node_for(self.value)
430            if node is not None:
431                icon = self._get_icon(node, object)
432                self._root_nid = nid = tree.AddRoot(
433                    node.get_label(object), icon, icon
434                )
435                self._map[id(object)] = [(node.get_children_id(object), nid)]
436                self._add_listeners(node, object)
437                self._set_node_data(nid, (False, node, object))
438                if self.factory.hide_root or self._has_children(node, object):
439                    tree.SetItemHasChildren(nid, True)
440                    self._expand_node(nid)
441                    if not self.factory.hide_root:
442                        tree.Expand(nid)
443                        tree.SelectItem(nid)
444                        self._on_tree_sel_changed()
445
446                self.expand_levels(nid, self.factory.auto_open, False)
447
448                # It seems like in some cases, an explicit Refresh is needed to
449                # trigger a screen update:
450                tree.Refresh()
451
452            # fixme: Clear the current editor (if any)...
453
454    def get_error_control(self):
455        """ Returns the editor's control for indicating error status.
456        """
457        return self._tree
458
459    def _append_node(self, nid, node, object):
460        """ Appends a new node to the specified node.
461        """
462        return self._insert_node(nid, None, node, object)
463
464    def _insert_node(self, nid, index, node, object):
465        """ Inserts a new node before a specified index into the children of the
466            specified node.
467        """
468        tree = self._tree
469        icon = self._get_icon(node, object)
470        label = node.get_label(object)
471        if index is None:
472            cnid = tree.AppendItem(nid, label, icon, icon)
473        else:
474            cnid = tree.InsertItem(nid, index, label, icon, icon)
475        has_children = self._has_children(node, object)
476        tree.SetItemHasChildren(cnid, has_children)
477        self._set_node_data(cnid, (False, node, object))
478        self._map.setdefault(id(object), []).append(
479            (node.get_children_id(object), cnid)
480        )
481        self._add_listeners(node, object)
482
483        # Automatically expand the new node (if requested):
484        if has_children and node.can_auto_open(object):
485            tree.Expand(cnid)
486
487        # Return the newly created node:
488        return cnid
489
490    def _delete_node(self, nid):
491        """ Deletes a specified tree node and all of its children.
492        """
493        for cnid in self._nodes_for(nid):
494            self._delete_node(cnid)
495
496        expanded, node, object = self._get_node_data(nid)
497        id_object = id(object)
498        object_info = self._map[id_object]
499        for i, info in enumerate(object_info):
500            if nid == info[1]:
501                del object_info[i]
502                break
503
504        if len(object_info) == 0:
505            self._remove_listeners(node, object)
506            del self._map[id_object]
507
508        # We set the '_locked' flag here because wx seems to generate a
509        # 'node selected' event when the node is deleted. This can lead to
510        # some bad side effects. So the 'node selected' event handler exits
511        # immediately if the '_locked' flag is set:
512        self._locked = True
513        self._tree.Delete(nid)
514        self._locked = False
515
516        # If the deleted node had an active editor panel showing, remove it:
517        if (self._editor is not None) and (nid == self._editor._editor_nid):
518            self._clear_editor()
519
520    def _expand_node(self, nid):
521        """ Expands the contents of a specified node (if required).
522        """
523        expanded, node, object = self._get_node_data(nid)
524
525        # Lazily populate the item's children:
526        if not expanded:
527            for child in node.get_children(object):
528                child, child_node = self._node_for(child)
529                if child_node is not None:
530                    self._append_node(nid, child_node, child)
531
532            # Indicate the item is now populated:
533            self._set_node_data(nid, (True, node, object))
534
535    def _nodes(self, nid):
536        """ Returns each of the child nodes of a specified node.
537        """
538        tree = self._tree
539        cnid, cookie = tree.GetFirstChild(nid)
540        while cnid.IsOk():
541            yield cnid
542            cnid, cookie = tree.GetNextChild(nid, cookie)
543
544    def _nodes_for(self, nid):
545        """ Returns all child node ids of a specified node id.
546        """
547        return [cnid for cnid in self._nodes(nid)]
548
549    def _node_index(self, nid):
550        pnid = self._tree.GetItemParent(nid)
551        if not pnid.IsOk():
552            return (None, None, None)
553
554        for i, cnid in enumerate(self._nodes(pnid)):
555            if cnid == nid:
556                ignore, pnode, pobject = self._get_node_data(pnid)
557
558                return (pnode, pobject, i)
559
560    def _has_children(self, node, object):
561        """ Returns whether a specified object has any children.
562        """
563        return node.allows_children(object) and node.has_children(object)
564
565    def _is_droppable(self, node, object, add_object, for_insert):
566        """ Returns whether a given object is droppable on the node.
567        """
568        if for_insert and (not node.can_insert(object)):
569            return False
570
571        return node.can_add(object, add_object)
572
573    def _drop_object(self, node, object, dropped_object, make_copy=True):
574        """ Returns a droppable version of a specified object.
575        """
576        new_object = node.drop_object(object, dropped_object)
577        if (new_object is not dropped_object) or (not make_copy):
578            return new_object
579
580        return copy.deepcopy(new_object)
581
582    def _get_icon(self, node, object, is_expanded=False):
583        """ Returns the index of the specified object icon.
584        """
585        if self._image_list is None:
586            return -1
587
588        icon_name = node.get_icon(object, is_expanded)
589        if isinstance(icon_name, str):
590            if icon_name.startswith("@"):
591                image = convert_image(icon_name, 3)
592                if image is None:
593                    return -1
594            else:
595                if icon_name[:1] == "<":
596                    icon_name = icon_name[1:-1]
597                    path = self
598                else:
599                    path = node.get_icon_path(object)
600                    if isinstance(path, str):
601                        path = [path, node]
602                    else:
603                        path.append(node)
604                image = ImageResource(icon_name, path).absolute_path
605        elif isinstance(icon_name, ImageResource):
606            image = icon_name.absolute_path
607        else:
608            raise ValueError(
609                "Icon value must be a string or IImageResource instance: "
610                + "given {!r}".format(icon_name)
611            )
612
613        return self._image_list.GetIndex(image)
614
615    def _add_listeners(self, node, object):
616        """ Adds the event listeners for a specified object.
617        """
618        if node.allows_children(object):
619            node.when_children_replaced(object, self._children_replaced, False)
620            node.when_children_changed(object, self._children_updated, False)
621
622        node.when_label_changed(object, self._label_updated, False)
623
624    def _remove_listeners(self, node, object):
625        """ Removes any event listeners from a specified object.
626        """
627        if node.allows_children(object):
628            node.when_children_replaced(object, self._children_replaced, True)
629            node.when_children_changed(object, self._children_updated, True)
630
631        node.when_label_changed(object, self._label_updated, True)
632
633    def _object_info(self, object, name=""):
634        """ Returns the tree node data for a specified object in the form
635            ( expanded, node, nid ).
636        """
637        info = self._map[id(object)]
638        for name2, nid in info:
639            if name == name2:
640                break
641        else:
642            nid = info[0][1]
643
644        expanded, node, ignore = self._get_node_data(nid)
645
646        return (expanded, node, nid)
647
648    def _object_info_for(self, object, name=""):
649        """ Returns the tree node data for a specified object as a list of the
650            form: [ ( expanded, node, nid ), ... ].
651        """
652        result = []
653        for name2, nid in self._map[id(object)]:
654            if name == name2:
655                expanded, node, ignore = self._get_node_data(nid)
656                result.append((expanded, node, nid))
657
658        return result
659
660    def _node_for(self, object):
661        """ Returns the TreeNode associated with a specified object.
662        """
663        if (
664            (isinstance(object, tuple))
665            and (len(object) == 2)
666            and isinstance(object[1], TreeNode)
667        ):
668            return object
669
670        # Select all nodes which understand this object:
671        factory = self.factory
672        nodes = [
673            node
674            for node in factory.nodes
675            if object is not None and node.is_node_for(object)
676        ]
677
678        # If only one found, we're done, return it:
679        if len(nodes) == 1:
680            return (object, nodes[0])
681
682        # If none found, try to create an adapted node for the object:
683        if len(nodes) == 0:
684            return (object, ITreeNodeAdapterBridge(adapter=object))
685
686        # Use all selected nodes that have the same 'node_for' list as the
687        # first selected node:
688        base = nodes[0].node_for
689        nodes = [node for node in nodes if base == node.node_for]
690
691        # If only one left, then return that node:
692        if len(nodes) == 1:
693            return (object, nodes[0])
694
695        # Otherwise, return a MultiTreeNode based on all selected nodes...
696
697        # Use the node with no specified children as the root node. If not
698        # found, just use the first selected node as the 'root node':
699        root_node = None
700        for i, node in enumerate(nodes):
701            if node.get_children_id(object) == "":
702                root_node = node
703                del nodes[i]
704                break
705        else:
706            root_node = nodes[0]
707
708        # If we have a matching MultiTreeNode already cached, return it:
709        key = (root_node,) + tuple(nodes)
710        if key in factory.multi_nodes:
711            return (object, factory.multi_nodes[key])
712
713        # Otherwise create one, cache it, and return it:
714        factory.multi_nodes[key] = multi_node = MultiTreeNode(
715            root_node=root_node, nodes=nodes
716        )
717
718        return (object, multi_node)
719
720    def _node_for_class(self, klass):
721        """ Returns the TreeNode associated with a specified class.
722        """
723        for node in self.factory.nodes:
724            if issubclass(klass, tuple(node.node_for)):
725                return node
726        return None
727
728    def _node_for_class_name(self, class_name):
729        """ Returns the node and class associated with a specified class name.
730        """
731        for node in self.factory.nodes:
732            for klass in node.node_for:
733                if class_name == klass.__name__:
734                    return (node, klass)
735        return (None, None)
736
737    def _update_icon(self, event, is_expanded):
738        """ Updates the icon for a specified node.
739        """
740        self._update_icon_for_nid(event.GetItem())
741
742    def _update_icon_for_nid(self, nid):
743        """ Updates the icon for a specified node ID.
744        """
745        if self._image_list is not None:
746            expanded, node, object = self._get_node_data(nid)
747            icon = self._get_icon(node, object, expanded)
748            self._tree.SetItemImage(nid, icon, wx.TreeItemIcon_Normal)
749            self._tree.SetItemImage(nid, icon, wx.TreeItemIcon_Selected)
750
751    def _unpack_event(self, event):
752        """ Unpacks an event to see whether a tree item was involved.
753        """
754        try:
755            point = event.GetPosition()
756        except:
757            point = event.GetPoint()
758
759        nid = None
760        if hasattr(event, "GetItem"):
761            nid = event.GetItem()
762
763        if (nid is None) or (not nid.IsOk()):
764            nid, flags = self._tree.HitTest(point)
765
766        if nid.IsOk():
767            return self._get_node_data(nid) + (nid, point)
768
769        return (None, None, None, nid, point)
770
771    def _hit_test(self, point):
772        """ Returns information about the node at a specified point.
773        """
774        nid, flags = self._tree.HitTest(point)
775        if nid.IsOk():
776            return self._get_node_data(nid) + (nid, point)
777        return (None, None, None, nid, point)
778
779    def _begin_undo(self):
780        """ Begins an "undoable" transaction.
781        """
782        ui = self.ui
783        self._undoable.append(ui._undoable)
784        if (ui._undoable == -1) and (ui.history is not None):
785            ui._undoable = ui.history.now
786
787    def _end_undo(self):
788        if self._undoable.pop() == -1:
789            self.ui._undoable = -1
790
791    def _get_undo_item(self, object, name, event):
792        return ListUndoItem(
793            object=object,
794            name=name,
795            index=event.index,
796            added=event.added,
797            removed=event.removed,
798        )
799
800    def _undoable_append(self, node, object, data, make_copy=True):
801        """ Performs an undoable append operation.
802        """
803        try:
804            self._begin_undo()
805            if make_copy:
806                data = copy.deepcopy(data)
807            node.append_child(object, data)
808        finally:
809            self._end_undo()
810
811    def _undoable_insert(self, node, object, index, data, make_copy=True):
812        """ Performs an undoable insert operation.
813        """
814        try:
815            self._begin_undo()
816            if make_copy:
817                data = copy.deepcopy(data)
818            node.insert_child(object, index, data)
819        finally:
820            self._end_undo()
821
822    def _undoable_delete(self, node, object, index):
823        """ Performs an undoable delete operation.
824        """
825        try:
826            self._begin_undo()
827            node.delete_child(object, index)
828        finally:
829            self._end_undo()
830
831    def _get_object_nid(self, object, name=""):
832        """ Gets the ID associated with a specified object (if any).
833        """
834        info = self._map.get(id(object))
835        if info is None:
836            return None
837
838        for name2, nid in info:
839            if name == name2:
840                return nid
841        else:
842            return info[0][1]
843
844    def _clear_editor(self):
845        """ Clears the current editor pane (if any).
846        """
847        editor = self._editor
848        if editor._node_ui is not None:
849            editor.SetSizer(None)
850            editor._node_ui.dispose()
851            editor._node_ui = editor._editor_nid = None
852
853    def _get_node_data(self, nid):
854        """ Gets the node specific data.
855        """
856        if nid == self._root_nid:
857            return self._root_nid_data
858
859        return self._tree.GetItemData(nid)
860
861    def _set_node_data(self, nid, data):
862        """ Sets the node specific data.
863        """
864        if nid == self._root_nid:
865            self._root_nid_data = data
866        else:
867            self._tree.SetItemData(nid, data)
868
869    # ----- User callable methods: --------------------------------------------
870
871    def get_object(self, nid):
872        """ Gets the object associated with a specified node.
873        """
874        return self._get_node_data(nid)[2]
875
876    def get_parent(self, object, name=""):
877        """ Returns the object that is the immmediate parent of a specified
878            object in the tree.
879        """
880        nid = self._get_object_nid(object, name)
881        if nid is not None:
882            pnid = self._tree.GetItemParent(nid)
883            if pnid.IsOk():
884                return self.get_object(pnid)
885
886        return None
887
888    def get_node(self, object, name=""):
889        """ Returns the node associated with a specified object.
890        """
891        nid = self._get_object_nid(object, name)
892        if nid is not None:
893            return self._get_node_data(nid)[1]
894
895        return None
896
897    # -- Tree Event Handlers: -------------------------------------------------
898
899    def _on_tree_item_expanding(self, event):
900        """ Handles a tree node expanding.
901        """
902        if self._veto:
903            self._veto = False
904            event.Veto()
905            return
906
907        nid = event.GetItem()
908        tree = self._tree
909        expanded, node, object = self._get_node_data(nid)
910
911        # If 'auto_close' requested for this node type, close all of the node's
912        # siblings:
913        if node.can_auto_close(object):
914            snid = nid
915            while True:
916                snid = tree.GetPrevSibling(snid)
917                if not snid.IsOk():
918                    break
919                tree.Collapse(snid)
920            snid = nid
921            while True:
922                snid = tree.GetNextSibling(snid)
923                if not snid.IsOk():
924                    break
925                tree.Collapse(snid)
926
927        # Expand the node (i.e. populate its children if they are not there
928        # yet):
929        self._expand_node(nid)
930
931    def _on_tree_item_expanded(self, event):
932        """ Handles a tree node being expanded.
933        """
934        self._update_icon(event, True)
935
936    def _on_tree_item_collapsing(self, event):
937        """ Handles a tree node collapsing.
938        """
939        if self._veto:
940            self._veto = False
941            event.Veto()
942
943    def _on_tree_item_collapsed(self, event):
944        """ Handles a tree node being collapsed.
945        """
946        self._update_icon(event, False)
947
948    def _on_tree_sel_changed(self, event=None):
949        """ Handles a tree node being selected.
950        """
951        if self._locked:
952            return
953
954        # Get the new selection:
955        object = None
956        not_handled = True
957        nids = self._tree.GetSelections()
958
959        selected = []
960        for nid in nids:
961            if not nid.IsOk():
962                continue
963
964            # If there is a real selection, get the associated object:
965            expanded, node, sel_object = self._get_node_data(nid)
966            selected.append(sel_object)
967
968            # Try to inform the node specific handler of the selection,
969            # if there are multiple selections, we only care about the
970            # first (or maybe the last makes more sense?)
971            if nid == nids[0]:
972                object = sel_object
973                not_handled = node.select(object)
974
975        # Set the value of the new selection:
976        if self.factory.selection_mode == "single":
977            self._no_update_selected = True
978            self.selected = object
979            self._no_update_selected = False
980        else:
981            self._no_update_selected = True
982            self.selected = selected
983            self._no_update_selected = False
984
985        # If no one has been notified of the selection yet, inform the editor's
986        # select handler (if any) of the new selection:
987        if not_handled is True:
988            self.ui.evaluate(self.factory.on_select, object)
989
990        # Check to see if there is an associated node editor pane:
991        editor = self._editor
992        if editor is not None:
993            # If we already had a node editor, destroy it:
994            editor.Freeze()
995            self._clear_editor()
996
997            # If there is a selected object, create a new editor for it:
998            if object is not None:
999                # Try to chain the undo history to the main undo history:
1000                view = node.get_view(object)
1001
1002                if view is None or isinstance(view, str):
1003                    view = object.trait_view(view)
1004
1005                if (self.ui.history is not None) or (view.kind == "subpanel"):
1006                    ui = object.edit_traits(
1007                        parent=editor, view=view, kind="subpanel"
1008                    )
1009                else:
1010                    # Otherwise, just set up our own new one:
1011                    ui = object.edit_traits(
1012                        parent=editor, view=view, kind="panel"
1013                    )
1014
1015                # Make our UI the parent of the new UI:
1016                ui.parent = self.ui
1017
1018                # Remember the new editor's UI and node info:
1019                editor._node_ui = ui
1020                editor._editor_nid = nid
1021
1022                # Finish setting up the editor:
1023                sizer = wx.BoxSizer(wx.VERTICAL)
1024                sizer.Add(ui.control, 1, wx.EXPAND)
1025                editor.SetSizer(sizer)
1026                editor.Layout()
1027
1028            # fixme: The following is a hack needed to make the editor window
1029            # (which is a wx.ScrolledWindow) recognize that its contents have
1030            # been changed:
1031            dx, dy = editor.GetSize()
1032            editor.SetSize(wx.Size(dx, dy + 1))
1033            editor.SetSize(wx.Size(dx, dy))
1034
1035            # Allow the editor view to show any changes that have occurred:
1036            editor.Thaw()
1037
1038    def _on_hover(self, event):
1039        """ Handles the mouse moving over a tree node
1040        """
1041        id, flags = self._tree.HitTest(event.GetPosition())
1042        if flags & wx.TREE_HITTEST_ONITEMLABEL:
1043            expanded, node, object = self._get_node_data(id)
1044            if self.factory.on_hover is not None:
1045                self.ui.evaluate(self.factory.on_hover, object)
1046                self._veto = True
1047        elif self.factory and self.factory.on_hover is not None:
1048            self.ui.evaluate(self.factory.on_hover, None)
1049
1050        # allow other events to be processed
1051        event.Skip(True)
1052
1053    def _on_tree_item_activated(self, event):
1054        """ Handles a tree item being activated.
1055        """
1056        expanded, node, object = self._get_node_data(event.GetItem())
1057
1058        if node.activated(object) is True:
1059            if self.factory.on_activated is not None:
1060                self.ui.evaluate(self.factory.on_activated, object)
1061                self._veto = True
1062        else:
1063            self._veto = True
1064
1065        # Fire the 'activated' event with the clicked on object as value:
1066        self.activated = object
1067
1068        # FIXME: Firing the dclick event also for backward compatibility on wx.
1069        # Change it occur on mouse double click only.
1070        self.dclick = object
1071
1072    def _on_tree_begin_label_edit(self, event):
1073        """ Handles the user starting to edit a tree node label.
1074        """
1075        item = event.GetItem()
1076        parent = self._tree.GetItemParent(item)
1077        can_rename = True
1078        if parent.IsOk():
1079            expanded, node, object = self._get_node_data(parent)
1080            can_rename = node.can_rename(object)
1081
1082        if can_rename:
1083            expanded, node, object = self._get_node_data(item)
1084            if node.can_rename_me(object):
1085                return
1086
1087        event.Veto()
1088
1089    def _on_tree_end_label_edit(self, event):
1090        """ Handles the user completing tree node label editing.
1091        """
1092        label = event.GetLabel()
1093        if len(label) > 0:
1094            expanded, node, object = self._get_node_data(event.GetItem())
1095            # Tell the node to change the label. If it raises an exception,
1096            # that means it didn't like the label, so veto the tree node
1097            # change:
1098            try:
1099                node.set_label(object, label)
1100                return
1101            except:
1102                pass
1103        event.Veto()
1104
1105    def _on_tree_begin_drag(self, event):
1106        """ Handles a drag operation starting on a tree node.
1107        """
1108        if PythonDropSource is not None:
1109            expanded, node, object, nid, point = self._unpack_event(event)
1110            if node is not None:
1111                try:
1112                    self._dragging = nid
1113                    PythonDropSource(self._tree, node.get_drag_object(object))
1114                finally:
1115                    self._dragging = None
1116
1117    def _on_tree_item_gettooltip(self, event):
1118        """ Handles a tooltip request on a tree node.
1119        """
1120        nid = event.GetItem()
1121        if nid.IsOk():
1122            node_data = self._get_node_data(nid)
1123            if node_data is not None:
1124                expanded, node, object = node_data
1125                tooltip = node.get_tooltip(object)
1126                if tooltip != "":
1127                    event.SetToolTip(tooltip)
1128
1129        event.Skip()
1130
1131    def _on_left_dclick(self, event):
1132        """ Handle left mouse dclick to emit dclick event for associated node.
1133        """
1134        # Determine what node (if any) was clicked on:
1135        expanded, node, object, nid, point = self._unpack_event(event)
1136
1137        # If the mouse is over a node, then process the click:
1138        if node is not None:
1139            if node.dclick(object) is True:
1140                if self.factory.on_dclick is not None:
1141                    self.ui.evaluate(self.factory.on_dclick, object)
1142                    self._veto = True
1143            else:
1144                self._veto = True
1145
1146            # Fire the 'dclick' event with the object as its value:
1147            # FIXME: This is instead done in _on_item_activated for backward
1148            # compatibility only on wx toolkit.
1149            # self.dclick = object
1150
1151        # Allow normal mouse event processing to occur:
1152        event.Skip()
1153
1154    def _on_left_down(self, event):
1155        """ Handles the user right clicking on a tree node.
1156        """
1157        # Determine what node (if any) was clicked on:
1158        expanded, node, object, nid, point = self._unpack_event(event)
1159
1160        # If the mouse is over a node, then process the click:
1161        if node is not None:
1162            if (node.click(object) is True) and (
1163                self.factory.on_click is not None
1164            ):
1165                self.ui.evaluate(self.factory.on_click, object)
1166
1167            # Fire the 'click' event with the object as its value:
1168            self.click = object
1169
1170        # Allow normal mouse event processing to occur:
1171        event.Skip()
1172
1173    def _on_right_down(self, event):
1174        """ Handles the user right clicking on a tree node.
1175        """
1176        expanded, node, object, nid, point = self._unpack_event(event)
1177
1178        if node is not None:
1179            self._data = (node, object, nid)
1180            self._context = {
1181                "object": object,
1182                "editor": self,
1183                "node": node,
1184                "info": self.ui.info,
1185                "handler": self.ui.handler,
1186            }
1187
1188            # Try to get the parent node of the node clicked on:
1189            pnid = self._tree.GetItemParent(nid)
1190            if pnid.IsOk():
1191                ignore, parent_node, parent_object = self._get_node_data(pnid)
1192            else:
1193                parent_node = parent_object = None
1194
1195            self._menu_node = node
1196            self._menu_parent_node = parent_node
1197            self._menu_parent_object = parent_object
1198
1199            menu = node.get_menu(object)
1200
1201            if menu is None:
1202                # Use the standard, default menu:
1203                menu = self._standard_menu(node, object)
1204
1205            elif isinstance(menu, Menu):
1206                # Use the menu specified by the node:
1207                group = menu.find_group(NewAction)
1208                if group is not None:
1209                    # Only set it the first time:
1210                    group.id = ""
1211                    actions = self._new_actions(node, object)
1212                    if len(actions) > 0:
1213                        group.insert(0, Menu(name="New", *actions))
1214
1215            else:
1216                # All other values mean no menu should be displayed:
1217                menu = None
1218
1219            # Only display the menu if a valid menu is defined:
1220            if menu is not None:
1221                wxmenu = menu.create_menu(self._tree, self)
1222                self._tree.PopupMenu(wxmenu, point[0] - 10, point[1] - 10)
1223                wxmenu.Destroy()
1224
1225            # Reset all menu related cached values:
1226            self._data = (
1227                self._context
1228            ) = (
1229                self._menu_node
1230            ) = self._menu_parent_node = self._menu_parent_object = None
1231
1232    def _standard_menu(self, node, object):
1233        """ Returns the standard contextual pop-up menu.
1234        """
1235        actions = [
1236            CutAction,
1237            CopyAction,
1238            PasteAction,
1239            Separator(),
1240            DeleteAction,
1241            Separator(),
1242            RenameAction,
1243        ]
1244
1245        # See if the 'New' menu section should be added:
1246        items = self._new_actions(node, object)
1247        if len(items) > 0:
1248            actions[0:0] = [Menu(name="New", *items), Separator()]
1249
1250        return Menu(*actions)
1251
1252    def _new_actions(self, node, object):
1253        """ Returns a list of Actions that will create new objects.
1254        """
1255        object = self._data[1]
1256        items = []
1257        add = node.get_add(object)
1258        if len(add) > 0:
1259            for klass in add:
1260                prompt = False
1261                if isinstance(klass, tuple):
1262                    klass, prompt = klass
1263                add_node = self._node_for_class(klass)
1264                if add_node is not None:
1265                    class_name = klass.__name__
1266                    name = add_node.get_name(object)
1267                    if name == "":
1268                        name = class_name
1269                    items.append(
1270                        Action(
1271                            name=name,
1272                            action="editor._menu_new_node('%s',%s)"
1273                            % (class_name, prompt),
1274                        )
1275                    )
1276        return items
1277
1278    def _is_copyable(self, object):
1279        parent = self._menu_parent_node
1280        if isinstance(parent, ObjectTreeNode):
1281            return parent.can_copy(self._menu_parent_object)
1282        return (parent is not None) and parent.can_copy(object)
1283
1284    def _is_cutable(self, object):
1285        parent = self._menu_parent_node
1286        if isinstance(parent, ObjectTreeNode):
1287            can_cut = parent.can_copy(
1288                self._menu_parent_object
1289            ) and parent.can_delete(self._menu_parent_object)
1290        else:
1291            can_cut = (
1292                (parent is not None)
1293                and parent.can_copy(object)
1294                and parent.can_delete(object)
1295            )
1296        return can_cut and self._menu_node.can_delete_me(object)
1297
1298    def _is_pasteable(self, object):
1299        from pyface.wx.clipboard import clipboard
1300
1301        return self._menu_node.can_add(object, clipboard.object_type)
1302
1303    def _is_deletable(self, object):
1304        parent = self._menu_parent_node
1305        if isinstance(parent, ObjectTreeNode):
1306            can_delete = parent.can_delete(self._menu_parent_object)
1307        else:
1308            can_delete = (parent is not None) and parent.can_delete(object)
1309        return can_delete and self._menu_node.can_delete_me(object)
1310
1311    def _is_renameable(self, object):
1312        parent = self._menu_parent_node
1313        if isinstance(parent, ObjectTreeNode):
1314            can_rename = parent.can_rename(self._menu_parent_object)
1315        elif parent is not None:
1316            can_rename = parent.can_rename(object)
1317        else:
1318            can_rename = True
1319        return can_rename and self._menu_node.can_rename_me(object)
1320
1321    # ----- Drag and drop event handlers: -------------------------------------
1322
1323    def wx_dropped_on(self, x, y, data, drag_result):
1324        """ Handles a Python object being dropped on the tree.
1325        """
1326        if isinstance(data, list):
1327            rc = wx.DragNone
1328            for item in data:
1329                rc = self.wx_dropped_on(x, y, item, drag_result)
1330            return rc
1331
1332        expanded, node, object, nid, point = self._hit_test(wx.Point(x, y))
1333        if node is not None:
1334            if drag_result == wx.DragMove:
1335                if not self._is_droppable(node, object, data, False):
1336                    return wx.DragNone
1337
1338                if self._dragging is not None:
1339                    data = self._drop_object(node, object, data, False)
1340                    if data is not None:
1341                        try:
1342                            self._begin_undo()
1343                            self._undoable_delete(
1344                                *self._node_index(self._dragging)
1345                            )
1346                            self._undoable_append(node, object, data, False)
1347                        finally:
1348                            self._end_undo()
1349                else:
1350                    data = self._drop_object(node, object, data)
1351                    if data is not None:
1352                        self._undoable_append(node, object, data, False)
1353
1354                return drag_result
1355
1356            to_node, to_object, to_index = self._node_index(nid)
1357            if to_node is not None:
1358                if self._dragging is not None:
1359                    data = self._drop_object(node, to_object, data, False)
1360                    if data is not None:
1361                        from_node, from_object, from_index = self._node_index(
1362                            self._dragging
1363                        )
1364                        if (to_object is from_object) and (
1365                            to_index > from_index
1366                        ):
1367                            to_index -= 1
1368                        try:
1369                            self._begin_undo()
1370                            self._undoable_delete(
1371                                from_node, from_object, from_index
1372                            )
1373                            self._undoable_insert(
1374                                to_node, to_object, to_index, data, False
1375                            )
1376                        finally:
1377                            self._end_undo()
1378                else:
1379                    data = self._drop_object(to_node, to_object, data)
1380                    if data is not None:
1381                        self._undoable_insert(
1382                            to_node, to_object, to_index, data, False
1383                        )
1384
1385                return drag_result
1386
1387        return wx.DragNone
1388
1389    def wx_drag_over(self, x, y, data, drag_result):
1390        """ Handles a Python object being dragged over the tree.
1391        """
1392        expanded, node, object, nid, point = self._hit_test(wx.Point(x, y))
1393        insert = False
1394
1395        if (node is not None) and (drag_result == wx.DragCopy):
1396            node, object, index = self._node_index(nid)
1397            insert = True
1398
1399        if (self._dragging is not None) and (
1400            not self._is_drag_ok(self._dragging, data, object)
1401        ):
1402            return wx.DragNone
1403
1404        if (node is not None) and self._is_droppable(
1405            node, object, data, insert
1406        ):
1407            return drag_result
1408
1409        return wx.DragNone
1410
1411    def _is_drag_ok(self, snid, source, target):
1412        if (snid is None) or (target is source):
1413            return False
1414
1415        for cnid in self._nodes(snid):
1416            if not self._is_drag_ok(
1417                cnid, self._get_node_data(cnid)[2], target
1418            ):
1419                return False
1420
1421        return True
1422
1423    # ----- pyface.action 'controller' interface implementation: --------------
1424
1425    def add_to_menu(self, menu_item):
1426        """ Adds a menu item to the menu bar being constructed.
1427        """
1428        action = menu_item.item.action
1429        self.eval_when(action.enabled_when, menu_item, "enabled")
1430        self.eval_when(action.checked_when, menu_item, "checked")
1431
1432    def add_to_toolbar(self, toolbar_item):
1433        """ Adds a toolbar item to the toolbar being constructed.
1434        """
1435        self.add_to_menu(toolbar_item)
1436
1437    def can_add_to_menu(self, action):
1438        """ Returns whether the action should be defined in the user interface.
1439        """
1440        if action.defined_when != "":
1441            if not eval(action.defined_when, globals(), self._context):
1442                return False
1443
1444        if action.visible_when != "":
1445            if not eval(action.visible_when, globals(), self._context):
1446                return False
1447
1448        return True
1449
1450    def can_add_to_toolbar(self, action):
1451        """ Returns whether the toolbar action should be defined in the user
1452            interface.
1453        """
1454        return self.can_add_to_menu(action)
1455
1456    def perform(self, action, action_event=None):
1457        """ Performs the action described by a specified Action object.
1458        """
1459        self.ui.do_undoable(self._perform, action)
1460
1461    def _perform(self, action):
1462        node, object, nid = self._data
1463        method_name = action.action
1464        info = self.ui.info
1465        handler = self.ui.handler
1466
1467        if method_name.find(".") >= 0:
1468            if method_name.find("(") < 0:
1469                method_name += "()"
1470            try:
1471                eval(
1472                    method_name,
1473                    globals(),
1474                    {
1475                        "object": object,
1476                        "editor": self,
1477                        "node": node,
1478                        "info": info,
1479                        "handler": handler,
1480                    },
1481                )
1482            except:
1483                from traitsui.api import raise_to_debug
1484
1485                raise_to_debug()
1486
1487            return
1488
1489        method = getattr(handler, method_name, None)
1490        if method is not None:
1491            method(info, object)
1492            return
1493
1494        if action.on_perform is not None:
1495            action.on_perform(object)
1496
1497    # ----- Menu support methods: ---------------------------------------------
1498
1499    def eval_when(self, condition, object, trait):
1500        """ Evaluates a condition within a defined context, and sets a
1501        specified object trait based on the result, which is assumed to be a
1502        Boolean.
1503        """
1504        if condition != "":
1505            value = True
1506            if not eval(condition, globals(), self._context):
1507                value = False
1508            setattr(object, trait, value)
1509
1510    # ----- Menu event handlers: ----------------------------------------------
1511
1512    def _menu_copy_node(self):
1513        """ Copies the current tree node object to the paste buffer.
1514        """
1515        from pyface.wx.clipboard import clipboard
1516
1517        clipboard.data = self._data[1]
1518        self._data = None
1519
1520    def _menu_cut_node(self):
1521        """  Cuts the current tree node object into the paste buffer.
1522        """
1523        from pyface.wx.clipboard import clipboard
1524
1525        node, object, nid = self._data
1526        clipboard.data = object
1527        self._data = None
1528        self._undoable_delete(*self._node_index(nid))
1529
1530    def _menu_paste_node(self):
1531        """ Pastes the current contents of the paste buffer into the current
1532            node.
1533        """
1534        from pyface.wx.clipboard import clipboard
1535
1536        node, object, nid = self._data
1537        self._data = None
1538        self._undoable_append(node, object, clipboard.object_data, False)
1539
1540    def _menu_delete_node(self):
1541        """ Deletes the current node from the tree.
1542        """
1543        node, object, nid = self._data
1544        self._data = None
1545        rc = node.confirm_delete(object)
1546        if rc is not False:
1547            if rc is not True:
1548                if self.ui.history is None:
1549                    # If no undo history, ask user to confirm the delete:
1550                    dlg = wx.MessageDialog(
1551                        self._tree,
1552                        "Are you sure you want to delete %s?"
1553                        % node.get_label(object),
1554                        "Confirm Deletion",
1555                        style=wx.OK | wx.CANCEL | wx.ICON_EXCLAMATION,
1556                    )
1557                    if dlg.ShowModal() != wx.ID_OK:
1558                        return
1559
1560            self._undoable_delete(*self._node_index(nid))
1561
1562    def _menu_rename_node(self):
1563        """ Renames the current tree node.
1564        """
1565        node, object, nid = self._data
1566        self._data = None
1567        object_label = ObjectLabel(label=node.get_label(object))
1568        if object_label.edit_traits().result:
1569            label = object_label.label.strip()
1570            if label != "":
1571                node.set_label(object, label)
1572
1573    def _menu_new_node(self, class_name, prompt=False):
1574        """ Adds a new object to the current node.
1575        """
1576        node, object, nid = self._data
1577        self._data = None
1578        new_node, new_class = self._node_for_class_name(class_name)
1579        new_object = new_class()
1580        if (not prompt) or new_object.edit_traits(
1581            parent=self.control, kind="livemodal"
1582        ).result:
1583            self._undoable_append(node, object, new_object, False)
1584
1585            # Automatically select the new object if editing is being
1586            # performed:
1587            if self.factory.editable:
1588                self._tree.SelectItem(self._tree.GetLastChild(nid))
1589
1590    # -- Model event handlers -------------------------------------------------
1591
1592    def _children_replaced(self, object, name="", new=None):
1593        """ Handles the children of a node being completely replaced.
1594        """
1595        tree = self._tree
1596        for expanded, node, nid in self._object_info_for(object, name):
1597            children = node.get_children(object)
1598
1599            # Only add/remove the changes if the node has already been
1600            # expanded:
1601            if expanded:
1602                # Delete all current child nodes:
1603                for cnid in self._nodes_for(nid):
1604                    self._delete_node(cnid)
1605
1606                # Add all of the children back in as new nodes:
1607                for child in children:
1608                    child, child_node = self._node_for(child)
1609                    if child_node is not None:
1610                        self._append_node(nid, child_node, child)
1611
1612            # Indicate whether the node has any children now:
1613            tree.SetItemHasChildren(nid, len(children) > 0)
1614
1615            # Try to expand the node (if requested):
1616            if node.can_auto_open(object):
1617                tree.Expand(nid)
1618
1619    def _children_updated(self, object, name, event):
1620        """ Handles the children of a node being changed.
1621        """
1622        # Log the change that was made (removing '_items' from the end of the
1623        # name):
1624        name = name[:-6]
1625        self.log_change(self._get_undo_item, object, name, event)
1626
1627        start = event.index
1628        end = start + len(event.removed)
1629        tree = self._tree
1630
1631        for expanded, node, nid in self._object_info_for(object, name):
1632            n = len(node.get_children(object))
1633
1634            # Only add/remove the changes if the node has already been
1635            # expanded:
1636            if expanded:
1637                # Remove all of the children that were deleted:
1638                for cnid in self._nodes_for(nid)[start:end]:
1639                    self._delete_node(cnid)
1640
1641                # Add all of the children that were added:
1642                remaining = n - len(event.removed)
1643                child_index = 0
1644                for child in event.added:
1645                    child, child_node = self._node_for(child)
1646                    if child_node is not None:
1647                        insert_index = (
1648                            (start + child_index)
1649                            if (start < remaining)
1650                            else None
1651                        )
1652                        self._insert_node(nid, insert_index, child_node, child)
1653                        child_index += 1
1654
1655            # Indicate whether the node has any children now:
1656            tree.SetItemHasChildren(nid, n > 0)
1657
1658            # Try to expand the node (if requested):
1659            root = tree.GetRootItem()
1660            if node.can_auto_open(object):
1661                if (nid != root) or not self.factory.hide_root:
1662                    tree.Expand(nid)
1663
1664    def _label_updated(self, object, name, label):
1665        """  Handles the label of an object being changed.
1666        """
1667        nids = {}
1668        for name2, nid in self._map[id(object)]:
1669            if nid not in nids:
1670                nids[nid] = None
1671                node = self._get_node_data(nid)[1]
1672                self._tree.SetItemText(nid, node.get_label(object))
1673                self._update_icon_for_nid(nid)
1674
1675    # -- UI preference save/restore interface ---------------------------------
1676
1677    def restore_prefs(self, prefs):
1678        """ Restores any saved user preference information associated with the
1679            editor.
1680        """
1681        if self._is_dock_window:
1682            if isinstance(prefs, dict):
1683                structure = prefs.get("structure")
1684            else:
1685                structure = prefs
1686            self.control.GetSizer().SetStructure(self.control, structure)
1687
1688    def save_prefs(self):
1689        """ Returns any user preference information associated with the editor.
1690        """
1691        if self._is_dock_window:
1692            return {"structure": self.control.GetSizer().GetStructure()}
1693
1694        return None
1695
1696
1697# -- End UI preference save/restore interface -----------------------------
1698
1699
1700class ObjectLabel(HasStrictTraits):
1701    """ An editable label for an object.
1702    """
1703
1704    # -------------------------------------------------------------------------
1705    #  Trait definitions:
1706    # -------------------------------------------------------------------------
1707
1708    #: Label to be edited
1709    label = Str()
1710
1711    # -------------------------------------------------------------------------
1712    #  Traits view definition:
1713    # -------------------------------------------------------------------------
1714
1715    traits_view = View(
1716        "label", title="Edit Label", kind="modal", buttons=["OK", "Cancel"]
1717    )
1718