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

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)
[...]

Related

copy/select text in WhatsThis

Example:
class tab_1(QWidget):
def __init__(self):
super(tab_1, self).__init__()
self.initUI()
def initUI(self):
self.btn = QPushButton(self)
self.btn.setText("tab1")
self.btn.setWhatsThis(
"""''(parameter) self: ~tab
Sourcery Code Metrics
Complexity 0 ⭐
Size 108 🙂
Working Memory 4 ⭐
Quality Score 82 % ⭐
⟠ self: [tab] Docc"""
""
)
I use the setWhatsThis and try to copy/select the text of it, but
when I click the mouse
In WhatsThis area is Disappear!
Example
In general, the what's this behavior is to only handles clicks on hyperlinks (which normally launch the application help dialog), so if the user clicks it, it will automatically close anyway.
The only solution is to override the default behavior by mimicking what Qt normally does: install an event filter on the application whenever a top level window receives a EnterWhatsThisMode event type, and create a custom widget that is shown whenever it's appropriate.
Since this will cause to override all what's this cases, I'd suggest to also set the WA_CustomWhatsThis attribute for the widgets that will have selectable text, so that you can use the standard behavior for any other case.
The custom widget is actually a QLabel with the Popup window flag set, which will make the widget a top level window that also captures mouse events, and with the TextSelectableByMouse and LinksAccessibleByMouse text interaction flags set.
class SelectableWhatsThis(QtWidgets.QLabel):
def __init__(self, parent, pos):
super().__init__(parent.whatsThis(), parent, flags=QtCore.Qt.Popup)
self.setTextInteractionFlags(
QtCore.Qt.TextSelectableByMouse | QtCore.Qt.LinksAccessibleByMouse)
self.setBackgroundRole(QtGui.QPalette.ToolTipBase)
self.setForegroundRole(QtGui.QPalette.ToolTipText)
self.setAutoFillBackground(True)
self.setContentsMargins(12, 8, 12, 8)
self.move(pos)
self.show()
def mousePressEvent(self, event):
if event.pos() not in self.rect():
self.close()
else:
super().mousePressEvent(event)
def keyPressEvent(self, event):
self.close()
class WhatsThisHelper(QtCore.QObject):
installed = active = False
whatsThis = None
def __init__(self, parent):
super().__init__(parent)
if not self.installed:
# ensure that only one instance of WhatsThisHelper is installed
self.__class__.installed = True
QtWidgets.QApplication.instance().installEventFilter(self)
self.active = True
def eventFilter(self, obj, event):
if not obj.isWidgetType():
return False
if (event.type() == event.MouseButtonPress
and event.button() == QtCore.Qt.LeftButton
and obj.whatsThis() and
obj.testAttribute(QtCore.Qt.WA_CustomWhatsThis)):
self.whatsThis = SelectableWhatsThis(obj, event.globalPos())
QtWidgets.QApplication.restoreOverrideCursor()
return True
elif event.type() in (event.MouseButtonRelease, event.MouseButtonDblClick):
if self.whatsThis:
QtWidgets.QWhatsThis.leaveWhatsThisMode()
return True
elif event.type() == event.LeaveWhatsThisMode:
QtWidgets.QApplication.instance().removeEventFilter(self)
self.deleteLater()
return super().eventFilter(obj, event)
def __del__(self):
if self.active:
self.__class__.installed = False
class MainWindow(QtWidgets.QMainWindow):
# ...
def event(self, event):
if event.type() == event.EnterWhatsThisMode:
WhatsThisHelper(self)
return super().event(event)
Note that you need to override the event of all widgets that are going to be top level widgets (all QMainWindows, QDialogs, etc) and need to support this.

Qt: Prevent drag-and-drop on child widgets and show forbidden cursor

