From 1a0b5da44646b1d18ed8c9d8db5164d00e424ca6 Mon Sep 17 00:00:00 2001 From: soloturn Date: Sun, 23 Aug 2026 12:10:21 +0200 Subject: [PATCH] feat: pluggable ClipboardProvider for UIText Closes #84. UIText's clipboard access was hardwired to java.awt, which #83 could only make crash-safe (LinkageError caught, degrades to a no-op) on platforms without it - not actually give a working clipboard there. This makes the implementation swappable instead: - New ClipboardProvider interface in nui-input's org.terasology.input.device package, alongside the existing KeyboardDevice/MouseDevice device abstractions this mirrors. - AwtClipboardProvider (nui, org.terasology.nui.util) - the extracted, unchanged java.awt implementation, now the default rather than hardcoded. - UIText.setClipboardProvider(ClipboardProvider) lets an application swap the default at startup; getClipboardContents()/setClipboardContents() delegate to whatever's currently set, still catching LinkageError as a defensive fallback regardless of which implementation is active. - LibGDXClipboardProvider (nui-libgdx) wraps libGDX's own cross-platform Gdx.app.getClipboard() - backed by java.awt on desktop, Android's ClipboardManager on Android, and the browser clipboard on GWT/HTML, so it actually works everywhere nui-libgdx runs (which is everywhere DestinationSol runs). Applications call UIText.setClipboardProvider(new LibGDXClipboardProvider()) during startup to opt in. No behavior change for existing desktop consumers that don't call setClipboardProvider - AwtClipboardProvider is still the default. Verified: ./gradlew build succeeds, zero unignored AnimalSniffer violations. --- gradle/common.gradle | 9 +-- .../input/device/ClipboardProvider.java | 22 +++++++ .../libgdx/LibGDXClipboardProvider.java | 27 ++++++++ .../nui/util/AwtClipboardProvider.java | 46 +++++++++++++ .../org/terasology/nui/widgets/UIText.java | 64 +++++++------------ 5 files changed, 123 insertions(+), 45 deletions(-) create mode 100644 nui-input/src/main/java/org/terasology/input/device/ClipboardProvider.java create mode 100644 nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXClipboardProvider.java create mode 100644 nui/src/main/java/org/terasology/nui/util/AwtClipboardProvider.java diff --git a/gradle/common.gradle b/gradle/common.gradle index 4bf1233ddd..68aeb4e09d 100644 --- a/gradle/common.gradle +++ b/gradle/common.gradle @@ -18,10 +18,11 @@ dependencies { animalsniffer { // java.nio.* APIs can be desugared by D8. java.io.File.toPath() also needs to be excluded. // - // java.awt.Toolkit/datatransfer.* genuinely don't exist on Android at all - UIText.AwtClipboard - // isolates that usage behind a LinkageError-catching caller so it degrades gracefully instead - // of crashing, but the check itself can't be satisfied for a real platform absence like this - // one, so it's excluded here rather than by leaving the whole check non-fatal. + // java.awt.Toolkit/datatransfer.* genuinely don't exist on Android at all - AwtClipboardProvider + // (UIText's pluggable, but default, ClipboardProvider) isolates that usage behind a + // LinkageError-catching caller so it degrades gracefully instead of crashing, but the check + // itself can't be satisfied for a real platform absence like this one, so it's excluded here + // rather than by leaving the whole check non-fatal. ignore = ['java.nio.file.*', 'java.io.File', 'java.awt.Toolkit', 'java.awt.datatransfer.*'] } diff --git a/nui-input/src/main/java/org/terasology/input/device/ClipboardProvider.java b/nui-input/src/main/java/org/terasology/input/device/ClipboardProvider.java new file mode 100644 index 0000000000..48af026ccd --- /dev/null +++ b/nui-input/src/main/java/org/terasology/input/device/ClipboardProvider.java @@ -0,0 +1,22 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.input.device; + +/** + * Abstracts system clipboard access so widgets aren't tied to one platform's clipboard API - desktop + * Java has java.awt, Android has its own android.content.ClipboardManager, GWT/HTML has neither. + * Implementations are supplied by whatever backend the application is running under (see + * nui-libgdx's LibGDXClipboardProvider) rather than by this module. + */ +public interface ClipboardProvider { + /** + * @return the current textual contents of the clipboard, or an empty string if there are none + * or the clipboard isn't available on this platform. + */ + String getContents(); + + /** + * @param value the new textual contents of the clipboard. + */ + void setContents(String value); +} diff --git a/nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXClipboardProvider.java b/nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXClipboardProvider.java new file mode 100644 index 0000000000..dcd6608453 --- /dev/null +++ b/nui-libgdx/src/main/java/org/terasology/nui/backends/libgdx/LibGDXClipboardProvider.java @@ -0,0 +1,27 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.nui.backends.libgdx; + +import com.badlogic.gdx.Gdx; +import org.terasology.input.device.ClipboardProvider; + +/** + * Clipboard access via libGDX's own cross-platform {@link com.badlogic.gdx.utils.Clipboard} + * abstraction ({@code Gdx.app.getClipboard()}), which is backed by java.awt on desktop, + * android.content.ClipboardManager on Android, and the browser clipboard on GWT/HTML - unlike + * org.terasology.nui.util.AwtClipboardProvider (the default), this one actually works on every + * libGDX backend. Applications running NUI under libGDX should call + * {@code UIText.setClipboardProvider(new LibGDXClipboardProvider())} during startup. + */ +public class LibGDXClipboardProvider implements ClipboardProvider { + @Override + public String getContents() { + String contents = Gdx.app.getClipboard().getContents(); + return contents != null ? contents : ""; + } + + @Override + public void setContents(String value) { + Gdx.app.getClipboard().setContents(value); + } +} diff --git a/nui/src/main/java/org/terasology/nui/util/AwtClipboardProvider.java b/nui/src/main/java/org/terasology/nui/util/AwtClipboardProvider.java new file mode 100644 index 0000000000..0dda5d3bab --- /dev/null +++ b/nui/src/main/java/org/terasology/nui/util/AwtClipboardProvider.java @@ -0,0 +1,46 @@ +// Copyright 2020 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 +package org.terasology.nui.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.terasology.input.device.ClipboardProvider; + +import java.awt.Toolkit; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +/** + * The default {@link ClipboardProvider}: java.awt's system clipboard, available on desktop Java. + * java.awt genuinely doesn't exist on Android or GWT/HTML, so callers should catch {@link + * LinkageError} around use of this class rather than assume it always loads - see + * {@link org.terasology.nui.widgets.UIText#getClipboardContents()}. Platforms without java.awt + * should call {@link org.terasology.nui.widgets.UIText#setClipboardProvider} with a + * platform-appropriate implementation instead (e.g. nui-libgdx's LibGDXClipboardProvider). + */ +public class AwtClipboardProvider implements ClipboardProvider { + private static final Logger logger = LoggerFactory.getLogger(AwtClipboardProvider.class); + + @Override + public String getContents() { + Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); + + try { + if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) { + return (String) t.getTransferData(DataFlavor.stringFlavor); + } + } catch (UnsupportedFlavorException | IOException e) { + logger.warn("Failed to get data from clipboard", e); + } + + return ""; + } + + @Override + public void setContents(String value) { + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(value), null); + } +} diff --git a/nui/src/main/java/org/terasology/nui/widgets/UIText.java b/nui/src/main/java/org/terasology/nui/widgets/UIText.java index 4d20e7248b..a30032a763 100644 --- a/nui/src/main/java/org/terasology/nui/widgets/UIText.java +++ b/nui/src/main/java/org/terasology/nui/widgets/UIText.java @@ -24,6 +24,7 @@ import org.terasology.input.Keyboard; import org.terasology.input.Keyboard.KeyId; import org.terasology.input.MouseInput; +import org.terasology.input.device.ClipboardProvider; import org.terasology.input.device.KeyboardDevice; import org.terasology.nui.BaseInteractionListener; import org.terasology.nui.Canvas; @@ -45,15 +46,10 @@ import org.terasology.nui.events.NUIMouseDoubleClickEvent; import org.terasology.nui.events.NUIMouseDragEvent; import org.terasology.nui.events.NUIMouseReleaseEvent; +import org.terasology.nui.util.AwtClipboardProvider; import org.terasology.nui.util.NUIMathUtil; import org.terasology.nui.util.RectUtility; -import java.awt.Toolkit; -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.io.IOException; import java.util.Arrays; import java.util.List; @@ -66,6 +62,21 @@ public class UIText extends WidgetWithOrder { private static final float BLINK_RATE = 0.25f; + /** + * The clipboard implementation used by every UIText instance. Defaults to java.awt's system + * clipboard, which doesn't exist on Android or GWT/HTML - platforms without it should call + * {@link #setClipboardProvider} with a platform-appropriate implementation (e.g. nui-libgdx's + * LibGDXClipboardProvider) during application startup. + */ + private static ClipboardProvider clipboardProvider = new AwtClipboardProvider(); + + /** + * @param provider the clipboard implementation every UIText instance should use from now on. + */ + public static void setClipboardProvider(ClipboardProvider provider) { + clipboardProvider = provider; + } + /** Whether the content needs to be displayed on multiple lines. */ @LayoutConfig protected boolean multiline; @@ -600,12 +611,12 @@ protected void paste() { /** * Get the current clipboard contents. * - * @return The string currently in the clipboard, or an empty string if the system clipboard - * isn't available on this platform (e.g. Android, which has no java.awt). + * @return The string currently in the clipboard, or an empty string if the clipboard + * implementation in use (see {@link #setClipboardProvider}) fails to load on this platform. */ protected String getClipboardContents() { try { - return AwtClipboard.getContents(); + return clipboardProvider.getContents(); } catch (LinkageError e) { logger.warn("System clipboard is not available on this platform", e); return ""; @@ -613,48 +624,19 @@ protected String getClipboardContents() { } /** - * Set the contents of the clipboard to a given value. Does nothing if the system clipboard - * isn't available on this platform (e.g. Android, which has no java.awt). + * Set the contents of the clipboard to a given value. Does nothing if the clipboard + * implementation in use (see {@link #setClipboardProvider}) fails to load on this platform. * * @param str The new value of the clipboard contents */ protected void setClipboardContents(String str) { try { - AwtClipboard.setContents(str); + clipboardProvider.setContents(str); } catch (LinkageError e) { logger.warn("System clipboard is not available on this platform", e); } } - /** - * Isolates the java.awt.datatransfer clipboard access in its own class, rather than referencing - * it directly in getClipboardContents()/setClipboardContents(), so that loading UIText itself - * doesn't require java.awt to be resolvable - it genuinely doesn't exist on Android, unlike the - * other API-level gaps elsewhere in this codebase that have a same-behavior workaround. - * AwtClipboard is only classloaded the first time one of those two methods actually runs, and - * the LinkageError (a NoClassDefFoundError, on a platform without java.awt) is caught by the - * caller rather than here, since the failure happens on this class's own initialization. - */ - private static final class AwtClipboard { - static String getContents() { - Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null); - - try { - if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) { - return (String) t.getTransferData(DataFlavor.stringFlavor); - } - } catch (UnsupportedFlavorException | IOException e) { - logger.warn("Failed to get data from clipboard", e); - } - - return ""; - } - - static void setContents(String str) { - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(new StringSelection(str), null); - } - } - /** * Moves the cursor to a given position. *