Thursday, January 30, 2014

Wednesday, January 29, 2014

Qt's Drag-and-Drop Architecture for Python and PyQt5
A Doh! Moment

Sometimes you have to take the long way around to the obvious answer...

So I am concerned about knowing where the mouse is, when a drag action ends. I even hinted I could use a signal to send back the value of QCursor.pos() from inside some drag object.

Then it occurred to me: if I want the cursor position at the end of a drag, why don't I just, oh, I don't know, sample the cursor position at the end of the drag?

In other words, change the doSomeDraggin() routine like this:

        act = dragster.exec_(actions)
        global_pos = QCursor.pos() # cursor immediately after drag ends
        local_pos = self.mapFromGlobal(global_pos)
        print('cursor at local  {0}, {1}'.format(local_pos.x(),local_pos.y()))
        if not self.parentWidget().rect().contains(local_pos) :
            print('drop was outside my parent')

And of course this works fine. Even on a slow machine, the mouse can't have moved more than a pixel or two between release of the mouse button and the return from the exec_() of the drag.

Tuesday, January 28, 2014

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 10 Hacking QMimeData

Modifying QDrag is no use. What about QMimeData, which is key to the Delayed Encoding hack? I set up the following modified MIME data class.

class MaimData(QMimeData):
    def __init__(self):
        super().__init__()
    def retrieveData(self,mt,ty):
        print('MT retrieveData')
        return super().retrieveData(mt,ty)
    def formats(self):
        print('MT formats')
        return super().formats()

The results were very interesting (on Mac OS X 10.9, this is).

starting drag with actions: Copy Move Link
MT formats
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
drag enters at 0 20 kbd mods 0 buttons 1 offering actions: Copy Move Link
MT formats
target moved to <class '__main__.TargWidj'>
drag moving at 1 20
drag moving at 3 19
drag moving at 4 19
drag moving at 5 19
dropping at 5 19 actions: Move
 -- setting copy action!
MT formats
MT retrieveData
exec returns 1 default 2 target <class '__main__.TargWidj'> source <class '__main__.SorcWidj'>

There is an immediate call to formats() and then five (5!) successive calls to retrieveData(). These all take place the moment the drag begins, while the mouse has barely moved. This immediately shows why the Delayed Encoding hack fails on Mac OS: not only would the expensive data conversion not be delayed; it would be done multiple times!

The next call to formats happens when the dragEnterEvent() method of the target widget accesses event.mimeData().hasText(). Then both methods are called during drop event processing.

Well, this looks promising. What happens if the drag is dropped onto a different application?

starting drag with actions: Copy Move Link
MT formats
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
exec returns 0 default 2 target <class 'NoneType'> source <class '__main__.SorcWidj'>

OK, what happens if it is dropped on the desktop?

starting drag with actions: Copy Move Link
MT formats
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
exec returns 0 default 2 target <class 'NoneType'> source <class '__main__.SorcWidj'>

Oops. What about a drop that fails, releasing the mouse over an ineligible receiver?

starting drag with actions: Copy Move Link
MT formats
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
MT retrieveData
exec returns 0 default 2 target <class 'NoneType'> source <class '__main__.SorcWidj'>

Depressing: the MIME data methods are just never called after the start of the drag, except when the drag enters a target in the same app.

It may be this behavior is peculiar to Mac OS and the code would behave differently on Windows or Linux. Doesn't matter; Mac OS is one of two main targets for my app, and anyway I want to keep it platform independent.

For the moment my desire to detect the drag of a tab off the edge of its parent window—and for that matter, the complementary desire to detect the drag of a QDialog onto a QTabBar—appears to be out of reach. But stay tuned, something may turn up.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 9 Hacking QDrag

I began this project because I have an app that presents the user with a set of tabs (QTabSet). I have had user requests for the ability to pull some of these tabs out as separate windows, in the way that you can pull a tab out of a Firefox or Chrome browser window and it automatically expands to be an independent browser window.

How to do this in Qt5? Clearly, I thought, one starts with drag-and-drop. When the dragging cursor goes outside the bounds of the main window, end the drag and do the magic thing to move the widget that is now in a tab, to become a QDialog on its own.

All the prior descriptions are the result of my learning Qt drag-and-drop so I could misuse it in that way. And it appears so far that it can't be misused so, because when a drag ends the source widget doesn't get the info it needs. The source widget can tell that a drop succeeded with a target widget in the same Qt app. But it cannot tell the difference between a drop that didn't succeed and one that succeeded with a different app. And it particularly cannot tell where the cursor was, when the user released the button.

