Jellby
Jellby

Reputation: 2694

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

Upvotes: 1

Views: 2060

Answers (1)

Heike
Heike

Reputation: 24430

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

Upvotes: 1

Related Questions