From 204020954c08128603732775af95997617e1b42f Mon Sep 17 00:00:00 2001
From: Hammy <sgoudham@gmail.com>
Date: Mon, 26 Jul 2021 00:06:06 +0100
Subject: [PATCH] Port code over from MyClipboardHistory

---
 .../java/me/goudham/ClipboardListener.java    | 42 ++++++++++
 src/main/java/me/goudham/ClipboardUtils.java  | 44 +++++++++++
 .../java/me/goudham/MacClipboardListener.java | 61 +++++++++++++++
 src/main/java/me/goudham/Main.java            | 34 ++++++++
 src/main/java/me/goudham/MyClipboard.java     | 67 ++++++++++++++++
 .../WindowsOrUnixClipboardListener.java       | 78 +++++++++++++++++++
 src/main/java/me/goudham/domain/Contents.java | 37 +++++++++
 .../me/goudham/domain/MyClipboardContent.java | 16 ++++
 .../me/goudham/domain/TransferableImage.java  | 42 ++++++++++
 .../exception/UnsupportedSystemException.java | 10 +++
 .../listener/ClipboardEventListener.java      |  8 ++
 11 files changed, 439 insertions(+)
 create mode 100644 src/main/java/me/goudham/ClipboardListener.java
 create mode 100644 src/main/java/me/goudham/ClipboardUtils.java
 create mode 100644 src/main/java/me/goudham/MacClipboardListener.java
 create mode 100644 src/main/java/me/goudham/Main.java
 create mode 100644 src/main/java/me/goudham/MyClipboard.java
 create mode 100644 src/main/java/me/goudham/WindowsOrUnixClipboardListener.java
 create mode 100644 src/main/java/me/goudham/domain/Contents.java
 create mode 100644 src/main/java/me/goudham/domain/MyClipboardContent.java
 create mode 100644 src/main/java/me/goudham/domain/TransferableImage.java
 create mode 100644 src/main/java/me/goudham/exception/UnsupportedSystemException.java
 create mode 100644 src/main/java/me/goudham/listener/ClipboardEventListener.java

