Get currently selected cell of QTreeWidget - python

I'd like to modify QTreeWidget to make the selected cell editable when the enter key is hit, but keep the selection to full rows.
I've done a hacky implementation of figuring out where the last click was and saving the value, then sending those values to my edit_item function on the key press (also used for the itemDoubleClicked signal). It's not great though and I'm wondering if there's a much easier way to do it.
For the record, clicking on an item still selects the whole row. It's probably hidden behaviour by default, but in Maya there's a visible selection thing of the last cell that was moved over while the mouse button was held. If I could somehow get access to that, I could also add in behaviour to control it with the arrow keys.
This is an example of the selected cell:
This is my code so far:
class QTreeWidget(QtWidgets.QTreeWidget):
returnPressed = QtCore.Signal(QTreeWidget, int)
def __init__(self, *args, **kwargs):
QtWidgets.QTreeWidget.__init__(self, *args, **kwargs)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
self.returnPressed.emit(self._selected_item, self._selected_column)
else:
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def _mouse_pos_calculate(self, x_pos):
"""Find the currently selected column."""
try:
item = self.selectedItems()[0]
except IndexError:
item = None
header = self.header()
total_width = 0
for i in range(self.columnCount()):
total_width += header.sectionSize(i)
if total_width > x_pos:
return (item, i)
def mousePressEvent(self, event):
QtWidgets.QTreeWidget.mousePressEvent(self, event)
self._selected_item, self._selected_column = self._mouse_pos_calculate(event.pos().x())
def mouseReleaseEvent(self, event):
QtWidgets.QTreeWidget.mouseReleaseEvent(self, event)
self._selected_item, self._selected_column = self._mouse_pos_calculate(event.pos().x())
Edit: Improved function thanks to eyllanesc
class QTreeWidget(QtWidgets.QTreeWidget):
"""Add ability to edit cells when pressing return."""
itemEdit = QtCore.Signal(QtWidgets.QTreeWidgetItem, int)
def __init__(self, *args, **kwargs):
QtWidgets.QTreeWidget.__init__(self, *args, **kwargs)
self._last_item = None
self._last_column = 0
self.itemDoubleClicked.connect(self._edit_item_intercept)
def _edit_item_intercept(self, item=None, column=None):
if item is None:
item = self._last_item
if column is None:
column = self._last_column
self.itemEdit.emit(item, column)
def _store_last_cell(self, pos):
selected_item = self.itemAt(pos)
if selected_item is None:
return
self._last_item = selected_item
self._last_column = self.header().logicalIndexAt(pos.x())
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
return self._edit_item_intercept()
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def mouseMoveEvent(self, event):
QtWidgets.QTreeWidget.mouseMoveEvent(self, event)
self._store_last_cell(event.pos())

You are doing a lot of calculation unnecessarily, in the next part I show a cleaner solution:
from PySide2 import QtCore, QtGui, QtWidgets
class QTreeWidget(QtWidgets.QTreeWidget):
def __init__(self, *args, **kwargs):
super(TreeWidget, self).__init__(*args, **kwargs)
self.special_item = None
self.special_col = 0
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Return:
self.editItem(self.special_item, self.special_col)
QtWidgets.QTreeWidget.keyPressEvent(self, event)
def editEnable(self, pos):
press_item = self.itemAt(pos)
if press_item is None:
return
if press_item is self.selectedItems()[0]:
col = self.header().logicalIndexAt(pos.x())
self.special_item = press_item
self.special_col = col
def mousePressEvent(self, event):
QtWidgets.QTreeWidget.mousePressEvent(self, event)
self.editEnable(event.pos())

Related

How to add a menu only in the QTreeWidgetItem after pressing the right click? [duplicate]