The Delayed-Encoding Hack

Drag support as delivered doesn't give that info, but could it be hacked to do so? Qt documents the Delayed Encoding Example. This code attempts to solve the problem where it is an expensive operation to encode the data into a MIME object, a cost that would be wasted if the drag did not complete.

The solution offered is to modify a QMimeData object. This object is not loaded with data, but is modified to issue a signal when its retrieveData() method is called. That call means that a drag target is trying to access the data, in other words, the drop has found a target that accepts it. Supposedly the signal is passed to a slot in the drag source widget, and it quickly does the data conversion and calls the setData() method of the modified QMimeData object in time for it to be retrieved.

Seems rather iffy and implementation-dependent to me. And indeed, there is a bug report saying it doesn't work in OS X because there, the data is pulled out of the drag as soon as the operation starts.

The Delayed Coding example suggests that drag-related objects can be modified, for example they can be made to issue signals when something happens. A signal can carry data, for example, the current value of QCursor.pos(), and then we would know where the mouse was at that time.

Instrumenting QDrag

The Delayed Coding example modifies QMimeData. Let's step back a bit and start with QDrag. I figured some of its methods would be called when a drop was starting. The following code is from hackdraggin.py.

class  DragOn(QDrag):
    def __init__(self, parent):
        super().__init__(parent)

    def source(self):
        print('Drag: source called')
        return super().source()

    def mimeData(self):
        print('Drag: mimeData called')
        return super().mimeData()

    def event(self, event_obj):
        print('Drag: event# ',int(event_obj.type()) )
        super().event(event_obj)

I used this class in place of QDrag in the SorcWidj code. Three methods are modified to print something when they are entered. source() is called directly from the doSomeDraggin() code, and that is the only one of these messages that prints!

DragOn class overrides the event() method. That would be called if any event, keyboard, mouse, whatever, was delivered to the drag object. None are, apparently, because that message never prints. Nor does the message from the mimeData() method, and that is very puzzling because if it isn't called, how does the QDropEvent get access to the passed data?

Thinking that perhaps a QDrag object was in some special purgatory where it couldn't get to stdout, I modified the class to store debugging data in the MIME object itself:

class  DragOn2(QDrag):
    def __init__(self, parent):
        super().__init__(parent)
        self.log_text = ''

    def setMimeData(self,md_object):
        self.md_obj = md_object # save ref. to QMimeData
        return super().setMimeData(md_object)

    def logSomething(self, text):
        self.log_text += text
        self.md_obj.setText(self.log_text)

    def mimeData(self):
        self.logSomething('mimeData called')
        return super().mimeData()

    def event(self, event_obj):
        self.logSomething( 'event {0} '.format(int(event_obj.type()) ) )
        super().event(event_obj)

The logSomething() method, if called, adds some text to a string and makes that string the payload of the MIME data. If either the event() or the mimeData() method is entered, there will be evidence in the text that is actually dropped.

Result? Nada. The dropped text is always the original text. logSomething() is not being called.

Conclusion? At least under PyQt5, the QDrag class is a dummy, a fake, nothing but a parameter list to the real code. The Qt code reaches in and gets the QMimeData object from it without going through its mimeData() method, and puts that into (presumably) the QDropEvent object. It is the latter that gets all the action; the QDrag object is inert.

Next up: Hack QMimeData

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 8, Running Tests

To repeat, the entire code of the example program is in this PasteBin. Copy it; save it as draggin.py; and execute it from a command line:

$ python draggin.py

Try dragging and dropping:

  • From the source onto upper part of the target
  • Onto the forbidden (lower right) quadrant of the target
  • Onto another app that accepts text drops, like a text editor
  • Onto the desktop (in Mac OS, makes a "clipping" file)
  • Onto something that doesn't accept the drop

You can also start more than one copy:

$ python draggin.py &
$ python draggin.py &

Now you have two copies. Drag and drop from the source of one onto the target of the other and note, although the drop is accepted by a Qt widget, the source doesn't get any indication of success.

Next post: hacking the QDrag class for fun (but no profit)!

Monday, January 27, 2014

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 7, Drop the load

All right, let's drop that load!

    def dropEvent(self, event):
        msg = 'dropping at {0} {1}'.format(event.pos().x(), event.pos().y())
        actions = event.dropAction()
        print(msg, xlate_actions(actions))
        if actions & Qt.CopyAction :
            event.acceptProposedAction()
        else :
            print(' -- setting copy action!')
            event.setDropAction(Qt.CopyAction)
        self.setText( event.mimeData().text() )
        event.accept()

