From 470e3a0112ff310c5e7e6b6b115352620c21a7ec Mon Sep 17 00:00:00 2001 From: Jonah Graham Date: Thu, 25 Sep 2025 11:17:53 -0400 Subject: [PATCH] New test for Clipboard Testing the Clipboard can be difficult because there are a lot of system issues that can interfere, but of particular concern are: 1. System clipboard managers which may take ownership of the clipboard at unexpected times. 2. Limitations as to when processes can access clipboard, such as on Wayland where only active window can access clipboard. 3. Different behaviour when copying within a single process than between processes. These tests aim to resolve these issues. For the system clipboard manager there are a lot of extra sleep calls to allow clipboard manager to complete operations before continuing tests. In addition, we run all the tests multiple times by default to ensure stability. For the process limitations, we carefully control when the shell is created because we often cannot get focus back when shell ends up in the background. See the openAndFocusShell and openAndFocusRemote methods. For the different behaviours, we spin up a simple Swing app in a new process (the "remote" in openAndFocusRemote above). This app can be directed, over RMI, to access the clipboard. This allows our test to place data on the clipboard and ensure that the remote app can read the data successfully. For now this test only covers basic text (and a little of RTF). Adding Image and other transfers is part of the future work as such functionality is added in GTK4 while working on #2126 For the changes to SwtTestUtil that we required: 1. isGTK4 moved from Test_org_eclipse_swt_widgets_Shell.java to the Utils 2. processEvents was limited to 20 readAndDispatch calls per second due to the ordering of the targetTimestamp check. This change allows full speed readAndDispatching. 3. getPath was refactored to allow better control of source and destination of files extracted. See extracting of class files for remote Swing app in startRemoteClipboardCommands method Part of #2126 Split out of #2538 --- .../swt/tests/junit/AllNonBrowserTests.java | 1 + .../eclipse/swt/tests/junit/SwtTestUtil.java | 34 +- .../Test_org_eclipse_swt_dnd_Clipboard.java | 358 ++++++++++++++++++ .../Test_org_eclipse_swt_widgets_Shell.java | 11 +- .../data/clipboard/ClipboardCommands.java | 29 ++ .../data/clipboard/ClipboardCommandsImpl.java | 93 +++++ .../data/clipboard/ClipboardTest.java | 143 +++++++ 7 files changed, 649 insertions(+), 20 deletions(-) create mode 100644 tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java create mode 100644 tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java create mode 100644 tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java create mode 100644 tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java index 19c1e822903..a125c34ad3d 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/AllNonBrowserTests.java @@ -43,6 +43,7 @@ Test_org_eclipse_swt_accessibility_AccessibleControlEvent.class, // Test_org_eclipse_swt_accessibility_AccessibleEvent.class, // Test_org_eclipse_swt_accessibility_AccessibleTextEvent.class, // + Test_org_eclipse_swt_dnd_Clipboard.class, Test_org_eclipse_swt_events_ArmEvent.class, // Test_org_eclipse_swt_events_ControlEvent.class, // Test_org_eclipse_swt_events_DisposeEvent.class, // diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java index cce810b32bc..beb7d4af66e 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/SwtTestUtil.java @@ -105,7 +105,8 @@ public class SwtTestUtil { public final static boolean isX11 = isGTK && "x11".equals(System.getProperty("org.eclipse.swt.internal.gdk.backend")); - + public final static boolean isGTK4 = isGTK + && System.getProperty("org.eclipse.swt.internal.gtk.version", "").startsWith("4"); /** * The palette used by images. See {@link #getAllPixels(Image)} and {@link #createImage} @@ -400,13 +401,16 @@ public static void processEvents(int timeoutMs, BooleanSupplier breakCondition) long targetTimestamp = System.currentTimeMillis() + timeoutMs; Display display = Display.getCurrent(); while (!breakCondition.getAsBoolean()) { - if (!display.readAndDispatch()) { - if (System.currentTimeMillis() < targetTimestamp) { - Thread.sleep(50); - } else { + while (display.readAndDispatch()) { + if (System.currentTimeMillis() >= targetTimestamp) { return; } } + if (System.currentTimeMillis() < targetTimestamp) { + Thread.sleep(50); + } else { + return; + } } } @@ -583,18 +587,24 @@ public static boolean hasPixelNotMatching(Image image, Color nonMatchingColor, R } public static Path getPath(String fileName, TemporaryFolder tempFolder) { - Path filePath = tempFolder.getRoot().toPath().resolve("image-resources").resolve(Path.of(fileName)); - if (!Files.isRegularFile(filePath)) { + Path path = tempFolder.getRoot().toPath(); + Path filePath = path.resolve("image-resources").resolve(Path.of(fileName)); + return getPath(fileName, filePath); +} + +public static Path getPath(String sourceFilename, Path destinationPath) { + if (!Files.isRegularFile(destinationPath)) { // Extract resource on the classpath to a temporary file to ensure it's // available as plain file, even if this bundle is packed as jar - try (InputStream inStream = SwtTestUtil.class.getResourceAsStream(fileName)) { - assertNotNull(inStream, "InputStream == null for file " + fileName); - Files.createDirectories(filePath.getParent()); - Files.copy(inStream, filePath); + try (InputStream inStream = SwtTestUtil.class.getResourceAsStream(sourceFilename)) { + assertNotNull(inStream, "InputStream == null for file " + sourceFilename); + Files.createDirectories(destinationPath.getParent()); + Files.copy(inStream, destinationPath); } catch (IOException e) { throw new IllegalArgumentException(e); } } - return filePath; + return destinationPath; } + } diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java new file mode 100644 index 00000000000..bc6ef9778d0 --- /dev/null +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_dnd_Clipboard.java @@ -0,0 +1,358 @@ +/******************************************************************************* + * Copyright (c) 2025 Kichwa Coders Canada, Inc. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.tests.junit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.lang.ProcessBuilder.Redirect; +import java.nio.file.Files; +import java.nio.file.Path; +import java.rmi.NotBoundException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; + +import org.eclipse.swt.dnd.Clipboard; +import org.eclipse.swt.dnd.RTFTransfer; +import org.eclipse.swt.dnd.TextTransfer; +import org.eclipse.swt.dnd.Transfer; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import clipboard.ClipboardCommands; + +/** + * Automated Test Suite for class org.eclipse.swt.dnd.Clipboard + * + * @see org.eclipse.swt.dnd.Clipboard + * @see Test_org_eclipse_swt_custom_StyledText StyledText tests as it also does + * some clipboard tests + */ +public class Test_org_eclipse_swt_dnd_Clipboard { + + @TempDir + static Path tempFolder; + static int uniqueId = 1; + private Display display; + private Shell shell; + private Clipboard clipboard; + private TextTransfer textTransfer; + private RTFTransfer rtfTransfer; + private ClipboardCommands remote; + private Process remoteClipboardProcess; + + @BeforeEach + public void setUp() { + display = Display.getCurrent(); + if (display == null) { + display = Display.getDefault(); + } + + clipboard = new Clipboard(display); + textTransfer = TextTransfer.getInstance(); + rtfTransfer = RTFTransfer.getInstance(); + } + + private void sleep() throws InterruptedException { + if (SwtTestUtil.isGTK4) { + /** + * TODO remove all uses of sleep and change them to processEvents with the + * suitable conditional, or entirely remove them + */ + SwtTestUtil.processEvents(100, null); + } else { + SwtTestUtil.processEvents(); + } + } + + /** + * Note: Wayland backend does not allow access to system clipboard from + * non-focussed windows. So we have to create/open and focus a window here so + * that clipboard operations work. + */ + private void openAndFocusShell() throws InterruptedException { + shell = new Shell(display); + shell.open(); + shell.setFocus(); + sleep(); + } + + /** + * Note: Wayland backend does not allow access to system clipboard from + * non-focussed windows. So we have to open and focus remote here so that + * clipboard operations work. + */ + private void openAndFocusRemote() throws Exception { + startRemoteClipboardCommands(); + remote.setFocus(); + remote.waitUntilReady(); + sleep(); + } + + @AfterEach + public void tearDown() throws Exception { + sleep(); + try { + stopRemoteClipboardCommands(); + } finally { + if (clipboard != null) { + clipboard.dispose(); + } + if (shell != null) { + shell.dispose(); + } + SwtTestUtil.processEvents(); + } + } + + private void startRemoteClipboardCommands() throws Exception { + /* + * The below copy using getPath may be redundant (i.e. it may be possible to run + * the class files from where they currently reside in the bin folder or the + * jar), but this method of setting up the class files is very simple and is + * done the same way that other files are extracted for tests. + * + * If the ClipboardTest starts to get more complicated, or other tests want to + * replicate this design element, then refactoring this is an option. + */ + List.of( // + "ClipboardTest", // + "ClipboardCommands", // + "ClipboardCommandsImpl", // + "ClipboardTest$LocalHostOnlySocketFactory" // + ).forEach((f) -> { + // extract the files and put them in the temp directory + SwtTestUtil.getPath("/clipboard/" + f + ".class", tempFolder.resolve("clipboard/" + f + ".class")); + }); + + String javaHome = System.getProperty("java.home"); + String javaExe = javaHome + "/bin/java" + (SwtTestUtil.isWindowsOS ? ".exe" : ""); + assertTrue(Files.exists(Path.of(javaExe))); + + ProcessBuilder pb = new ProcessBuilder(javaExe, "clipboard.ClipboardTest").directory(tempFolder.toFile()); + pb.inheritIO(); + pb.redirectOutput(Redirect.PIPE); + remoteClipboardProcess = pb.start(); + + // Read server output to find the port + int port = runOperationInThread(() -> { + BufferedReader reader = new BufferedReader(new InputStreamReader(remoteClipboardProcess.getInputStream())); + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith(ClipboardCommands.PORT_MESSAGE)) { + String[] parts = line.split(":"); + return Integer.parseInt(parts[1].trim()); + } + } + throw new RuntimeException("Failed to get port"); + }); + assertNotEquals(0, port); + Registry reg = LocateRegistry.getRegistry("127.0.0.1", port); + long stopTime = System.currentTimeMillis() + 1000; + do { + try { + remote = (ClipboardCommands) reg.lookup(ClipboardCommands.ID); + break; + } catch (NotBoundException e) { + // try again because the remote app probably hasn't bound yet + } + } while (System.currentTimeMillis() < stopTime); + + // Run a no-op on the Swing event loop so that we know it is idle + // and we can continue startup + remote.waitUntilReady(); + } + + private void stopRemoteClipboardCommands() throws Exception { + try { + if (remote != null) { + remote.stop(); + remote = null; + } + } finally { + if (remoteClipboardProcess != null) { + try { + remoteClipboardProcess.waitFor(1, TimeUnit.SECONDS); + } finally { + remoteClipboardProcess.destroyForcibly(); + remoteClipboardProcess = null; + } + } + } + } + + /** + * Make sure to always copy/paste unique strings - this ensures that tests run + * under {@link RepeatedTest}s don't false pass because of clipboard value on + * previous iteration. + */ + private String getUniqueTestString() { + return "Hello World " + uniqueId++; + } + + /** + * Test that the remote application clipboard works + */ + @Test + public void test_Remote() throws Exception { + openAndFocusRemote(); + String helloWorld = getUniqueTestString(); + remote.setContents(helloWorld); + assertEquals(helloWorld, remote.getStringContents()); + } + + /** + * This tests set + get on local clipboard. Remote clipboard can have different + * behaviours and has additional tests. + */ + @Test + public void test_LocalClipboard() throws Exception { + openAndFocusShell(); + + String helloWorld = getUniqueTestString(); + clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }); + assertEquals(helloWorld, clipboard.getContents(textTransfer)); + assertNull(clipboard.getContents(rtfTransfer)); + + helloWorld = getUniqueTestString(); + String helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}"; + clipboard.setContents(new Object[] { helloWorld, helloWorldRtf }, new Transfer[] { textTransfer, rtfTransfer }); + assertEquals(helloWorld, clipboard.getContents(textTransfer)); + assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer)); + + helloWorld = getUniqueTestString(); + helloWorldRtf = "{\\rtf1\\b\\i " + helloWorld + "}"; + clipboard.setContents(new Object[] { helloWorldRtf }, new Transfer[] { rtfTransfer }); + if (SwtTestUtil.isCocoa) { + /* + * macOS's pasteboard has some extra functionality that even if you don't + * provide a plain text version, the pasteboard will convert the rtf to plain + * text. This isn't in SWT's API contract so if this test fails in the future it + * can be removed. + * + * From the apple docs + * + * For example, if you provided a requestor object for the NSPasteboardTypeRTF + * type, write data to the pasteboard in the RTF format. You don’t need to write + * multiple data formats to the pasteboard to ensure interoperability with other + * apps. + */ + assertEquals(helloWorld, clipboard.getContents(textTransfer)); + } else { + assertNull(clipboard.getContents(textTransfer)); + } + assertEquals(helloWorldRtf, clipboard.getContents(rtfTransfer)); + } + + @Test + public void test_setContents() throws Exception { + try { + openAndFocusShell(); + String helloWorld = getUniqueTestString(); + + clipboard.setContents(new Object[] { helloWorld }, new Transfer[] { textTransfer }); + sleep(); + + openAndFocusRemote(); + SwtTestUtil.processEvents(1000, () -> helloWorld.equals(runOperationInThread(remote::getStringContents))); + String result = runOperationInThread(remote::getStringContents); + assertEquals(helloWorld, result); + } catch (Exception | AssertionError e) { + if (SwtTestUtil.isGTK4 && !SwtTestUtil.isX11) { + // TODO make the code + test stable + throw new RuntimeException( + "This test is really unstable on wayland backend, at least with Ubuntu 25.04", e); + } + throw e; + } + } + + @Test + public void test_getContents() throws Exception { + openAndFocusRemote(); + String helloWorld = getUniqueTestString(); + remote.setContents(helloWorld); + + openAndFocusShell(); + SwtTestUtil.processEvents(1000, () -> { + return helloWorld.equals(clipboard.getContents(textTransfer)); + }); + assertEquals(helloWorld, clipboard.getContents(textTransfer)); + } + + @FunctionalInterface + public interface ExceptionalSupplier { + T get() throws Exception; + } + + /** + * When running some operations, such as requesting remote process read the + * clipboard, we need to have the event queue processing otherwise the remote + * won't be able to read our clipboard contribution. + * + * This method starts the supplier in a new thread and runs the event loop until + * the thread completes, or until a timeout is reached. + */ + private T runOperationInThread(ExceptionalSupplier supplier) throws RuntimeException { + return runOperationInThread(2000, supplier); + } + + /** + * When running some operations, such as requesting remote process read the + * clipboard, we need to have the event queue processing otherwise the remote + * won't be able to read our clipboard contribution. + * + * This method starts the supplier in a new thread and runs the event loop until + * the thread completes, or until a timeout is reached. + */ + private T runOperationInThread(int timeoutMs, ExceptionalSupplier supplier) throws RuntimeException { + Object[] supplierValue = new Object[1]; + Exception[] supplierException = new Exception[1]; + Runnable task = () -> { + try { + supplierValue[0] = supplier.get(); + } catch (Exception e) { + supplierValue[0] = null; + supplierException[0] = e; + } + }; + Thread thread = new Thread(task, this.getClass().getName() + ".runOperationInThread"); + thread.setDaemon(true); + thread.start(); + BooleanSupplier done = () -> !thread.isAlive(); + try { + SwtTestUtil.processEvents(timeoutMs, done); + } catch (InterruptedException e) { + throw new RuntimeException("Failed while running thread", e); + } + assertTrue(done.getAsBoolean()); + if (supplierException[0] != null) { + throw new RuntimeException("Failed while running thread", supplierException[0]); + } + @SuppressWarnings("unchecked") + T result = (T) supplierValue[0]; + return result; + } +} diff --git a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java index 6257137905c..dccc2589885 100644 --- a/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java +++ b/tests/org.eclipse.swt.tests/JUnit Tests/org/eclipse/swt/tests/junit/Test_org_eclipse_swt_widgets_Shell.java @@ -458,7 +458,7 @@ public void test_getImeInputMode() { @Test public void test_getLocation() { //Setting location for Windows is not supported in GTK4 - if (isGTK4()) { + if (SwtTestUtil.isGTK4) { return; } shell.setLocation(10,15); @@ -1007,7 +1007,7 @@ public void test_Issue450_NoShellActivateOnSetFocus() { @Override public void test_setLocationLorg_eclipse_swt_graphics_Point() { //Setting location for Windows is not supported in GTK4 - if (isGTK4()) { + if (SwtTestUtil.isGTK4) { return; } super.test_setLocationLorg_eclipse_swt_graphics_Point(); @@ -1016,14 +1016,9 @@ public void test_setLocationLorg_eclipse_swt_graphics_Point() { @Override public void test_setLocationII() { //Setting location for Windows is not supported in GTK4 - if (isGTK4()) { + if (SwtTestUtil.isGTK4) { return; } super.test_setLocationII(); } - -public static boolean isGTK4() { - String gtkVersion = System.getProperty("org.eclipse.swt.internal.gtk.version", ""); - return SwtTestUtil.isGTK && gtkVersion.startsWith("4"); -} } diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java new file mode 100644 index 00000000000..adf595f842a --- /dev/null +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommands.java @@ -0,0 +1,29 @@ +/******************************************************************************* + * Copyright (c) 2025 Kichwa Coders Canada, Inc. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package clipboard; + +import java.rmi.Remote; +import java.rmi.RemoteException; + +public interface ClipboardCommands extends Remote { + String PORT_MESSAGE = "ClipboardCommands Registry Port: "; + String ID = "ClipboardCommands"; + + void stop() throws RemoteException; + + void setContents(String string) throws RemoteException; + + void setFocus() throws RemoteException; + + String getStringContents() throws RemoteException; + + void waitUntilReady() throws RemoteException; +} diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java new file mode 100644 index 00000000000..3ada14d8622 --- /dev/null +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardCommandsImpl.java @@ -0,0 +1,93 @@ +/******************************************************************************* + * Copyright (c) 2025 Kichwa Coders Canada, Inc. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package clipboard; + +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.lang.reflect.InvocationTargetException; +import java.rmi.RemoteException; +import java.rmi.server.UnicastRemoteObject; +import java.util.Arrays; + +import javax.swing.SwingUtilities; + +public class ClipboardCommandsImpl extends UnicastRemoteObject implements ClipboardCommands { + private static final long serialVersionUID = 330098269086266134L; + private ClipboardTest clipboardTest; + + protected ClipboardCommandsImpl(ClipboardTest clipboardTest) throws RemoteException { + super(); + this.clipboardTest = clipboardTest; + } + + @Override + public void waitUntilReady() throws RemoteException { + invokeAndWait(() -> { + clipboardTest.log("waitUntilReady()"); + }); + } + + @Override + public void stop() throws RemoteException { + invokeAndWait(() -> { + clipboardTest.log("stop()"); + clipboardTest.dispose(); + }); + } + + @Override + public void setContents(String text) throws RemoteException { + invokeAndWait(() -> { + clipboardTest.log("setContents(\"" + text + "\")"); + StringSelection selection = new StringSelection(text); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + + }); + } + + @Override + public String getStringContents() throws RemoteException { + String[] data = new String[] { null }; + invokeAndWait(() -> { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + try { + data[0] = (String) clipboard.getData(DataFlavor.stringFlavor); + clipboardTest.log("getStringContents() returned " + data[0]); + } catch (Exception e) { + data[0] = null; + DataFlavor[] availableDataFlavors = clipboard.getAvailableDataFlavors(); + clipboardTest.log("getStringContents() threw " + e.toString() + + " and returned null. The clipboard had availableDataFlavors = " + + Arrays.asList(availableDataFlavors)); + } + }); + return data[0]; + } + + @Override + public void setFocus() throws RemoteException { + invokeAndWait(() -> { + clipboardTest.log("setFocus()"); + clipboardTest.requestFocus(); + }); + } + + private void invokeAndWait(Runnable run) throws RemoteException { + try { + SwingUtilities.invokeAndWait(run); + } catch (InvocationTargetException | InterruptedException e) { + throw new RemoteException("Failed to run in Swing", e); + } + } + +} \ No newline at end of file diff --git a/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java new file mode 100644 index 00000000000..07279cbf111 --- /dev/null +++ b/tests/org.eclipse.swt.tests/data/clipboard/ClipboardTest.java @@ -0,0 +1,143 @@ +/******************************************************************************* + * Copyright (c) 2025 Kichwa Coders Canada, Inc. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package clipboard; + +import java.awt.BorderLayout; +import java.awt.Toolkit; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.rmi.RemoteException; +import java.rmi.registry.LocateRegistry; +import java.rmi.registry.Registry; +import java.rmi.server.RMISocketFactory; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import org.eclipse.swt.tests.junit.Test_org_eclipse_swt_dnd_Clipboard; + +/** + * Test program used by {@link Test_org_eclipse_swt_dnd_Clipboard}. + * + * + */ +@SuppressWarnings("serial") +public class ClipboardTest extends JFrame { + private static final class LocalHostOnlySocketFactory extends RMISocketFactory { + @Override + public ServerSocket createServerSocket(int port) throws IOException { + return new ServerSocket(port, 50, InetAddress.getLoopbackAddress()); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return new Socket(InetAddress.getLoopbackAddress(), port); + } + } + + private static Registry rmiRegistry; + private JTextArea textArea; + private ClipboardCommands commands; + + public ClipboardTest() throws RemoteException { + super("ClipboardTest"); + commands = new ClipboardCommandsImpl(this); + rmiRegistry.rebind(ClipboardCommands.ID, commands); + + + + textArea = new JTextArea(10, 40); + JScrollPane scrollPane = new JScrollPane(textArea); + + JButton copyButton = new JButton("Copy"); + JButton pasteButton = new JButton("Paste"); + + copyButton.addActionListener(e -> { + String text = textArea.getSelectedText(); + if (text != null) { + StringSelection selection = new StringSelection(text); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + } + }); + + pasteButton.addActionListener(e -> { + Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); + try { + String data = (String) clipboard.getData(DataFlavor.stringFlavor); + textArea.insert(data, textArea.getCaretPosition()); + } catch (Exception ex) { + JOptionPane.showMessageDialog(ClipboardTest.this, "Could not paste from clipboard", "Error", + JOptionPane.ERROR_MESSAGE); + } + }); + + JPanel buttonPanel = new JPanel(); + buttonPanel.add(copyButton); + buttonPanel.add(pasteButton); + + add(scrollPane, BorderLayout.CENTER); + add(buttonPanel, BorderLayout.SOUTH); + + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + pack(); + setLocationRelativeTo(null); // Center on screen + setVisible(true); + } + + public void log(String log) { + textArea.insert(log, textArea.getCaretPosition()); + if (!log.endsWith("\n")) { + textArea.insert("\n", textArea.getCaretPosition()); + } + } + + public static void main(String[] args) throws IOException { + System.setProperty("java.rmi.server.hostname", "127.0.0.1"); + + // Make sure RMI is localhost only + RMISocketFactory.setSocketFactory(new LocalHostOnlySocketFactory()); + int chosenPort = getAvailablePort(); + rmiRegistry = LocateRegistry.createRegistry(chosenPort); + System.out.println(ClipboardCommands.PORT_MESSAGE + chosenPort); + + + + SwingUtilities.invokeLater(() -> { + try { + new ClipboardTest(); + } catch (RemoteException e) { + System.err.println("Failed to start ClipboardTest"); + e.printStackTrace(); + System.exit(1); + } + }); + } + + /** + * Because LocateRegistry requires reflection and/or using sun.* packages to get + * the running port, use ServerSocket to get a free port. + */ + private static int getAvailablePort() throws IOException { + try (var ss = new java.net.ServerSocket(0)) { + return ss.getLocalPort(); + } + } +}