In my example I want users to have the ability to drag and drop children between families of all kinds. However I want to restrict users from having the ability to drag and drop a person onto another person, or dropping a person as a root object which in return would make it become a family.
I've gotten very close. There is a single bug though. If a user drags a person to a family, while hovering over the cursor over the family, if you carefully move the cursor to the top most edge of the family you'll see the indicator allowing you to drop the item so it's above the Family, not inside of Family. The code tests true and allows the user to successfully drop the person. The result places the person outside of the family. The image below shows what i mean.
The main focus on the code is between lines 75 - 125.
# Imports
# ------------------------------------------------------------------------------
import sys
from PySide import QtCore, QtGui
# Class Object
# ------------------------------------------------------------------------------
class Person():
def __init__(self, name="", age=0):
self.name = name
self.age = age
class Family():
def __init__(self, name=""):
self.name = name
DROP_VALIDATION_DICT = {
Person : ( Family, None ),
Family : ( None, ),
}
# Custom QTreeWidgetItem
# ------------------------------------------------------------------------------
class CustomTreeNode( QtGui.QTreeWidgetItem ):
def __init__( self, parent, data ):
super( CustomTreeNode, self ).__init__( parent )
self.data = data
@property
def data(self):
return self._data
@data.setter
def data(self, value):
self._data = value
self.setText( 0, self.data.name )
def update(self):
if self.data:
self.setText(0, self.data.name)
# Custom QTreeWidgetItem
# ------------------------------------------------------------------------------
class CustomTreeWidget( QtGui.QTreeWidget ):
def __init__(self, parent=None):
QtGui.QTreeWidget.__init__(self, parent)
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.setItemsExpandable(True)
self.setAnimated(True)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setAlternatingRowColors(True)
# self._dragroot = self.itemRootIndex() # added
# signals
self.itemExpanded.connect(self.on_item_expanded)
self.itemCollapsed.connect(self.on_item_collapsed)
def set_headers(self, headers=[]):
self.setColumnCount( len(headers) )
self.setHeaderLabels( headers )
def keyPressEvent(self, event):
if (event.key() == QtCore.Qt.Key_Escape and
event.modifiers() == QtCore.Qt.NoModifier):
self.clearSelection()
else:
QtGui.QTreeWidget.keyPressEvent(self, event)
def mousePressEvent(self, event):
item = self.itemAt(event.pos())
if item is None:
self.clearSelection()
QtGui.QTreeWidget.mousePressEvent(self, event)
def on_item_expanded(self):
key_mod = QtGui.QApplication.keyboardModifiers()
if key_mod == QtCore.Qt.ControlModifier:
self.expandAll()
def on_item_collapsed(self):
key_mod = QtGui.QApplication.keyboardModifiers()
if key_mod == QtCore.Qt.ControlModifier:
self.collapseAll()
# drag-n-drop with constraints
def dragEnterEvent(self, event):
item = self.itemAt(event.pos())
if item is not None and ( isinstance(item.data, Family) ):
# if event.mimeData().hasUrls():
event.acceptProposedAction()
else:
super(CustomTreeWidget, self).dragEnterEvent(event)
def dragMoveEvent(self, event):
print "testing"
print "moving..."
item = self.itemAt(event.pos())
rect = self.visualItemRect(item)
# QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.ForbiddenCursor))
print rect, event.pos()
print "Cursor: ", event.pos().y(), event.pos().y()+2
if event.pos().y() < rect.y()+2:
print "not inside"
else:
print "inside"
super(CustomTreeWidget, self).dragMoveEvent(event)
def dropEvent(self, event):
item = self.itemAt(event.pos())
if item is not None and ( isinstance(item.data, Family) ):
super(CustomTreeWidget,self).dropEvent(event)
else:
print "ignored"
event.setDropAction(QtCore.Qt.IgnoreAction)
# UI
# ------------------------------------------------------------------------------
class ExampleWidget(QtGui.QWidget):
def __init__( self, parent=None ):
super(ExampleWidget, self).__init__()
self.initUI()
def initUI(self):
# widgets
self.treewidget = CustomTreeWidget()
self.treewidget.set_headers( ["items"] )
self.btn_add_family_tree_node = QtGui.QPushButton("Add Family")
self.btn_add_person_tree_node = QtGui.QPushButton("Add Person")
# layout
self.mainLayout = QtGui.QGridLayout(self)
self.mainLayout.addWidget(self.btn_add_family_tree_node, 0,0)
self.mainLayout.addWidget(self.btn_add_person_tree_node, 0,1)
self.mainLayout.addWidget(self.treewidget, 1,0,1,3)
# signals
self.btn_add_family_tree_node.clicked.connect(self.add_family_tree_node_clicked)
self.btn_add_person_tree_node.clicked.connect(self.add_person_tree_node_clicked)
# display
self.resize(300, 400)
self.show()
self.center_window(self, True)
# test data
self.add_test_families("Roberstons", ["Kevin", "Mindy", "Riley"])
self.add_test_families("Rodriguez", ["Matt", "Kim", "Stephanie"])
def add_test_families(self, name, children):
family = Family( name )
node = CustomTreeNode( self.treewidget, family )
node.setExpanded(True)
# # disable drag and drop flags
# # QtCore.Qt.ItemIsDragEnabled
# flags = QtCore.Qt.ItemIsSelectable | \
# QtCore.Qt.ItemIsUserCheckable | \
# QtCore.Qt.ItemIsEnabled | \
# QtCore.Qt.ItemIsDropEnabled
# node.setFlags(flags)
for i in xrange(len(children)):
person = Person( children[i] )
CustomTreeNode( node, person )
# Functions
# --------------------------------------------------------------------------
def add_family_tree_node_clicked(self):
print "Created new family!"
roots = [self.treewidget]
text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter family name:')
if ok:
for root in roots:
family = Family( text )
node = CustomTreeNode( root, family )
node.setExpanded(True)
self.treewidget.itemSelectionChanged.emit()
def add_person_tree_node_clicked(self):
print "Created new person!"
roots = self.treewidget.selectedItems()
text, ok = QtGui.QInputDialog.getText(self, 'Input Dialog', 'Enter your name:')
if ok:
for root in roots:
person = Person( text )
node = CustomTreeNode( root, person )
node.setExpanded(True)
self.treewidget.itemSelectionChanged.emit()
def center_window(self, window , cursor=False):
qr = window.frameGeometry()
cp = QtGui.QCursor.pos() if cursor else QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
window.move(qr.topLeft())
# __name__
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = ExampleWidget()
sys.exit(app.exec_())