How do i retrieve the correctly selected item from my custom QAbstractListModel which contains a custom sorting algorithm?
You can test the tool by simply making selections in the UI and looking at the console. You can see it's printing the wrong information for the selected item.
I'm assuming the issue relates to how i use the selection indexes to get the item in the Model.
complete code:
import os, sys
from PySide import QtGui, QtCore
class ExplorerItem(object):
def __init__(self, name, tags):
self.name = name
self.tags = tags
class ElementModel(QtCore.QAbstractListModel):
TagsRole = QtCore.Qt.UserRole + 1
NameRole = QtCore.Qt.UserRole + 2
def __init__(self, *args, **kwargs):
QtCore.QAbstractListModel.__init__(self, *args, **kwargs)
self._items = []
self._icons = {}
def rowCount(self, index=QtCore.QModelIndex()):
return len(self._items)
def addItem(self, item):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self._items.append(item)
self.endInsertRows()
def getItem(self, index):
row = index.row()
if index.isValid() and 0 <= row < self.rowCount():
return self._items[row]
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if 0 <= index.row() < self.rowCount():
item = self._items[index.row()]
if role == ElementModel.TagsRole:
return item.tags
elif role == ElementModel.NameRole:
return item.colors
elif role == QtCore.Qt.DisplayRole:
return item.name
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignCenter
class ExplorerSortModel(QtGui.QSortFilterProxyModel):
def __init__(self, *args, **kwargs):
super(ExplorerSortModel, self).__init__(*args, **kwargs)
self._patterns = {}
self.setDynamicSortFilter(True)
self.setSourceModel(ElementModel())
self.sort(0, QtCore.Qt.AscendingOrder)
def set_pattern(self, role, value):
self._patterns[role] = value
def lessThan(self, left, right):
leftData = self.sourceModel()._items[left.row()]
rightData = self.sourceModel()._items[right.row()]
if leftData and rightData:
l = getattr(leftData, 'name', '')
r = getattr(rightData, 'name', '')
return l > r
return True
def filterAcceptsRow(self, sourceRow, sourceParent):
sm = self.sourceModel()
ix = sm.index(sourceRow)
if ix.isValid():
val = True
for role, fvalue in self._patterns.items():
value = ix.data(role)
val = val and self.filter(value, fvalue, role)
return val
return False
#staticmethod
def filter(value, fvalue, role):
'''
fvalue: search value
value: properties value being tested
'''
if role == ElementModel.TagsRole:
if fvalue == []:
return True
else:
return all(any(x in y for y in value) for x in fvalue)
elif role == ElementModel.NameRole:
return True
else:
return False
class QExplorerWidget(QtGui.QWidget):
def __init__(self, *args, **kwargs):
super(QExplorerWidget, self).__init__(*args, **kwargs)
self.resize(400,400)
# control
self.ui_explorer = QtGui.QListView()
self.ui_explorer.setResizeMode(QtGui.QListView.Adjust)
self.ui_explorer.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.ui_explorer.setMovement(QtGui.QListView.Static)
self.ui_explorer.setSpacing(10)
self.explorer_model = ExplorerSortModel()
self.ui_explorer.setModel(self.explorer_model)
self.ui_explorer_selection = self.ui_explorer.selectionModel()
lay = QtGui.QVBoxLayout()
lay.addWidget(self.ui_explorer)
self.setLayout(lay)
# connections
self.ui_explorer_selection.selectionChanged.connect(self.changed_selection)
# test data
self.explorer_model.sourceModel().addItem(ExplorerItem('John',['john','sports']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Apple',['apple','fruit']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Kevin',['kevin','money']))
self.explorer_model.sourceModel().addItem(ExplorerItem('Zoo',['zoo','animals']))
def changed_selection(self):
indexes = self.ui_explorer_selection.selectedIndexes()
for index in indexes:
item = self.explorer_model.sourceModel().getItem(index)
print item.name, item.tags, index
if __name__ == '__main__':
''
app = QtGui.QApplication(sys.argv)
ex = QExplorerWidget()
ex.show()
sys.exit(app.exec_())
The QModelIndex of the selectedIndexes belong to the model that was established in the view, and in this case it is the ExplorerSortModel, so these indexes can not be passed directly to the getItem() method of ElementModel since that method expects that the QModelIndex belongs to ElementModel.
In your case you must convert that QModelIndex belonging to ExplorerSortModel to the corresponding QModelIndex that belongs to ElementModel using the mapSource() method.
def changed_selection(self):
indexes = self.ui_explorer_selection.selectedIndexes()
for index in indexes:
ix_source = self.explorer_model.mapToSource(index)
item = self.explorer_model.sourceModel().getItem(ix_source)
print(item.name, item.tags)
Related
I have this TreeItem:
class QJsonTreeItem(object):
def __init__(self, data, parent=None):
self._parent = parent
self._key = ""
self._value = ""
self._type = None
self._children = list()
self.itemData = data
...
def data(self, column):
if column is 0:
return self.key
elif column is 1:
return self.value
def setData(self, column, value):
if column is 0:
self.key = value
if column is 1:
self.value = value
...
def insertChildren(self, position, rows, columns):
if position < 0 or position > len(self._children):
return False
for row in range(rows):
data = [None for v in range(columns)]
item = QJsonTreeItem(data, self)
self._children.insert(position, item)
return True
...
And custom QAbstractItemModel:
class QJsonTreeModel(QAbstractItemModel):
def __init__(self, parent=None):
super(QJsonTreeModel, self).__init__(parent)
self._rootItem = QJsonTreeItem(["Key", "Value"])
self._headers = ("Key", "Value")
...
def data(self, index, role):
if not index.isValid():
return None
if role != Qt.DisplayRole and role != Qt.EditRole:
return None
item = index.internalPointer()
if role == Qt.DisplayRole or role == Qt.EditRole:
if index.column() == 0:
return item.data(index.column())
if index.column() == 1:
return item.value
return None
def getItem(self, index):
if index.isValid():
item = index.internalPointer()
if item:
return item
return self._rootItem
def setData(self, index, value, role):
if role == Qt.EditRole:
item = index.internalPointer()
item.setData(index.column(), value)
self.dataChanged.emit(index, index, [Qt.EditRole])
return True
return False
def parent(self, index):
if not index.isValid():
return QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self._rootItem:
return QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
...
def insertRows(self, position, rows, parent, *args, **kwargs):
parentItem = self.getItem(parent)
self.beginInsertRows(parent, position, position + rows - 1)
success = parentItem.insertChildren(position, rows, self._rootItem.columnCount())
self.endInsertRows()
return success
And in my MainWindow file there is a button for adding new items in QTreeView which looks like this:
self.treeView = QTreeView()
self.model = QJsonTreeModel()
self.treeView.setModel(self.model)
...
rightClickMenu = QMenu()
actionAddItem = rightClickMenu.addAction(self.tr("Add Item"))
actionAddItem.triggered.connect(partial(self.treeAddItem))
...
def treeAddItem(self):
try:
index = self.treeView.selectionModel().currentIndex()
parent = index.parent()
if self.model.data(parent, Qt.EditRole) == None:
if not self.model.insertRow(index.row() + 1, parent):
return
for column in range(self.model.columnCount(parent)):
child = self.model.index(index.row() + 1, column, parent)
self.model.setData(child, "[No data]", Qt.EditRole)
else:
pass
except Exception as exception:
QMessageBox.about(self, "Exception", "Exception in treeAddItem() function: " + str(exception))
return
The question is can I somehow add not an "[No data]" string for QtreeView, but fo example an empty dict() or list()? As far as I understand it only adds an empty strings to the QTreeView, but my task still needs dictionaries and list. If it`s not possible my main idea is to get full tree back to dictionary and to work directly with dictionary items and then load back changed dict to tree, but it seems kinda "bad style".
Can someone help me with this task or offer another idea?
The soultion I did is this:
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
item = index.internalPointer()
item.setData(index.column(), value)
self.dataChanged.emit(index, index, [Qt.EditRole])
return True
if role == Qt.DisplayRole:
item = index.internalPointer()
item.setData(index.column(), dict())
self.dataChanged.emit(index, index, [Qt.EditRole])
return True
if role == Qt.ToolTipRole:
item = index.internalPointer()
item.setData(index.column(), list())
self.dataChanged.emit(index, index, [Qt.EditRole])
return True
return False
How do I properly remove items from my custom QAbstractTableModel? Do i need to change this to QStandardItemModel instead?
This is the before:
This is the after...it leaves empty rows and the selection doesn't seem to clear either.
import os
import sys
from PySide import QtCore, QtGui
import random
class CustomJobs(object):
def __init__(self, **kwargs):
super(CustomJobs, self).__init__()
# instance properties
self.name = ''
self.status = ''
# initialize attribute values
for k, v in kwargs.items():
if hasattr(self, k):
setattr(self, k, v)
class PlayblastTableModel(QtCore.QAbstractTableModel):
HEADERS = ['Name', 'Status']
def __init__(self):
super(PlayblastTableModel, self).__init__()
self.items = []
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
return self.HEADERS[section]
return None
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.HEADERS)
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def addItem(self, *items):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount() + len(items) - 1)
for item in items:
assert isinstance(item, CustomJobs)
self.items.append(item)
self.endInsertRows()
def removeItems(self, items):
self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.items = [x for x in self.items if x not in items]
self.endRemoveRows()
def clear(self):
self.beginRemoveRows(QtCore.QModelIndex(), 0, self.rowCount())
self.items = []
self.endRemoveRows()
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
row = index.row()
col = index.column()
if 0 <= row < self.rowCount():
item = self.items[row]
if role == QtCore.Qt.DisplayRole:
if col == 0:
return item.name
elif col == 1:
return item.status.title()
elif role == QtCore.Qt.UserRole:
return item
return None
class CustomJobsQueue(QtGui.QWidget):
'''
Description:
Widget that manages the Jobs Queue
'''
def __init__(self):
super(CustomJobsQueue, self).__init__()
self.resize(400,600)
# controls
self.uiAddNewJob = QtGui.QPushButton('Add')
self.uiRemoveSelectedJobs = QtGui.QPushButton('Remove')
self.playblastJobModel = PlayblastTableModel()
self.uiJobTableView = QtGui.QTableView()
self.uiJobTableView.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.uiJobTableView.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
self.uiJobTableView.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.uiJobTableView.setModel(self.playblastJobModel)
self.jobSelection = self.uiJobTableView.selectionModel()
# sub layouts
self.jobQueueToolsLayout = QtGui.QHBoxLayout()
self.jobQueueToolsLayout.addWidget(self.uiAddNewJob)
self.jobQueueToolsLayout.addWidget(self.uiRemoveSelectedJobs)
self.jobQueueToolsLayout.addStretch()
# layout
self.mainLayout = QtGui.QVBoxLayout()
self.mainLayout.addLayout(self.jobQueueToolsLayout)
self.mainLayout.addWidget(self.uiJobTableView)
self.setLayout(self.mainLayout)
# connections
self.uiAddNewJob.clicked.connect(self.addNewJob)
self.uiRemoveSelectedJobs.clicked.connect(self.removeSelectedJobs)
# methods
def addNewJob(self):
name = random.choice(['Kevin','Suzie','Melissa'])
status = random.choice(['error','warning','successa'])
job = CustomJobs(name=name, status=status)
self.playblastJobModel.addItem(job)
def removeSelectedJobs(self):
jobs = self.getSelectedJobs()
self.playblastJobModel.removeItems(jobs)
def getSelectedJobs(self):
jobs = [x.data(QtCore.Qt.UserRole) for x in self.jobSelection.selectedRows()]
return jobs
def main():
app = QtGui.QApplication(sys.argv)
window = CustomJobsQueue()
window.show()
app.exec_()
if __name__ == '__main__':
main()
The reason for this behavior is that you're using the wrong row in beginRemoveRows(): you should use the row number you're removing, and since you're using rowCount() that row index is invalid.
def removeItems(self, items):
self.beginRemoveRows(QtCore.QModelIndex(), self.rowCount() - 1, self.rowCount() - 1)
self.items = [x for x in self.items if x not in items]
self.endRemoveRows()
To be more correct, you should remove the actual rows in the model. In your simple case it won't matter that much, but in case your model becomes more complex, keep in mind this.
def removeItems(self, items):
removeRows = []
for row, item in enumerate(self.items):
if item in items:
removeRows.append(row)
for row in sorted(removeRows, reverse=True):
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.items.pop(row)
self.endRemoveRows()
The reason for the reversed row order in the for cycle is that for list consistency reasons the row removal should always begin from the bottom. This can be important if you want to remove rows arbitrarily while keeping the current selection in case the removed items are not selected.
That said, as already suggested in the comments, if you don't need specific behavior and implementation, creating a QAbstractItemModel (or any abstract model) subclass is unnecessary, as QStandardItemModel will usually be enough, as it already provides all required features (including drag and drop support, which can be rather complex if you don't know how the Qt data model works).
Well, unless it's for learning purposes, obviously.
I have this simple example: a value in last column of my QAbstractTableModel equals value in column 1 multiplied by 2. So every time a change is made to value in column 1 - it results to a change in column 2.
When the value in the last column has changed - a message box is shown asking whether user wishes confirm action.
Imagine user have changed value in column 1 and sees this message: I wish my app to undo changes if cancel is clicked (return both old values in column 1 and 2), can you help me with that? I need to define my 'go_back()' function:
class Mainwindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.setCentralWidget(self.table)
self.data = [
[1, 0.18, 0.36],
[2, 0.25, 0.50],
[3, 0.43, 0.86],
]
self.model = MyModel(self.data)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.setSelectionMode(self.table.SingleSelection)
self.model.dataChanged.connect(lambda index: self.count_last_column(index))
self.model.dataChanged.connect(lambda index: self.if_last_column_changed(index))
def calculations(self, position):
value = self.model.list_data[position][1] * 2
return value
def count_last_column(self, index):
if index.column() == 1:
position = index.row()
self.model.setData(self.model.index(position,2), self.calculations(position))
def if_last_column_changed(self, index):
if index.column() == 2:
message_box, message_box_button = self.show_message_box()
if message_box_button == 'Ok':
pass
elif message_box_button == 'Cancel':
self.go_back()
def show_message_box(self):
self.message_box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning, 'Action', 'Value in column 3 has changed, confirm action?')
self.message_box.Ok = self.message_box.addButton(QtWidgets.QMessageBox.Ok)
self.message_box.Cancel = self.message_box.addButton(QtWidgets.QMessageBox.Cancel)
self.message_box.exec()
if self.message_box.clickedButton() == self.message_box.Ok:
return (self.message_box, 'Ok')
elif self.message_box.clickedButton() == self.message_box.Cancel:
return (self.message_box, 'Cancel')
def go_back(self):
pass #################
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, list_data = [[]], parent = None):
super(MyModel, self).__init__()
self.list_data = list_data
def rowCount(self, parent):
return len(self.list_data)
def columnCount(self, parent):
return len(self.list_data[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.list_data[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.list_data[row][column]
return value
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.list_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
One option is to use a QUndoStack with the item model, and push QUndoCommand objects onto the stack in setData. The benefit of this approach is that it makes it simple to implement more undo/redo controls going forward, if you want to.
In MyModel, just create a stack in the constructor and add a line to push a command onto the stack right before you modify the list data (so the previous value can be stored in the command). The rest of the class is unchanged.
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, list_data = [[]], parent = None):
super(MyModel, self).__init__()
self.list_data = list_data
self.stack = QtWidgets.QUndoStack()
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.stack.push(CellEdit(index, value, self))
self.list_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
Create the QUndoCommand with the index, value, and model passed to the constructor so the desired cell can be modified with calls to undo or redo.
class CellEdit(QtWidgets.QUndoCommand):
def __init__(self, index, value, model, *args, **kwargs):
super().__init__(*args, **kwargs)
self.index = index
self.value = value
self.prev = model.list_data[index.row()][index.column()]
self.model = model
def undo(self):
self.model.list_data[self.index.row()][self.index.column()] = self.prev
def redo(self):
self.model.list_data[self.index.row()][self.index.column()] = self.value
And now all that needs to be done in go_back is to call the undo method twice, for both cells that were modified.
def go_back(self):
self.model.stack.undo()
self.model.stack.undo()
Im trying to build a little list of entries within a QTreeView, and based on the example posted here, I got it to delete any child items via the right click context menu i added. but when i delete it the parent tree i have collapses. And in some cases if i delete a certain item in a certain order this crashes
My understanding is that this is because upon deletion the indices of the items changes, and to prevent that QtCore.QPersistentModelIndex() can be utilized according to this thread: How to delete multiple rows in a QTableView widget?
Although that example uses a QStandardItemModel(), since mine uses a QAbstractItemModel() how can I implement a similar concept, and also prevent this from crashing...?
import sys
from functools import partial
from PyQt4 import QtGui, QtCore
HORIZONTAL_HEADERS = ("Asset Name", "Date Added")
class AssetClass(object):
'''
a trivial custom data object
'''
def __init__(self, **kwargs):
if not kwargs.get('name') or not kwargs.get('type'):
return
self.name = kwargs.get('name')
self.date_added = kwargs.get('date_added')
self.type = kwargs.get('type')
def __repr__(self):
return "%s - %s %s" % (self.type, self.name, self.date_added)
class TreeItem(object):
'''
a python object used to return row/column data, and keep note of
it's parents and/or children
'''
def __init__(self, asset, header, parent_item):
self.asset = asset
self.parent_item = parent_item
self.header = header
self.child_items = []
def appendChild(self, item):
self.child_items.append(item)
def removeChild(self, item):
print 'removeChild: item is %s' % item
print 'removeChild: self.child_items is %s' % self.child_items
self.child_items.remove(item)
def child(self, row):
return self.child_items[row]
def childCount(self):
return len(self.child_items)
def columnCount(self):
return 2
def data(self, column):
if self.asset == None:
if column == 0:
return QtCore.QVariant(self.header)
if column == 1:
return QtCore.QVariant("")
else:
if column == 0:
return QtCore.QVariant(self.asset.name)
if column == 1:
return QtCore.QVariant(self.asset.date_added)
return QtCore.QVariant()
def parent(self):
return self.parent_item
def row(self):
if self.parent_item:
return self.parent_item.child_items.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
'''
a model to display a few names, ordered by sex
'''
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.assets = []
model_data = (("VEHICLE", "Truck", 'May 27th, 2020'),
("VEHICLE", "Car", 'May 25th, 2020'),
("CHARACTER", "Peter", 'May 27th, 2020'),
("CHARACTER", "Rachel", 'May 29th, 2020'),
("PROP", "Chair", 'May 27th, 2020'),
("PROP", "Axe", 'May 17th, 2020'))
for asset_type, name, date in model_data:
asset = AssetClass(type=asset_type, name=name, date_added=date)
self.assets.append(asset)
self.rootItem = TreeItem(None, "ALL", None)
self.parents = {0: self.rootItem}
self.setupModelData()
def columnCount(self, parent=None):
if parent and parent.isValid():
return parent.internalPointer().columnCount()
else:
return len(HORIZONTAL_HEADERS)
def data(self, index, role):
if not index.isValid():
return QtCore.QVariant()
item = index.internalPointer()
if role == QtCore.Qt.DisplayRole:
return item.data(index.column())
if role == QtCore.Qt.UserRole:
if item:
return item.asset
return QtCore.QVariant()
def headerData(self, column, orientation, role):
if (orientation == QtCore.Qt.Horizontal and
role == QtCore.Qt.DisplayRole):
try:
return QtCore.QVariant(HORIZONTAL_HEADERS[column])
except IndexError:
pass
return QtCore.QVariant()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parent_item = self.rootItem
else:
parent_item = parent.internalPointer()
childItem = parent_item.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
if not childItem:
return QtCore.QModelIndex()
parent_item = childItem.parent()
if parent_item == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parent_item.row(), 0, parent_item)
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.column() > 0:
return 0
if not parent.isValid():
p_Item = self.rootItem
else:
p_Item = parent.internalPointer()
return p_Item.childCount()
def setupModelData(self):
for asset in self.assets:
asset_type = asset.type
if not self.parents.has_key(asset_type):
new_parent = TreeItem(None, asset_type, self.rootItem)
self.rootItem.appendChild(new_parent)
self.parents[asset_type] = new_parent
print 'self.parents: ', self.parents
parent_item = self.parents[asset_type]
new_item = TreeItem(asset, "", parent_item)
parent_item.appendChild(new_item)
def addSubRow(self, new_asset):
asset_type, name, date = new_asset
asset = AssetClass(type=asset_type, name=name, date_added=date)
parent_item = self.parents[asset_type]
already_exists = False
for child in parent_item.child_items:
if child.asset.name == name and child.asset.type == asset_type:
already_exists = True
if already_exists:
print 'this asset already exists'
return
new_item = TreeItem(asset, "", parent_item)
parent_item.appendChild(new_item)
def removeRow(self, rowIndexes):
child_tree_item = rowIndexes[0].internalPointer()
asset_type = rowIndexes[0].parent().data().toString()
parent_item = self.parents[str(asset_type)]
# hint to keep the tree open after deleting: https://stackoverflow.com/questions/48121393/how-to-delete-multiple-rows-in-a-qtableview-widget
self.beginRemoveRows(QtCore.QModelIndex(), rowIndexes[0].row(), rowIndexes[0].row() + 1)
parent_item.removeChild(child_tree_item)
self.endRemoveRows()
def searchModel(self, asset):
'''
get the modelIndex for a given appointment
'''
def searchNode(node):
'''
a function called recursively, looking at all nodes beneath node
'''
for child in node.child_items:
print child.childCount()
if asset == child.asset:
index = self.createIndex(child.row(), 0, child)
return index
if child.childCount() > 0:
result = searchNode(child)
if result:
return result
retarg = searchNode(self.parents[0])
print retarg
return retarg
def findAssetName(self, name):
app = None
for asset in self.assets:
print asset.name
if asset.name == name:
app = asset
break
if app != None:
index = self.searchModel(app)
return (True, index)
return (False, None)
class TreeView(QtGui.QTreeView):
right_button_clicked = QtCore.pyqtSignal(list, int)
def __init__(self, parent=None):
super(TreeView, self).__init__(parent)
# self.clicked.connect(self.on_treeView_clicked)
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.openMenu)
def selectedRows(self):
rows = []
for index in self.selectedIndexes():
if index.column() == 0:
rows.append(index.row())
print type(rows)
return rows
def openMenu(self, position):
indexes = self.selectedIndexes()
if len(indexes) > 0:
level = 0
index = indexes[0]
while index.parent().isValid():
index = index.parent()
level += 1
menu = QtGui.QMenu()
editMenu = None
if level == 0:
editMenu = QtGui.QAction("Edit person", self)
menu.addAction(editMenu)
elif level == 1:
editMenu = QtGui.QAction("Delete", self)
menu.addAction(editMenu)
elif level == 2:
editMenu = QtGui.QAction("Edit object", self)
menu.addAction(editMenu)
if editMenu:
editMenu.triggered.connect(partial(self._on_right_click, indexes, level))
menu.exec_(self.viewport().mapToGlobal(position))
def _on_right_click(self, indexes, level):
self.right_button_clicked.emit(indexes, level)
def delete_test(self):
print 'addButton clicked'
new_asset = ("CHARACTER", "Smith", 'May 28th, 2020')
asset_type, name, date = new_asset
asset = AssetClass(type=asset_type, name=name, date_added=date)
parent_item = self.tree_view.model().parents[asset_type]
already_exists = False
for child in parent_item.child_items:
if child.asset.name == name and child.asset.type == asset_type:
already_exists = True
if already_exists:
print 'this asset already exists'
return
new_item = TreeItem(asset, "", parent_item)
parent_item.appendChild(new_item)
self.tree_view.model().layoutChanged.emit()
class Dialog(QtGui.QDialog):
add_signal = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setMinimumSize(300, 150)
self.model = TreeModel()
layout = QtGui.QVBoxLayout(self)
self.tree_view = TreeView(self)
self.tree_view.setModel(self.model)
self.tree_view.setAlternatingRowColors(True)
self.tree_view.right_button_clicked.connect(self.deleteButtonClicked)
layout.addWidget(self.tree_view)
label = QtGui.QLabel("Search for the following person")
layout.addWidget(label)
buts = []
frame = QtGui.QFrame(self)
layout2 = QtGui.QHBoxLayout(frame)
for asset in self.model.assets:
but = QtGui.QPushButton(asset.name, frame)
buts.append(but)
layout2.addWidget(but)
QtCore.QObject.connect(but, QtCore.SIGNAL("clicked()"), self.but_clicked)
layout.addWidget(frame)
self.add_button = QtGui.QPushButton("Add \"Character - Smith\"")
layout.addWidget(self.add_button)
QtCore.QObject.connect(self.add_button, QtCore.SIGNAL("clicked()"), self.addButtonClicked)
self.delete_button = QtGui.QPushButton("Delete Selected")
layout.addWidget(self.delete_button)
QtCore.QObject.connect(self.delete_button, QtCore.SIGNAL("clicked()"), self.tree_view.clearSelection)
self.but = QtGui.QPushButton("Clear Selection")
layout.addWidget(self.but)
QtCore.QObject.connect(self.but, QtCore.SIGNAL("clicked()"), self.tree_view.clearSelection)
QtCore.QObject.connect(self.tree_view, QtCore.SIGNAL("clicked (QModelIndex)"), self.row_clicked)
def row_clicked(self, index):
'''
when a row is clicked... show the name
'''
print 'row_clicked index type: %s' % index
print self.tree_view.model().data(index, QtCore.Qt.UserRole)
def but_clicked(self):
'''
when a name button is clicked, I iterate over the model,
find the person with this name, and set the treeviews current item
'''
name = self.sender().text()
print "BUTTON CLICKED:", name
result, index = self.model.findAssetName(name)
if result:
if index:
self.tree_view.setCurrentIndex(index)
return
self.tree_view.clearSelection()
def addButtonClicked(self):
print 'addButton clicked'
new_asset = ("CHARACTER", "Smith", 'May 28th, 2020')
self.tree_view.model().addSubRow(new_asset)
self.tree_view.model().layoutChanged.emit()
#QtCore.pyqtSlot(list, int)
def deleteButtonClicked(self, indexes, level):
print 'deleteButton clicked'
self.tree_view.model().removeRow(indexes)
self.tree_view.model().layoutChanged.emit()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
dialog = Dialog()
dialog.show()
sys.exit(app.exec_())
beginRemoveRows() expects the QModelIndex, which is the parent of the QModelIndex to be removed, as the first parameter. Regarding the example that you indicate in the comments of your code in the table type models, the indexes do not have a parent, so it is passed an invalid QModelIndex.
def removeRow(self, rowIndexes):
child_tree_item = rowIndexes[0].internalPointer()
parent_item = child_tree_item.parent()
self.beginRemoveRows(
rowIndexes[0].parent(), rowIndexes[0].row(), rowIndexes[0].row() + 1
)
parent_item.removeChild(child_tree_item)
self.endRemoveRows()
I'm using my custom item model (subclassed from QAbstractItemModel) with custom QTreeView. I want to allow internal drag-n-drop movement (MoveAction) and, when modifier key or right mouse button is pressed, pass CopyAction to my model (to dropMimeData) to copy items. However, default implementation of dropEvent() in QTreeView seems (from C code) only capable of passing MoveAction but when I try to reimplement dropEvent() in my QTreeView subclass like this:
def dropEvent(self, e):
index = self.indexAt(e.pos())
parent = index.parent()
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
... it works, but works horribly in terms of user interaction because there are tons of comlex code determining right index to drop item on in default implementation.
When i'm trying to modify action and call to superclass: super(Tree, self).dropEvent(e) dropAction() data is also lost.
What can I do in order to modify dropAction without loosing all fancy things that default dropEvent is doing for me?
Horrible mess of my current WIP code (i hope it's somewhere near minimal example)
from copy import deepcopy
import pickle
import config_editor
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt
from PyQt5.QtGui import QCursor, QStandardItemModel
from PyQt5.QtWidgets import QAbstractItemView, QTreeView, QMenu
class ConfigModelItem:
def __init__(self, label, value="", is_section=False, state='default', parent=None):
self.itemData = [label, value]
self.is_section = is_section
self.state = state
self.childItems = []
self.parentItem = parent
if self.parentItem is not None:
self.parentItem.appendChild(self)
def appendChild(self, item):
self.childItems.append(item)
item.parentItem = self
def addChildren(self, items, row):
if row == -1:
row = 0
self.childItems[row:row] = items
for item in items:
item.parentItem = self
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return 2
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def set_data(self, data, column):
try:
self.itemData[column] = data
except IndexError:
return False
return True
def parent(self):
return self.parentItem
def row(self):
if self.parentItem is not None:
return self.parentItem.childItems.index(self)
return 0
def removeChild(self, position):
if position < 0 or position > len(self.childItems):
return False
child = self.childItems.pop(position)
child.parentItem = None
return True
def __repr__(self):
return str(self.itemData)
class ConfigModel(QtCore.QAbstractItemModel):
def __init__(self, data, parent=None):
super(ConfigModel, self).__init__(parent)
self.rootItem = ConfigModelItem("Option", "Value")
self.setup(data)
def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return self.rootItem.data(section)
def columnCount(self, parent):
return 2
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
parentItem = self.nodeFromIndex(parent)
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem or parentItem is None:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def nodeFromIndex(self, index):
if index.isValid():
return index.internalPointer()
return self.rootItem
def data(self, index, role):
if not index.isValid():
return None
item = index.internalPointer()
if role == Qt.DisplayRole or role == Qt.EditRole:
return item.data(index.column())
return None
def setData(self, index, value, role=Qt.EditRole):
if not index.isValid():
return False
item = index.internalPointer()
if role == Qt.EditRole:
item.set_data(value, index.column())
self.dataChanged.emit(index, index, (role,))
return True
def flags(self, index):
if not index.isValid():
return QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled # Qt.NoItemFlags
item = index.internalPointer()
flags = Qt.ItemIsEnabled | Qt.ItemIsSelectable
if index.column() == 0:
flags |= int(QtCore.Qt.ItemIsDragEnabled)
if item.is_section:
flags |= int(QtCore.Qt.ItemIsDropEnabled)
if index.column() == 1 and not item.is_section:
flags |= Qt.ItemIsEditable
return flags
def supportedDropActions(self):
return QtCore.Qt.CopyAction | QtCore.Qt.MoveAction
def mimeTypes(self):
return ['app/configitem', 'text/xml']
def mimeData(self, indexes):
mimedata = QtCore.QMimeData()
index = indexes[0]
mimedata.setData('app/configitem', pickle.dumps(self.nodeFromIndex(index)))
return mimedata
def dropMimeData(self, mimedata, action, row, column, parentIndex):
print('action', action)
if action == Qt.IgnoreAction:
return True
droppedNode = deepcopy(pickle.loads(mimedata.data('app/configitem')))
print('copy', action & Qt.CopyAction)
print(droppedNode.itemData, 'node')
self.insertItems(row, [droppedNode], parentIndex)
self.dataChanged.emit(parentIndex, parentIndex)
if action & Qt.CopyAction:
return False # to not delete original item
return True
def removeRows(self, row, count, parent):
print('rem', row, count)
self.beginRemoveRows(parent, row, row+count-1)
parentItem = self.nodeFromIndex(parent)
for x in range(count):
parentItem.removeChild(row)
self.endRemoveRows()
print('removed')
return True
#QtCore.pyqtSlot()
def removeRow(self, index):
parent = index.parent()
self.beginRemoveRows(parent, index.row(), index.row())
parentItem = self.nodeFromIndex(parent)
parentItem.removeChild(index.row())
self.endRemoveRows()
return True
def insertItems(self, row, items, parentIndex):
print('ins', row)
parent = self.nodeFromIndex(parentIndex)
self.beginInsertRows(parentIndex, row, row+len(items)-1)
parent.addChildren(items, row)
print(parent.childItems)
self.endInsertRows()
self.dataChanged.emit(parentIndex, parentIndex)
return True
def setup(self, data: dict, parent=None):
if parent is None:
parent = self.rootItem
for key, value in data.items():
if isinstance(value, dict):
item = ConfigModelItem(key, parent=parent, is_section=True)
self.setup(value, parent=item)
else:
parent.appendChild(ConfigModelItem(key, value))
def to_dict(self, parent=None) -> dict:
if parent is None:
parent = self.rootItem
data = {}
for item in parent.childItems:
item_name, item_data = item.itemData
if item.childItems:
data[item_name] = self.to_dict(item)
else:
data[item_name] = item_data
return data
#property
def dict(self):
return self.to_dict()
class ConfigDialog(config_editor.Ui_config_dialog):
def __init__(self, data):
super(ConfigDialog, self).__init__()
self.model = ConfigModel(data)
def setupUi(self, config_dialog):
super(ConfigDialog, self).setupUi(config_dialog)
self.config_view = Tree()
self.config_view.setObjectName("config_view")
self.config_view.setModel(self.model)
self.gridLayout.addWidget(self.config_view, 0, 0, 1, 1)
self.config_view.expandAll()
#self.config_view.setDragDropMode(True)
#self.setDragDropMode(QAbstractItemView.InternalMove)
#self.setDragEnabled(True)
#self.setAcceptDrops(True)
#self.setDropIndicatorShown(True)
self.delete_button.pressed.connect(self.remove_selected)
def remove_selected(self):
index = self.config_view.selectedIndexes()[0]
self.model.removeRow(index)\
class Tree(QTreeView):
def __init__(self):
QTreeView.__init__(self)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.open_menu)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.setDropIndicatorShown(True)
self.setAnimated(True)
def dropEvent(self, e):
print(e.dropAction(), 'baseact', QtCore.Qt.CopyAction)
# if e.keyboardModifiers() & QtCore.Qt.AltModifier:
# #e.setDropAction(QtCore.Qt.CopyAction)
# print('copy')
# else:
# #e.setDropAction(QtCore.Qt.MoveAction)
# print("drop")
print(e.dropAction())
#super(Tree, self).dropEvent(e)
index = self.indexAt(e.pos())
parent = index.parent()
print('in', index.row())
self.model().dropMimeData(e.mimeData(), e.dropAction(), index.row(), index.column(), parent)
e.accept()
def open_menu(self):
menu = QMenu()
menu.addAction("Create new item")
menu.exec_(QCursor.pos())
if __name__ == '__main__':
import sys
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
sys.excepthook = except_hook
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
data = {"section 1": {"opt1": "str", "opt2": 123, "opt3": 1.23, "opt4": False, "...": {'subopt': 'bal'}},
"section 2": {"opt1": "str", "opt2": [1.1, 2.3, 34], "opt3": 1.23, "opt4": False, "...": ""}}
ui = ConfigDialog(data)
ui.setupUi(Dialog)
print(Qt.DisplayRole)
Dialog.show()
print(app.exec_())
print(Dialog.result())
print(ui.model.to_dict())
sys.exit()
setDragDropMode(QAbstractItemView.InternalMove) only allows move operations (as the name would suggest, although the docs do leave some uncertainty in the way this is stated). You probably want to set it to QAbstractItemView.DragDrop mode. You can set the default action with setDefaultDropAction(). Other than that, it's up to the model to return the right item flags and supportedDropActions()/canDropMimeData(), which it looks like yours does. There's also a dragDropOverwriteMode property which may be interesting.
One thing that has surprised me before is that in the model's dropMimeData() method if you return True from a Qt.MoveAction, the QAbstractItemView will remove the dragged item from the model automatically (with a removeRows()/removeColumns() call to your model). This can cause some puzzling results if your model has already actually moved that row (and deleted the old one). I never quite understood that behavior. OTOH if you return False it doesn't matter to the item view, as long as the data is actually moved/updated properly.