I want to enable drag and drop functionality in some custom widgets, to allow reordering and rearranging the widgets within a layout. The basic functionality is working, but I'd like to prevent dropping a widget on itself, or on a child of itself. This is straightforward in the dropEvent method, but I can't find a way to also show the "forbidden" cursor while dragging, such that the user is aware that a drop will not be allowed.
The example below shows a test implementation, where the widgets "One" to "Five" can be dragged and dropped (no rearrangement will happen, only a message will be printed on the terminal, you may need to press Ctrl or something to initiate a drag). The problem line is the first ev.accept() in the dragEnterEvent method. By accepting the event, the cursor is shown in an "allowed" state (a grabbing hand for me). For example, trying to drag "One" and drop it onto "Three" appears as allowed, although nothing will happen. However, ignoring the event results in the event being propagated to the parent, so in that case dragging "Three" and dropping it onto "Three" results in "One" getting the drop instead. Setting the WA_NoMousePropagation attribute does not seem to make any difference.
So, what I would need is a way to "accept" the event, so that it will not be propagated to the parent, but still show a "forbidden" cursor, as if nobody accepted the event. Any ideas?
#!/usr/bin/env python3
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class WidgetMimeData(QtCore.QMimeData):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.itemObject = None
def hasFormat(self, mime):
if (self.itemObject and (mime == 'widgetitem')):
return True
return super().hasFormat(mime)
def setItem(self, obj):
self.itemObject = obj
def item(self):
return self.itemObject
class DraggableWidget(QGroupBox):
def __init__(self):
QWidget.__init__(self)
layout = QVBoxLayout()
self.setLayout(layout)
self.setAcceptDrops(True)
def addWidget(self, widget):
return self.layout().addWidget(widget)
def mouseMoveEvent(self, ev):
pixmap = QPixmap(self.size())
pixmap.fill(QtCore.Qt.transparent)
painter = QPainter()
painter.begin(pixmap)
painter.setOpacity(0.8)
painter.drawPixmap(0, 0, self.grab())
painter.end()
drag = QDrag(self)
mimedata = WidgetMimeData()
mimedata.setItem(self)
drag.setMimeData(mimedata)
drag.setPixmap(pixmap)
drag.setHotSpot(ev.pos())
drag.exec_(QtCore.Qt.MoveAction)
def dragEnterEvent(self, ev):
item = ev.mimeData().item()
if item.isAncestorOf(self):
#ev.ignore()
ev.accept()
else:
ev.accept()
def dropEvent(self, ev):
item = ev.mimeData().item()
if not item.isAncestorOf(self):
print('dropped on', self.layout().itemAt(0).widget().text())
ev.accept()
class HelloWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
w1 = DraggableWidget()
w1.addWidget(QLabel('One'))
w2 = DraggableWidget()
w2.addWidget(QLabel('Two'))
w3 = DraggableWidget()
w3.addWidget(QLabel('Three'))
w4 = DraggableWidget()
w4.addWidget(QLabel('Four'))
w5 = DraggableWidget()
w5.addWidget(QLabel('Five'))
w1.addWidget(w3)
w1.addWidget(w4)
w2.addWidget(w5)
layout = QVBoxLayout()
layout.addWidget(w1)
layout.addWidget(w2)
layout.addStretch(1)
centralWidget = QWidget(self)
centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mainWin = HelloWindow()
mainWin.show()
sys.exit( app.exec_() )
Probably the easiest way is to always accept the event in dragEnterEvent and in dragMoveEvent ignore it when the source of the event is an ancestor of self, i.e.
def dragEnterEvent(self, ev):
ev.accept()
def dragMoveEvent(self, ev):
item = ev.source()
if item.isAncestorOf(self):
ev.ignore()
By ignoring the event in dragMoveEvent you wouldn't need the check in dropEvent either and can simply do
def dropEvent(self, ev):
print('Dropped of', self.layout().itemAt(0).widget().text())
ev.accept()

Can I have eventFilter to ignore widget's events