I have the following code to create a QTreeWidget and a contextmenu with 2 actions.
import sys
from PyQt5 import QtCore, QtWidgets
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__()
self.tw = QtWidgets.QTreeWidget()
self.tw.setHeaderLabels(['Name', 'Cost ($)'])
cg = QtWidgets.QTreeWidgetItem(['carrots', '0.99'])
c1 = QtWidgets.QTreeWidgetItem(['carrot', '0.33'])
self.tw.addTopLevelItem(cg)
self.tw.addTopLevelItem(c1)
self.tw.installEventFilter(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.tw)
def eventFilter(self, source: QtWidgets.QTreeWidget, event):
if (event.type() == QtCore.QEvent.ContextMenu and
source is self.tw):
menu = QtWidgets.QMenu()
AAction = QtWidgets.QAction("AAAAA")
AAction.triggered.connect(lambda :self.a(source.itemAt(event.pos())))
BAction = QtWidgets.QAction("BBBBBB")
BAction.triggered.connect(lambda :self.b(source, event))
menu.addAction(AAction)
menu.addAction(BAction)
menu.exec_(event.globalPos())
return True
return super(Dialog, self).eventFilter(source, event)
def a(self, item):
if item is None:
return
print("A: {}".format([item.text(i) for i in range(self.tw.columnCount())]))
def b(self, source, event):
item = source.itemAt(event.pos())
if item is None:
return
print("B: {}".format([item.text(i) for i in range(source.columnCount())]))
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = Dialog()
window.setGeometry(600, 100, 300, 200)
window.show()
sys.exit(app.exec_())
When opening the contextmenu in the header and clicking on one of the actions it prints either carrot or carrots, depending on where in the contextmenu I click. But I give the position of right click event to the functions.
So why is this happening and what can I do to stop it?
Your code has 2 errors:
The main error is that the itemAt() method uses the coordinates with respect to the viewport() and not with respect to the view (the QTreeWidget) so you will get incorrect items (the header occupies a space making the positions with respect to the QTreeWidget and the viewport() have an offset).
You should not block the eventloop, for example blocking the eventFilter you may be blocking other events that would cause errors that are difficult to debug, in this case it is better to use a signal.
class Dialog(QtWidgets.QDialog):
rightClicked = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, parent=None):
super(Dialog, self).__init__()
self.tw = QtWidgets.QTreeWidget()
self.tw.setHeaderLabels(["Name", "Cost ($)"])
cg = QtWidgets.QTreeWidgetItem(["carrots", "0.99"])
c1 = QtWidgets.QTreeWidgetItem(["carrot", "0.33"])
self.tw.addTopLevelItem(cg)
self.tw.addTopLevelItem(c1)
self.tw.viewport().installEventFilter(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.tw)
self.rightClicked.connect(self.handle_rightClicked)
def eventFilter(self, source: QtWidgets.QTreeWidget, event):
if event.type() == QtCore.QEvent.ContextMenu and source is self.tw.viewport():
self.rightClicked.emit(event.pos())
return True
return super(Dialog, self).eventFilter(source, event)
def handle_rightClicked(self, pos):
item = self.tw.itemAt(pos)
if item is None:
return
menu = QtWidgets.QMenu()
print_action = QtWidgets.QAction("Print")
print_action.triggered.connect(lambda checked, item=item: self.print_item(item))
menu.addAction(print_action)
menu.exec_(self.tw.viewport().mapToGlobal(pos))
def print_item(self, item):
if item is None:
return
texts = []
for i in range(item.columnCount()):
text = item.text(i)
texts.append(text)
print("B: {}".format(",".join(texts)))
Although it is unnecessary that you use an eventFilter to handle the contextmenu since a simpler solution is to set the contextMenuPolicy of the QTreeWidget to Qt::CustomContextMenu and use the customContextMenuRequested signal:
class Dialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__()
self.tw = QtWidgets.QTreeWidget()
self.tw.setHeaderLabels(["Name", "Cost ($)"])
cg = QtWidgets.QTreeWidgetItem(["carrots", "0.99"])
c1 = QtWidgets.QTreeWidgetItem(["carrot", "0.33"])
self.tw.addTopLevelItem(cg)
self.tw.addTopLevelItem(c1)
self.tw.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.tw.customContextMenuRequested.connect(self.handle_rightClicked)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.tw)
def handle_rightClicked(self, pos):
item = self.tw.itemAt(pos)
if item is None:
return
menu = QtWidgets.QMenu()
print_action = QtWidgets.QAction("Print")
print_action.triggered.connect(lambda checked, item=item: self.print_item(item))
menu.addAction(print_action)
menu.exec_(self.tw.viewport().mapToGlobal(pos))
def print_item(self, item):
if item is None:
return
texts = []
for i in range(item.columnCount()):
text = item.text(i)
texts.append(text)
print("B: {}".format(",".join(texts)))

PyQt5: QTableView drag and drop cells not rows

