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