Can I set my eventFilter to ignore the 'default' events of a widget eg. mousePressEvents etc? Or can these two be mixed in the first place?
In my code, I have a mousePressEvent and a custom event that I have created for a right-click menu for the menu items within a QMenu.
And from time to time, as I execute my code/ or when doing the right mouse click on the tab, I am seeing either:
AttributeError: 'PySide2.QtCore.QEvent' object has no attribute 'pos'
or sometimes:
RuntimeWarning: Invalid return value in function QTabBar.eventFilter, expected bool, got NoneType.
As such, is there a better way to get around it, or should I re-factor of how I proceed with the Rename Item and Delete Item as denoted in show_adv_qmenu()?
class MyWin(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MyWin, self).__init__()
self.rename_menu_allowed = False
central_widget = QtGui.QWidget()
self.setCentralWidget(central_widget)
vlay = QtGui.QVBoxLayout(central_widget)
hlay = QtGui.QHBoxLayout()
vlay.addLayout(hlay)
vlay.addStretch()
self.add_button = QtGui.QToolButton()
self.tab_bar = QtGui.QTabBar(self)
self.add_button.setIcon(QtGui.QIcon('add.png'))
self.add_button.setMenu(self.set_menu())
self.add_button.setPopupMode(QtGui.QToolButton.InstantPopup)
self.tab_bar.setTabButton(
0,
QtGui.QTabBar.ButtonPosition.RightSide,
self.add_button
)
hlay.addWidget(self.add_button)
hlay.addWidget(self.tab_bar)
self.my_extra_menus = defaultdict(list)
def set_menu(self):
menu_options = ['food', 'drinks', 'snacks']
qmenu = QtGui.QMenu(self.add_button)
for opt in menu_options:
qmenu.addAction(opt, partial(self.set_new_tab, opt))
return qmenu
def set_new_tab(self, opt):
self.tab_bar.addTab(opt)
def mousePressEvent(self, event):
index = self.tab_bar.tabAt(event.pos())
if event.button() == QtCore.Qt.RightButton:
self._showContextMenu(event.pos(), index)
else:
super(MyWin, self).mousePressEvent(event)
def eventFilter(self, obj, event):
# Custom event only for right mouse click within the qmenu
if obj == self.qmenu:
if event.type() == QtCore.QEvent.MouseButtonPress and event.button() == QtCore.Qt.RightButton:
index = self.tab_bar.tabAt(event.pos())
action = obj.actionAt(event.pos())
self.show_adv_qmenu(obj, action, index)
def _showContextMenu(self, position, index):
self.qmenu = QtGui.QMenu(self)
self.qmenu.setTearOffEnabled(True) # New item is not shown in tearoff mode
self.qmenu.setTitle(self.tab_bar.tabText(index))
self.qmenu.installEventFilter(self)
add_item_action = QtGui.QAction('Add Menu Item', self)
slot = partial(self._addMenuItem, index)
add_item_action.triggered.connect(slot)
self.qmenu.addAction(add_item_action)
self.qmenu.addSeparator()
if self.my_extra_menus.get(index):
for menuItem in self.my_extra_menus[index]:
self.qmenu.addMenu(menuItem)
self.qmenu.addSeparator()
global_position = self.mapToGlobal(self.pos())
self.qmenu.exec_(QtCore.QPoint(
global_position.x() - self.pos().x() + position.x(),
global_position.y() - self.pos().y() + position.y()
))
def _addMenuItem(self, index):
# For first tier menu
first_tier_menu = []
for i in self.qmenu.actions():
first_tier_menu.append(i.text())
new_menu_name, ok = QtGui.QInputDialog.getText(
self,
"Name of Menu",
"Name of new Menu Item:"
)
if ok:
if new_menu_name in list(filter(None, first_tier_menu)):
self.err_popup()
else:
menu = QtGui.QMenu(new_menu_name, self)
menu.setTearOffEnabled(True) # New item is shown in tearoff mode, unless I close and re-tearoff
add_item_action = QtGui.QAction('Add sub Item', menu)
slot = partial(self._addActionItem, menu)
add_item_action.triggered.connect(slot)
menu.addAction(add_item_action)
menu.addSeparator()
self.my_extra_menus[index].append(menu)
def _addActionItem(self, menu):
# For second tier menu
new_item_name, ok = QtGui.QInputDialog.getText(
self,
"Name of Menu Item",
"Name of new Menu Item:"
)
second_tier_menu = []
for i in menu.actions():
second_tier_menu.append(i.text())
if ok:
if new_item_name in list(filter(None, second_tier_menu)):
self.err_popup()
else:
action = QtGui.QAction(new_item_name, self)
slot = partial(self._callActionItem, new_item_name)
action.setCheckable(True)
action.toggled.connect(slot)
menu.addAction(action)
def _callActionItem(self, name, flag):
# Function for the checked items..
print name
print flag
def show_adv_qmenu(self, obj, action, index):
self.adv_qmenu = QtGui.QMenu()
rename_menu_action = QtGui.QAction('Rename Item', self)
rename_slot = partial(self.rename_menu_item, obj, action)
rename_menu_action.triggered.connect(rename_slot)
self.adv_qmenu.addAction(rename_menu_action)
delete_menu_action = QtGui.QAction('Delete Item', self)
delete_slot = partial(self.delete_menu_item, obj, action, index)
delete_menu_action.triggered.connect(delete_slot)
self.adv_qmenu.addAction(delete_menu_action)
# global_position = self.mapToGlobal(self.pos())
# self.adv_qmenu.exec_(QtCore.QPoint(global_position))
self.adv_qmenu.exec_(QtGui.QCursor().pos())
def rename_menu_item(self, obj, selected_action):
rename_name, ok = QtGui.QInputDialog.getText(
self,
"renaming",
"New name:"
)
if ok:
for i in obj.actions():
if selected_action.text() == i.text():
i.setText(rename_name)
print "Name changed : {0} --> {1}".format(selected_action.text(), i.text())
def delete_menu_item(self, obj, selected_action, index):
obj.removeAction(selected_action)
def err_popup(self):
msg = QtGui.QMessageBox()
msg.setIcon(QtGui.QMessageBox.Critical)
msg.setText("Input name already exists. Please check.")
msg.setWindowTitle('Unable to add item')
msg.setStandardButtons(QtGui.QMessageBox.Ok)
msg.exec_()
Sorry for the long code..
eventFilter must return a boolean, but in your case it does not return anything so the second error is throwing you. Also I have improved your logic:
def eventFilter(self, obj, event):
# Custom event only for right mouse click within the qmenu
if obj is self.qmenu and event.type() == QtCore.QEvent.MouseButtonPress:
if event.button() == QtCore.Qt.RightButton:
index = self.tab_bar.tabAt(event.pos())
action = self.qmenu.actionAt(event.pos())
if index != -1 and action is not None:
self.show_adv_qmenu(obj, action, index)
return super(MyWin, self).eventFilter(obj, event)

Get currently selected cell of QTreeWidget

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())

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