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