diff --git a/src/main/java/me/goudham/ClipboardListener.java b/src/main/java/me/goudham/ClipboardListener.java
new file mode 100644
index 0000000..70260b0
--- /dev/null
+++ b/src/main/java/me/goudham/ClipboardListener.java
@@ -0,0 +1,42 @@
+package me.goudham;
+
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.util.ArrayList;
+import java.util.List;
+import me.goudham.listener.ClipboardEventListener;
+
+abstract class ClipboardListener {
+    final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+    List<ClipboardEventListener> eventsListener = new ArrayList<>();
+    private boolean imagesMonitored = true;
+    private boolean textMonitored = true;
+
+    void addEventListener(ClipboardEventListener clipboardEventListener) {
+        if (!eventsListener.contains(clipboardEventListener)) {
+            eventsListener.add(clipboardEventListener);
+        }
+    }
+
+    void removeEventListener(ClipboardEventListener clipboardEventListener) {
+        eventsListener.remove(clipboardEventListener);
+    }
+
+    boolean isImagesMonitored() {
+        return imagesMonitored;
+    }
+
+    void setImagesMonitored(boolean imagesMonitored) {
+        this.imagesMonitored = imagesMonitored;
+    }
+
+    boolean isTextMonitored() {
+        return textMonitored;
+    }
+
+    void setTextMonitored(boolean textMonitored) {
+        this.textMonitored = textMonitored;
+    }
+
+    abstract void execute();
+}
diff --git a/src/main/java/me/goudham/ClipboardUtils.java b/src/main/java/me/goudham/ClipboardUtils.java
new file mode 100644
index 0000000..be9b6da
--- /dev/null
+++ b/src/main/java/me/goudham/ClipboardUtils.java
@@ -0,0 +1,44 @@
+package me.goudham;
+
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import me.goudham.domain.MyClipboardContent;
+
+import static me.goudham.domain.Contents.IMAGE;
+import static me.goudham.domain.Contents.STRING;
+
+class ClipboardUtils {
+
+    static MyClipboardContent<?> getClipboardContents(Transferable contents, Clipboard clipboard) {
+        MyClipboardContent<?> myClipboardContent = new MyClipboardContent<>();
+
+        try {
+            if (STRING.isAvailable(clipboard)) {
+                myClipboardContent.setOldContent(contents.getTransferData(STRING.getDataFlavor()));
+            } else if (IMAGE.isAvailable(clipboard)) {
+                BufferedImage bufferedImage = convertToBufferedImage((Image) contents.getTransferData(IMAGE.getDataFlavor()));
+                myClipboardContent.setOldContent(new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight()));
+            }
+        } catch (UnsupportedFlavorException | IOException exp) {
+            exp.printStackTrace();
+        }
+
+        return myClipboardContent;
+    }
+
+    static BufferedImage convertToBufferedImage(Image image) {
+        BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
+
+        Graphics2D graphics = newImage.createGraphics();
+        graphics.drawImage(image, 0, 0, null);
+        graphics.dispose();
+
+        return newImage;
+    }
+}
diff --git a/src/main/java/me/goudham/MacClipboardListener.java b/src/main/java/me/goudham/MacClipboardListener.java
new file mode 100644
index 0000000..5045a6f
--- /dev/null
+++ b/src/main/java/me/goudham/MacClipboardListener.java
@@ -0,0 +1,61 @@
+package me.goudham;
+
+import java.awt.Dimension;
+import java.awt.Image;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import me.goudham.listener.ClipboardEventListener;
+import me.goudham.domain.MyClipboardContent;
+
+import static me.goudham.domain.Contents.IMAGE;
+import static me.goudham.domain.Contents.STRING;
+
+class MacClipboardListener extends ClipboardListener {
+
+    MacClipboardListener() { }
+
+    @Override
+    public void execute() {
+        Transferable oldClipboardContents = clipboard.getContents(null);
+        final MyClipboardContent<?>[] myClipboardContents = new MyClipboardContent[]{ ClipboardUtils.getClipboardContents(oldClipboardContents, clipboard) };
+
+        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
+        executor.scheduleAtFixedRate(() -> {
+            Transferable newClipboardContents = clipboard.getContents(null);
+
+            try {
+                if (isTextMonitored()) {
+                    if (STRING.isAvailable(clipboard)) {
+                        String newContent = (String) newClipboardContents.getTransferData(STRING.getDataFlavor());
+                        if (!newContent.equals(myClipboardContents[0].getOldContent())) {
+                            for (ClipboardEventListener clipboardEventListener : eventsListener) {
+                                clipboardEventListener.onCopyString(newContent);
+                            }
+                            myClipboardContents[0].setOldContent(newContent);
+                        }
+                    }
+                }
+
+                if (isImagesMonitored()) {
+                    if (IMAGE.isAvailable(clipboard)) {
+                        BufferedImage bufferedImage = ClipboardUtils.convertToBufferedImage((Image) newClipboardContents.getTransferData(IMAGE.getDataFlavor()));
+                        Dimension newContent = new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight());
+                        if (!newContent.equals(myClipboardContents[0].getOldContent())) {
+                            for (ClipboardEventListener clipboardEventListener : eventsListener) {
+                                clipboardEventListener.onCopyImage(bufferedImage);
+                            }
+                            myClipboardContents[0].setOldContent(newContent);
+                        }
+                    }
+                }
+            } catch (UnsupportedFlavorException | IOException exp) {
+                exp.printStackTrace();
+            }
+        }, 0, 350, TimeUnit.MILLISECONDS);
+    }
+}
diff --git a/src/main/java/me/goudham/Main.java b/src/main/java/me/goudham/Main.java
new file mode 100644
index 0000000..fc03fee
--- /dev/null
+++ b/src/main/java/me/goudham/Main.java
@@ -0,0 +1,34 @@
+package me.goudham;
+
+import java.awt.image.BufferedImage;
+import me.goudham.exception.UnsupportedSystemException;
+import me.goudham.listener.ClipboardEventListener;
+
+public class Main {
+    public static void main(String[] args) throws UnsupportedSystemException {
+        MyClipboard myClipboard = MyClipboard.getSystemClipboard();
+        myClipboard.addEventListener(new ClipboardEventListener() {
+            @Override
+            public void onCopyString(String stringContent) {
+                System.out.println(stringContent);
+            }
+
+            @Override
+            public void onCopyImage(BufferedImage imageContent) {
+                System.out.println(imageContent);
+            }
+        });
+//
+//        while (true) {
+//
+//        }
+//        JClipboard jClipboard = JClipboard.getDefault();
+//        jClipboard.addEventListener(new JClipboardEventListener() {
+//            @Override
+//            public void onClipboardTextChange(JClipboardEvent event) {
+//                System.out.println("Was: " + event.getOldValue()); // The old value
+//                System.out.println("Is: " + event.getNewValue()); // The new value
+//            }
+//        });
+    }
+}
diff --git a/src/main/java/me/goudham/MyClipboard.java b/src/main/java/me/goudham/MyClipboard.java
new file mode 100644
index 0000000..baae546
--- /dev/null
+++ b/src/main/java/me/goudham/MyClipboard.java
@@ -0,0 +1,67 @@
+package me.goudham;
+
+import me.goudham.exception.UnsupportedSystemException;
+import me.goudham.listener.ClipboardEventListener;
+import org.apache.commons.lang3.SystemUtils;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Entry Class for User to interact with the System Clipboard
+ *
+ * The abstract class {@link ClipboardListener} is responsible for handling all operations
+ */
+public class MyClipboard {
+    private final @NotNull ClipboardListener clipboardListener;
+
+    /**
+     * Creates an instance of {@link MyClipboard}
+     *
+     * @param clipboardListener The underlying {@link ClipboardListener}
+     */
+    private MyClipboard(@NotNull ClipboardListener clipboardListener) {
+       this.clipboardListener = clipboardListener;
+       this.clipboardListener.execute();
+    }
+
+    /**
+     * Creates an instance of {@link MyClipboard} with an instance of {@link ClipboardListener} dependant on the OS
+     * <p>A {@link WindowsOrUnixClipboardListener} or {@link MacClipboardListener} can be created</p>
+     *
+     * @return {@link MyClipboard}
+     * @throws UnsupportedSystemException If {@link MyClipboard} detects an operating system which is not Mac or Windows/*Unix
+     */
+    public static MyClipboard getSystemClipboard() throws UnsupportedSystemException {
+        ClipboardListener clipboardListener;
+
+        if (isMac()) {
+            clipboardListener = new MacClipboardListener();
+        } else if (isWindows() || isUnix()) {
+            clipboardListener = new WindowsOrUnixClipboardListener();
+        } else {
+            throw new UnsupportedSystemException("Your Operating System: " + System.getProperty("os.name") + "is not supported");
+        }
+
+        return new MyClipboard(clipboardListener);
+    }
+
+    /**
+     * Adds an {@link ClipboardEventListener} to the underlying {@link ClipboardListener}
+     *
+     * @param clipboardEventListener The {@link ClipboardEventListener} to be added
+     */
+    public void addEventListener(ClipboardEventListener clipboardEventListener) {
+        clipboardListener.addEventListener(clipboardEventListener);
+    }
+
+    private static boolean isMac() {
+        return SystemUtils.IS_OS_MAC;
+    }
+
+    private static boolean isUnix() {
+        return SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_LINUX;
+    }
+
+    private static boolean isWindows() {
+        return SystemUtils.IS_OS_WINDOWS;
+    }
+}
diff --git a/src/main/java/me/goudham/WindowsOrUnixClipboardListener.java b/src/main/java/me/goudham/WindowsOrUnixClipboardListener.java
new file mode 100644
index 0000000..b838545
--- /dev/null
+++ b/src/main/java/me/goudham/WindowsOrUnixClipboardListener.java
@@ -0,0 +1,78 @@
+package me.goudham;
+
+import java.awt.Image;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.ClipboardOwner;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import me.goudham.listener.ClipboardEventListener;
+
+import static java.lang.Thread.currentThread;
+import static java.lang.Thread.sleep;
+import static me.goudham.domain.Contents.IMAGE;
+import static me.goudham.domain.Contents.STRING;
+
+class WindowsOrUnixClipboardListener extends ClipboardListener implements Runnable, ClipboardOwner {
+
+    WindowsOrUnixClipboardListener() { }
+
+    @Override
+    public void lostOwnership(Clipboard oldClipboard, Transferable oldClipboardContents) {
+        try {
+            sleep(200);
+        } catch (InterruptedException ignored) {
+        }
+
+        Transferable newClipboardContents = oldClipboard.getContents(currentThread());
+        processContents(oldClipboard, newClipboardContents);
+        regainOwnership(oldClipboard, newClipboardContents);
+    }
+
+    public void processContents(Clipboard oldClipboard, Transferable newClipboardContents) {
+        try {
+            if (STRING.isAvailable(oldClipboard)) {
+                String stringContent = (String) newClipboardContents.getTransferData(DataFlavor.stringFlavor);
+                for (ClipboardEventListener clipboardEventListener : eventsListener) {
+                    clipboardEventListener.onCopyString(stringContent);
+                }
+            } else if (IMAGE.isAvailable(oldClipboard)) {
+                BufferedImage imageContent = ClipboardUtils.convertToBufferedImage((Image) newClipboardContents.getTransferData(DataFlavor.imageFlavor));
+                for (ClipboardEventListener clipboardEventListener : eventsListener) {
+                    clipboardEventListener.onCopyImage(imageContent);
+                }
+            }
+        } catch (UnsupportedFlavorException | IOException ignored) {
+        }
+    }
+
+    public void regainOwnership(Clipboard clipboard, Transferable newClipboardContents) {
+        try {
+            clipboard.setContents(newClipboardContents, this);
+        } catch (IllegalStateException ise) {
+            try {
+                sleep(200);
+            } catch (InterruptedException ie) {
+                ie.printStackTrace();
+            }
+            regainOwnership(clipboard, newClipboardContents);
+        }
+    }
+
+    @Override
+    public void run() {
+        Transferable currentClipboardContents = clipboard.getContents(null);
+        processContents(clipboard, currentClipboardContents);
+        regainOwnership(clipboard, currentClipboardContents);
+    }
+
+    @Override
+    public void execute() {
+        ExecutorService executorService = Executors.newCachedThreadPool();
+        executorService.submit(this);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/me/goudham/domain/Contents.java b/src/main/java/me/goudham/domain/Contents.java
new file mode 100644
index 0000000..556a0ff
--- /dev/null
+++ b/src/main/java/me/goudham/domain/Contents.java
@@ -0,0 +1,37 @@
+package me.goudham.domain;
+
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+
+public enum Contents {
+    STRING(DataFlavor.stringFlavor) {
+        @Override
+        public boolean isAvailable(Clipboard clipboard) {
+            return clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor);
+        }
+    },
+    IMAGE(DataFlavor.imageFlavor) {
+        @Override
+        public boolean isAvailable(Clipboard clipboard) {
+            return clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor);
+        }
+    },
+    FILELIST(DataFlavor.javaFileListFlavor) {
+        @Override
+        public boolean isAvailable(Clipboard clipboard) {
+            return clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor);
+        }
+    };
+
+    private final DataFlavor dataFlavor;
+
+    Contents(DataFlavor dataFlavor) {
+       this.dataFlavor = dataFlavor;
+    }
+
+    public DataFlavor getDataFlavor() {
+        return dataFlavor;
+    }
+
+    public abstract boolean isAvailable(Clipboard clipboard);
+}
diff --git a/src/main/java/me/goudham/domain/MyClipboardContent.java b/src/main/java/me/goudham/domain/MyClipboardContent.java
new file mode 100644
index 0000000..032a9ad
--- /dev/null
+++ b/src/main/java/me/goudham/domain/MyClipboardContent.java
@@ -0,0 +1,16 @@
+package me.goudham.domain;
+
+public class MyClipboardContent<T> {
+    private T oldContent;
+
+    public MyClipboardContent() {
+    }
+
+    public void setOldContent(Object oldContent) {
+        this.oldContent = (T) oldContent;
+    }
+
+    public T getOldContent() {
+        return oldContent;
+    }
+}
diff --git a/src/main/java/me/goudham/domain/TransferableImage.java b/src/main/java/me/goudham/domain/TransferableImage.java
new file mode 100644
index 0000000..a5f73d9
--- /dev/null
+++ b/src/main/java/me/goudham/domain/TransferableImage.java
@@ -0,0 +1,42 @@
+package me.goudham.domain;
+
+import java.awt.Image;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import org.jetbrains.annotations.NotNull;
+
+public class TransferableImage implements Transferable {
+
+	private final Image image;
+
+	public TransferableImage(@NotNull Image image) {
+		this.image = image;
+	}
+
+	@Override
+	public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
+		if (flavor.equals(DataFlavor.imageFlavor)) {
+			return image;
+		} else {
+			throw new UnsupportedFlavorException(flavor);
+		}
+	}
+
+	@Override
+	public DataFlavor[] getTransferDataFlavors() {
+		return new DataFlavor[] { DataFlavor.imageFlavor };
+	}
+
+	@Override
+	public boolean isDataFlavorSupported(DataFlavor flavor) {
+		DataFlavor[] flavors = getTransferDataFlavors();
+		for (DataFlavor dataFlavor : flavors) {
+			if (flavor.equals(dataFlavor)) {
+				return true;
+			}
+		}
+
+		return false;
+	}
+}
diff --git a/src/main/java/me/goudham/exception/UnsupportedSystemException.java b/src/main/java/me/goudham/exception/UnsupportedSystemException.java
new file mode 100644
index 0000000..6cdc64d
--- /dev/null
+++ b/src/main/java/me/goudham/exception/UnsupportedSystemException.java
@@ -0,0 +1,10 @@
+package me.goudham.exception;
+
+/**
+ * Thrown when {@link me.goudham.MyClipboard} detects an operating system which is not {@code Mac} or {@code Windows/*Unix}
+ */
+public class UnsupportedSystemException extends Throwable {
+    public UnsupportedSystemException(String exceptionMessage) {
+        super(exceptionMessage);
+    }
+}
diff --git a/src/main/java/me/goudham/listener/ClipboardEventListener.java b/src/main/java/me/goudham/listener/ClipboardEventListener.java
new file mode 100644
index 0000000..2851ad7
--- /dev/null
+++ b/src/main/java/me/goudham/listener/ClipboardEventListener.java
@@ -0,0 +1,8 @@
+package me.goudham.listener;
+
+import java.awt.image.BufferedImage;
+
+public interface ClipboardEventListener {
+	void onCopyString(String stringContent);
+	void onCopyImage(BufferedImage imageContent);
+}