If dragMoveEvent() is not implemented, or it executes event.accept() just before the mouse button is released, the drop proceeds by calling your dropEvent() method.

The first three lines above just print debugging info; they would not be in production code.

The next lines make sure that we will do a copy and that the drag source widget will know this was the case. The event.dropaction() value is the value that will be returned by the drag object's exec_() method, so we force it to Copy if it isn't already Copy.

Of course, that is only useful if indeed the drag was started by a Qt drag source like the one we coded at a few posts back; and if that drag source is in this same Qt app. If the drag was begun in another Qt app, nothing is reported back; and if it began in a non-Qt app, who knows what it expects?

Finally we take the data from the MIME object and use it. In this pathetically simple example that means setting it as this QLabel's text. Then we accept the event, and this ends the drag successfully.

It is possible to reject the drop even at this late point. Your code could examine the MIME data and decide it is not acceptable, or perhaps some resource it needs is not available just now. If the drop can't be accepted for any reason, just call event.ignore() and exit. The drop fails.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 6, Drag Moves

Once dragEnterEvent() has accepted the drag, the widget begins to receive a stream of calls to its dragMoveEvent() method. You don't need to implement this if you don't care where the drop happens upon your widget's rectangle. But here is an example of one.

    def dragMoveEvent(self, event):
        pos = event.pos()
        if pos != self.move_point:
            print('drag moving at {0} {1}'.format(pos.x(), pos.y()))
            self.move_point = pos
        # To illustrate forbidden areas, we mark the lower right quadrant as
        # invalid. The lower right quadrant is the rect with top-left at w/2,
        # h/2 and with size w/2, h/2. It doesn't make sense to specify this
        # over and over, but there's no other way.
        half_width = self.width()/2
        half_height = self.height()/2
        forbidden_rect = QRect(half_width,half_height,half_width,half_height)
        #event.ignore(forbidden_rect)
        if forbidden_rect.contains(pos):
            event.ignore()
        else:
            event.accept()

