Skip to content

Drag and drop

This page shows how a d/Python window accepts something dragged onto it, how it starts a drag, and how d/OS's negotiated drag sessions make a move safe. Like copy and paste, drag and drop needs the pasteboard and a window, which the macOS host provides and this browser playground does not, so the samples here are for reading and for the macOS kit.

Two kinds of drag

d/OS has two layers, and it helps to name them.

A native drag is the ordinary desktop gesture: someone drags text out of a Mac application onto your window, or drags your card into TextEdit. The runtime bridges it to the pasteboard, your program sees an offer with representations, and a drop is always a copy. The other side is a native application; d/OS does not know what it did with the bytes.

A semantic drag session is between two d/OS programs (or two parts of one). It is a small transaction with negotiation, a commit on the destination, and a receipt, and it is the only way a drag can be a move, because it is the only way the source can be told, with proof, that the destination has the object.

Accept a native drop

# fragment: an ON_EVENT handler in a window program on macOS
import dos
from dos import drag, pasteboard

@dos.on("ON_EVENT")
def event(kind, lease, size):
    if kind != drag.EVENT_KIND:
        return
    offer = drag.from_event(lease, size)
    if offer.phase in (drag.ENTER, drag.OVER):
        tag, length = offer.representation(0)
        offer.feedback(drag.COPY if tag == pasteboard.TEXT else drag.REFUSE)
    elif offer.phase == drag.DROP:
        tag, length = offer.representation(0)
        if tag == pasteboard.TEXT:
            payload = offer.read(0, limit=8192)
            # Check the payload, then apply it to your model and redraw.

Drags arrive through the general ON_EVENT handler. drag.from_event checks the event record and gives you an offer object. While the pointer is over your window (ENTER, then OVER on each movement), you look at the representations on offer and answer with feedback: COPY to show the pointer that a drop would be accepted, REFUSE if you cannot use anything offered. On DROP, you read the representation you want. offer.x_fixed and offer.y_fixed give the pointer position in 1/64-pixel units, so you can tell which part of your window is under it.

The offer is valid only during the callback that delivered it. Keeping the object around does not keep the bytes available; read what you need while the event is live.

Start a native drag

# fragment: publish first, then start the drag from a SOURCE_GESTURE event
source = pasteboard.Publication([(pasteboard.TEXT, b"Hello from d/Python!")],
                                board=drag.SOURCE_BOARD, drag=True)

@dos.on("WINDOW_OPEN")
def opened():
    source.publish()          # keep the receipt outstanding while dragging is possible

@dos.on("ON_EVENT")
def event(kind, lease, size):
    if kind != drag.EVENT_KIND:
        return
    offer = drag.from_event(lease, size)
    if offer.phase == drag.SOURCE_GESTURE:
        offer.start_copy(source, 16, 48, 288, 40)   # the rectangle becomes the drag ghost
    elif offer.phase in (drag.SOURCE_DELIVERED, drag.SOURCE_CANCELLED):
        pass  # the source is unchanged either way

A drag source is a publication on a private drag board (drag.SOURCE_BOARD), separate from the general clipboard, so starting a drag never disturbs what the user copied earlier. Publish it when the window opens and leave its receipt unacknowledged while a drag can start. When the user begins dragging in your window, d/OS sends a SOURCE_GESTURE event; if it started on the thing you want to drag, call start_copy with the publication and the rectangle to lift as the ghost image. SOURCE_DELIVERED tells you the drop landed somewhere; it does not tell you what the other application did, and it never authorizes deleting anything.

The interchange_window.py example in the macOS kit is this program in full, with copy and paste on the C and V keys.

Semantic drag sessions

When both ends are d/OS programs, a drag can be negotiated. The session binds a publication to the source's document and revision, freezes the destination's proposal, commits, and hands back a receipt. All of it goes through the same eleven DRAG intents d/BASIC uses.

# fragment: the source side of a session
from dos import pasteboard, drag_session

publication = pasteboard.Publication([(pasteboard.TEXT, b"Selected text")],
                                     board=dos.constant("ui", "DRAG_SOURCE_BOARD"), drag=True)
publication.publish()

session = drag_session.Session(
    publication,
    operation=b"operation-id-001",
    source_artifact=b"source-artifact1",
    selection=b"selection-id-001",
    source_revision=7,
)
token = session.begin()

The identities are sixteen-byte values your program chooses: a fresh operation ID for each logical drag, the document and selection it came from, and the document's revision at that moment. begin() returns a token the destination will use. If the reply to begin() is lost, calling it again replays the same request and recovers the same token; the session remembers what it sent.

# fragment: the destination side of a session
proposal = drag_session.Proposal(
    token,
    target_artifact=b"target-artifact1",
    target_base_revision=12,
    drop_zone=b"drop-zone-id-001",
    interpretation=b"interpret-id-001",
    representations=[pasteboard.TEXT],
    operations=drag_session.COPY,
)
if proposal.negotiate():
    proposal.accept()
    # Apply the edit to your document, once, against the base revision you named.
    receipt = proposal.commit_destination(
        result_artifact=b"result-artifact1",
        target_post_revision=13,
        result_identity=b"result-ident-001",
    )
    receipt.ack()

The destination says what it would accept (representations, in preference order, and whether it allows copy, move or both), where the drop goes and how it will interpret it. negotiate() asks the host whether the two sides fit; the host picks the first representation both accept, and when both allow copy and move it picks copy, so a deletion is never chosen by ambiguity. After accept(), your program applies its own edit, then commit_destination reports the result. The receipt proves the destination received exactly the agreed bytes.

A move never deletes early

For a move, publish with allow_move=True, request MOVE in the proposal, and commit the destination first. Only then can the source be authorized to delete:

# fragment: the source deletes only after the destination has committed
authorization = session.authorize_move(current_source_revision)
if authorization.claim_source_edit(current_source_revision):
    # Delete the selection from your document, once, and record it for undo.
    receipt = authorization.commit_source()
    receipt.ack()

The authorization is good for one edit at one exact revision. If the source document changed in the meantime, deletion is declined and the destination's copy remains, recoverable. If confirmation of the source edit is lost, session.recover() fetches the terminal receipt; nothing tempts the program to delete twice. Today a move works within one application (source and destination roles in the same program); a move between two different applications is declined by design until the cross-application checks exist.

Host-side failures raise RuntimeError with d/OS's status inside; bad arguments raise ValueError before anything is sent. session.cancel(reason) and proposal.reject(reason) end a session with a stated reason.

Where it is proven

All eleven drag intents have the same test written in d/BASIC and d/Python through the real macOS provider, including a local move, reject, cancel and lost-completion recovery; the three native copy routes are tested as well. The intent coverage page lists them. Not yet tested: physical mouse gestures in a real session, moves between two applications, and other platforms.

Further reading

The concepts here, the pasteboard with several representations, negotiation before commit, and the receipt, come from NeXTSTEP by way of d/OS, and the d/BASIC guide tells that story in full: Drag, Drop & DOBJ.