From 1388fca72ea44f2e8defbff578f77cb82d0d0985 Mon Sep 17 00:00:00 2001 From: crschnick Date: Sun, 1 Jun 2025 08:16:35 +0000 Subject: [PATCH] Fix clipboard threading crash --- .../app/browser/file/BrowserClipboard.java | 65 ++++++++----------- .../ContextualFileReferenceChoiceComp.java | 2 +- .../io/xpipe/app/core/mode/OperationMode.java | 1 + .../io/xpipe/app/util/GlobalClipboard.java | 55 ++++++++++++++++ 4 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 app/src/main/java/io/xpipe/app/util/GlobalClipboard.java diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java index 95a067963..8fb5b2f8b 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserClipboard.java @@ -2,6 +2,7 @@ package io.xpipe.app.browser.file; import io.xpipe.app.ext.ProcessControlProvider; import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.app.util.GlobalClipboard; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileEntry; import io.xpipe.core.util.FailableRunnable; @@ -23,6 +24,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.UUID; +import java.util.function.Consumer; import java.util.stream.Collectors; public class BrowserClipboard { @@ -32,44 +34,33 @@ public class BrowserClipboard { private static final DataFormat DATA_FORMAT = new DataFormat("application/xpipe-file-list"); static { - Toolkit.getDefaultToolkit() - .getSystemClipboard() - .addFlavorListener(e -> ThreadHelper.runFailableAsync(new FailableRunnable<>() { - @Override - @SuppressWarnings("unchecked") - public void run() { - // Fix clipboard open issues: https://stackoverflow.com/a/51797746 - ThreadHelper.sleep(20); - - Clipboard clipboard = (Clipboard) e.getSource(); - try { - if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { - return; - } - - List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); - // Sometimes file data can contain invalid chars. Why? - var files = data.stream() - .filter(file -> - file.toString().chars().noneMatch(value -> Character.isISOControl(value))) - .map(f -> f.toPath()) - .toList(); - if (files.size() == 0) { - return; - } - - var entries = new ArrayList(); - for (Path file : files) { - entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file)); - } - - currentCopyClipboard.setValue( - new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY)); - } catch (Exception e) { - ErrorEvent.fromThrowable(e).expected().omit().handle(); - } + GlobalClipboard.addListener(new Consumer<>() { + @Override + @SuppressWarnings("unchecked") + public void accept(Clipboard clipboard) { + try { + if (!clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor)) { + return; } - })); + + List data = (List) clipboard.getData(DataFlavor.javaFileListFlavor); + // Sometimes file data can contain invalid chars. Why? + var files = data.stream().filter(file -> file.toString().chars().noneMatch(value -> Character.isISOControl(value))).map(f -> f.toPath()).toList(); + if (files.size() == 0) { + return; + } + + var entries = new ArrayList(); + for (Path file : files) { + entries.add(BrowserLocalFileSystem.getLocalBrowserEntry(file)); + } + + currentCopyClipboard.setValue(new Instance(UUID.randomUUID(), null, entries, BrowserFileTransferMode.COPY)); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).expected().omit().handle(); + } + } + }); } @SneakyThrows diff --git a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java index af48dff88..629e1fc99 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ContextualFileReferenceChoiceComp.java @@ -122,7 +122,7 @@ public class ContextualFileReferenceChoiceComp extends Comp> var pubSource = Path.of(source + ".pub"); if (Files.exists(pubSource)) { var pubTarget = sync.getTargetLocation().apply(pubSource); - DataStorageSyncHandler.getInstance() + handler .addDataFile( pubSource, pubTarget, sync.getPerUser().test(pubSource)); } diff --git a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java index 0e29d1f05..4fa313cd0 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/OperationMode.java @@ -111,6 +111,7 @@ public abstract class OperationMode { TrackEvent.info("Initial setup"); AppMainWindow.loadingText("initializingApp"); GlobalTimer.init(); + GlobalClipboard.init(); AppProperties.init(args); NodeCallback.init(); AppLogs.init(); diff --git a/app/src/main/java/io/xpipe/app/util/GlobalClipboard.java b/app/src/main/java/io/xpipe/app/util/GlobalClipboard.java new file mode 100644 index 000000000..c14ef37f7 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/util/GlobalClipboard.java @@ -0,0 +1,55 @@ +package io.xpipe.app.util; + +import io.xpipe.app.browser.file.BrowserEntry; +import io.xpipe.app.browser.file.BrowserFileTransferMode; +import io.xpipe.app.browser.file.BrowserLocalFileSystem; +import io.xpipe.app.ext.ProcessControlProvider; +import io.xpipe.app.issue.ErrorEvent; +import io.xpipe.core.store.FileEntry; +import io.xpipe.core.util.FailableRunnable; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.input.ClipboardContent; +import javafx.scene.input.DataFormat; +import javafx.scene.input.Dragboard; +import lombok.SneakyThrows; +import lombok.Value; + +import java.awt.*; +import java.awt.datatransfer.Clipboard; +import java.awt.datatransfer.DataFlavor; +import java.io.File; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public class GlobalClipboard { + + private static final List> clipboardListeners = new ArrayList<>(); + + public static synchronized void addListener(Consumer listener) { + clipboardListeners.add(listener); + } + + public static void init() { + // Only access from one thread to fix https://bugs.openjdk.org/browse/JDK-8332271 + Toolkit.getDefaultToolkit() + .getSystemClipboard() + .addFlavorListener(e -> { + // Fix clipboard open issues: https://stackoverflow.com/a/51797746 + ThreadHelper.sleep(20); + + var cp = (Clipboard) e.getSource(); + ThreadHelper.runFailableAsync(() -> { + synchronized (GlobalClipboard.class) { + for (Consumer clipboardListener : clipboardListeners) { + clipboardListener.accept(cp); + } + } + }); + }); + } +}