Drag a clipboard entry out into another app - #596
Merged
Conversation
|
Closing this automatically — it doesn't link an approved issue. Tinycast requires the bug or feature to be agreed before code is written, so effort isn't To get this merged:
Docs-only changes ( |
Left open, the type checker reads .prefix as the Sequence overload, types the result as PrefixSequence and stops resolving the whole + chain. The Swift 6.2.1 toolchain in the Command Line Tools rejects the line outright, which takes clipboard-test, clipboard-search-test and every other harness that compiles the store down with it. Behaviour is unchanged.
An image or file row becomes a drag source for the file it already is, so reaching another app costs one gesture instead of Reveal in Finder plus a second drag. The drag is AppKit rather than SwiftUI's onDrag for one reason: imagesDir sits on the boot volume, where a file-URL drop defaults to a move, and a move carries the blob out of the history and strands its row. Only an NSDraggingSource can answer sourceOperationMaskFor, so ClipDragView answers .copy everywhere. The handle claims mouse-down the way WindowDragHandle does, since the hosting view eats the click first, and forwards anything under 4pt of slop as the click it was. Text rows are untouched: dragURL is nil for them, so no overlay is installed and their gestures stay as they were.
A text row had no payload and so no overlay, which left every entry that is not an image or a file undraggable. dragPayload now answers for all three: the file URL for anything on disk, and for a text row the classification the store already derives. A link writes two pasteboard types from one item. A browser reads public.url, a text field reads the string, and neither has to settle for the other's flavour. A bare domain gets https:// so the URL is one a browser accepts, which is the same assumption the address bar makes. The drag preview is now the row as drawn, since a text row has no thumbnail to fall back on.
Four things the review caught. The row snapshot never drew anything. SwiftUI renders into layers, so cacheDisplay hands back a transparent bitmap and the drag carried no image at all. Previews are drawn per payload now: the cached tile for a file, a rounded text tile for a link or a copy. The dragging frame is sized to that image and centred on the cursor, and a refused drop animates back, so a drag that achieved nothing says so. dragPayload touched no store state and pushed ClipboardStore past 1000 lines. It is a computed property on ClipboardItem now, in its own Model file beside the ClipDragPayload it returns, which is where textForm and colorValue already live. The bespoke bare-domain helper is gone. QuicklinkDestination.detect already parses schemes, network shares and deeplinks. textForm stays the one answer to whether an entry is a link, so the drag and the type filter cannot disagree; the detector only builds the URL. A vanished file is reported rather than dragged out as a dead path, which is what Reveal and Open already do. The payload resolves on mouse-down instead of on every row render, so the stat costs one call per drag. Also deletes the row's onTapGesture and double-tap gesture. The overlay claims mouse-down on every row, so both were unreachable duplicates of the closures the handle already calls.
The section still described dragURL on the store, a nil payload for text and the onTapGesture the overlay replaced, all three of which went in the last two commits. It now covers what is there: dragPayload on the item, the mouse-down resolution and its stat, the QuicklinkDestination reuse, and why previews are drawn rather than snapshotted.
Three things from the review. The drag overlay sat above the actions catcher and answered every event it was offered, right-mouse included. NSView forwards an unhandled rightMouseDown up the superview chain, never to a sibling, so the catcher underneath was unreachable and the row's actions menu silently stopped opening. ClipDragView now declines right events in hitTest, the mirror of what RightClickCatcher already does with the left button, so neither overlay can claim what the other needs. Verified against a real event loop: the right click lands on the catcher and the left one still starts the drag. lockFocus and unlockFocus are deprecated, and the SDK names NSImage(size:flipped:drawingHandler:) as the replacement. The text tile draws through that instead. The comments were stacked two and three lines deep. Each is one line now, and the ordering invariant the overlay depends on is written down in the feature doc rather than argued in the file.
abue-ammar
force-pushed
the
clipboard-drag-out
branch
from
September 11, 2026 20:47
148455a to
ca12df4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Related issue
Closes #544
What changed
A clipboard row is now a drag source. Press, move four points, and the entry goes to whatever is under the cursor: an image or file as a file URL, a link as
public.urlplus its string, plain text as a string.The one non-obvious thing is copy versus move.
imagesDirsits in Application Support on the boot volume, which is the same volume as almost every drop target, so a file-URL drop there defaults to a move. That carries the blob out of the history and leaves the row pointing at nothing. OnlyNSDraggingSourcecan answersourceOperationMaskForwith.copy, and SwiftUI'sonDragtakes anNSItemProviderand nothing else. SoClipDragHandleis an AppKit overlay that tracks the gesture itself, the same shape asWindowDragHandle.The overlay claims the left button outright, so it answers the click and the double click too. The row's
onTapGestureand its double-tapsimultaneousGestureare deleted rather than left underneath, where they would never fire.It reuses what is there rather than adding vocabulary:
textFormstill decides what counts as a link, so the drag and the type filter cannot disagree.QuicklinkDestination.detectbuilds the URL.ClipboardCoordinator.dragPayload(for:)stats the file first, so a vanished one raises the HUD instead of handing another app a dead path.cacheDisplayon the row returns a transparent bitmap. A file uses the thumbnail already cached at 64 px, a link or text gets a drawn tile.One commit is unrelated to the feature.
ClipboardStore.searchhad a bareprefix(_:)inside a+chain, which the Command Line Tools Swift 6.2.1 type checker resolves to theSequenceoverload and then fails the whole expression. Spelling outArray(...)fixes it and changes no behavior. Without it four harnesses cannot compile outside Xcode.Memory footprint
Not measured. There is no Xcode on the machine this was written on, so there is no Instruments run and no leak test.
What the code does allocate: one
NSViewper visible row, and oneNSImageper drag that lives for the length of the gesture. The file preview reads the thumbnail cache only and never decodes on mouse-down. Nothing is retained afterdraggingSession(_:endedAt:operation:)returns.Leak-tested: no
Demo
Not recorded yet.
Drawbacks
clipDraggableinstead of a SwiftUI gesture..copyis returned for every context, including inside the app. There is no in-app drop target today.Tests & validation
Scripts/run-tests.sh, all 68 harnesses pass. Three of them globClipboard/Model/*.swiftand now compileClipDragPayload.swift, so the script passesQuicklinkDestination.swiftalongside them. No cases were added.SwiftLint and the purity grep are clean.
docs/features/clipboard.mdhas a "Dragging out" section.By hand, against a local build: dragging an image into Finder and into a browser. Links, text, and the drawn previews are not yet hand-tested.