Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading
, '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
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fa7eb89
Native operating system drag and drop, carrying the clipboard's own p…
shai-almog Sep 2, 2026
d75db23
Three things CI caught: a stolen tap, a broken watch build, a forbidd…
shai-almog Sep 2, 2026
f8888cd
Review: five ways the payload or the outcome was quietly losing somet…
shai-almog Sep 2, 2026
433863f
Review: stop the bridges narrowing the payload on the way in as well
shai-almog Sep 2, 2026
e7a6b3b
Review: one drag at a time, and a typed file is its type as well as a…
shai-almog Sep 2, 2026
108d886
Review: a move nobody performed, a refusal that was overruled, a phon…
shai-almog Sep 2, 2026
144bb28
Review: a disabled control could be dragged, and a stale callback cou…
shai-almog Sep 2, 2026
c48082d
Review: the drop discarded the target's answer, and Android mislabell…
shai-almog Sep 2, 2026
574c373
Review: a leaked payload, a type identifier nobody recognised, a MIME…
shai-almog Sep 2, 2026
e28834d
Review: the title area could not drag, and a promise was only a promi…
shai-almog Sep 2, 2026
f544737
Review: the lifting preview was the wrong class, and the desktop modi…
shai-almog Sep 2, 2026
05423cc
Review: one direction of a mapping, an empty payload, and text return…
shai-almog Sep 2, 2026
25f144c
Review: a refusal undone twice over, a payload that outlives its gesture
shai-almog Sep 2, 2026
e819b12
Review: a target that could do nothing, a stale picture, and bytes fr…
shai-almog Sep 2, 2026
4353c41
Review: an invisible source, a flavor nothing could serve, a value re…
shai-almog Sep 2, 2026
2452f1f
Review: empty markup is a value, and two clip files could be one file
shai-almog Sep 2, 2026
f67663b
Review: an image that stopped being a file, an extension that could n…
shai-almog Sep 2, 2026
db0c0c0
Review: a dragged document arriving empty, and a fallback served the …
shai-almog Sep 2, 2026
f21e3e1
Review: a file list that was not a URI list, text decoded as the wron…
shai-almog Sep 2, 2026
a34ad99
Review: a target left hovering forever, a list that could not be grab…
shai-almog Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -80,6 +80,7 @@
importcom.codename1.ui.CN;
importcom.codename1.ui.Command;
importcom.codename1.ui.ClipboardContent;
importcom.codename1.ui.NativeDragOperation;
importcom.codename1.ui.Component;
importcom.codename1.ui.Container;
importcom.codename1.ui.Dialog;
Expand DownExpand Up@@ -5566,6 +5567,117 @@ public void installNativeTheme() {
thrownewRuntimeException();
}

// ------------------------------------------------------------------------------------
// Native (operating system) drag and drop. The payload is a ClipboardContent, the same one
// copyToClipboard publishes, because a drag is a copy the user aims with the pointer.
//
// A port implements the outbound half here and calls
// com.codename1.ui.NativeDragAndDrop for the inbound half.
// ------------------------------------------------------------------------------------

/// Returns true when this platform can hand a drag to the operating system. False here
/// leaves the framework with only its lightweight in-form drag and drop, which is what
/// every port had before.
///
/// #### Returns
///
/// true when native drag and drop is available
publicbooleanisNativeDragAndDropSupported() {
returnfalse;
}

/// Returns true when a drag started in this application can be dropped outside it -- on the
/// desktop, in a file manager, or in another application's window. Ports that can route a
/// drag between their own components but not out of the application return false while
/// still returning true from `#isNativeDragAndDropSupported()`.
///
/// #### Returns
///
/// true when a drag can leave the application
publicbooleanisNativeDragOutsideApplicationSupported() {
returnisNativeDragAndDropSupported();
}

/// Hands the port the drag that would start if the press currently down turns into a drag.
///
/// Ports whose platform owns the gesture -- where the operating system's own recognizer
/// decides a drag has begun and then asks what is being dragged -- answer from what they
/// were given here. Ports that start the session themselves can ignore this and use
/// `#startNativeDrag(com.codename1.ui.NativeDragOperation)`.
///
/// Invoked on the event dispatch thread, once per press. A press that produces no drag is
/// followed by `#cancelNativeDrag()`.
///
/// #### Parameters
///
/// - `op`: the drag that is now possible
publicvoidprepareNativeDrag(NativeDragOperationop) {
}

