391 lines
14 KiB
Python
391 lines
14 KiB
Python
import sys
|
|
import os
|
|
from PyQt6 import QtGui, QtCore
|
|
from PyQt6.QtGui import *
|
|
from PyQt6.QtWidgets import *
|
|
from PyQt6.QtCore import *
|
|
from copy import deepcopy
|
|
import pickle
|
|
import typing
|
|
|
|
"""
|
|
https://stackoverflow.com/questions/22020091/how-to-handle-drag-and-drop-properly-using-pyqt-qabstractitemmodel
|
|
|
|
Here is a code I ended up after two days of TreeView/Model madness. The subject appeared to be much more broad
|
|
than I thought.
|
|
I barely can spend so much time creating a singe widget.
|
|
Anyway.
|
|
The drag-and-drop functionality of TreeView items has been enabled.
|
|
But other than few interesting printout there is not much there.
|
|
The double click on an item allows the user to enter a new item name which won't be picked up.
|
|
EDITED A DAY LATER WITH A REVISED CODE.
|
|
It is now by 90% functional tool.
|
|
|
|
The user can manipulate the TreeView items by drag and dropping, creating/duplicating/deleting and renaming.
|
|
The TreeView items are representing the directories or folders in hierarchical fashion before they are created on
|
|
a drive by hitting 'Print' button (instead of os.makedirs() the tool still simply prints each directory as a string.
|
|
I would say I am pretty happy with the result. Thanks to hackyday and to everyone who responded and helped with my
|
|
questions.
|
|
|
|
A few last wishes...
|
|
A wish number 01:
|
|
|
|
I wish the PrintOut() method would use a more elegant smarter function to loop through the TreeView items to
|
|
build a dictionary that is being passed to make_dirs_from_dict() method.
|
|
|
|
A wish number 02:
|
|
|
|
I wish deleting the items would be more stable. By some unknown reason a tool crashes on third/fourth Delete button
|
|
clicks. So far, I was unable to trace the problem down.
|
|
|
|
A wish number 03: 3. I wish everyone the best and thanks for your help :
|
|
|
|
REPLY:
|
|
I am not totally sure what you are trying to achieve, but it sounds like you want to retrieve the dragged item in the
|
|
drop operation, and have double click save a new node name.
|
|
|
|
Firstly, you need to save the dragged item into the mimeData. Currently, you are only saving the string 'mimeData',
|
|
which doesn't tell you much. The mimeType string that it is saved as (here I used 'bstream') can actually be anything.
|
|
As long as it matches what you use to retrieve the data, and is in the list returned by the mimeTypes method of the
|
|
model.
|
|
|
|
To pass the object itself, you must first serialize it (you can convert your object to xml alternatively,
|
|
if that was something you are planning on doing), since it is not a standard type for mime data.
|
|
In order for the data you enter to be saved you must re-implement the setData method of the model and define
|
|
behaviour for EditRole.
|
|
|
|
EDIT:
|
|
That is a lot of code you updated, but I will oblige on the points you highlighted. Avoid calling createIndex outside
|
|
of the model class. This is a protected method in Qt; Python doesn't enforce private/protected variables or methods,
|
|
but when using a library from another language that does, I try to respect the intended organization of the classes,
|
|
and access to them.
|
|
|
|
The purpose of the model is to provide an interface to your data. You should access it using the index, data,
|
|
parent etc. public functions of the model.
|
|
o get the parent of a given index, use that index's (or the model's) parent function,
|
|
which will also return a QModelIndex. This way, you don't have to go through (or indeed know about) the internal
|
|
structure of the data. This is what I did in the deleteLevel method.
|
|
|
|
From the qt docs:
|
|
|
|
To ensure that the representation of the data is kept separate from the way it is accessed, the concept of a model
|
|
index is introduced. Each piece of information that can be obtained via a model is represented by a model index...
|
|
only the model needs to know how to obtain data, and the type of data managed by the model can be defined fairly
|
|
generally.
|
|
|
|
Also, you can use recursion to simplify the print method.
|
|
"""
|
|
|
|
|
|
class TreeItem(object):
|
|
def __init__(self, name, parent=None):
|
|
self.name = str(name)
|
|
self.parent = parent
|
|
self.children = []
|
|
self.setParent(parent)
|
|
|
|
def setParent(self, parent):
|
|
if parent is not None:
|
|
self.parent = parent
|
|
self.parent.appendChild(self)
|
|
else:
|
|
self.parent = None
|
|
|
|
def appendChild(self, child):
|
|
self.children.append(child)
|
|
|
|
def childAtRow(self, row):
|
|
if len(self.children) > row:
|
|
return self.children[row]
|
|
|
|
def rowOfChild(self, child):
|
|
for i, item in enumerate(self.children):
|
|
if item == child:
|
|
return i
|
|
return -1
|
|
|
|
def removeChild(self, row):
|
|
value = self.children[row]
|
|
self.children.remove(value)
|
|
return True
|
|
|
|
def __len__(self):
|
|
return len(self.children)
|
|
|
|
|
|
class TreeModel(QtCore.QAbstractItemModel):
|
|
def __init__(self):
|
|
|
|
QtCore.QAbstractItemModel.__init__(self)
|
|
|
|
self.columns = 1
|
|
self.clickedItem = None
|
|
|
|
self.root = TreeItem('root', None)
|
|
# each instance of TreeItem adds himself as a child to its parent
|
|
levelA = TreeItem('levelA', self.root)
|
|
levelB = TreeItem('levelB', levelA)
|
|
levelC1 = TreeItem('levelC1', levelB)
|
|
levelC2 = TreeItem('levelC2', levelB)
|
|
levelC3 = TreeItem('levelC3', levelB)
|
|
levelD = TreeItem('levelD', levelC3)
|
|
|
|
levelE = TreeItem('levelE', levelD)
|
|
levelF = TreeItem('levelF', levelE)
|
|
|
|
def nodeFromIndex(self, index):
|
|
return index.internalPointer() if index.isValid() else self.root
|
|
|
|
def index(self, row: int, column: int, parent: QModelIndex = ...) -> QModelIndex:
|
|
node = self.nodeFromIndex(parent)
|
|
return self.createIndex(row, column, node.childAtRow(row))
|
|
|
|
def parent(self, child):
|
|
# print '\n parent(child)', child # PyQt4.QtCore.QModelIndex
|
|
if not child.isValid():
|
|
return QModelIndex()
|
|
node = self.nodeFromIndex(child)
|
|
if node is None:
|
|
return QModelIndex()
|
|
parent = node.parent
|
|
if parent is None:
|
|
return QModelIndex()
|
|
grandparent = parent.parent
|
|
|
|
if grandparent is None:
|
|
return QModelIndex()
|
|
|
|
row = grandparent.rowOfChild(parent)
|
|
assert row != - 1
|
|
|
|
return self.createIndex(row, 0, parent)
|
|
|
|
def rowCount(self, parent: QModelIndex = ...) -> int:
|
|
node = self.nodeFromIndex(parent)
|
|
if node is None:
|
|
return 0
|
|
return len(node)
|
|
|
|
def columnCount(self, parent: QModelIndex = ...) -> int:
|
|
return self.columns
|
|
|
|
def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
|
|
if role == Qt.ItemDataRole.DecorationRole:
|
|
return QVariant()
|
|
if role == Qt.ItemDataRole.TextAlignmentRole:
|
|
return QVariant(int(Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft))
|
|
if role != Qt.ItemDataRole.DisplayRole:
|
|
return QVariant()
|
|
|
|
node = self.nodeFromIndex(index)
|
|
if index.column() == 0:
|
|
return QVariant(node.name)
|
|
elif index.column() == 1:
|
|
return QVariant(node.state)
|
|
elif index.column() == 2:
|
|
return QVariant(node.description)
|
|
else:
|
|
return QVariant()
|
|
|
|
def supportedDropActions(self):
|
|
return Qt.DropAction.CopyAction | Qt.DropAction.MoveAction
|
|
|
|
def flags(self, index):
|
|
defaultFlags = QAbstractItemModel.flags(self, index)
|
|
if index.isValid():
|
|
return (Qt.ItemFlag.ItemIsEditable | Qt.ItemFlag.ItemIsDragEnabled |
|
|
Qt.ItemFlag.ItemIsDropEnabled | defaultFlags)
|
|
else:
|
|
return Qt.ItemIsDropEnabled | defaultFlags
|
|
|
|
def setData(self, index: QModelIndex, value: typing.Any, role: int = ...) -> bool:
|
|
if role == Qt.EditRole:
|
|
if value.toString() and len(value.toString()) > 0:
|
|
self.nodeFromIndex(index).name = value.toString()
|
|
self.dataChanged.emit(index, index)
|
|
return True
|
|
|
|
def mimeTypes(self):
|
|
return ['bstream', 'text/xml']
|
|
|
|
def mimeData(self, indexes):
|
|
mimedata = QMimeData()
|
|
bstream = pickle.dumps(self.nodeFromIndex(indexes[0]))
|
|
mimedata.setData('bstream', bstream)
|
|
return mimedata
|
|
|
|
def dropMimeData(self, mimedata, action, row, column, parentIndex):
|
|
if action == Qt.DropAction.IgnoreAction:
|
|
return True
|
|
|
|
droppedNode = pickle.loads(mimedata.data('bstream'))
|
|
droppedIndex = self.createIndex(row, column, droppedNode)
|
|
|
|
parentNode = self.nodeFromIndex(parentIndex)
|
|
|
|
newNode = deepcopy(droppedNode)
|
|
newNode.setParent(parentNode)
|
|
|
|
self.insertRow(len(parentNode)-1, parentIndex)
|
|
# self.emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
|
|
self.dataChanged.emit(parentIndex, parentIndex)
|
|
|
|
return True
|
|
|
|
def insertRow(self, row: int, parent: QModelIndex = ...) -> bool:
|
|
return self.insertRows(row, 1, parent)
|
|
|
|
def insertRows(self, row: int, count: int, parent: QModelIndex = ...) -> bool:
|
|
self.beginInsertRows(parent, row, (row + (count - 1)))
|
|
self.endInsertRows()
|
|
return True
|
|
|
|
def removeRow(self, row: int, parent: QModelIndex = ...) -> bool:
|
|
ret = self.removeRows(row, 1, parent)
|
|
return ret
|
|
|
|
def removeRows(self, row: int, count: int, parent: QModelIndex = ...) -> bool:
|
|
self.beginRemoveRows(parent, row, row)
|
|
node = self.nodeFromIndex(parent)
|
|
node.removeChild(row)
|
|
self.endRemoveRows()
|
|
return True
|
|
|
|
|
|
class GUI(QDialog):
|
|
def __init__(self, parent=None, *args, **kwargs):
|
|
super().__init__(parent, *args, **kwargs)
|
|
self.myWidget = None
|
|
self.boxLayout = None
|
|
self.treeView = None
|
|
self.treeModel = None
|
|
self.PrintButton = None
|
|
self.DeleteButton = None
|
|
self.insertButton = None
|
|
self.duplicateButton = None
|
|
|
|
def build(self, my_window):
|
|
my_window.resize(600, 400)
|
|
self.myWidget = QWidget(my_window)
|
|
self.boxLayout = QVBoxLayout(self.myWidget)
|
|
|
|
self.treeView = QTreeView()
|
|
|
|
self.treeModel = TreeModel()
|
|
self.treeView.setModel(self.treeModel)
|
|
self.treeView.expandAll()
|
|
self.treeView.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
|
|
# self.treeView.connect(
|
|
# self.treeView.model(), SIGNAL("dataChanged(QModelIndex,QModelIndex)"), self.onDataChanged)
|
|
self.treeView.model().dataChanged.connect(self.onDataChanged)
|
|
# QtCore.QObject.connect(self.treeView, QtCore.SIGNAL("clicked (QModelIndex)"), self.treeItemClicked)
|
|
self.treeView.clicked.connect(self.treeItemClicked)
|
|
self.boxLayout.addWidget(self.treeView)
|
|
|
|
self.PrintButton = QPushButton("Print")
|
|
self.PrintButton.clicked.connect(self.printOut)
|
|
self.boxLayout.addWidget(self.PrintButton)
|
|
|
|
self.DeleteButton = QPushButton("Delete")
|
|
self.DeleteButton.clicked.connect(self.deleteLevel)
|
|
self.boxLayout.addWidget(self.DeleteButton)
|
|
|
|
self.insertButton = QPushButton("Insert")
|
|
self.insertButton.clicked.connect(self.insertLevel)
|
|
self.boxLayout.addWidget(self.insertButton)
|
|
|
|
self.duplicateButton = QPushButton("Duplicate")
|
|
self.duplicateButton.clicked.connect(self.duplicateLevel)
|
|
self.boxLayout.addWidget(self.duplicateButton)
|
|
|
|
my_window.setCentralWidget(self.myWidget)
|
|
|
|
def make_dirs_from_dict(self, dirDict, current_dir='C:\\'):
|
|
for key, val in dirDict.items():
|
|
os.mkdir(os.path.join(current_dir, key))
|
|
print("Creating directory: ", os.path.join(current_dir, key))
|
|
if type(val) == dict and val:
|
|
self.make_dirs_from_dict(val, os.path.join(current_dir, val))
|
|
|
|
def printOut(self):
|
|
result_dict = dictify(self.treeView.model().root)
|
|
self.make_dirs_from_dict(result_dict)
|
|
|
|
def deleteLevel(self):
|
|
if len(self.treeView.selectedIndexes()) == 0:
|
|
return
|
|
|
|
currentIndex = self.treeView.selectedIndexes()[0]
|
|
self.treeView.model().removeRow(currentIndex.row(), currentIndex.parent())
|
|
|
|
def insertLevel(self):
|
|
if len(self.treeView.selectedIndexes()) == 0:
|
|
return
|
|
|
|
currentIndex = self.treeView.selectedIndexes()[0]
|
|
currentNode = currentIndex.internalPointer()
|
|
try:
|
|
newItem = TreeItem('Brand New', currentNode)
|
|
self.treeView.model().insertRow(len(currentNode)-1, currentIndex)
|
|
# self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), currentIndex, currentIndex)
|
|
except Exception as err:
|
|
print(err)
|
|
self.treeView.model().dataChanged.emit(currentIndex, currentIndex)
|
|
|
|
def duplicateLevel(self):
|
|
if len(self.treeView.selectedIndexes()) == 0:
|
|
return
|
|
|
|
currentIndex = self.treeView.selectedIndexes()[0]
|
|
currentRow = currentIndex.row()
|
|
currentColumn = currentIndex.column()
|
|
currentNode = currentIndex.internalPointer()
|
|
|
|
parentNode = currentNode.parent
|
|
parentIndex = self.treeView.model().createIndex(currentRow, currentColumn, parentNode)
|
|
parentRow = parentIndex.row()
|
|
parentColumn = parentIndex.column()
|
|
|
|
newNode = deepcopy(currentNode)
|
|
newNode.setParent(parentNode)
|
|
|
|
self.treeView.model().insertRow(len(parentNode)-1, parentIndex)
|
|
# self.treeView.model().emit(SIGNAL("dataChanged(QModelIndex,QModelIndex)"), parentIndex, parentIndex)
|
|
self.treeView.model().dataChanged.emit(parentIndex, parentIndex)
|
|
|
|
print(
|
|
'\n\t\t\t CurrentNode:', currentNode.name, ', ParentNode:', parentNode.name, ', currentColumn:',
|
|
currentColumn, ', currentRow:', currentRow, ', parentColumn:', parentColumn, ', parentRow:', parentRow
|
|
)
|
|
self.treeView.update()
|
|
self.treeView.expandAll()
|
|
|
|
def treeItemClicked(self, index):
|
|
print("\n clicked item ----------->", index.internalPointer().name)
|
|
|
|
def onDataChanged(self, indexA, indexB):
|
|
print("\n onDataChanged NEVER TRIGGERED! ####################### \n ", indexA.internalPointer().name)
|
|
self.treeView.update(indexA)
|
|
self.treeView.expandAll()
|
|
# self.treeView.expanded()
|
|
|
|
|
|
def dictify(node):
|
|
kids = {}
|
|
try:
|
|
for child in node.children:
|
|
kids.update(dictify(child))
|
|
except TypeError:
|
|
return {str(node.name): kids}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
app = QApplication(sys.argv)
|
|
|
|
myWindow = QMainWindow()
|
|
myGui = GUI()
|
|
myGui.build(myWindow)
|
|
myWindow.show()
|
|
sys.exit(app.exec())
|