I know how to drag rows
QTableView.verticalHeader().setSectionsMovable(True)
QTableView.verticalHeader().setDragEnabled(True)
QTableView.verticalHeader().setDragDropMode(qtw.QAbstractItemView.InternalMove)
but I want to be able to drag and drop just a single (or a pair of) cell.
Can anyone point me in the right direction?
PS: my current idea would be to intercept the clicked() -> dragEnter() -> dragLeave() or fork to dragDrop() -> dragIndicatorPosition()
events, but it sounds kinda convoluted
I did read this but I am confused on how to implement it, especially the section "Enabling drag and drop for items" seems to address exactly what I need. I'll see if I can post a "slim" example.
EDIT:
here an example with some other stuff. in MyStandardItemModel I try to do the trick:
from PyQt5 import QtCore, QtWidgets, QtSql
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView, QTableWidget
class MyStandardItemModel(QStandardItemModel):
def __init__(self, parent=None, *arg, **kwargs):
super().__init__(parent, *arg, **kwargs)
self.__readonly_cols = []
def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
try:
default_Flags = QStandardItemModel.flags(self,index)
if (index.column() in self.__readonly_cols):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
else:
if (index.isValid()):
return default_Flags | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
else:
return default_Flags
except Exception as ex:
print(ex)
def setReadOnly(self, columns: [int]):
for i in columns:
if i <= (self.columnCount() - 1) and i not in self.__readonly_cols:
self.__readonly_cols.append(i)
def resetReadOnly(self):
self.__readonly_cols = []
class MyTableView(QtWidgets.QTableView):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
# signal to inform clicking
user_click = QtCore.pyqtSignal(int, int, bool)
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
# if event.type() == QtCore.QEvent.MouseButtonPress or event.type() == QtCore.QEvent.MouseMove:
# return False
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
try:
if int(index.data()) == 0:
model.setData(index, 1, QtCore.Qt.EditRole)
ret = True
else:
model.setData(index, 0, QtCore.Qt.EditRole)
ret = False
# emit signal with row, col, and the status
self.user_click.emit(index.row(), index.column(), ret)
except Exception as ex:
print(ex)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = MyStandardItemModel(5, 5)
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model.setHorizontalHeaderLabels(header_labels)
tableView = MyTableView()
tableView.setModel(model)
delegate = CheckBoxDelegate(None)
tableView.setItemDelegateForColumn(0, delegate)
for row in range(5):
for column in range(4):
index = model.index(row, column, QModelIndex())
model.setData(index, 0)
model.setReadOnly([1,2])
tableView.setWindowTitle("CheckBox, readonly and drag&drop example")
tableView.show()
sys.exit(app.exec_())

CheckableComboBox in PyQGIS (waiting for choice)