/// Starts a native drag session now, because the pointer has moved far enough to be a drag.
/// Invoked on the event dispatch thread while the pointer is still down.
///
/// A port that starts the session must eventually report the outcome through
/// `com.codename1.ui.NativeDragAndDrop#dragCompleted(int)`, or a source that offered a move
/// never learns whether to delete its copy.
///
/// #### Parameters
///
/// - `op`: what is being dragged
///
/// #### Returns
///
/// true when the operating system took the drag
publicbooleanstartNativeDrag(NativeDragOperationop) {
returnfalse;
}

/// Discards whatever `#prepareNativeDrag(com.codename1.ui.NativeDragOperation)` staged,
/// because the press turned out to be a click.
publicvoidcancelNativeDrag() {
}

/// Notifies the port that the application has a component that can be dragged through the
/// operating system.
///
/// A platform whose drag gesture is its own -- UIKit's is -- recognizes that gesture with a
/// recognizer installed on the surface, and installing one changes how every touch on that
/// surface is delivered. Doing it unconditionally would alter touch handling for every
/// application, including the overwhelming majority that never drags anything; a port that
/// needs a recognizer therefore installs it here, the first time an application says it
/// wants one.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDragSourceRegistered() {
}

/// Notifies the port that the application has a component that accepts drops from the
/// operating system.
///
/// The counterpart of `#nativeDragSourceRegistered()`, for the same reason: a port attaches
/// whatever the platform needs in order to receive drops only for applications that asked
/// to receive them, so an application that never does keeps exactly the input handling it
/// had.
///
/// Called on the event dispatch thread, possibly many times; a port must make it idempotent.
publicvoidnativeDropTargetRegistered() {
}

/// True when the port needs the drag image at the moment the operation is staged rather
/// than when the drag begins.
///
/// A port that starts the session itself renders the image then, which costs nothing for a
/// press that turns out to be a click. A port whose platform owns the drag gesture is asked
/// for the preview from inside that platform's own callback, which is not a moment at which
/// a component can be rendered, so it has to have the image already.
///
/// #### Returns
///
/// true to render the drag image on every press over a native drag source
publicbooleanisNativeDragImageNeededOnPrepare() {
returnfalse;
}

/// Performs a clipboard copy operation, if the native clipboard is supported by the implementation it would be used
///
/// #### Parameters
Expand Down
99 changes: 98 additions & 1 deletion CodenameOne/src/com/codename1/ui/ClipboardContent.java
Original file line numberDiff line numberDiff line change
Expand Up@@ -46,12 +46,40 @@ public class ClipboardContent {
publicstaticfinalStringMIME_GIF = "image/gif";
/// A local file reference (a file path / URI `String`, or a `String[]` for several files).
publicstaticfinalStringMIME_FILE = "application/x-file-list";
/// A newline separated list of URIs, the format desktop browsers and file managers use when
/// a link or a file is dragged out of them.
publicstaticfinalStringMIME_URI_LIST = "text/uri-list";

privatefinalList<String> mimeTypes = newArrayList<String>();
privatefinalList<Object> values = newArrayList<Object>();

/// Adds or replaces a representation. Passing null removes the MIME type.
publicClipboardContentsetData(StringmimeType, Objectvalue) {
returnput(mimeType, value);
}

/// Declares a representation that is only built if something reads it, which is how a drag
/// offers a file it has not written yet or an image it has not encoded yet.
///
/// The provider is asked at most once and its answer is then cached, so
/// `#getData(java.lang.String)` behaves exactly as it would for a value passed to
/// `#setData(java.lang.String, java.lang.Object)`. It may run on a native clipboard or drag
/// thread; see `ClipboardDataProvider`. Passing a null provider removes the MIME type.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type this provider can produce
///
/// - `provider`: the provider, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetDataProvider(StringmimeType, ClipboardDataProviderprovider) {
returnput(mimeType, provider == null ? null : newLazyValue(provider));
}

