How do I change the dropEvent behavior correctly?

Viewed 28

I am trying to change the default behavior when dropping a file in QtextEdit

from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Dialog(object):
    def setupUi(self, Dialog):
        Dialog.setObjectName("Dialog")
        Dialog.resize(869, 499)
        self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
        self.verticalLayout.setObjectName("verticalLayout")
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.textEdit = QtWidgets.QTextEdit(Dialog)
        self.textEdit.setObjectName("textEdit")
        self.horizontalLayout_2.addWidget(self.textEdit)
        self.verticalLayout.addLayout(self.horizontalLayout_2)
        self.textEdit.setAcceptDrops(True)

        self.textEdit.dropEvent = self.dropEvent
    
    
    def dropEvent(self, event):
        event.setDropAction(QtCore.Qt.CopyAction)
        if event.mimeData().hasText():
            print(event.mimeData().text())
        event.accept()
    
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Dialog = QtWidgets.QDialog()
    ui = Ui_Dialog()
    ui.setupUi(Dialog)
    Dialog.show()
    sys.exit(app.exec_())      

But after performing dropEvent, the cursor in TextEdit stops moving.

What am I missing?

1 Answers

QTextEdit uses internal flags (not exposed to the API) that properly update the text cursor during drag and drop operations, mostly to allow pasting in the exact position within the text based on the mouse cursor, and in the meantime show the "cursor caret" to the user so that they will know where the content would be inserted.

This means that the default implementation of QTextEdit dropEvent() must always be called in order to properly update the cursor.

Now, proper drag&drop implementation of QTextEdit should always be done through insertFromMimeData() (and eventually canInsertFromMimeData() to prevent drop at all).

If you want to alter the behavior when dropping certain contents, then just override that function:

from PyQt5 import QtWidgets

class DropEdit(QtWidgets.QTextEdit):
    def insertFromMimeData(self, data):
        if data.hasUrls():
            self.insertPlainText('%ONEFILE%')
        else:
            super().insertFromMimeData(data)

    
if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    test = DropEdit()
    test.show()
    sys.exit(app.exec_())

Now, the issue is that insertFromMimeData() is called no matter if the operation is done from clipboard (through Ctrl+V or via the context menu) or from drag&drop. Another issue is that drag&drop can also happen within the text edit, for instance to move a selected text somewhere else.

A basic solution, which would prevent pasting from d&d but not from clipboard, would be to use an internal flag that can be set in the dropEvent() and would be cleared in insertFromMimeData().

The following example will accept drops only if the dropped data has no urls in it, but will still accept pasting from clipboard if it contains urls (for instance, copying an object in the file browser):

class DropEdit(QtWidgets.QTextEdit):
    acceptDrop = True
    def insertFromMimeData(self, data):
        if self.acceptDrop:
            super().insertFromMimeData(data)
        self.acceptDrop = True

    def dropEvent(self, event):
        self.acceptDrop = not event.mimeData().hasUrls()
        super().dropEvent(event)
Related