The first four lines implement a debugging display. These events are continuous and rapid, even if the mouse is not moving. (That's right: dragMoveEvent is called even when the mouse does not move!) For this reason we save the last-displayed point and only display again if the mouse has actually changed position.

You could track drag move events in order to change the appearance of the widget depending on the position of the cursor, for example changing its border, or somehow highlighting a child widget when the cursor was over it.

Another use is to put restrictions on the particular part of a widget that will receive the drop. The QDragMoveEvent reference claims that if you call event.ignore(rectangle), "Moves within the rectangle are not acceptable, and will be ignored." This does not seem to be true. If you enable the example line above, event.ignore(forbidden_rect), it has no effect on the behavior of the drag and drop operation. The drop will take place in the forbidden area if that's where the mouse is released.

What does make a difference is the explicit call to event.ignore() when the drag is moving within the forbidden rectangle. If the last call to dragMoveEvent before the mouse is released ends in event.ignore(), the drop doesn't happen. The cursor wanders away and the drag ends without a drop.

If you have changed the look of the widget at the start of the drag, or during the drag moves, you would like to change it back to normal if the drag doesn't happen. That's the purpose of this code:

    def dragLeaveEvent(self, event):
        print('drag leaving')
        event.accept()

This event is delivered in two cases: one, if the user drags the cursor out of the widget's boundary; and two, if the user releases the mouse button and your dragMoveEvent ends in event.ignore(). Either counts as the drag "leaving".

Next post: dropping a load.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 5, Detecting a Drop

Let's begin to code up a drop target widget. The code that follows contains some debugging displays. With those stripped out there isn't a lot of code.

class TargWidj(QLabel):
    '''A simple class that can detect an incoming drag
    and accept it, but only if it's a Copy of plain text.'''
    def __init__(self,text):
        super().__init__()
        self.setAcceptDrops(True)
        self.setText(text)
        self.move_point = QPoint(-1,-1)

The example drop target is another QLabel with a few extra members. The important step is setting self.setAcceptDrops(True). If this property is not set, or if it is set to False during execution, no drop-related events will be reported.

    def dragEnterEvent(self, event):
        actions = event.possibleActions()
        self.move_point = event.pos()
        msg1 = 'drag enters at {0} {1}'.format(event.pos().x(), event.pos().y())
        msg2 = 'kbd mods {0:0x} buttons {1:0x}'.format(
            int(event.keyboardModifiers()), int(event.mouseButtons()) )
        print(msg1,msg2,'offering',xlate_actions(actions))
        if event.mimeData().hasText():
            if actions & Qt.CopyAction :
                event.acceptProposedAction()
            else :
                print(' -- setting copy action')
                event.setDropAction(Qt.CopyAction)
                event.accept()

The first sign that a drag is is underway is a call to this event handler. It tells you that a dragging cursor has crossed the boundary of this widget. The first five lines above would not appear in your production code. Here they display something like

drag enters at 0 38 kbd mods 0 buttons 1 offering actions: Copy Move

The real code starts with actions = event.possibleActions(). The QDragEvent class offers a confusing array of "actions" inherited from QDropEvent.

First, event.possibleActions() returns the set of actions that were passed-in to the exec_(actions) method of the Drag object (or its equivalent in some other app!). These are the actions that the source widget is offering, or permitting, or expecting, or something like that.

Then, event.proposedAction() is one of the "possible" actions, usually just Qt.MoveAction, and I can't say why this action is singled out as a "proposal".

event.dropAction() is the action that will be performed at drop time. If you don't agree with dropAction, you can call event.setDropAction() and set a different one.

In our example, we are insisting on doing a Copy. So if Copy is offered we call event.acceptProposedAction(). If not, we set the drop action to Qt.CopyAction and call event.accept().

Why are there two different ways of accepting the event? I don't know. What I do know is that if dragEnterEvent() exits without accepting the event in one of those two ways, it has rejected the drop, and no further events related to this drop will be presented to this widget. The drop is over, as far as this widget is concerned.

In the next post we look at what happens after the drag is accepted.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 4, Coding a Drag Source

Code for a Drag Source

The whole code for the example program is at this pastebin link. Here we will look at it in pieces. Let's implement a drag-source.

class SorcWidj(QLabel):
    '''A simple drag-source with ability
    to recognize the start of a drag motion
    and implement the drag.'''
    def __init__(self,text):
        super().__init__()
        self.setText(text)
        self.mouse_down = False # has a left-click happened yet?
        self.mouse_posn = QPoint() # if so, this was where...
        self.mouse_time = QTime() # ...and this was when.

SorcWidj is just a QLabel with a few extra features, especially three members where we note the time and place of a click. We set these fields in the following method:

    def mousePressEvent(self,event):
        if event.button() == Qt.LeftButton :
            self.mouse_down = True # we are left-clicked-upon
            self.mouse_posn = event.pos() # here and...
            self.mouse_time.start() # ...now
        event.ignore()
        super().mousePressEvent(event) # pass it on up

When the user clicks down with any mouse button on this widget, mousePressEvent is entered. For this example we are only supporting left-clicks and left-drags. So, if this is a left-click, we save the position (x and y in local coordinates) and we start a millisecond timer going. Why do we want this info? Here's why:

    def mouseMoveEvent(self,event):
        if self.mouse_down :
            # Mouse left-clicked and is now moving. Is this the start of a
            # drag? Note time since the click and approximate distance moved
            # since the click and test against the app's standard.
            t = self.mouse_time.elapsed()
            d = (event.pos() - self.mouse_posn).manhattanLength()
            if t >= QApplication.startDragTime() \
            or d >= QApplication.startDragDistance() :
                # Yes, a proper drag is indicated. Commence dragging.
                self.doSomeDraggin(Qt.CopyAction|Qt.MoveAction)
                event.accept()
                return
        # Move does not (yet) constitute a drag, ignore it.
        event.ignore()
        super().mouseMoveEvent(event)

This logic is taken straight from the Qt documentation. Whenever the mouse moves above our widget with a button down, mouseMoveEvent() is called. If our mousePressEvent decided it was valid (in this case, if it was a left-click), we note the time t and distance d since that click event.

The application has a platform-dependent standard for the amount of time and distance that the mouse should move before the motion constitutes a "drag". We test against those standards. If they are met, then we initiate a drag, accept the event, and exit. Otherwise we pass the event along. Now, let's get to the beef. How do we initiate a drag?

    def doSomeDraggin(self, actions):
        # Create the QDrag object
        dragster = QDrag(self)
        # Make a scaled pixmap of our widget to put under the cursor.
        thumb = self.grab().scaledToHeight(50)
        dragster.setPixmap(thumb)
        dragster.setHotSpot(QPoint(thumb.width()/2,thumb.height()/2))
        # Create some data to be dragged and load it in the dragster.
        md = QMimeData()
        md.setText(self.text())
        dragster.setMimeData(md)
        # Initiate the drag, which really is a form of modal dialog.
        # Result is supposed to be the action performed at the drop.
        act = dragster.exec_(actions)
        defact = dragster.defaultAction()
        # Display the results of the drag.
        targ = dragster.target() # s.b. the widget that received the drop
        src = dragster.source() # s.b. this very widget
        print('exec returns',int(act),'default',int(defact),'target',type(targ), 'source',type(src))
        return

Once you have decided that a drag is necessary, this is how you initiate it. Let's go over it in pieces.

        dragster = QDrag(self)

The QDrag object represents the drag. We will initialize it and then execute it much as we execute a modal dialog.

        thumb = self.grab().scaledToHeight(50)
        dragster.setPixmap(thumb)

The QWidget.grab() method was added in Qt5. It returns a pixmap of that widget as it looks now. Here we grab a pixmap of our own widget (we take a selfie!). And scale it to be no more than 50px high. We apply our selfie pixmap to the drag object. It will be displayed under the cursor and follow it around during the drag. The pixmap is optional; in your application you might not use it, or you might use a pixmap of something else.

        dragster.setHotSpot(QPoint(thumb.width()/2,thumb.height()/2))

Another optional step repositions the selfie thumbnail so that it is centered under the cursor. Without this, cursor will be at the top left corner of the thumbnail pixmap.

        md = QMimeData()
        md.setText(self.text())
        dragster.setMimeData(md)

This is the heart of drag initiation. You are supposed to package the data that is being dragged in the form of MIME data. MIME began as a standard for attaching arbitrary data to emails. It has been extended to allow passing data between any programs.

In principle you can package just about anything as MIME data. You load the QMimeData object with the data and set it to have the appropriate MIME type. Then you assign it to the drag object.

Why do this? Because you don't know where the drag is going. It isn't necessarily going to some other part of your app. It might be dropped anywhere, on any app, or on the desktop. By packaging the data as a MIME type, you ensure that any other application that supports MIME can accept it.

In this example, we are punting the whole issue and setting the MIME data to the current text of this QLabel. If your application has to pass something more structured than simple text, you will have to study the QMimeData reference and the Qt page on MIME data.

        act = dragster.exec_(actions)

This statement initiates the drag operation. Just as with a modal dialog, you exec_() the drag object. The argument is the set of actions you will permit the drop to perform: some OR-combination of Qt.MoveAction, Qt.CopyAction, and Qt.LinkAction. (We passed these from the mouseMoveEvent() code.)

Once the drag starts, this code is effectively suspended until the user lets go of the mouse. In Linux and Mac OS, signals continue to be processed and other threads of the app keep executing. In Windows, the whole app stops.

Eventually the user will relax her finger on the mouse and end the drag. Then, in theory, the action code that was actually performed is returned. The statements that follow in our example print out what can be learned after the drag completes: the supposed action, the default action, and the identities of the source widget (this one) and the target widget that accepted the drag.

If and only if the drop is accepted by a Qt widget in this same application, the returned action will be one of Qt.MoveAction, Qt.CopyAction, or Qt.LinkAction. And the widget returned by the target() method of the drag object will be a reference to the widget that accepted the drop.

If the drop completes in some other application, whether written in Qt or not, the returned action will be 0, and the value returned by dragster.target() will be None. Those things will also be the case if the drop simply doesn't complete, for example if the user releases the mouse over some location that doesn't accept drops.

This is a bit of a hole in the Qt drag-and-drop support. There is no standard way to tell if a drag completed successfully in another app's window, or just didn't complete.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 3, The Drag Source

A drag source in Qt is any QWidget derivative in which:

  • MousePressEvent() is implemented to note when and where the mouse is clicked-down
  • MouseReleaseEvent() is implemented to note that the mouse is no longer clicked-down
  • MouseMoveEvent() is implemented and detects when the mouse has moved far enough, or been down long enough, to show that the user wants to begin a drag, and then...
  • It starts a drag by creating a QDrag object, loading it with data, and executing it.

One way to look at drag is that it is a peculiar kind of modal dialog, much like a QFileDialog. When the user manipulates the mouse in a certain way, you know you should initiate this "dialog". When the drag pseudo-dialog completes, you have a result that sometimes indicates what happened. Other times you are left guessing.

In the next post, we'll look at some real code to see how this is done.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt. 2, The Drop Target

The Drop Target

A drop target in Qt is a widget (any QWidget derivative) in which:

  • The widget at some time sets self.setAcceptDrops(True)
  • The widget implements the dragEnterEvent() method
  • The widget implements the dropEvent() method

The widget may optionally implement the dragMoveEvent() and/or dragLeaveEvent() methods, but these are not usually required.

When a widget sets acceptDrops to True, it will be called at dragEnterEvent() when the mouse cursor of a user drag crosses into the widget's boundary rectangle. In this method your code inspects the purpose and content of the drag and decides if it's for you. The code can look at the modifier keys (is this an Alt/Option- or Control-drag?). It can look at the mouse buttons (left-button drag, or right-button?). It can interrogate the type and even the content of the data that would be dropped.

If dragEnterEvent() rejects the drag, nothing further happens—the mouse cursor might change to show that this drag is not allowed, or not. But no other drag-related event methods will be delivered to this widget for this drag.

If the dragEnterEvent() code indicates that the drag is acceptable, more things will happen. The cursor will move across your widget's surface, and your dragMoveEvent(), if implemented, will be called repeatedly as it does so. The cursor may wander out of your widget without dropping; if so, your dragLeaveEvent() will be called, if implemented.

Or, the user may release the mouse button over your widget. Then your dropEvent() is called. At this point you can still reject the drag; otherwise your code is supposed to take the data out of the drag and do something with it.

If this sounds complex, it is. Fortunately many Qt widgets handle drops automatically. For example, QListView and QTableView handle dragging and dropping, and it's a blessing that they do.

In the next post we'll review the design of a drag source widget.

Qt's Drag-and-Drop Architecture for Python and PyQt5
Pt 1, an Overview

In the following series of posts I will review the classes and methods needed to implement Drag-and-Drop functionality in a Qt5 program written in PyQt5.

The official documentation (for C++ of course) is found in this overview. It contains links to the reference pages for most of the the important classes, and it covers the basics for a C++ programmer. Doing the mental translation from C++ syntax to Python/PyQt syntax is a habit that the Python programmer needs to learn.

However, I found the official overview somewhat confusing. One problem is that it does not clearly distinguish the design of a drop target, a widget that receives dropped data, and the design of a drag source, a widget that recognizes a mouse drag motion and initiates a drag. These two are quite distinct. They are executed at different times and might be executed in different apps, with the drag beginning in one app and the drop ending in another. Thus you can have a drop without a drag and vice versa. They use different classes and require you to override different class methods. All told, they need separate treatment, which I will give them in this series of posts.

The User's View

The user thinks of drag-and-drop as a single smooth mouse operation: click down on something; move the mouse to something else; let go. During the drag the mouse cursor may change its appearance in some familiar way, perhaps acquiring a plus-sign to indicate a copy will happen or a slashed-circle to indicate that no dropping is allowed.

It is possible for the mouse to acquire a little thumbnail image of the thing being dragged, as a reminder. For example when dragging text in an editor, a translucent copy of the dragged text, or part of it, may follow the mouse cursor.

The user will often be dragging from one place in an application to another place in the same application: dragging a paragraph of text from one place in a document to another, for example; or dragging a list item to a different position in the same list.

But it may be that the user is dragging something from one application and dropping it into a completely different application: for example, dragging text from a Qt editor and dropping it on the host Desktop as a "clipping"; or dragging a URL from a browser window and dropping it into a Qt widget of some kind.

All in all, drag-and-drop is a simple, quick, familiar operation to the user—or should be. But making it happen at the level of program code turns out to be quite complicated.

The Program's View

To the program written in Qt (and specifically PyQt5: in these posts, "Qt" and "PyQt" are synonyms), it is actually not correct to speak of "drag-and-drop" as a single thing. There is drag, the initiating of a drag operation, and there is drop, the delivery of content to a target. These two use completely different classes and methods and are designed in isolation from each other.

Moreover, remember that the drag might start in a completely different application, so it arrives at your Qt code as an unheralded drop with data you didn't prepare. Or a drag that you initiate in your Qt code might end being dropped in some completely unrelated program.

The Qt drag-and-drop support is rather unhelpful in these cases of dragging between different applications. It only works fully as documented when the drag and drop are between widgets in the same application. I'll point out these issues as they come up.

In the next post we'll take a high-level look at drop target code.