I am using this code in QGIS's built-in python-editor.
How can I make the script wait while I mark the necessary lines, and only then continue execution? So far, I cannot do this: the window starts, but the script continues to execute and therefore there is no way to use the select list. For example, can I somehow add the "Ok" button there (as in a standard dialog box)?
from qgis.PyQt import *
from qgis.core import *
class CheckableComboBox(QComboBox):
# Subclass Delegate to increase item height
class Delegate(QStyledItemDelegate):
def sizeHint(self, option, index):
size = super().sizeHint(option, index)
size.setHeight(20)
return size
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make the combo editable to set a custom text, but readonly
self.setEditable(True)
self.lineEdit().setReadOnly(True)
# Make the lineedit the same color as QPushButton
palette = qApp.palette()
palette.setBrush(QPalette.Base, palette.button())
self.lineEdit().setPalette(palette)
# Use custom delegate
self.setItemDelegate(CheckableComboBox.Delegate())
# Update the text when an item is toggled
self.model().dataChanged.connect(self.updateText)
# Hide and show popup when clicking the line edit
self.lineEdit().installEventFilter(self)
self.closeOnLineEditClick = False
# Prevent popup from closing when clicking on an item
self.view().viewport().installEventFilter(self)
def resizeEvent(self, event):
# Recompute text to elide as needed
self.updateText()
super().resizeEvent(event)
def eventFilter(self, object, event):
if object == self.lineEdit():
if event.type() == QEvent.MouseButtonRelease:
if self.closeOnLineEditClick:
self.hidePopup()
else:
self.showPopup()
return True
return False
if object == self.view().viewport():
if event.type() == QEvent.MouseButtonRelease:
index = self.view().indexAt(event.pos())
item = self.model().item(index.row())
if item.checkState() == Qt.Checked:
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
return True
return False
def showPopup(self):
super().showPopup()
# When the popup is displayed, a click on the lineedit should close it
self.closeOnLineEditClick = True
def hidePopup(self):
super().hidePopup()
# Used to prevent immediate reopening when clicking on the lineEdit
self.startTimer(100)
# Refresh the display text when closing
self.updateText()
def timerEvent(self, event):
# After timeout, kill timer, and reenable click on line edit
self.killTimer(event.timerId())
self.closeOnLineEditClick = False
def updateText(self):
texts = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
texts.append(self.model().item(i).text())
text = ", ".join(texts)
# Compute elided text (with "...")
metrics = QFontMetrics(self.lineEdit().font())
elidedText = metrics.elidedText(text, Qt.ElideRight, self.lineEdit().width())
self.lineEdit().setText(elidedText)
def addItem(self, text, data=None):
item = QStandardItem()
item.setText(text)
if data is None:
item.setData(text)
else:
item.setData(data)
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable)
item.setData(Qt.Unchecked, Qt.CheckStateRole)
self.model().appendRow(item)
def addItems(self, texts, datalist=None):
for i, text in enumerate(texts):
try:
data = datalist[i]
except (TypeError, IndexError):
data = None
self.addItem(text, data)
def currentData(self):
# Return the list of selected items data
res = []
for i in range(self.model().rowCount()):
if self.model().item(i).checkState() == Qt.Checked:
res.append(self.model().item(i).data())
return res
print("Starting...")
comunes = ['Ameglia', 'Arcola', 'Bagnone', 'Bolano', 'Carrara', 'Casola', 'Barnaul', 'London']
combo = CheckableComboBox()
combo.resize(350,300)
combo.setWindowTitle('CheckableComboBox')
combo.setWindowFlags(Qt.WindowStaysOnTopHint)
combo.show()
combo.addItems(comunes)
print(combo.currentData())
The problem solved by adding checkable_combobox to the QDialog. Here is the code using QgsCheckableComboBox():
https://gis.stackexchange.com/questions/376901/how-to-put-qgscheckablecombobox-into-standby
from qgis.PyQt import QtGui
from qgis.core import *
planet_list = ["Venus", "Earth", "Mars", "Jupiter", "Pluto"]
items = QgsCheckableComboBox()
items.addItems(planet_list)
dlg = QDialog()
layout = QVBoxLayout()
layout.addWidget(items)
dlg.setLayout(layout)
dlg.exec_()
print('\n\n-----------CheckedItems: ', items.checkedItems())

dragMoveEvent() doesn't work properly when overriding mouseMoveEvent() - Qt Drag&Drop

