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(); + } + } +}