privateClipboardContentput(StringmimeType, Objectvalue) {
Stringnormalized = normalizeMimeType(mimeType);
if (normalized.length() == 0) {
thrownewIllegalArgumentException("MIME type must not be empty");
Expand All@@ -74,9 +102,41 @@ public ClipboardContent setData(String mimeType, Object value) {
}

/// Returns the representation for a MIME type, or null when it isn't available.
///
/// A representation registered through
/// `#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)` is produced
/// here, on the first call for that MIME type.
publicObjectgetData(StringmimeType) {
intindex = mimeTypes.indexOf(normalizeMimeType(mimeType));
returnindex < 0 ? null : values.get(index);
if (index < 0) {
returnnull;
}
Objectvalue = values.get(index);
if (valueinstanceofLazyValue) {
return ((LazyValue) value).resolve(mimeTypes.get(index));
}
returnvalue;
}

/// A provider plus the value it produced. The value is resolved once and remembered, so a
/// target that asks twice -- as a drop does when it queries and then reads -- does not run
/// the provider twice and does not, for instance, write the promised file twice.
privatestaticfinalclassLazyValue {
privatefinalClipboardDataProviderprovider;
privateObjectresolved;
privatebooleandone;

LazyValue(ClipboardDataProviderprovider) {
this.provider = provider;
}

synchronizedObjectresolve(StringmimeType) {
if (!done) {
done = true;
resolved = provider.getClipboardData(mimeType);
}
returnresolved;
}
}

/// Returns the binary (`byte[]`) representation for a MIME type -- e.g. the raw bytes of an image
Expand All@@ -102,6 +162,43 @@ public String[] getMimeTypes() {
returnmimeTypes.toArray(newString[mimeTypes.size()]);
}

/// Sets the `#MIME_FILE` representation from a list of file paths or `file:` URIs.
///
/// Ports differ on whether a single file is carried as a `String` or a one element
/// `String[]`; this writes the form the rest of the framework expects, and
/// `#getFiles()` reads either.
///
/// #### Parameters
///
/// - `paths`: the file paths, or null to remove the representation
///
/// #### Returns
///
/// this instance, for chaining
publicClipboardContentsetFiles(String[] paths) {
if (paths == null || paths.length == 0) {
returnsetData(MIME_FILE, null);
}
if (paths.length == 1) {
returnsetData(MIME_FILE, paths[0]);
}
returnsetData(MIME_FILE, paths.clone());
}

/// Returns the `#MIME_FILE` representation as a path array regardless of whether the
/// producer stored one `String` or a `String[]`, or null when no files are available.
publicString[] getFiles() {
Objectvalue = getData(MIME_FILE);
if (valueinstanceofString[]) {
String[] paths = (String[]) value;
returnpaths.length == 0 ? null : paths.clone();
}
if (valueinstanceofString && ((String) value).length() > 0) {
returnnewString[] { (String) value };
}
returnnull;
}

/// Returns the first available MIME type from the caller's preference list, or null.
publicStringfindPreferredMimeType(String[] preferredMimeTypes) {
if (preferredMimeTypes == null) {
Expand Down
66 changes: 66 additions & 0 deletions CodenameOne/src/com/codename1/ui/ClipboardDataProvider.java
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
/*
* Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Codename One designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Codename One through http://www.codenameone.com/ if you
* need additional information or have any questions.
*/


package com.codename1.ui;

/// Produces a clipboard or drag-and-drop representation on demand, so a payload that is
/// expensive to build is only built if something actually asks for it.
///
/// This is what makes "drag a file out of the application" workable. A drag that offers
/// `ClipboardContent#MIME_FILE` has to name the file when the drag *starts*, but the drop may
/// never happen -- the user may let go over nothing -- and the target may prefer a different
/// representation entirely. Registering a provider with
/// `ClipboardContent#setDataProvider(java.lang.String, com.codename1.ui.ClipboardDataProvider)`
/// declares that the representation is available without paying for it up front; the bytes are
/// written, or the temporary file created, at the moment the receiving application reads that
/// MIME type.
///
/// A provider is invoked at most once per `ClipboardContent` and MIME type -- the result is
/// cached -- and it may be invoked from a native drag or clipboard thread rather than the event
/// dispatch thread, so it must not touch the user interface.
///
/// #### When it actually runs
///
/// On the desktop and on iOS the provider runs when a receiver reads that representation, so a
/// drag the user abandons costs nothing. Android is the exception: `startDragAndDrop` takes a
/// complete clip, and a clip carries text or a reference to a file that already exists, so
/// every provider runs as the drag begins. A drag out of an iOS application resolves its *file
/// list* at the same moment for a related reason -- the system needs the number of items the
/// drag carries, and for a file drag that is the number of files.
///
/// So a provider should be cheap enough to run once per drag, and must not assume it will only
/// run when its data is wanted.
public interface ClipboardDataProvider {
/// Produces the value for one representation.
///
/// #### Parameters
///
/// - `mimeType`: the MIME type being requested, always one this provider was registered for
///
/// #### Returns
///
/// the value, normally a `String`, a `String[]` of file paths or a `byte[]`, or null when
/// the representation turned out to be unavailable
Object getClipboardData(String mimeType);
}
Loading
Loading