Uh oh!
There was an error while loading. Please reload this page.
Native operating system drag and drop, carrying the clipboard's own payload - #5662
Native operating system drag and drop, carrying the clipboard's own payload#5662shai-almog wants to merge 13 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:5d480d757f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Compared 12 screenshots: 12 matched. |
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
Compared 151 screenshots: 151 matched. Native Android coverage
✅ Native Android screenshot tests passed. Native Android coverage
Benchmark ResultsDetailed Performance Metrics
|
Cloudflare Preview
|
Compared 181 screenshots: 181 matched. |
Compared 160 screenshots: 160 matched. Benchmark Results
Detailed Performance Metrics
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:739f5d94ad
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Compared 148 screenshots: 148 matched. Benchmark Results
Detailed Performance Metrics
|
Compared 144 screenshots: 144 matched. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:7668b3794a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:8c8190afaf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:ee8afadae5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:945cc52052
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:8c6e6b0377
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:281c900eec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:ab88b4f47c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:ab5dc154c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
ab5dc15 to
737eb52CompareThere was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:737eb52c73
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:8ac7e4326c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…ayload
Codename One's drag and drop has always been lightweight: setDraggable and
setDropTarget move a rendered image around inside one form. It never leaves the
application, so it cannot drop a file on the desktop, cannot carry text into
another application's window, and cannot receive anything from one.
This adds the other half. The payload is a ClipboardContent -- the same object a
copy publishes -- because a drag is a copy the user aims with the pointer:
whatever the application can already put on the clipboard it can already drag
out, and whatever it can paste it can already accept as a drop. Offering several
representations is what lets one drag land correctly in unrelated applications;
a text editor takes text/html, a plain text field takes text/plain, and the
desktop takes the file list.
Core
----
Label file = new Label("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.addNativeDropListener(e -> ((NativeDropEvent)e).getFiles() ...);
ClipboardContent gains lazily built representations. That is what makes dragging
a file out workable: the drag has to name the file when it starts, but the user
may drop it nowhere, so setDataProvider declares the representation without
paying for it and the file is written at the moment a receiver reads it. It also
gains setFiles/getFiles, which replaces the String-or-String[] duality every
port was open-coding, and text/uri-list.
NativeDragOperation carries the payload, the allowed actions and the drag image.
ACTION_MOVE means the receiver takes ownership and the source deletes its copy;
the source only learns whether that happened once the platform has finished, so
the outcome arrives through a completion listener rather than from the call that
started the drag.
Threading. Drops arrive on the platform's own drag thread. The target is
resolved there, from the accepted MIME types and actions alone, and the
callbacks run on the event dispatch thread. That is not fastidiousness: in the
JavaSE port the event dispatch thread blocks on the AWT thread to blit every
frame, so an AWT callback that waits on the event dispatch thread deadlocks on
the first drag. The consequence is that a MIME filter is exact from the first
drag event while a decision made inside a callback reaches the cursor one event
later, which is a frame. canAcceptNativeDrop is the one method that runs off the
event dispatch thread, and says so.
Ports
-----
JavaSE (the simulator and "run as desktop app"): both directions, through AWT's
own drag machinery, so a drag ends on another window, on the desktop or in a
file manager. The transferable that publishes a copy now publishes a drag too;
it derives its flavors from the MIME types alone rather than by reading values,
which is what keeps a promised file unwritten until the drop.
Android: startDragAndDrop with DRAG_FLAG_GLOBAL, so a drag crosses applications
from Nougat onwards. The ClipData conversion the clipboard already had is now
shared with the drag rather than duplicated, including the file provider URIs
that let the receiving application read generated bytes.
iOS, iPadOS and Mac Catalyst: UIDragInteraction and UIDropInteraction. UIKit
owns the gesture -- its own recognizer decides a drag has begun and then asks
what is being dragged -- so the framework stages the operation on the press and
the native side announces the session afterwards. The payload is fetched at that
later moment, so a drag offering a file the application has not written yet does
not write it every time the user merely touches the component.
Everything else answers false from NativeDragAndDrop.isSupported() and keeps the
lightweight drag and drop unchanged.
Not covered: the JavaScript port and the native macOS, Windows and Linux ports.
Also fixed here, found while reviewing: a top level primes drag and drop twice
per press -- once on the component under the pointer, once on its nearest
draggable ancestor -- and the second pass discarded what the first had staged
when the drag source sat between the two.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>…en modifier Every one of these was invisible to the checks I ran before pushing, and two of them are the kind that would have shipped. **A tap that never arrived.** Installing UIDragInteraction on the Codename One surface unconditionally cost the iOS input-validation suite its tap: drag and long press still worked, tap timed out. UIKit recognizes the drag gesture with a recognizer on the view, and having one there changes how every touch on that view is delivered -- so an application that never drags anything was paying for a gesture it does not use, in the one currency that matters. Both interactions are now attached on demand. Component tells the port when the application marks its first native drag source or drop target (nativeDragSourceRegistered / nativeDropTargetRegistered), and the iOS port attaches the matching interaction then. An application that never asks keeps exactly the input handling it had, which is the whole of what the suite was telling us. The drop half is withheld on the same principle rather than on measurement; it is not known to have been implicated. **A header that reached watchOS.** CN1DragAndDrop.h named CN1View unconditionally, and CN1AppleUI.h deliberately leaves that alias undefined on watchOS -- WatchKit draws through WKInterface objects and there is nothing a CN1View could be there. Every watch build failed on an unknown type name. The declaration now degrades to id on that slice, which is what CN1RenderingView already does with its peer argument and for the same reason. Compile-checked against the iOS, Mac Catalyst, macOS, watchOS and tvOS SDKs, each proved non-vacuous with a deliberate error. **Forbidden PMD rules.** volatile is on the repository's forbidden list and the new router had six of them, plus an unnecessary interface modifier and three anonymous run() methods without @OverRide. The shared state is now behind one lock, held only across field access and never across a call out -- which is the same rule the threading design already had for its own reasons. Restructuring pressedOn so it installs what a press staged in one unconditional write, rather than clearing and filling in later, also settles the LI_LAZY_INIT_STATIC that the first attempt at this traded the PMD finding for. The lesson for next time is in the middle of that list: I ran SpotBugs locally but not generate-quality-report.py, which is the thing that actually gates PMD. Running it locally now reproduces the failure and the fix, and a probe confirms it is not vacuous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hing All five findings held up against the code. Every one of them is a case of the bridge narrowing what the framework handed it, and none of them fails loudly. **Android reported every move as a copy.** ACTION_DRAG_ENDED read the allowed actions after clearing the exporting operation, so allowedActions() answered with its copy fallback; and a local drop's real answer had already been thrown away in drop(). A source that offered ACTION_MOVE and deletes its data on completion therefore never did. The action a local drop settled on is now kept until the session ends, and the completion is settled before the operation is forgotten. A drop into another application still reports copy, because Android's drag protocol has no notion of copy versus move and ACTION_DRAG_ENDED carries only a boolean -- that is now stated where the decision is made, along with why copy rather than move is the safe reading of "it worked and we do not know how". **Android advertised only text.** clipDataFor() built a text ClipData and then appended URI items, and ClipData.addItem does not widen the description -- so a clip carrying text *and* a file described itself as text only. A Codename One target filtering on MIME_FILE rejected it and an external receiver could not select the richer representation. The clip is now constructed from the union of its types. This also fixes the same defect on the clipboard, which shares the conversion. **iOS told local drop sessions the source allowed only a copy.** A move-only drag then had no action in common with a move-only target and could not be dropped at all, and a copy-or-move drag could only ever be proposed as a copy, so no in-application reorder could report a move back to its source. A session this application started is now described by the actions it actually allows, taken from the framework at session start. A session from another application is still told copy, because UIKit tells a drop interaction nothing about what the far side permits. **iOS forwarded five representations out of however many were advertised.** prepare advertises everything the content holds, but the payload bridge carried a fixed list, so an operation holding only MIME_MARKDOWN advertised a type it then could not produce -- and a drag that begins with no items is cancelled on the spot. The bridge now takes one representation at a time and the Java side pushes all of them, resolving promised values as it goes. Unmapped MIME types reach the system through UTType, falling back to the MIME type itself as an opaque identifier: unread by a receiver that does not know it, which is a great deal better than dropped. This also stops JPEG bytes being published as PNG. **A reused operation reported the last drag's result.** setNativeDragOperation documents the instance as reusable, so getPerformedAction() went on answering ACTION_MOVE through the whole of the next drag, contradicting its own contract that the value before completion is ACTION_NONE. It is cleared when the operation is installed as the active one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The last round fixed this going out. The same defect was sitting on the receiving side of all three ports, and it has a sharper edge there: a drag is filtered twice -- once against what it advertises while it hovers, and again against what it materializes when it is dropped -- so a bridge that produces less than it advertised refuses the very target that just agreed to take it. **iOS dropped everything but five formats.** performDrop: loaded every registered type and then forwarded plain text, HTML, RTF, one image and files. A drag carrying markdown, a GIF or an application's own type was accepted while it hovered and arrived without it, so its target got no drop at all. Worse, the bridge's refusal was discarded: UIKit had already proposed an operation, so dragInteraction:session:didEndWithOperation: reported a move for a drop nothing received, and a source that deletes on ACTION_MOVE would delete data on the strength of it. The drop is now assembled one representation at a time, like the outbound payload, and the completion of a local drag waits for the drop's real answer -- UIKit asks the source what happened before the asynchronous loads have returned, so whichever arrives first now hands off to the other. **Android carried only text, images and files.** A content holding only MIME_MARKDOWN, MIME_ASCIIDOC or another byte-backed type produced an empty plain-text clip. A clip has one text payload, so where there is no text/plain the first text representation becomes that payload and its type is advertised with it; byte-backed types become typed content URIs, which is the only labelled way an Android clip carries bytes. A second, *different* text representation is deliberately not advertised: the clip cannot produce it, and advertising it is precisely how a target ends up accepting a hover it will then be refused. **Android lost the advertised types at materialization.** A URI item became MIME_FILE alone, so a component filtering on MIME_URI_LIST accepted the hover and was rejected at the drop. The drop now materializes with the description in hand and fills the types it advertised from what the clip actually produced -- nothing is invented, and a type with no value to give it is left absent rather than advertised empty. Paste passes no description and so is unchanged. **JavaSE discarded arbitrary binary flavors.** application/pdf and its like were refused on the way in purely for not being text or image, though readValue already handled streams and RichTransferable exports the same types on the way out. Any flavor in a shape this can read is now accepted, except AWT's own x-java transport flavors, which describe how a payload moves between Java processes rather than what it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… file Three more, all real, though one is fixed differently from the way it was put. **A second drag displaced the first.** startDrag installed the new operation before the port had answered, so a refusal cleared the session outright and a success attributed the running session's completion to the newcomer. Either way the original source never learned its outcome -- and one waiting for ACTION_MOVE to delete its data would wait for a completion that was no longer addressed to it. A start while a session is running is now refused, which is also all any of these platforms would have done. dragSessionStarted answers null in the same case, which is how a port whose platform owns the gesture declines. **A typed Android URI arrived as a file and nothing else.** A content: URI with type application/pdf became MIME_FILE alone, so a target filtering on the type accepted the hover -- the description advertised it -- and was refused the drop. The type is now offered as well, promised rather than read: a target that only wants the path should not pay for a document it never opens, and the drag-and-drop grant lasts the life of the activity, so the deferred read still succeeds. **An iOS file provider's other representations were skipped**, so the same advertise-then-refuse mismatch applied to a document dropped from Files. The review asked for the `continue` to be dropped, which would load every representation the provider offers -- and for a file provider that means reading the whole document into memory on top of the copy this already makes. A large video dropped from Files would be copied and then read into a byte array, which is a worse failure than the one being fixed. So the provider's other types are named against the copy instead and read only if a target asks for one: the advertised set and the deliverable set agree, which is the point, and nothing large is read that nobody wanted. That reasoning is in the code, since it is where the next reader will need it. The cast-semantics baseline is regenerated, and the diff is worth reading rather than trusting: two entries go because they are genuinely fixed -- AndroidDB's was corrected upstream by the portable-database change and never re-baselined, and the ClipboardContent cast is instanceof-guarded by this branch's own copyToClipboard refactor -- and the third moves from $62 to $63 because adding an anonymous class renumbered the ones after it. No finding is being silenced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e that grew up Three more, and the first is a correction to reasoning I wrote in the last round rather than to code I merely forgot to write. **Android reported a move nobody performed.** The completion for a successful external drop fell back to the source's preferred action, and I had defended that in a comment: an operation allowing only a move "still reports a move, since there is nothing else it could have been". That is wrong, and destructively so. What the source was willing to permit says nothing about what the receiver did -- Android's drag protocol has no notion of copy versus move at all, so an ordinary external target simply reads the clip. Reporting ACTION_MOVE on that basis has the documented completion handler delete the only remaining copy. A successful external drop now reports a copy whatever the source allowed, which is what actually happened. **Android overruled a target's refusal.** A target that calls NativeDropEvent.reject() leaves ACTION_NONE as the hover's answer, and Android delivers ACTION_DROP to a subscribed view regardless of what it answered to the location events. Treating that ACTION_NONE as "no answer yet" and substituting a default turned the refusal back into a delivered drop, against the contract that rejection prevents delivery. The last answer now distinguishes refused from not yet asked, and a refusal ends the drop and reports failure. iOS does not have the same hole and is deliberately left alone: UIKit consults the proposal from sessionDidUpdate: before it calls performDrop: at all, so a refusal means the drop never arrives and ACTION_NONE there really does mean "never updated". **iPhones can drag between applications now.** isDragOutsideApplicationSupported answered on the idiom alone, so every phone was told a drag could not leave the application. iOS 15 brought drag and drop between applications to the phone -- hold the item with one finger, switch applications with another, drop -- so an application hiding its export-by-drag affordance on this answer was hiding something the installed UIDragInteraction supports. Version gated now, and the developer guide's platform table says so rather than a flat no. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ld answer for a live drag **A disabled component staged a drag.** A Form primes drag and drop before it applies its own isEnabled gate, where a Window applies the gate first, so on the main surface a disabled native drag source staged an operation and the form level drag callback -- which runs before pressedCmp is consulted -- then started an operating system drag from a control that receives no ordinary press. The walk that looks for a drag source now skips components that are not enabled, so both surfaces behave alike, while an enabled draggable ancestor of a disabled child still drags exactly as the lightweight path lets it. **A stale callback could answer for a newer drag.** The callbacks are queued onto the event dispatch thread, so one can still be waiting when its drag leaves and another arrives over the same component. Guarding on component identity cannot tell those apart -- it is the same component -- so the old drag's decision was written into the new one's, and a move or a refusal from a drag that had already gone could be handed to a copy-only drag that had just arrived. Every target and session change now bumps a generation that each callback carries, and a callback only speaks while its own generation is current. The pending-dispatch flag is cleared on a target change for the same reason: its owner's callback will no longer clear it, and a flag left standing would silence the new target. The test for that one earned its keep the hard way. The obvious version passed with and without the fix: the corruption is repaired by the newer drag's own callback a moment later, so an assertion after the queue drains sees the right answer either way, and the recorder read its decision when it ran rather than when it was queued. It now decides from the payload and reads the answer from inside the queue, between the stale callback and the new drag's own, which is the only place the window is visible. Removing the guard makes it fail with the first drag's move where the second drag's copy belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed what it carried **The shared drop path threw away the target's decision.** It recomputed the accepted action from what the port handed it and the target's declarative mask, and never looked at what the target's own callback had most recently said. The port's action is one drag event behind by construction -- it is whatever the last drag event returned -- so a target that called reject() in a callback that has since run had the refusal discarded and was handed the drop anyway. This is worth being clear about, because I reported it fixed two rounds ago. The Android port now refuses such a drop before the framework sees it, and that half is real: it makes Android report the drag as unsuccessful, which nothing else could. But it left JavaSE and iOS untouched, and I described the class of bug as closed. The drop now takes the target's latest word whenever the drop lands on the component the callbacks were about, and falls back to the declarative answer only when the pointer has moved to a different one. **Android dropped a distinct text representation rather than carrying it.** The previous round advertised a second text type only when its value matched the text the clip carries, on the grounds that advertising what cannot be produced is how a target accepts a hover and is then refused. That reasoning was sound and the conclusion was still wrong: a clip can carry the thing, as a typed content URI, exactly as binary travels. Markdown beside its plain rendering now goes out that way and comes back through the typed-URI provider, which decodes a text type to a String so getText() answers rather than returning bytes the caller cannot read. **Android filed WebP bytes as a PNG.** mimeForImageType answers PNG for any image type it does not recognize, so the bytes were stored under a label nothing could decode them by, and a target filtering on the type the drag advertised was accepted on the hover and refused at the drop. Incoming images now keep the type the content resolver reported. This is the same mislabelling as the JPEG published as PNG that the second round fixed on iOS; I did not think to look for Android's own version of it then. Both new tests were checked by removing the fix and watching them fail -- the rejection test reports a copy where none was allowed, and last round's stale callback test needed rewriting for exactly that reason. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… lost in a URI **Every iOS drag leaked its whole payload.** Each representation was retained explicitly before its load handler was registered, but copying a block already retains what it captures -- and registering the handler copies it -- so the extra retain had nothing to balance it. Repeated drags of images or documents grew the footprint until the system took the application. Nothing local could have caught this: it compiles clean, passes every gate, and only shows on a device over many drags. **Below iOS 14 the type identifiers were meaningless.** UTType arrives in 14, so on 11 through 13 every type not named in the table -- application/pdf among them -- was published under its raw MIME string, which no application asking for com.adobe.pdf would ever match. That range is reachable: the builder defaults to 14 but ios.deployment_target lets an application go lower. Those releases now go through MobileCoreServices, with the deprecation silenced at the call rather than the call avoided, since it is the only way there to name a type the system knows. A dynamic identifier is refused, because it tells a receiver no more than the MIME type does and reads worse. **Android lost an application defined type inside its own URI.** The writers added in the previous rounds name the temporary file with an extension synthesized from the MIME type, and a FileProvider derives the URI's type from that extension -- so anything Android's table does not know came back as octet-stream and the advertised type was unrecoverable, leaving a target that accepted the hover refused at the drop. Android's own MimeTypeMap now supplies the extension wherever it has one, which settles every type it knows exactly. For the rest, a single unnamed URI is paired with a single unsatisfied advertised type, because that pairing cannot be anything else; with more of either it could be, so those are left absent and the target correctly refuses rather than being told it has something it may not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…se on one port **A Toolbar component could not be dragged.** Form.pointerPressed has a branch of its own for the title area, and it is the one branch that never primes drag and drop -- so a component given a native drag operation there silently could not be dragged while the identical component in the content pane could. Native dragging is primed there now. Only native: the lightweight drag has never worked in the title area either, and quietly switching that on is a different change from this one. **JavaSE committed an action the framework had not agreed to.** This is fallout from honouring the target's latest decision two commits ago. AWT wants the action when the drop is accepted, and that is before the transferable can be read, so accepting AWT's proposal and only then learning the target had chosen otherwise told the source through exportDone that a copy had happened while handing the target a move. NativeDragAndDrop.plannedDropAction answers the same question without dispatching anything, so what is committed to AWT is what the drop goes on to report. **iOS built every promised representation at the start of a drag.** Beginning a drag and abandoning it wrote every promised file and encoded every promised image, which is the opposite of what setDataProvider says. The item providers now resolve a representation when a receiver reads it, answering asynchronously so the fetch happens on the main thread like every other call into the framework from that file. The file list is the exception and stays eager: UIKit needs the number of items when the session begins, and for a file drag that number is the number of files -- deferring it would mean carrying only one, and dragging several files out is the feature. **Android cannot defer at all, so the promise was corrected instead of the code.** startDragAndDrop takes a complete ClipData, and a clip carries text or a URI to a file that already exists; there is no later moment to run a provider in. A content provider resolving bytes on demand would restore it and needs a second provider in the generated manifest, which lives in the builder repository, so it is not something this change can reach. ClipboardDataProvider, the Android bridge and the developer guide now each say where laziness holds and where it does not, and that a provider must be cheap enough to run once per drag. The javadoc promising more than two of the three ports could deliver was the actual defect. Also here: the casts in nativeDragResolveCallback moved out from under catch(Throwable). They were instanceof-guarded, which the cast-semantics gate does not recognize for an array type -- but the broad catch only ever needed to cover the provider call, which is the part that runs application code and can throw anything, so the narrower try is what should have been written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…fier did nothing **iOS was handed an object of a class it did not ask for.** UIDragInteractionDelegate declares previewForLiftingItem: as returning a UITargetedDragPreview; this returned a UIDragPreview, which is an unrelated class, so UIKit was going to send it messages it does not answer. Clang says nothing about the mismatch -- the file compiles without a single warning -- and only a drag on a device with a custom drag image would have found it. The review found it from the other end: cn1PreparedTouch was being written and never read, so setDragImageOffset had no effect. It has none because an untargeted preview is positioned wherever UIKit likes; the fix is the targeted preview the delegate was asking for all along, placed so the point the finger grabbed stays under the finger. Every other delegate method in the file was checked against the SDK headers rather than only this one -- the other seven match. Compile-clean with no warnings at iOS 11, 14 and 15; the iOS 11 spellings UIDragPreviewTarget and UIDragPreviewParameters are used deliberately, because UITargetedPreview and UIPreviewTarget arrive in 13 and this feature claims 11. **The desktop modifier could not select a move.** getSourceActions is the whole mask the source offered, and handing the framework that alone made it prefer a copy every time -- so holding the platform modifier changed nothing, because getDropAction, which is where AWT records the user's choice, was never read. That choice now wins where the source allows it, and the full mask stands where it does not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed as bytes Three of my own, and the first is the shape worth naming: a fix applied to one direction of a symmetric pair and not the other. **The legacy type mapping only went one way.** Two rounds ago the MIME to UTI conversion below iOS 14 was fixed through MobileCoreServices, and the reverse -- UTI to MIME -- was left answering nil unconditionally on those releases. So on iOS 11 through 13 a standard type such as com.adobe.pdf was still neither discovered while a drag hovered nor materialized when it dropped. The diff looked complete because the direction it touched was complete. **Empty was being treated as absent.** A drop representation had to have a positive length to be stored, so a representation the drag advertised and that is legitimately empty -- an empty string, a zero byte payload -- vanished, and the drop was then refused by the very target the hover had accepted. Null is absent; empty is present. Android's provider writer had the identical test and is fixed with it rather than waiting to be found separately. **File-backed text came back as bytes.** A document provider from Files offers a plain text representation beside its file URL, and the file-backed provider always answered with a byte array, so getText() and NativeDropEvent.getText() were null for a type the drop had just accepted. It decodes text/* as UTF-8 now, which is what the Android provider and the other iOS drop path already did -- so this was an inconsistency between three paths that should have read alike. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four, and two of them were made by the fixes of the last two rounds. **A queued drag-over undid a refusal made in enter.** The callbacks are queued onto the event dispatch thread, so an over event can be queued before the enter event ahead of it has run -- and the starting action was captured when the event was queued, so the over event then restored the default over whatever the enter callback had since decided. A target that rejects only in nativeDragEnter had its rejection undone by the very next no-op nativeDragOver and was handed the drop. The starting action is read as the callback runs now. **Making iOS lazy left its payload unreachable.** An item provider's load handler is asynchronous by design and a receiving application may defer reading a representation until after the session has ended -- at which point dragCompleted has cleared the active drag and the lookup answered with nothing. The exported operation is now held independently of the gesture until the next drag replaces it. Deferring the work meant keeping it alive longer than the gesture, and the previous round did only the first half of that. **The desktop modifier fix left a stale cached action.** Narrowing the permitted set to the modifier's choice means an action agreed under the old set may no longer be on offer, and the same-target path returned it unchanged. It is revalidated against the current set now. **And PNG bytes were filed under image/jpeg.** A decoded java.awt.Image can only be produced as a PNG, so PNG is what it advertises; filing PNG bytes under whatever the flavor called itself handed a target bytes it could not decode by the type it asked for. Third port this has happened on -- iOS, then Android, now the desktop. The interesting part is the interaction. Revalidating a cached action recomputes anything not in the permitted set, and ACTION_NONE trivially is not -- so the second fix above resurrected the refusals the first one exists to protect, which is the same defect arriving from the other direction. Both would have shipped looking right. ACTION_NONE is excluded now, being a decision rather than a stale value, and the test covers both routes: it fails with the queue-time capture restored and it failed with the resurrection present, so it passes only while both hold. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8ac7e43 to
25f144cCompareThere was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:25f144c4eb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| while (cmp != null) { | ||
| if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) { | ||
| try { | ||
| if (cmp.canAcceptNativeDrop(content)) { | ||
| return cmp; |
There was a problem hiding this comment.
Skip targets that cannot perform any source action
When a copy-only drag is over a nested target configured for move-only inside a copy-capable ancestor, findTarget() returns the inner target based only on its MIME/content decision. dragOver() subsequently computes ACTION_NONE for that target and never considers the ancestor, so a valid drop destination is incorrectly blocked; include the source/target action intersection while walking ancestors.
Useful? React with 👍 / 👎.
| if (op.getDragImage() == null && Display.impl.isNativeDragImageNeededOnPrepare()) { | ||
| // The platform asks for the preview from inside its own gesture callback, | ||
| // which is not a moment at which a component can be rendered. Rendering here | ||
| // costs a snapshot per press on a drag source, which is what the lightweight | ||
| // drag has always cost when one starts. | ||
| op.setDragImage(source.getDragImage()); | ||
| op.setDragImageOffset(x - source.getAbsoluteX(), y - source.getAbsoluteY()); |
There was a problem hiding this comment.
Regenerate framework drag previews for each gesture
For the normal reusable operation installed by setNativeDragOperation(), this writes the framework-generated component snapshot and grab offset permanently into the operation. Every later drag then treats that snapshot as application-supplied, so changes to the component and presses at a different point retain the first drag's stale image and offset; keep generated previews session-local or clear them after completion.
Useful? React with 👍 / 👎.
| if (op == null) { | ||
| return 0; | ||
| } | ||
| exportedDrag = op; |
There was a problem hiding this comment.
Bind iOS provider loads to their originating drag
If an external receiver requests a representation from an earlier drag after the user has begun another drag, replacing this global makes the old NSItemProvider load handler resolve against the new operation, returning unrelated bytes or null. Fresh evidence beyond the earlier session-end issue is that exportedDrag is now retained past completion but is still overwritten unconditionally by the next session; each provider must retain or identify its own operation.
Useful? React with 👍 / 👎.
| UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider]; | ||
| [items addObject:item]; |
There was a problem hiding this comment.
Keep iOS alternatives on one logical drag item
When content offers a file plus a text fallback, as the new sample does, the file loop has already appended one UIDragItem per file and this appends another item for the text representation. UIKit therefore exposes the alternatives as separate dragged objects, so receivers may import both a file and an extra text item instead of selecting the best representation of one object; attach applicable representations to the file item's provider rather than creating an additional logical item.
Useful? React with 👍 / 👎.
Codename One's drag and drop has always been lightweight:
setDraggableandsetDropTargetmove a rendered image around inside one form. It never leaves the application, so it cannot drop a file on the desktop, cannot carry text into another application's window, and cannot receive anything from one.This adds the other half.
The idea
The payload is a
ClipboardContent-- the same object a copy publishes -- because a drag is a copy the user aims with the pointer. Whatever the application can already put on the clipboard it can already drag out, and whatever it can paste it can already accept as a drop. Offering several representations is what lets one drag land correctly in unrelated applications: a text editor takestext/html, a plain text field takestext/plain, and the desktop takes the file list.Core
ClipboardContentgains lazily built representations. That is what makes dragging a file out workable: the drag has to name the file when it starts, but the user may drop it nowhere, sosetDataProviderdeclares the representation without paying for it and the file is written at the moment a receiver reads it. It also gainssetFiles/getFiles-- which replaces theString-or-String[]duality every port was open-coding -- andtext/uri-list.NativeDragOperationcarries the payload, the allowed actions and the drag image.ACTION_MOVEmeans the receiver takes ownership and the source deletes its copy; the source only learns whether that happened once the platform has finished, so the outcome arrives through a completion listener rather than from the call that started the drag.New API, all in
com.codename1.ui:NativeDragAndDrop,NativeDragOperation,NativeDropEvent,ClipboardDataProvider, and onComponentthe drag-source and drop-target pairs. Orthogonal to the existingsetDraggable/setDropTarget, which are untouched.Threading
Drops arrive on the platform's own drag thread. The target is resolved there, from the accepted MIME types and actions alone, and the callbacks run on the event dispatch thread.
That is not fastidiousness. In the JavaSE port the event dispatch thread blocks on the AWT thread to blit every frame, so an AWT callback that waits on the event dispatch thread deadlocks on the first drag. The consequence is that a MIME filter is exact from the first drag event, while a decision made inside a callback reaches the cursor one event later -- a frame.
canAcceptNativeDropis the one method that runs off the event dispatch thread, and says so.Ports
DRAG_FLAG_GLOBALJavaSE goes through AWT's own drag machinery, so our own window is a drop target for our own drags too. The transferable that publishes a copy now publishes a drag as well; it derives its flavors from the MIME types alone rather than by reading values, which is what keeps a promised file unwritten until the drop.
Android shares the
ClipDataconversion the clipboard already had rather than growing a second one that would drift from it, including the file provider URIs that let the receiving application read generated bytes.iOS, iPadOS and Mac Catalyst use
UIDragInteraction/UIDropInteraction. UIKit owns the gesture -- its own recognizer decides a drag has begun and then asks what is being dragged -- so the framework stages the operation on the press and the native side announces the session afterwards. The payload is fetched at that later moment, so a drag offering a file the application has not written yet does not write it every time the user merely touches the component.Where the platform has none of this,
NativeDragAndDrop.isSupported()answers false, every call is a no-op, and the lightweight drag and drop is unaffected.Not covered: the JavaScript port and the native macOS, Windows and Linux ports.
Also fixed
A top level primes drag and drop twice per press -- once on the component under the pointer, once on its nearest draggable ancestor -- and the second pass discarded what the first had staged when the drag source sat between the two. Found while reviewing; covered by a regression test.
Verification
text/uri-list, target resolution and the action mapping.core-unittests,androidandios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.CN1DragAndDrop.mcompiles for real arm64 iOS, for Mac Catalyst and for the macOS stub branch. A full translation of the sample app confirms the new native sources ship and that all five Java callbacks survive dead-code elimination.Not verified: a physically driven operating system drag. Synthetic mouse input does not reach the window server on the machine this was built on, so a scripted drag proved nothing either way. Android and iOS are compile- and analysis-verified rather than device-run.
🤖 Generated with Claude Code