Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Native operating system drag and drop, carrying the clipboard's own payload - #5662

Open
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop
Open

Native operating system drag and drop, carrying the clipboard's own payload#5662
shai-almog wants to merge 13 commits into
masterfrom
native-os-drag-and-drop

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

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 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 takes text/html, a plain text field takes text/plain, and the desktop takes the file list.

Labelfile = newLabel("report.pdf");
file.setNativeDragOperation(NativeDragOperation.createFileDrag(paths));
inbox.setNativeDropTarget(true);
inbox.setAcceptedDropMimeTypes(ClipboardContent.MIME_FILE);
inbox.addNativeDropListener(e -> load(((NativeDropEvent)e).getFiles()));

Core

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.

New API, all in com.codename1.ui: NativeDragAndDrop, NativeDragOperation, NativeDropEvent, ClipboardDataProvider, and on Component the drag-source and drop-target pairs. Orthogonal to the existing setDraggable/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. canAcceptNativeDrop is the one method that runs off the event dispatch thread, and says so.

Ports

PortDragsLeaves the app
JavaSE (simulator and "run as desktop app")yesyes -- other windows, the desktop, file managers
AndroidyesNougat and later, via DRAG_FLAG_GLOBAL
iPadOS and Mac Catalystyesyes
iPhoneyesno -- nothing on screen to drop into
everything elsenono

JavaSE 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 ClipData conversion 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

  • 6091 core tests and 327 JavaSE tests green. 17 new core tests, 11 new JavaSE tests covering both transferable conversions, the promised-file path, text/uri-list, target resolution and the action mapping.
  • SpotBugs clean on core-unittests, android and ios. Copyright, control-character, package-info, cast-semantics, native-signature and build-hint gates clean; Vale and LanguageTool clean on the guide.
  • The simulator runs the new sample and reports that drags can leave the application.
  • CN1DragAndDrop.m compiles 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

@chatgpt-codex-connector

chatgpt-codex-connectorBot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

ReviewStatusCommitReview trigger
📝 Code ReviewCompleted2026-09-02T14:40:52.665442Z25f144cNew commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 12 screenshots: 12 matched.
✅ JavaSE simulator integration screenshots matched stored baselines.

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

@github-actions

github-actionsBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs[Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • build-hint-catalog: 0 findings (no issues)
    • build-hint-tools: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 9.09% (9013/99118 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 8.88% (46483/523733), branch 3.50% (1735/49629), complexity 3.47% (1838/52924), method 5.33% (1485/27841), class 10.72% (399/3723)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

MetricDuration
SIMD kernel backendscalar fallback (no native SIMD)
SIMD int-add (64K x300)java 214ms / native 296ms = 0.7x speedup
SIMD float-mul (64K x300)java 145ms / native 187ms = 0.7x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathgated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode95.000 ms
Base64 CN1 decode85.000 ms
Base64 native encode310.000 ms
Base64 encode ratio (CN1/native)0.306x (69.4% faster)
Base64 native decode276.000 ms
Base64 decode ratio (CN1/native)0.308x (69.2% faster)
Image encode benchmark statusskipped (SIMD unsupported)

@github-actions

Copy link
Copy Markdown
Contributor

Cloudflare Preview

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 160 screenshots: 160 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 164 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 68ms / native 2ms = 34.0x speedup
SIMD float-mul (64K x300)java 75ms / native 3ms = 25.0x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 native bridgeunavailable (CN1 + SIMD + image benchmarks only)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode206.000 ms
Base64 CN1 decode120.000 ms
Image encode benchmark iterations100
Image createMask (SIMD off)15.000 ms
Image createMask (SIMD on)4.000 ms
Image createMask ratio (SIMD on/off)0.267x (73.3% faster)
Image applyMask (SIMD off)90.000 ms
Image applyMask (SIMD on)87.000 ms
Image applyMask ratio (SIMD on/off)0.967x (3.3% faster)
Image modifyAlpha (SIMD off)94.000 ms
Image modifyAlpha (SIMD on)73.000 ms
Image modifyAlpha ratio (SIMD on/off)0.777x (22.3% faster)
Image modifyAlpha removeColor (SIMD off)57.000 ms
Image modifyAlpha removeColor (SIMD on)49.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.860x (14.0% faster)

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 242 seconds

Detailed Performance Metrics

MetricDuration
SIMD kernel backendSSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300)java 85ms / native 11ms = 7.7x speedup
SIMD float-mul (64K x300)java 53ms / native 2ms = 26.5x speedup
SIMD kernel correctnessPASS (native result == scalar reference)
Base64 payload size8192 bytes
Base64 benchmark iterations6000
Base64 SIMD byte pathactive (NEON-accelerated)
Base64 CN1 encode147.000 ms
Base64 CN1 decode87.000 ms
Base64 native encode449.000 ms
Base64 encode ratio (CN1/native)0.327x (67.3% faster)
Base64 native decode180.000 ms
Base64 decode ratio (CN1/native)0.483x (51.7% faster)
Base64 SIMD encode44.000 ms
Base64 encode ratio (SIMD/CN1)0.299x (70.1% faster)
Base64 SIMD decode42.000 ms
Base64 decode ratio (SIMD/CN1)0.483x (51.7% faster)
Base64 encode ratio (SIMD/native)0.098x (90.2% faster)
Base64 decode ratio (SIMD/native)0.233x (76.7% faster)
Image encode benchmark iterations100
Image createMask (SIMD off)6.000 ms
Image createMask (SIMD on)1.000 ms
Image createMask ratio (SIMD on/off)0.167x (83.3% faster)
Image applyMask (SIMD off)38.000 ms
Image applyMask (SIMD on)28.000 ms
Image applyMask ratio (SIMD on/off)0.737x (26.3% faster)
Image modifyAlpha (SIMD off)31.000 ms
Image modifyAlpha (SIMD on)29.000 ms
Image modifyAlpha ratio (SIMD on/off)0.935x (6.5% faster)
Image modifyAlpha removeColor (SIMD off)36.000 ms
Image modifyAlpha removeColor (SIMD on)30.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off)0.833x (16.7% faster)

@shai-almog

shai-almog commented Sep 2, 2026

Copy link
Copy Markdown
CollaboratorAuthor

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidNativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadPorts/Android/src/com/codename1/impl/android/AndroidImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/Component.java
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/JavaSE/src/com/codename1/impl/javase/JavaSENativeDragAndDrop.java Outdated
Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from ab5dc15 to 737eb52CompareSeptember 2, 2026 12:11

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadPorts/iOSPort/nativeSources/CN1DragAndDrop.m
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated
Comment threadPorts/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java Outdated

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
Comment threadCodenameOne/src/com/codename1/ui/NativeDragAndDrop.java Outdated
shai-almogand others added 7 commits September 2, 2026 17:33
…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>
shai-almogand others added 6 commits September 2, 2026 17:33
…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>
@shai-almog
shai-almogforce-pushed the native-os-drag-and-drop branch from 8ac7e43 to 25f144cCompareSeptember 2, 2026 14:33

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +713 to +717
while (cmp != null) {
if (cmp.isNativeDropTarget() && !cmp.isIgnorePointerEvents() && cmp.isEnabled()) {
try {
if (cmp.canAcceptNativeDrop(content)) {
return cmp;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +299 to +305
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());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +503 to +504
UIDragItem* item = [[UIDragItem alloc] initWithItemProvider:provider];
[items addObject:item];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@shai-almog