Chapter 23. Drag and drop

Table of Contents
Handling drops
Initiating drags
Conclusion

PyQt fully supports standard drag and drop operations on all platforms. This includes both Windows OLE drag and drop, and the two X11 standards: XDND (which uses MIME) and the legacy Motif Drag'n'Drop protocol.

MIME, which you may know as a way to encode all kinds of datatypes for e-mail, is used to encode the dragged data in Qt. This is a very flexible standard, regulated by IANA (http://www.isi.edu/in-notes/iana/assignments/media-types/). This means that almost any kind of data can be handled by the Qt drag and drop mechanism, not just text or images.

Handling drops

Our Kalam editor must be able to accepts drops from other applications, so we will have to implement this functionality.

Incoming drop operations are easy enough to handle. Basically, you call self.setAcceptDrops(TRUE) for the widget or widgets that should accept drop events, and then actually handle those drop events.

The widget that should accept the drops is our KalamView class. But setting self.setAcceptDrops(TRUE) in the constructor of KalamView won't work. This is because KalamView doesn't actually handle the drops; rather, they are handled by the QMultiLineEdit class, which is encapsulated by KalamView.

The easiest solution is to extend the small subclass of QMultiLineEdit (which we already created) to handle drop events.

Example 23-1. Handling drop events

"""
kalamview.py - the editor view component for Kalam

copyright: (C) 2001, Boudewijn Rempt
email:     boud@rempt.xs4all.nl
"""
from qt import *
import kalamconfig
from resources import TRUE, FALSE
class KalamMultiLineEdit(QMultiLineEdit):

    def __init__(self, *args):
        apply(QMultiLineEdit.__init__,(self,) + args)
        self.setAcceptDrops(TRUE)

    def dragEnterEvent(self, e):
        e.accept(QTextDrag.canDecode(e))

    def dropEvent(self, e):
        t=QString()
        if QTextDrag.decode(e, t): # fills t with decoded text
            self.insert(t)
            self.emit(SIGNAL("textChanged()"), ())

    def event(self, e):
        if e.type() == QEvent.KeyPress:
            QMultiLineEdit.keyPressEvent(self, e)
            return TRUE
        else:
            return QMultiLineEdit.event(self, e)
      

How does this bit of code work? First, you can see that the custom widget accepts drop events: self.setAcceptDrops(TRUE).

The dragEnterEvent() method is fired whenever something is dragged over the widget. In this function we can determine whether the object that is dragged over our application is something we'd like to accept. If the function QTextDrag.canDecode() returns true, then we know we can get some text out of the data the drop event carries. We accept the event, which means that whenever the cursor enters the widget, the shape will change to indicate that the contents can be dropped.

If the user releases the mouse button, the function dropEvent() is called. This presents us with a QDropEvent object, which we can decode to get the contents.

However, here we come across one of the inconsistencies of the PyQt bindings to Qt. Sometimes a function wants to return two values: a boolean to indicate whether the operation was successful, and the result of the operation. In the case of QFontDialog, the results are returned in a tuple:

        (font, ok) = QFontDialog.getFont(self, "FontDialog")
    

Likewise, QTextDrag.decode wants to return two values: a boolean and a string. However, here you need to first create a QString, and pass that to QTextDrag.decode, which fills it with the text, while returning a boolean indicating whether the decoding went well.

However, having got the text by hook or by crook, we can insert it in the view, and tell the world that the text has changed (so other views on the text can update themselves).

You can now select and drag that text to a Kalam document. If you drag a file, only the filename will be inserted, because a filename can also be decoded as text. If you want to open the file instead, you still have some work to do.