Source code for exopy.utils.widgets.qt_tree_widget

# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2015-2018-2018 by Exopy Authors, see AUTHORS for more details.
#
# Distributed under the terms of the BSD license.
#
# The full license is in the file LICENCE, distributed with this software.
# -----------------------------------------------------------------------------
"""Tree widget for enaml.

This tree widget has limited functionality, it supports only :
- single selection
- single column tree
- no undo capabilities
- single TreeNode fitting an object.

It should be used with the TreeNode declarative class and the Menu item given
in qt_tree_menu.enaml.

This is vastly inspired from TraitsUI implementation.

"""
import copy
import os
from atom.api import (Bool, List, Value, Dict, Int)

from enaml.widgets.api import RawWidget
from enaml.core.declarative import d_
from enaml.qt import QtCore, QtGui, QtWidgets

from .qt_clipboard import CLIPBOARD, PyMimeData
from .tree_nodes import TreeNode


[docs]def pixmap_cache(name, path=None): """ Return the QPixmap corresponding to a filename. If the filename does not contain a path component, 'path' is used (or if 'path' is not specified, the local 'images' directory is used). """ name_path, name = os.path.split(name) name = name.replace(' ', '_').lower() if name_path: filename = os.path.join(name_path, name) else: if path is None: filename = os.path.join(os.path.dirname(__file__), 'images', name) else: filename = os.path.join(path, name) filename = os.path.abspath(filename) pm = QtGui.QPixmap() if not QtGui.QPixmapCache.find(filename, pm): pm.load(filename) QtGui.QPixmapCache.insert(filename, pm) return pm
#: Cyclic notification guard flags INDEX_GUARD = 0x1 #: Standard icons map. STD_ICON_MAP = { '<item>': QtWidgets.QStyle.SP_FileIcon, '<group>': QtWidgets.QStyle.SP_DirClosedIcon, '<open>': QtWidgets.QStyle.SP_DirOpenIcon }
[docs]class QtTreeWidget(RawWidget): """Simple style of tree editor. """ # ========================================================================= # --- Members definitions ------------------------------------------------- # ========================================================================= #: Root object of the tree. root_node = d_(Value()) #: Is the tree editor is scrollable? This value overrides the default. scrollable = d_(Bool(True)) #: The currently selected object selected_item = d_(Value()) #: Flag to hide the root node of the tree. hide_root = d_(Bool()) #: Flag controlling the automatic expansion of nodes. auto_expand = d_(Bool(True)) #: Is drag and drop allowed on the tree. drag_drop = d_(Bool(True)) #: Whether or not to show the icons for the leaves and nodes. show_icons = d_(Bool(True)) #: Nodes declared by the user as children of this widget. nodes = List() hug_height = 'ignore' #: Cyclic selection notification guard. _guard = Int() #: Object id to object map used internally. _map = Dict() # PySide requires weakrefs for using bound methods as slots. # PyQt doesn't, but executes unsafe code if not using weakrefs. __slots__ = '__weakref__' # ========================================================================= # --- Enaml Raw widget interface ------------------------------------------ # =========================================================================
[docs] def create_widget(self, parent): """Finishes initializing the editor by creating the underlying toolkit widget. """ # Create tree widget and connect signal tree = _TreeWidget(parent, self.drag_drop) tree._controller = self # Hide the header as we have a single column. tree.setHeaderHidden(True) self.nodes = [ch for ch in self.children if isinstance(ch, TreeNode)] tree.itemExpanded.connect(self._on_item_expanded) tree.itemCollapsed.connect(self._on_item_collapsed) tree.itemSelectionChanged.connect(self._on_tree_sel_changed) tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) tree.customContextMenuRequested.connect(self._on_context_menu) tree.itemDelegate().commitData.connect(self._on_data_committed) # Disable the default double click to rename behavior tree.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) nid = self._set_root_node(self.root_node, tree) # The proxy is not yet active so we must set the selected item manually self._guard ^= INDEX_GUARD if not self.selected_item: self.selected_item = self.get_object(nid) else: new = self.selected_item if id(new) in self._map: # TODO handle the automatic expanding of the tree tree.setCurrentItem(self._object_info(new)[2]) self._guard ^= INDEX_GUARD return tree
[docs] def destroy(self): """ Disposes of the contents of an editor. """ tree = self.get_widget() if tree is not None: # Stop the chatter (specifically about the changing selection). tree.blockSignals(True) self._delete_node(tree.invisibleRootItem()) super(QtTreeWidget, self).destroy()
# ========================================================================= # --- Public API ---------------------------------------------------------- # =========================================================================
[docs] def get_object(self, nid): """Gets the object associated with a specified node. """ return self._get_node_data(nid)[2]
[docs] def get_parent(self, obj, name=''): """Returns the object that is the immmediate parent of a specified object in the tree. """ nid = self._get_object_nid(obj, name) if nid is not None: pnid = nid.parent() if pnid is not self.get_widget().invisibleRootItem(): return self.get_object(pnid) return None
[docs] def get_node(self, obj, name=''): """Returns the node associated with a specified object. """ nid = self._get_object_nid(obj, name) if nid is not None: return self._get_node_data(nid)[1] return None
# ========================================================================= # --- Observers ----------------------------------------------------------- # ========================================================================= def _post_setattr_selected_item(self, old, new): """Update the selection when it changes externally. """ tree = self.get_widget() if not tree: return if not self._guard & INDEX_GUARD: self._guard ^= INDEX_GUARD try: if id(new) not in self._map: # TODO handle the automatic expanding of the tree return # Otherwise would crash tree.setCurrentItem(self._object_info(new)[2]) except Exception: self._guard ^= INDEX_GUARD raise def _post_setattr_root_node(self, old, new): if self.proxy_is_active: self._set_root_node(new) # ========================================================================= # --- Node manipulation methods ------------------------------------------- # ========================================================================= def _expand_levels(self, nid, levels, expand=True): """Expands from the specified node the specified number of sub-levels. """ if levels > 0: expanded, node, obj = self._get_node_data(nid) if self._has_children(node, obj): self._expand_node(nid) if expand: nid.setExpanded(True) for cnid in self._nodes_for(nid): self._expand_levels(cnid, levels - 1) def _set_root_node(self, model, tree=None): """Set the root node of the tree. """ if not tree: tree = self.get_widget() self._guard ^= INDEX_GUARD tree.clear() self._guard ^= INDEX_GUARD self._map = {} obj, node = self._node_for(model) if node is not None: if self.hide_root: nid = tree.invisibleRootItem() else: nid = self._create_item(tree, node, obj) self._map[id(obj)] = [(node.get_children_id(obj), nid)] self._add_listeners(node, obj) self._set_node_data(nid, (False, node, obj)) if self.hide_root or self._has_children(node, obj): self._expand_node(nid) if not self.hide_root: nid.setExpanded(True) self._expand_levels(nid, 2, self.auto_expand) ncolumns = tree.columnCount() if ncolumns > 1: for i in range(ncolumns): tree.resizeColumnToContents(i) top_nid = tree.topLevelItem(0) nid = top_nid or nid tree.setCurrentItem(nid) return nid def _create_item(self, nid, node, obj, index=None): """Create a new TreeWidgetItem as per word_wrap policy. Index is the index of the new node in the parent: None implies append the child to the end. """ if index is None: cnid = QtWidgets.QTreeWidgetItem(nid) else: cnid = QtWidgets.QTreeWidgetItem() nid.insertChild(index, cnid) cnid.setText(0, node.get_label(obj)) cnid.setIcon(0, self._get_icon(node, obj)) cnid.setToolTip(0, node.get_tooltip(obj)) color = node.get_background(obj) if color: cnid.setBackground(0, self._get_brush(color)) color = node.get_foreground(obj) if color: cnid.setForeground(0, self._get_brush(color)) return cnid def _set_label(self, nid, text): """ Set the label of the specified item. """ expanded, node, obj = self._get_node_data(nid) nid.setText(0, node.get_label(obj)) def _append_node(self, nid, node, obj): """ Appends a new node to the specified node. """ return self._insert_node(nid, None, node, obj) def _insert_node(self, nid, index, node, obj): """ Inserts a new node before a specified index into the children of the specified node. """ cnid = self._create_item(nid, node, obj, index) has_children = self._has_children(node, obj) self._set_node_data(cnid, (False, node, obj)) self._map.setdefault(id(obj), []).append((node.get_children_id(obj), cnid)) self._add_listeners(node, obj) # Automatically expand the new node (if requested): if node.allows_children(obj): if has_children and node.can_auto_open(obj): cnid.setExpanded(True) else: # Qt only draws the control that expands the tree if there is a # child. As the tree is being populated lazily we create a # dummy that will be removed when the node is expanded for the # first time. cnid._dummy = QtWidgets.QTreeWidgetItem(cnid) # Return the newly created node: return cnid def _delete_node(self, nid): """ Deletes a specified tree node and all its children. """ for cnid in self._nodes_for(nid): self._delete_node(cnid) # See if it is a dummy. pnid = nid.parent() if pnid is not None and getattr(pnid, '_dummy', None) is nid: pnid.removeChild(nid) del pnid._dummy return try: expanded, node, obj = self._get_node_data(nid) except AttributeError: # The node has already been deleted. pass else: id_object = id(obj) object_info = self._map[id_object] for i, info in enumerate(object_info): # QTreeWidgetItem does not have an equal operator, so use id() if id(nid) == id(info[1]): del object_info[i] break if len(object_info) == 0: self._remove_listeners(node, obj) del self._map[id_object] if pnid is None and self.proxy_is_active: tree = self.get_widget() tree.takeTopLevelItem(tree.indexOfTopLevelItem(nid)) else: pnid.removeChild(nid) def _expand_node(self, nid): """ Expands the contents of a specified node (if required). """ expanded, node, obj = self._get_node_data(nid) # Lazily populate the item's children: if not expanded: # Remove any dummy node. dummy = getattr(nid, '_dummy', None) if dummy is not None: nid.removeChild(dummy) del nid._dummy for child in node.get_children(obj): child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) else: msg = 'No Node type found for child {}.' raise ValueError(msg.format(child)) # Indicate the item is now populated: self._set_node_data(nid, (True, node, obj)) def _nodes_for(self, nid): """ Returns all child node ids of a specified node id. """ return [nid.child(i) for i in range(nid.childCount())] def _node_index(self, nid): """Return the index of a specified node id within its parent. """ pnid = nid.parent() if pnid is None: if self.hide_root and self.proxy_is_active: pnid = self.get_widget().invisibleRootItem() if pnid is None: return (None, None, None) for i in range(pnid.childCount()): if pnid.child(i) is nid: _, pnode, pobject = self._get_node_data(pnid) return (pnode, pobject, i) # doesn't match any node, so return None return (None, None, None) def _has_children(self, node, obj): """ Returns whether a specified object has any children. """ return bool(node.allows_children(obj) and node.has_children(obj)) def _get_icon(self, node, obj, is_expanded=False): """ Returns the index of the specified object icon. """ if True: return QtGui.QIcon() icon_name = node.get_icon(obj, is_expanded) if isinstance(icon_name, str): icon = STD_ICON_MAP.get(icon_name) if icon is not None and self.proxy_is_active: return self.get_widget().style().standardIcon(icon) path = node.get_icon_path(obj) if isinstance(path, str): path = [path, node] else: path.append(node) # resource_manager.locate_image( icon_name, path ) reference = None if reference is None: return QtGui.QIcon() file_name = reference.filename else: # Assume it is an ImageResource, and get its file name directly: file_name = icon_name.absolute_path return QtGui.QIcon(pixmap_cache(file_name)) def _add_listeners(self, node, obj): """Adds the event listeners for a specified object. """ if node.allows_children(obj): obj.observe(node.children_member, self._children_replaced) obj.observe(node.children_changed, self._children_updated) node.when_label_changed(obj, self._label_updated, False) def _remove_listeners(self, node, obj): """Removes any event listeners from a specified object. """ if node.allows_children(obj): obj.unobserve(node.children_member, self._children_replaced) obj.unobserve(node.children_changed, self._children_updated) node.when_label_changed(obj, self._label_updated, True) # ========================================================================= # --- Object instrospection ----------------------------------------------- # ========================================================================= def _object_info(self, obj, name=''): """Tree node data for an object in the form (expanded, node, nid). """ info = self._map[id(obj)] for name2, nid in info: if name == name2: break else: nid = info[0][1] expanded, node, ignore = self._get_node_data(nid) return (expanded, node, nid) def _object_info_for(self, obj, name=''): """Returns the tree node data for a specified object as a list of the form: [ (expanded, node, nid), ... ]. """ result = [] for name2, nid in self._map[id(obj)]: if name == name2: expanded, node, ignore = self._get_node_data(nid) result.append((expanded, node, nid)) return result def _node_for(self, obj): """Returns the TreeNode associated with a specified object. """ if ((type(obj) is tuple) and (len(obj) == 2) and isinstance(obj[1], TreeNode)): return obj # Select all nodes which understand this object: nodes = [node for node in self.nodes if node.is_node_for(obj)] # If only one found, we're done, return it: if len(nodes) == 1: return (obj, nodes[0]) # If none found, give up: if len(nodes) == 0: return (obj, None) def _node_for_class(self, klass): """Returns the TreeNode associated with a specified class. """ for node in self.nodes: if issubclass(klass, tuple(node.node_for)): return node return None def _node_for_class_name(self, class_name): """Returns the node and class associated with a specified class name. """ for node in self.nodes: for klass in node.node_for: if class_name == klass.__name__: return (node, klass) return (None, None) def _get_object_nid(self, obj, name=''): """Gets the ID associated with a specified object (if any). """ info = self._map.get(id(obj)) if info is None: return None for name2, nid in info: if name == name2: return nid return info[0][1] @staticmethod def _get_node_data(nid): """Gets the node specific data. """ return nid._py_data @staticmethod def _set_node_data(nid, data): """Sets the node specific data. """ nid._py_data = data # ========================================================================= # --- Object operations --------------------------------------------------- # ========================================================================= def _append(self, node, obj, data, make_copy=False): """Performs an append operation. """ if make_copy: data = copy.deepcopy(data) node.append_child(obj, data) def _insert(self, node, obj, index, data, make_copy=False): """Performs an insert operation. """ if make_copy: data = copy.deepcopy(data) node.insert_child(obj, index, data) def _delete(self, node, obj, index): """Performs an delete operation. """ node.delete_child(obj, index) def _move(self, node, obj, old, new): """Performs a move operation. """ node.move_child(obj, old, new) # ========================================================================= # --- Tree event handlers ------------------------------------------------- # ========================================================================= def _on_item_expanded(self, nid): """ Handles a tree node being expanded. """ expanded, node, obj = self._get_node_data(nid) # If 'auto_close' requested for this node type, close all of the node's # siblings: if node.can_auto_close(obj): parent = nid.parent() if parent is not None: for snid in self._nodes_for(parent): if snid is not nid: snid.setExpanded(False) # Expand the node (i.e. populate its children if they are not there # yet): self._expand_node(nid) self._update_icon(nid) def _on_item_collapsed(self, nid): """ Handles a tree node being collapsed. """ self._update_icon(nid) def _on_tree_sel_changed(self): """ Handles a tree node being selected. """ if self.proxy_is_active and not self._guard & INDEX_GUARD: # Get the new selection: nids = self.get_widget().selectedItems() selected = [] if len(nids) > 0: for nid in nids: # If there is a real selection, get the associated object: expanded, node, sel_object = self._get_node_data(nid) selected.append(sel_object) # QTreeWidgetItem does not have an equal operator, so use # id() if id(nid) == id(nids[0]): obj = sel_object # not_handled = node.select(sel_object) else: nid = None obj = None # not_handled = True # Set the value of the new selection: self._guard ^= INDEX_GUARD self.selected_item = obj self._guard ^= INDEX_GUARD def _on_context_menu(self, pos): """ Handles the user requesting a context menu, right clicking on a tree node. """ tree = self.get_widget() nid = tree.itemAt(pos) if nid is None: return _, node, obj = self._get_node_data(nid) # Try to get the parent node of the node clicked on: pnid = nid.parent() if pnid is None or pnid is tree.invisibleRootItem(): parent_node = parent_object = None else: _, parent_node, parent_object = self._get_node_data(pnid) context = {'copyable': self._is_copyable(obj, node, parent_node), 'cutable': self._is_cutable(obj, node, parent_node), 'pasteable': self._is_pasteable(obj, node, parent_node), 'renamable': self._is_renameable(obj, node, parent_node), 'deletable': self._is_deletable(obj, node, parent_node), 'not_root': parent_node is not None, 'data': (self, node, obj, nid)} menu = node.get_menu(context) if menu is not None: if not all((not action.visible or action.separator) for action in menu.items()): # Use the menu specified by the node: menu.popup() # ========================================================================= # Menu action helper methods: # ========================================================================= def _is_copyable(self, obj, node, parent_node): return bool((parent_node is not None) and parent_node.can_copy(obj)) def _is_cutable(self, obj, node, parent_node): can_cut = ((parent_node is not None) and parent_node.can_copy(obj) and parent_node.can_delete(obj)) return bool(can_cut and node.can_delete_me(obj)) def _is_pasteable(self, obj, node, parent_node): return node.can_add(obj, CLIPBOARD.instance_type) def _is_deletable(self, obj, node, parent_node): can_delete = ((parent_node is not None) and parent_node.can_delete(obj)) return bool(can_delete and node.can_delete_me(obj)) def _is_renameable(self, obj, node, parent_node): can_rename = ((parent_node is not None) and parent_node.can_rename(obj)) can_rename = (can_rename and node.can_rename_me(obj)) # Set the widget item's editable flag appropriately. nid = self._get_object_nid(obj) flags = nid.flags() if can_rename: flags |= QtCore.Qt.ItemIsEditable else: flags &= ~QtCore.Qt.ItemIsEditable nid.setFlags(flags) return can_rename def _is_droppable(self, node, obj, add_object, for_insert): """ Returns whether a given object is droppable on the node. """ if for_insert and (not node.can_insert(obj)): return False return node.can_add(obj, add_object) def _drop_object(self, node, obj, dropped_object, make_copy=True): """ Returns a droppable version of a specified object. """ new_object = node.drop_object(obj, dropped_object) if (new_object is not dropped_object) or (not make_copy): return new_object return copy.deepcopy(new_object) def _on_data_committed(self, editor): """Handle changes to a widget item subsequent to a renaming operation. """ nid = self.get_widget().currentItem() col = self.get_widget().currentColumn() _, node, obj = self._get_node_data(nid) new_label = str(nid.text(col)) old_label = node.get_label(obj) if new_label != old_label: if new_label != '': node.exit_rename(obj, new_label) self._set_label(nid, node.get_label(obj)) def _children_replaced(self, change): """ Handles the children of a node being completely replaced. """ obj = change['object'] name = change['name'] for expanded, node, nid in self._object_info_for(obj, name): children = node.get_children(obj) # Only add/remove the changes if the node has already been expanded if expanded: # Delete all current child nodes: for cnid in self._nodes_for(nid): self._delete_node(cnid) # Add all of the children back in as new nodes: for child in children: child, child_node = self._node_for(child) if child_node is not None: self._append_node(nid, child_node, child) else: msg = 'No Node type found for child {}' raise ValueError(msg.format(child)) # Try to expand the node (if requested): if node.can_auto_open(obj): nid.setExpanded(True) def _children_updated(self, change): """ Handles the children of a node being changed. Parameters ---------- change : exopy.utils.container_change.ContainerChange Describes the modification that occured on the children of the observed node. """ obj = change.obj name = change.name if change.collapsed: for ch in change.collapsed: self._children_updated(ch) elif change.added: for expanded, node, nid in self._object_info_for(obj, name): # Only add the changes if the node has already been expanded if expanded: # Add all of the children that were added: for index, child in change.added: child, child_node = self._node_for(child) if child_node is not None: self._insert_node(nid, index, child_node, child) else: msg = 'No Node type found for child {} at index {}' raise ValueError(msg.format(child, index)) # Try to expand the node (if requested): if node.can_auto_open(obj): nid.setExpanded(True) elif change.removed: for expanded, node, nid in self._object_info_for(obj, name): # Only remove the changes if the node has already been expanded if expanded: # Remove all of the children that were deleted: nodes = self._nodes_for(nid) for i, _ in change.removed: self._delete_node(nodes[i]) # Try to expand the node (if requested): if node.can_auto_open(obj): nid.setExpanded(True) elif change.moved: for expanded, node, nid in self._object_info_for(obj, name): # Only move nodes if the parent has already been expanded if expanded: for old, new, child in change.moved: # Remove the node that moved: self._delete_node(self._nodes_for(nid)[old]) # Get the node class for the child, we know it exists # since it was there previously child, child_node = self._node_for(child) # Number of sub nodes remaining on the parent node nodes_number = len(self._nodes_for(nid)) # If the child was moved down towards the last # available slot, turn the insertion operation in # an append as we always insert before another node if new > old and new >= nodes_number: new = None self._insert_node(nid, new, child_node, child) # Try to expand the node (if requested): if node.can_auto_open(obj): nid.setExpanded(True) def _label_updated(self, change): """ Handles the label of an object being changed. """ if self.proxy_is_active: tree = self.get_widget() obj = change['object'] # Prevent the itemChanged() signal from being emitted. blk = tree.blockSignals(True) nids = [] # HINT QTreeWidgetItem is not hashable in Python 3 for name2, nid in self._map[id(obj)]: if nid not in nids: nids.append(nid) node = self._get_node_data(nid)[1] self._set_label(nid, node.get_label(obj)) self._update_icon(nid) tree.blockSignals(blk) # ========================================================================= # --- Miscellaneous methods ----------------------------------------------- # ========================================================================= def _get_brush(self, color): """Get brush associated to a color. """ if isinstance(color, list) or isinstance(color, tuple): q_color = QtGui.QColor(*color) else: q_color = QtGui.QColor(color) return QtGui.QBrush(q_color) def _update_icon(self, nid): """ Updates the icon for a specified node. """ expanded, node, obj = self._get_node_data(nid) nid.setIcon(0, self._get_icon(node, obj, expanded))
class _TreeWidget(QtWidgets.QTreeWidget): """ The _TreeWidget class is a specialised QTreeWidget that reimplements the drag'n'drop support so that it hooks into the provided support. """ def __init__(self, parent, drag_drop): """ Initialise the tree widget. """ QtWidgets.QTreeWidget.__init__(self, parent) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) if drag_drop: self.setDragEnabled(True) self.setAcceptDrops(True) self._dragging = None self._controller = None def resizeEvent(self, event): """ Overridden to emit sizeHintChanged() of items for word wrapping """ super(self.__class__, self).resizeEvent(event) def startDrag(self, actions): """ Reimplemented to start the drag of a tree widget item. """ nid = self.currentItem() if nid is None: return self._dragging = nid _, node, obj = self._controller._get_node_data(nid) # Convert the item being dragged to MIME data. drag_object = node.get_drag_object(obj) md = PyMimeData.coerce(drag_object) # Render the item being dragged as a pixmap. nid_rect = self.visualItemRect(nid) rect = nid_rect.intersected(self.viewport().rect()) pm = QtGui.QPixmap(rect.size()) pm.fill(self.palette().base().color()) painter = QtGui.QPainter(pm) option = self.viewOptions() option.state |= QtWidgets.QStyle.State_Selected option.rect = QtCore.QRect(nid_rect.topLeft() - rect.topLeft(), nid_rect.size()) self.itemDelegate().paint(painter, option, self.indexFromItem(nid)) painter.end() # Calculate the hotspot so that the pixmap appears on top of the # original item. hspos = self.viewport().mapFromGlobal(QtGui.QCursor.pos()) - \ nid_rect.topLeft() # Start the drag. drag = QtGui.QDrag(self) drag.setMimeData(md) drag.setPixmap(pm) drag.setHotSpot(hspos) drag.exec_(actions) def dragEnterEvent(self, e): """ Reimplemented to see if the current drag can be handled by the tree. """ # Assume the drag is invalid. e.ignore() # Check if we have a python object instance, we might be interested data = PyMimeData.coerce(e.mimeData()).instance() if data is None: return # We might be able to handle it (but it depends on what the final # target is). e.acceptProposedAction() def dragMoveEvent(self, e): """ Reimplemented to see if the current drag can be handled by the particular tree widget item underneath the cursor. """ # Assume the drag is invalid. e.ignore() action, to_node, to_object, to_index, data = self._get_action(e) if action is not None: e.acceptProposedAction() def dropEvent(self, e): """ Reimplemented to update the model and tree. """ # Assume the drop is invalid. e.ignore() control = self._controller dragging = self._dragging self._dragging = None action, to_node, to_object, to_index, data = self._get_action(e) if action == 'move' and dragging is not None: data = control._drop_object(to_node, to_object, data, False) if data is not None: _, _, from_index = control._node_index(dragging) control._move(to_node, to_object, from_index, to_index) elif action == 'append': if dragging is not None: data = control._drop_object(to_node, to_object, data, False) if data is not None: control._delete(*control._node_index(dragging)) control._append(to_node, to_object, data, False) else: data = control._drop_object(to_node, to_object, data, True) if data is not None: control._append(to_node, to_object, data, False) elif action == 'insert': if dragging is not None: data = control._drop_object(to_node, to_object, data, False) if data is not None: from_node, from_object, from_index = \ control._node_index(dragging) if ((to_object is from_object) and (to_index > from_index)): to_index -= 1 control._delete(from_node, from_object, from_index) control._insert(to_node, to_object, to_index, data, False) else: data = control._drop_object(to_node, to_object, data, True) if data is not None: control._insert(to_node, to_object, to_index, data, False) else: return self.viewport().update() e.acceptProposedAction() def _get_action(self, event): """ Work out what action on what object to perform for a drop event. """ # default values to return action = None to_node = None to_object = None to_index = None data = None control = self._controller # Get the tree widget item under the cursor. nid = self.itemAt(event.pos()) if nid is None: if control.hide_root: nid = self.invisibleRootItem() else: return (action, to_node, to_object, to_index, data) # Check that the target is not the source of a child of the source. if self._dragging is not None: pnid = nid while pnid is not None: if pnid is self._dragging: return (action, to_node, to_object, to_index, data) pnid = pnid.parent() # Dropped object data = PyMimeData.coerce(event.mimeData()).instance() # Node and object dropped onto _, node, obj = control._get_node_data(nid) # Test whether or not the dragged object is droppable on the underlying # node. if event.proposedAction() == QtCore.Qt.MoveAction and \ control._is_droppable(node, obj, data, False): # If the node we drop on is already the parent of the dropped # object simply issue a move to index 0 if control.get_parent(data) is obj: action = 'move' to_node = node to_object = obj to_index = 0 # Otherwise insert to first position in node being dropped on or # append if insertion is not allowed elif control._is_droppable(node, obj, data, True): action = 'insert' to_node = node to_object = obj to_index = 0 else: action = 'append' to_node = node to_object = obj to_index = None else: # get parent of node being dropped on to_node, to_object, to_index = control._node_index(nid) if to_node is None: # no parent, can't do anything action = None elif control._is_droppable(to_node, to_object, data, True): if to_object is control.get_parent(data): action = 'move' # insert into the parent of the node being dropped on else: action = 'insert' # Move or insert below the node dropped onto to_index += 1 elif control._is_droppable(to_node, to_object, data, False): # append to the parent of the node being dropped on action = 'append' else: # parent can't be modified, can't do anything action = None return (action, to_node, to_object, to_index, data)