I'm building a quite complex GUI using PySide2 and I have to implement a Drag and Drop system for a QTreeView widget (both internal and external moves shall be accepted).
GOAL
Be able to copy items from a QTreeView widget (File explorer) to another QTreeView widget (Tests widget) and move items within this Tests widget. Every dragged file shall be checked to make the user understands if he can move that file and where he should put it.
Also, the items of the Tests widget shall be highlighted (one by one) when the mouse with the dragged element hovers them. (Actually, I would like to draw a line between the items but I'm not able to do it for now: any suggestion is welcome).
PROBLEM
The external move works perfectly whereas the internal move doesn't work properly: during the drag and drop operation the 'stop' icon is always showing even if the move should be accepted. And it is actually accepted, since the drop operation is succesfull.
The highlighting of the hover items doesn't work too.
I think that the problem is given by the overriding of the mouseMoveEvent() method of the Tests widget, which I was forced to implement in order to set the QMimeData object.
CODE
Note that some parts of the code have been replaced by '[...]' for privacy reasons. Anyway those parts are not important for the functioning of the system.
class MyStandardItem(QStandardItem):
def __init__(self, text, icon_path='', value='', num=0, font_size=8, set_bold=False):
super().__init__()
self.setDragEnabled(True)
self.setDropEnabled(True)
self.num = num
self.value = value
self.setText(text)
font = QFont('Segoe UI', font_size)
font.setBold(set_bold)
self.setFont(font)
self.setIcon(QIcon(icon_path))
def setCheckState(self, checkState):
super().setCheckState(checkState)
if checkState == Qt.Unchecked:
self.setForeground(QColor(150, 150, 150))
def get_data(self):
return self.text(), self.value, self.num
class MyTreeView(QTreeView):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.viewport().setAcceptDrops(True)
self.hover_item = None
self.setMouseTracking(True)
self.start_drag_pos = None
self.model = QStandardItemModel()
self.root = self.model.invisibleRootItem()
self.setModel(self.model)
def mousePressEvent(self, event: QtGui.QMouseEvent):
if event.button() == Qt.LeftButton:
self.start_drag_pos = event.pos()
super().mousePressEvent(event)
def mouseMoveEvent(self, event: QtGui.QMouseEvent):
super().mouseMoveEvent(event)
if not event.buttons() == Qt.LeftButton:
return
if (event.pos() - self.start_drag_pos).manhattanLength() < QApplication.startDragDistance():
return
index = self.indexAt(self.start_drag_pos)
item = self.model.itemFromIndex(index)
if item:
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setText(str(item.get_data()))
drag.setMimeData(mime_data)
drag.exec_(Qt.MoveAction)
def dragEnterEvent(self, event: QDragEnterEvent):
self.selectionModel().clear()
if event.mimeData().hasText():
mime_text = event.mimeData().text()
if event.source() == self:
mime_tuple = eval(mime_text)
if [...]:
event.acceptProposedAction()
else:
path = Path(mime_text)
accepted_extensions = ['.txt']
if path.suffix in accepted_extensions:
event.acceptProposedAction()
def dragMoveEvent(self, event: QDragMoveEvent):
cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
index = self.indexAt(cursor_pos)
item = self.model.itemFromIndex(index)
if self.hover_item is not item:
self.hover_item = item
if self.hover_item is not None:
self.selectionModel().clear()
self.selectionModel().select(item.index(), QItemSelectionModel.Rows | QItemSelectionModel.Select)
super().dragMoveEvent(event)
if event.source() == self:
item_data = eval(event.mimeData().text())
if item is not None:
if [...]:
event.acceptProposedAction()
else:
if [...]:
event.acceptProposedAction()
else:
path = event.mimeData().text().replace('file:///', '')
if item is not None:
if [...]:
if [...] in item.text():
event.acceptProposedAction()
else:
if [...]:
if [...] in item.value:
event.acceptProposedAction()
else:
if [...] in item.value:
event.acceptProposedAction()
def dropEvent(self, event: QDropEvent):
self.hover_item = None
cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
index = self.indexAt(cursor_pos)
over_dropped_item = self.model.itemFromIndex(index)
dropped = event.mimeData().text().replace('file:///', '')
print('Moved item: ', dropped)
print('Moved over: ', over_dropped_item.get_data())
[...]
event.acceptProposedAction()
After thinking about it quite a bit I decided not to override the mouseMoveEvent() method and this solved the problem.
As I thought there was a conflict between mouseMoveEvent() and dragMoveEvent(). In fact dragMoveEvent() was called only once (it shoud have been called multiple times during the drag movements) and just before dropEvent(). I believe that the problem was given by the fact that the QDrag object was created continuosly during the mouse movement and so dragMoveEvent() was never called until the item was dropped - but it was too late.
Without the custom QDrop object - previoulsy created in mouseMoveEvent() - I had to find another way to save the dragged QStandardItem and use it afterwards. The solution was quite simple: store the selected item into a class attribute when dragEnterEvent() is called:
def dragEnterEvent(self, event: QDragEnterEvent):
if event.source() == self:
self.dragged_index = self.selectionModel().selectedIndexes()[0]
self.dragged_item = self.model.itemFromIndex(self.dragged_index)
Also, thanks to the native Drag&Drop I don't need anymore to select the items I'm hovering on, since this functionality is built-in and the black line between the rows too. Native Drag&Drop works only with internal moves and only with QStandardItem items, therefore I deleted MyStandardItem class and used QStandardItem instead.
CODE
class MyTreeView(QTreeView):
def __init__(self):
super().__init__()
self.setAcceptDrops(True)
self.setDragEnabled(True)
self.setDropIndicatorShown(True)
self.viewport().setAcceptDrops(True)
self.hover_item = None
self.setMouseTracking(True)
self.dragged_item = None
self.dragged_index = None
def dragEnterEvent(self, event: QDragEnterEvent):
if event.source() == self:
self.dragged_index = self.selectionModel().selectedIndexes()[0]
self.dragged_item = self.model.itemFromIndex(self.dragged_index)
if [...] not in self.dragged_item.text():
super().dragEnterEvent(event)
else:
[...]
def dragMoveEvent(self, event: QDragMoveEvent):
super().dragMoveEvent(event)
cursor_pos = self.viewport().mapFromGlobal(QtGui.QCursor().pos())
index = self.indexAt(cursor_pos)
item = self.model.itemFromIndex(index)
if event.source() == self:
if item is not None:
if [...] in self.dragged_item.data()[0]:
if [...] in item.data()[0]:
event.acceptProposedAction()
else:
if [...] in item.data()[0]:
event.acceptProposedAction()
else:
[...]
def dropEvent(self, event: QDropEvent):
super().dropEvent(event)
[...]

pyqt model/view drag and drop:QAbstractItemModel::endMoveRows: Invalid index and QAbstractEventDispatcher: INTERNAL ERROR, timer ID 0 is too large

I've got a pyqt implementation of QTableView and QAbstractItemModel.
In QTableView I've implemented Drag-and-Drop to allow user to drag rows with mouse by clicking on a cell and dragging it. mousePressEvent, startDrag and dropEvent functions are relevant to this process:
class View(QTableView):
def __init__(self, parent=None):
QTableView.__init__(self, parent=None)
#self.setSelectionMode(self.ExtendedSelection)
self.setSelectionMode(self.MultiSelection)
self.setDragEnabled(True)
self.acceptDrops()
self.setDragDropMode(self.InternalMove)
self.setDropIndicatorShown(True)
self.verticalHeader().setVisible(False)
self.horizontalHeader().setVisible(False)
self.horizontalHeader().setStretchLastSection(True)
def dragEnterEvent(self, event):
event.accept()
def dragMoveEvent(self, event):
event.accept()
def dropEvent(self, event):
point = event.pos()
index = self.indexAt(event.pos())
event.accept()
if int(index.row()) != self.drag_row:
try:
self.model().moveRows(QModelIndex(), self.drag_row, self.drag_row, QModelIn
except Exception as E:
print E
def mousePressEvent(self, event):
print "mousePressEvent called"
self.startDrag(event)
super(View, self).mousePressEvent(event)
def startDrag(self, event):
index = self.indexAt(event.pos())
if not index.isValid():
return
self.moved_data = self.model().alignment.sequences[index.row()]
drag = QDrag(self)
mimeData = QMimeData()
mimeData.setData("application/blabla", "")
drag.setMimeData(mimeData)
pixmap = QPixmap()
pixmap = pixmap.grabWidget(self, self.visualRect(index))
drag.setPixmap(pixmap)
self.drag_row = int(index.row())
Now, here is my model, which responds to drag, started in view with mousePressEvent, by moveRows function:
class Model(QAbstractTableModel):
def __init__(self, alignment):
QAbstractTableModel.__init__(self, parent=None)
self.alignment = alignment #THIS IS THE REAL STORAGE OF MY DATA
self.setSupportedDragActions(Qt.MoveAction)
def flags(self, index):
if index.isValid():
return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.Item
else:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsSelectable
def rowCount(self, parent=QModelIndex()):
return len(self.alignment.sequences)
def columnCount(self, parent=QModelIndex()):
return 1
def data(self, index, role):
if role == Qt.DisplayRole:
return QVariant(self.alignment.sequences[index.row()].name)
return QVariant()
def dragMoveEvent(self, event):
event.setDropAction(QtCore.Qt.MoveAction)
event.accept()
def moveRows(self, parent, source_first, source_last, parent2, dest):
self.beginMoveRows(parent, source_first, source_last, parent2, dest)
sequences = self.alignment.sequences
if source_first <= dest:
new_order = sequences[:source_first] + sequences[source_last+1:dest+1] + sequences[source_first:source_last+1] + sequences[dest+1:]
else: #TODO what if source_first < dest < source_last
new_order = sequences[:dest] + sequences[source_first:source_last+1] + sequences[dest:source_first] + sequences[source_last+1:]
self.alignment.set_sequences(new_order, notify=True)
print "BEFORE endMoveRows in edit_sequence_list.Model, %s" % self
self.endMoveRows()
The problem is: application, running this Model/View pair first reports the following messages after the drag was performed:
BEFORE endMoveRows in edit_sequence_list.Model,
(previous line was generated by my print)
QAbstractItemModel::endMoveRows: Invalid index ( 1493172231 , 0 ) in
model Model(0xa973068)
At this point it properly displays drag results and has underlying data in self.alignment successfully changed.
But when I close subwindow, displaying this Model/View pair, my application crashes:
QAbstractEventDispatcher: INTERNAL ERROR, timer ID 0 is too large Aborted
Do you have any idea of what's the cause of these errors and how to fix them? What's wrong with index, why does it have huge first number?
What also seems strange to me is that Model address, printed from python (0xa8c1bfc) doesn't match Model address, printed by Qt in error message: Model(0xa973068).

Categories