Merge pull request #2 from sgoudham/main

Release v0.2.0
release
Hamothy 3 years ago committed by GitHub
commit c11eeb668c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,4 +1,9 @@
FROM maven:3.8.1-adoptopenjdk-11
MAINTAINER Goudham Suresh
RUN apt-get update && apt-get install -y gpg
RUN apt-get update && apt-get install -y \
gpg \
xvfb \
libxrender1 libxtst6 libxi6 libxext6
RUN /usr/bin/Xvfb :99 &
RUN export DISPLAY=:99

4
Jenkinsfile vendored

@ -6,7 +6,7 @@ pipeline {
}
environment {
CODECOV_TOKEN = credentials('44a3c021-5cbb-4a6f-bea2-ae6c51d43038')
CODECOV_TOKEN = credentials('ea3e54d7-7f4f-40ed-af70-d8132e1c405b')
GPG_SECRET_KEY = credentials('4dbfd4ed-bba4-44e0-8410-fbce1a9bba73')
GPG_OWNER_TRUST = credentials('8703bbe8-c099-481f-8337-1dce32d51771')
@ -31,8 +31,10 @@ pipeline {
}
stage("Test") {
steps {
wrap([$class: 'Xvfb']) {
sh "mvn test"
}
}
post {
success {
echo "Generating Test Report..."

@ -1,37 +1,68 @@
[license]: https://img.shields.io/github/license/sgoudham/MyClipboard
[comment]: <> ([maven-central]: )
[comment]: <> ([build-status]:)
[comment]: <> ([codecov]:)
[maven-central]: https://img.shields.io/maven-central/v/me.goudham/MyClipboard.svg?label=Maven%20Central
[build-status]: https://goudham.me/jenkins/job/MyClipboard/job/release/badge/icon
[codecov]: https://codecov.io/gh/sgoudham/MyClipboard/branch/main/graph/badge.svg?token=F4LKql7rIq
[issues]: https://img.shields.io/github/issues/sgoudham/MyClipboard?label=issues
[pull-requests]: https://img.shields.io/github/issues-pr/sgoudham/MyClipboard
[fossa]: https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsgoudham%2FMyClipboard.svg?type=shield
![fossa]
![license]
[comment]: <> (![maven-central])
[comment]: <> (![build-status])
[comment]: <> (![codecov])
![maven-central]
![build-status]
![codecov]
![issues]
![pull-requests]
# MyClipboard
Utility Program to Access and Manipulate the Default System Clipboard
Utility Library to Access and Manipulate the Default System Clipboard
# About
The inspiration for this project came from my frustration of macOS not having clipboard history
built-in unlike Windows. This library will allow you to access the system clipboard and manipulate it.
# Configuration
TODO
# Windows / *Unix
This approach differs from the macOS section below as Windows/*Unix properly notify the program with global clipboard events.
This allows for a more event-driven approach as lostOwnership() is triggered whenever the clipboard has lost ownership - clipboard
has new content within it - and the contents can be observed by multiple consumers.
# Mac OSX
# macOS
Unlike the aforementioned event-driven approach, macOS unfortunately is not very good at notifying the program if the
system clipboard has changed. To query the system clipboard contents, we need to employ a polling schedule. I have chosen
**200ms** to ensure that images and large files can be copied over as well as reducing the load on the CPU.
# Contributing
TODO
# Installation
Latest Stable Version: ![maven-central]
<p>Be sure to replace the <strong>VERSION</strong> key below with the one of the versions shown above!</p>
**Maven**
```xml
<!-- https://mvnrepository.com/artifact/me.goudham/MyClipboard -->
<dependency>
<groupId>me.goudham</groupId>
<artifactId>MyClipboard</artifactId>
<version>VERSION</version>
</dependency>
```
**Gradle**
```gradle
repositories {
mavenCentral()
}
dependencies {
// https://mvnrepository.com/artifact/me.goudham/MyClipboard
implementation group: 'me.goudham', name: 'MyClipboard', version: 'VERSION'
}
```
# License
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsgoudham%2FMyClipboard.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsgoudham%2FMyClipboard?ref=badge_large)

@ -6,11 +6,11 @@
<groupId>me.goudham</groupId>
<artifactId>MyClipboard</artifactId>
<version>0.1.0</version>
<version>0.2.0</version>
<packaging>jar</packaging>
<name>MyClipboard</name>
<description>Utility Program to Access and Manipulate the Default System Clipboard</description>
<description>Utility Library to Access and Manipulate the Default System Clipboard</description>
<url>https://github.com/sgoudham/MyClipboard</url>
<properties>
@ -59,6 +59,12 @@
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@ -69,6 +75,7 @@
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains</groupId>
@ -76,6 +83,11 @@
<version>21.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0-alpha2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@ -145,7 +157,7 @@
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>false</autoReleaseAfterClose>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>

@ -1,94 +0,0 @@
package me.goudham;
import java.awt.Image;
import java.awt.Toolkit;
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 java.util.ArrayList;
import java.util.List;
import me.goudham.listener.ClipboardEventListener;
import static me.goudham.domain.Contents.IMAGE;
import static me.goudham.domain.Contents.STRING;
abstract class ClipboardListener {
final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
List<ClipboardEventListener> eventsListener = new ArrayList<>();
private boolean imagesMonitored = true;
private boolean textMonitored = true;
String getStringContent(Transferable clipboardContents) {
String newContent = null;
try {
newContent = (String) clipboardContents.getTransferData(STRING.getDataFlavor());
} catch (UnsupportedFlavorException | IOException exp) {
exp.printStackTrace();
}
return newContent;
}
BufferedImage getImageContent(Transferable clipboardContents) {
BufferedImage bufferedImage = null;
try {
bufferedImage = ClipboardUtils.convertToBufferedImage((Image) clipboardContents.getTransferData(IMAGE.getDataFlavor()));
} catch (UnsupportedFlavorException | IOException exp) {
exp.printStackTrace();
}
return bufferedImage;
}
void notifyStringEvent(String stringContent) {
for (ClipboardEventListener clipboardEventListener : eventsListener) {
clipboardEventListener.onCopyString(stringContent);
}
}
void notifyImageEvent(BufferedImage imageContent) {
for (ClipboardEventListener clipboardEventListener : eventsListener) {
clipboardEventListener.onCopyImage(imageContent);
}
}
void addEventListener(ClipboardEventListener clipboardEventListener) {
if (!eventsListener.contains(clipboardEventListener)) {
eventsListener.add(clipboardEventListener);
}
}
void removeEventListener(ClipboardEventListener clipboardEventListener) {
eventsListener.remove(clipboardEventListener);
}
void toggleTextMonitored() {
this.textMonitored = !textMonitored;
}
void toggleImagesMonitored() {
this.imagesMonitored = !imagesMonitored;
}
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();
}

@ -1,39 +1,164 @@
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.File;
import java.io.IOException;
import me.goudham.domain.MyClipboardContent;
import java.util.List;
import me.goudham.domain.ClipboardContent;
import me.goudham.domain.GenericClipboardContent;
import me.goudham.domain.MyBufferedImage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static me.goudham.domain.Contents.IMAGE;
import static me.goudham.domain.Contents.STRING;
import static me.goudham.Contents.FILE;
import static me.goudham.Contents.IMAGE;
import static me.goudham.Contents.TEXT;
class ClipboardUtils {
private static Logger logger = LoggerFactory.getLogger(ClipboardUtils.class);
static MyClipboardContent<?> getClipboardContents(Transferable contents, Clipboard clipboard) {
MyClipboardContent<?> myClipboardContent = new MyClipboardContent<>();
/**
* Try to unmarshal {@link Transferable} into {@link String}
*
* @param clipboardContents The {@link Transferable} to be converted into {@link String}
* @return {@link String} representation of {@code clipboardContents}
*/
String getStringContent(Transferable clipboardContents) {
String newContent = null;
try {
if (STRING.isAvailable(clipboard)) {
myClipboardContent.setOldContent(contents.getTransferData(STRING.getDataFlavor()));
} else if (IMAGE.isAvailable(clipboard)) {
if (clipboardContents.isDataFlavorSupported(TEXT.getDataFlavor())) {
newContent = (String) clipboardContents.getTransferData(TEXT.getDataFlavor());
}
} catch (UnsupportedFlavorException | IOException exp) {
logger.error("Exception Thrown When Retrieving String Content", exp);
}
return newContent;
}
/**
* Try to unmarshal {@link Transferable} into {@link MyBufferedImage#getBufferedImage()}
*
* @param clipboardContents The {@link Transferable} to be converted into {@link MyBufferedImage}
* @return {@link MyBufferedImage} representation of {@code clipboardContents}
*/
MyBufferedImage getImageContent(Transferable clipboardContents) {
MyBufferedImage myBufferedImage = null;
try {
if (clipboardContents.isDataFlavorSupported(IMAGE.getDataFlavor())) {
BufferedImage bufferedImage = convertToBufferedImage((Image) clipboardContents.getTransferData(IMAGE.getDataFlavor()));
myBufferedImage = new MyBufferedImage(bufferedImage);
}
} catch (UnsupportedFlavorException | IOException exp) {
logger.error("Exception Thrown When Retrieving Image Content", exp);
}
return myBufferedImage;
}
/**
* Try to unmarshal {@link Transferable} into {@link List} of {@link File}
*
* @param clipboardContents The {@link Transferable} to be converted into {@link List} of {@link File}
* @return {@link List} of {@link File} representation of {@code clipboardContents}
*/
List<File> getFileContent(Transferable clipboardContents) {
List<File> fileList = null;
try {
if (clipboardContents.isDataFlavorSupported(FILE.getDataFlavor())) {
fileList = (List<File>) clipboardContents.getTransferData(FILE.getDataFlavor());
}
} catch (UnsupportedFlavorException | IOException exp) {
logger.error("Exception Thrown When Retrieving File Content", exp);
}
return fileList;
}
/**
* Store contents from the given {@link Transferable} into {@link GenericClipboardContent}
*
* @param contents The {@link Transferable} which holds the clipboard contents
* @return {@link GenericClipboardContent} containing clipboard contents
*/
GenericClipboardContent<?> getGenericClipboardContents(Transferable contents) {
GenericClipboardContent<?> genericClipboardContent = null;
try {
if (contents.isDataFlavorSupported(TEXT.getDataFlavor())) {
genericClipboardContent = new GenericClipboardContent<>(contents.getTransferData(TEXT.getDataFlavor()));
} else if (contents.isDataFlavorSupported(IMAGE.getDataFlavor())) {
BufferedImage bufferedImage = convertToBufferedImage((Image) contents.getTransferData(IMAGE.getDataFlavor()));
myClipboardContent.setOldContent(new Dimension(bufferedImage.getWidth(), bufferedImage.getHeight()));
genericClipboardContent = new GenericClipboardContent<>(new MyBufferedImage(bufferedImage));
} else if (contents.isDataFlavorSupported(FILE.getDataFlavor())) {
genericClipboardContent = new GenericClipboardContent<>(contents.getTransferData(FILE.getDataFlavor()));
}
} catch (UnsupportedFlavorException | IOException exp) {
logger.error("Exception Thrown When Retrieving Clipboard Contents", exp);
}
return genericClipboardContent;
}
/**
* Store contents from the given {@link Transferable} into {@link ClipboardContent}
*
* @param oldContents The given {@link Transferable} which holds the clipboard contents
* @return {@link ClipboardContent} containing old clipboard contents
*/
ClipboardContent getClipboardContent(Transferable oldContents) {
ClipboardContent clipboardContent = null;
try {
if (oldContents.isDataFlavorSupported(TEXT.getDataFlavor())) {
clipboardContent = new ClipboardContent((String) oldContents.getTransferData(TEXT.getDataFlavor()));
} else if (oldContents.isDataFlavorSupported(IMAGE.getDataFlavor())) {
clipboardContent = new ClipboardContent(convertToBufferedImage((Image) oldContents.getTransferData(IMAGE.getDataFlavor())));
} else if (oldContents.isDataFlavorSupported(FILE.getDataFlavor())) {
clipboardContent = new ClipboardContent((List<File>) oldContents.getTransferData(FILE.getDataFlavor()));
}
} catch (UnsupportedFlavorException | IOException exp) {
exp.printStackTrace();
logger.error("Exception Thrown When Retrieving Clipboard Contents", exp);
}
return clipboardContent;
}
/**
* Store contents from the given {@link Object} into {@link ClipboardContent}
*
* @param object The given {@link Object} which holds the clipboard contents
* @return {@link ClipboardContent} containing old clipboard contents
*/
ClipboardContent getClipboardContent(Object object) {
ClipboardContent clipboardContent = null;
if (object instanceof String) {
clipboardContent = new ClipboardContent((String) object);
} else if (object instanceof MyBufferedImage) {
clipboardContent = new ClipboardContent(((MyBufferedImage) object).getBufferedImage());
} else if (object instanceof List) {
clipboardContent = new ClipboardContent((List<File>) object);
}
return myClipboardContent;
return clipboardContent;
}
static BufferedImage convertToBufferedImage(Image image) {
BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_ARGB);
/**
* Utility method for converting {@link Image} into {@link BufferedImage}
*
* @param image The given {@link Image} to convert
* @return The converted {@link BufferedImage}
*/
BufferedImage convertToBufferedImage(Image image) {
BufferedImage newImage = new BufferedImage(image.getWidth(null), image.getHeight(null), BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = newImage.createGraphics();
graphics.drawImage(image, 0, 0, null);
@ -41,4 +166,12 @@ class ClipboardUtils {
return newImage;
}
public static Logger getLogger() {
return logger;
}
public static void setLogger(Logger logger) {
ClipboardUtils.logger = logger;
}
}

@ -1,10 +1,10 @@
package me.goudham.domain;
package me.goudham;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
public enum Contents {
STRING(DataFlavor.stringFlavor) {
enum Contents {
TEXT(DataFlavor.stringFlavor) {
@Override
public boolean isAvailable(Clipboard clipboard) {
return clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor);
@ -16,7 +16,7 @@ public enum Contents {
return clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor);
}
},
FILELIST(DataFlavor.javaFileListFlavor) {
FILE(DataFlavor.javaFileListFlavor) {
@Override
public boolean isAvailable(Clipboard clipboard) {
return clipboard.isDataFlavorAvailable(DataFlavor.javaFileListFlavor);
@ -29,9 +29,9 @@ public enum Contents {
this.dataFlavor = dataFlavor;
}
public DataFlavor getDataFlavor() {
DataFlavor getDataFlavor() {
return dataFlavor;
}
public abstract boolean isAvailable(Clipboard clipboard);
abstract boolean isAvailable(Clipboard clipboard);
}

@ -0,0 +1,109 @@
package me.goudham;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import me.goudham.domain.ClipboardContent;
import me.goudham.event.FileEvent;
import me.goudham.event.ImageEvent;
import me.goudham.event.TextEvent;
/**
* Stores all eventListeners and produces notifications that are to be consumed by users using {@link MyClipboard}
*/
class EventManager {
List<TextEvent> textEventListener = new ArrayList<>();
List<ImageEvent> imageEventListener = new ArrayList<>();
List<FileEvent> fileEventListener = new ArrayList<>();
/**
* Adds a {@link TextEvent} to the {@code textEventListener}
*
* @param textEvent The {@link TextEvent} to be added
*/
void addEventListener(TextEvent textEvent) {
textEventListener.add(textEvent);
}
/**
* Adds a {@link ImageEvent} to the {@code imageEventListener}
*
* @param imageEvent The {@link ImageEvent} to be added
*/
void addEventListener(ImageEvent imageEvent) {
imageEventListener.add(imageEvent);
}
/**
* Adds a {@link FileEvent} to the {@code fileEventListener}
*
* @param fileEvent The {@link FileEvent} to be added
*/
void addEventListener(FileEvent fileEvent) {
fileEventListener.add(fileEvent);
}
/**
* Removes a {@link TextEvent} from the {@code textEventListener}
*
* @param textEvent The {@link TextEvent} to be removed
*/
void removeEventListener(TextEvent textEvent) {
textEventListener.remove(textEvent);
}
/**
* Removes a {@link ImageEvent} from the {@code imageEventListener}
*
* @param imageEvent The {@link ImageEvent} to be removed
*/
void removeEventListener(ImageEvent imageEvent) {
imageEventListener.remove(imageEvent);
}
/**
* Removes a {@link FileEvent} from the {@code fileEventListener}
*
* @param fileEvent The {@link FileEvent} to be removed
*/
void removeEventListener(FileEvent fileEvent) {
fileEventListener.remove(fileEvent);
}
/**
* Produces {@link String} change notifications to all consumers listening
*
* @param clipboardContent The previous clipboard contents
* @param stringContent {@link String} to be consumed
*/
void notifyTextEvent(ClipboardContent clipboardContent, String stringContent) {
for (TextEvent textEvent : textEventListener) {
textEvent.onCopyText(clipboardContent, stringContent);
}
}
/**
* Produces {@link BufferedImage} change notifications to all consumers listening
*
* @param clipboardContent The previous clipboard contents
* @param imageContent {@link BufferedImage} to be consumed
*/
void notifyImageEvent(ClipboardContent clipboardContent, BufferedImage imageContent) {
for (ImageEvent imageEvent : imageEventListener) {
imageEvent.onCopyImage(clipboardContent, imageContent);
}
}
/**
* Produces {@link List} of {@link File} change notifications to all consumers listening
*
* @param clipboardContent The previous clipboard contents
* @param fileContent {@link List} of {@link File} to be consumed
*/
void notifyFilesEvent(ClipboardContent clipboardContent, List<File> fileContent) {
for (FileEvent fileEvent : fileEventListener) {
fileEvent.onCopyFiles(clipboardContent, fileContent);
}
}
}

@ -0,0 +1,196 @@
package me.goudham;
import java.awt.Image;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.io.File;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import me.goudham.domain.ClipboardContent;
import me.goudham.domain.GenericClipboardContent;
import me.goudham.domain.MyBufferedImage;
import me.goudham.domain.TransferableFile;
import me.goudham.domain.TransferableImage;
import static java.lang.Thread.sleep;
import static me.goudham.Contents.FILE;
import static me.goudham.Contents.IMAGE;
import static me.goudham.Contents.TEXT;
/**
* Clipboard for the macOS operating system
*/
class MacClipboard extends SystemClipboard implements Runnable {
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
GenericClipboardContent<?>[] genericClipboardContents;
private boolean listening = false;
MacClipboard() {
super();
}
/**
* Checks if {@link String} is within the clipboard and changed
*
* @param newClipboardContents {@link Transferable} containing new clipboard contents
* @param genericClipboardContents {@link GenericClipboardContent[]} of Unknown {@link Class} containing previous contents
*/
void checkText(Transferable newClipboardContents, GenericClipboardContent<?>[] genericClipboardContents) {
if (TEXT.isAvailable(clipboard) && !FILE.isAvailable(clipboard)) {
String newStringContent = clipboardUtils.getStringContent(newClipboardContents);
if (newStringContent == null) return;
if (isTextMonitored()) {
Object oldContent = genericClipboardContents[0].getOldContent();
if (!newStringContent.equals(oldContent)) {
ClipboardContent clipboardContent = clipboardUtils.getClipboardContent(oldContent);
eventManager.notifyTextEvent(clipboardContent, newStringContent);
}
}
genericClipboardContents[0].setOldContent(newStringContent);
}
}
/**
* Checks if {@link java.awt.Image} is within the clipboard and changed
*
* @param newClipboardContents {@link Transferable} containing new clipboard contents
* @param genericClipboardContents {@link GenericClipboardContent[]} of Unknown {@link Class} containing previous contents
*/
void checkImages(Transferable newClipboardContents, GenericClipboardContent<?>[] genericClipboardContents) {
if (IMAGE.isAvailable(clipboard)) {
MyBufferedImage bufferedImageContent = clipboardUtils.getImageContent(newClipboardContents);
if (bufferedImageContent.getBufferedImage() == null) return;
if (isImageMonitored()) {
if (!bufferedImageContent.equals(genericClipboardContents[0].getOldContent())) {
ClipboardContent clipboardContent = clipboardUtils.getClipboardContent(genericClipboardContents[0].getOldContent());
eventManager.notifyImageEvent(clipboardContent, bufferedImageContent.getBufferedImage());
}
}
genericClipboardContents[0].setOldContent(bufferedImageContent);
}
}
/**
* Checks if {@link java.util.List} of {@link java.io.File} is within the clipboard and changed
*
* @param newClipboardContents {@link Transferable} containing new clipboard contents
* @param genericClipboardContents {@link GenericClipboardContent[]} of Unknown {@link Class} containing previous contents
*/
void checkFiles(Transferable newClipboardContents, GenericClipboardContent<?>[] genericClipboardContents) {
if (FILE.isAvailable(clipboard)) {
List<File> fileListContent = clipboardUtils.getFileContent(newClipboardContents);
if (fileListContent == null) return;
if (isFileMonitored()) {
if (!fileListContent.equals(genericClipboardContents[0].getOldContent())) {
ClipboardContent clipboardContent = clipboardUtils.getClipboardContent(genericClipboardContents[0].getOldContent());
eventManager.notifyFilesEvent(clipboardContent, fileListContent);
}
}
genericClipboardContents[0].setOldContent(fileListContent);
}
}
@Override
void startListening() {
if (!listening) {
listening = true;
execute();
}
}
@Override
void stopListening() {
if (listening) {
scheduledExecutorService.shutdown();
scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
try {
sleep(200);
} catch (InterruptedException ie) {
logger.error("Exception Thrown As Thread Cannot Sleep", ie);
}
listening = false;
}
}
@Override
void insert(String stringContent) {
insertAndNotify(stringContent);
}
@Override
void insert(Image imageContent) {
insertAndNotify(imageContent);
}
@Override
void insert(List<File> fileContent) {
insertAndNotify(fileContent);
}
@Override
void insertAndNotify(String stringContent) {
try {
sleep(200);
} catch (InterruptedException ie) {
logger.error("Exception Thrown As Thread Cannot Sleep", ie);
}
clipboard.setContents(new StringSelection(stringContent), null);
}
@Override
void insertAndNotify(Image imageContent) {
try {
sleep(200);
} catch (InterruptedException ie) {
logger.error("Exception Thrown As Thread Cannot Sleep", ie);
}
clipboard.setContents(new TransferableImage(imageContent), null);
}
@Override
void insertAndNotify(List<File> fileContent) {
try {
sleep(200);
} catch (InterruptedException ie) {
logger.error("Exception Thrown As Thread Cannot Sleep", ie);
}
clipboard.setContents(new TransferableFile(fileContent), null);
}
@Override
public void run() {
try {
Transferable newClipboardContents = clipboard.getContents(null);
checkText(newClipboardContents, genericClipboardContents);
checkImages(newClipboardContents, genericClipboardContents);
checkFiles(newClipboardContents, genericClipboardContents);
} catch (IllegalStateException ise) {
logger.error("Exception Thrown As Clipboard Cannot Be Accessed", ise);
}
}
/**
* Main entry point for {@link MacClipboard}
* <p>Retrieves thread from {@link Executors#newSingleThreadScheduledExecutor()} and executes code on a fixed delay</p>
*/
@Override
void execute() {
Transferable oldClipboardContents = clipboard.getContents(null);
genericClipboardContents = new GenericClipboardContent[] { clipboardUtils.getGenericClipboardContents(oldClipboardContents) };
scheduledExecutorService.scheduleAtFixedRate(this, 0, 200, TimeUnit.MILLISECONDS);
}
}

@ -1,49 +0,0 @@
package me.goudham;
import java.awt.Dimension;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
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);
if (isTextMonitored()) {
if (STRING.isAvailable(clipboard)) {
String newStringContent = getStringContent(newClipboardContents);
if (!newStringContent.equals(myClipboardContents[0].getOldContent())) {
notifyStringEvent(newStringContent);
myClipboardContents[0].setOldContent(newStringContent);
}
}
}
if (isImagesMonitored()) {
if (IMAGE.isAvailable(clipboard)) {
BufferedImage bufferedImageContent = getImageContent(newClipboardContents);
Dimension newDimensionContent = new Dimension(bufferedImageContent.getWidth(), bufferedImageContent.getHeight());
if (!newDimensionContent.equals(myClipboardContents[0].getOldContent())) {
notifyImageEvent(bufferedImageContent);
myClipboardContents[0].setOldContent(newDimensionContent);
}
}
}
}, 0, 350, TimeUnit.MILLISECONDS);
}
}

@ -1,100 +1,286 @@
package me.goudham;
import java.awt.Image;
import java.io.File;
import java.util.List;
import me.goudham.domain.ClipboardContent;
import me.goudham.event.FileEvent;
import me.goudham.event.ImageEvent;
import me.goudham.event.TextEvent;
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
* <p>
* The abstract class {@link SystemClipboard} is responsible for handling all operations
*/
public class MyClipboard {
private final @NotNull ClipboardListener clipboardListener;
private @NotNull SystemClipboard systemClipboard;
private static SystemUtils systemUtils = new SystemUtils();
/**
* Creates an instance of {@link MyClipboard}
*
* @param clipboardListener The underlying {@link ClipboardListener}
* @param systemClipboard The underlying {@link SystemClipboard}
*/
private MyClipboard(@NotNull ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
this.clipboardListener.execute();
private MyClipboard(@NotNull SystemClipboard systemClipboard) {
this.systemClipboard = systemClipboard;
}
/**
* 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>
* Creates an instance of {@link MyClipboard} with an instance of {@link SystemClipboard} dependent on the OS
* <p>A {@link WindowsOrUnixClipboard} or {@link MacClipboard} 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;
SystemClipboard systemClipboard;
if (isMac()) {
clipboardListener = new MacClipboardListener();
} else if (isWindows() || isUnix()) {
clipboardListener = new WindowsOrUnixClipboardListener();
if (systemUtils.isMac()) {
systemClipboard = new MacClipboard();
} else if (systemUtils.isWindows() || systemUtils.isUnix()) {
systemClipboard = new WindowsOrUnixClipboard();
} else {
throw new UnsupportedSystemException("Your Operating System: " + System.getProperty("os.name") + "is not supported");
throw new UnsupportedSystemException("Your Operating System: '" + System.getProperty("os.name") + "' is not supported");
}
return new MyClipboard(systemClipboard);
}
/**
* Allows the correct {@link SystemClipboard} to start listening for clipboard changes
*
* @see WindowsOrUnixClipboard#startListening()
* @see MacClipboard#startListening()
*/
public void startListening() {
systemClipboard.startListening();
}
/**
* Stops the correct {@link SystemClipboard} to stop listening for clipboard changes
*
* @see WindowsOrUnixClipboard#stopListening()
* @see MacClipboard#stopListening()
*/
public void stopListening() {
systemClipboard.stopListening();
}
/**
* Insert the given {@link String} into the system clipboard
* <p>
* Due to the underlying {@link MacClipboard#insert(String)} implementation, inserting
* clipboard contents will always result in event notifications being sent
*
* @param stringContent The given {@link String} to insert
* @see WindowsOrUnixClipboard#insert(String)
* @see MacClipboard#insert(String)
*/
public void insert(String stringContent) {
systemClipboard.insert(stringContent);
}
/**
* Insert the given {@link Image} into the system clipboard
* <p>
* Due to the underlying {@link MacClipboard#insert(Image)} implementation, inserting
* clipboard contents will always result in event notifications being sent
*
* @param imageContent The given {@link Image} to insert
* @see WindowsOrUnixClipboard#insert(Image)
* @see MacClipboard#insert(Image)
*/
public void insert(Image imageContent) {
systemClipboard.insert(imageContent);
}
return new MyClipboard(clipboardListener);
/**
* Insert the given {@link List} of {@link File} into the system clipboard
* <p>
* Due to the underlying {@link MacClipboard#insert(List)} implementation, inserting
* clipboard contents will always result in event notifications being sent
*
* @param fileContent The given {@link List} of {@link File} to insert
* @see WindowsOrUnixClipboard#insert(List)
* @see MacClipboard#insert(List)
*/
public void insert(List<File> fileContent) {
systemClipboard.insert(fileContent);
}
/**
* Adds a {@link ClipboardEventListener} to the underlying {@link ClipboardListener}
* Insert the given {@link String} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param clipboardEventListener The {@link ClipboardEventListener} to be added
* @param stringContent The given {@link String} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(String)
* @see MacClipboard#insertAndNotify(String)
*/
public void addEventListener(ClipboardEventListener clipboardEventListener) {
clipboardListener.addEventListener(clipboardEventListener);
public void insertAndNotify(String stringContent) {
systemClipboard.insertAndNotify(stringContent);
}
/**
* Removes a {@link ClipboardEventListener} from the underlying {@link ClipboardListener}
* Insert the given {@link Image} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param clipboardEventListener The {@link ClipboardEventListener} to be removed
* @param imageContent The given {@link Image} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(Image)
* @see MacClipboard#insertAndNotify(Image)
*/
public void removeEventListener(ClipboardEventListener clipboardEventListener) {
clipboardListener.removeEventListener(clipboardEventListener);
public void insertAndNotify(Image imageContent) {
systemClipboard.insertAndNotify(imageContent);
}
/**
* Insert the given {@link List} of {@link File} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param fileContent The given {@link List} of {@link File} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(List)
* @see MacClipboard#insertAndNotify(List)
*/
public void insertAndNotify(List<File> fileContent) {
systemClipboard.insertAndNotify(fileContent);
}
/**
* Returns the current clipboard contents, {@code null} if clipboard has no contents
*
* @return {@link ClipboardContent} containing either {@code String}, {@code BufferedImage} or {@code List<File>}
* @see SystemClipboard#getContents()
*/
public ClipboardContent getContents() {
return systemClipboard.getContents();
}
/**
* Adds a {@link TextEvent} to the underlying {@link SystemClipboard}
*
* @param textEvent The {@link TextEvent} to be added
* @see EventManager#addEventListener(TextEvent)
*/
public void addEventListener(TextEvent textEvent) {
systemClipboard.getEventManager().addEventListener(textEvent);
}
/**
* Adds a {@link ImageEvent} to the underlying {@link SystemClipboard}
*
* @param imageEvent The {@link ImageEvent} to be added
* @see EventManager#addEventListener(ImageEvent)
*/
public void addEventListener(ImageEvent imageEvent) {
systemClipboard.getEventManager().addEventListener(imageEvent);
}
/**
* Adds a {@link FileEvent} to the underlying {@link SystemClipboard}
*
* @param fileEvent The {@link FileEvent} to be added
* @see EventManager#addEventListener(FileEvent)
*/
public void addEventListener(FileEvent fileEvent) {
systemClipboard.getEventManager().addEventListener(fileEvent);
}
/**
* Removes a {@link TextEvent} from the underlying {@link SystemClipboard}
*
* @param textEvent The {@link TextEvent} to be removed
* @see EventManager#removeEventListener(TextEvent)
*/
public void removeEventListener(TextEvent textEvent) {
systemClipboard.getEventManager().removeEventListener(textEvent);
}
/**
* Removes a {@link ImageEvent} from the underlying {@link SystemClipboard}
*
* @param imageEvent The {@link ImageEvent} to be removed
* @see EventManager#removeEventListener(ImageEvent)
*/
public void removeEventListener(ImageEvent imageEvent) {
systemClipboard.getEventManager().removeEventListener(imageEvent);
}
/**
* Removes a {@link FileEvent} from the underlying {@link SystemClipboard}
*
* @param fileEvent The {@link FileEvent} to be removed
* @see EventManager#removeEventListener(FileEvent)
*/
public void removeEventListener(FileEvent fileEvent) {
systemClipboard.getEventManager().removeEventListener(fileEvent);
}
/**
* Toggles the current value of text monitoring, the default value is set to {@code True}
*
* @see SystemClipboard#toggleTextMonitored()
*/
public void toggleTextMonitored() {
clipboardListener.toggleTextMonitored();
systemClipboard.toggleTextMonitored();
}
/**
* Toggles the current value of image monitoring, the default value is set to {@code True}
*
* @see SystemClipboard#toggleImageMonitored()
*/
public void toggleImagesMonitored() {
clipboardListener.toggleImagesMonitored();
systemClipboard.toggleImageMonitored();
}
/**
* Toggles the current value of file monitoring, the default value is set to {@code True}
*
* @see SystemClipboard#toggleFileMonitored()
*/
public void toggleFilesMonitored() {
systemClipboard.toggleFileMonitored();
}
public boolean isImagesMonitored() {
return clipboardListener.isImagesMonitored();
public boolean isImageMonitored() {
return systemClipboard.isImageMonitored();
}
public void setImagesMonitored(boolean imagesMonitored) {
clipboardListener.setImagesMonitored(imagesMonitored);
public void setImageMonitored(boolean imagesMonitored) {
systemClipboard.setImageMonitored(imagesMonitored);
}
public boolean isTextMonitored() {
return clipboardListener.isTextMonitored();
return systemClipboard.isTextMonitored();
}
public void setTextMonitored(boolean textMonitored) {
clipboardListener.setTextMonitored(textMonitored);
systemClipboard.setTextMonitored(textMonitored);
}
public boolean isFileMonitored() {
return systemClipboard.isFileMonitored();
}
public void setFileMonitored(boolean fileMonitored) {
systemClipboard.setFileMonitored(fileMonitored);
}
public @NotNull SystemClipboard getClipboardListener() {
return systemClipboard;
}
private static boolean isMac() {
return SystemUtils.IS_OS_MAC;
public void setClipboardListener(@NotNull SystemClipboard systemClipboard) {
this.systemClipboard = systemClipboard;
}
private static boolean isUnix() {
return SystemUtils.IS_OS_UNIX || SystemUtils.IS_OS_LINUX;
static SystemUtils getSystemUtils() {
return systemUtils;
}
private static boolean isWindows() {
return SystemUtils.IS_OS_WINDOWS;
static void setSystemUtils(SystemUtils systemUtils) {
MyClipboard.systemUtils = systemUtils;
}
}

@ -0,0 +1,200 @@
package me.goudham;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.io.File;
import java.util.List;
import me.goudham.domain.ClipboardContent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Abstract class containing common operations between {@link WindowsOrUnixClipboard} and
* {@link MacClipboard}
*/
abstract class SystemClipboard {
Clipboard clipboard;
Logger logger;
EventManager eventManager;
ClipboardUtils clipboardUtils;
private boolean imageMonitored = true;
private boolean textMonitored = true;
private boolean fileMonitored = true;
SystemClipboard() {
clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
logger = LoggerFactory.getLogger(getClass());
eventManager = new EventManager();
clipboardUtils = new ClipboardUtils();
}
/**
* Main entry point of execution for the correct {@link SystemClipboard}
*
* @see WindowsOrUnixClipboard#execute()
* @see MacClipboard#execute()
*/
abstract void execute();
/**
* Allows the correct {@link SystemClipboard} to start listening for clipboard changes
*
* @see WindowsOrUnixClipboard#startListening()
* @see MacClipboard#startListening()
*/
abstract void startListening();
/**
* Stops the correct {@link SystemClipboard} to stop listening for clipboard changes
*
* @see WindowsOrUnixClipboard#stopListening()
* @see MacClipboard#stopListening()
*/
abstract void stopListening();
/**
* Insert the given {@link String} into the system clipboard
*
* @param stringContent The given {@link String} to insert
* @see WindowsOrUnixClipboard#insert(String)
* @see MacClipboard#insert(String)
*/
abstract void insert(String stringContent);
/**
* Insert the given {@link Image} into the system clipboard
*
* @param imageContent The given {@link Image} to insert
* @see WindowsOrUnixClipboard#insert(Image)
* @see MacClipboard#insert(Image)
*/
abstract void insert(Image imageContent);
/**
* Insert the given {@link List} of {@link File} into the system clipboard
*
* @param fileContent The given {@link List} of {@link File} to insert
* @see WindowsOrUnixClipboard#insert(List)
* @see MacClipboard#insert(List)
*/
abstract void insert(List<File> fileContent);
/**
* Insert the given {@link String} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param stringContent The given {@link String} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(String)
* @see MacClipboard#insertAndNotify(String)
*/
abstract void insertAndNotify(String stringContent);
/**
* Insert the given {@link Image} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param imageContent The given {@link Image} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(Image)
* @see MacClipboard#insertAndNotify(Image)
*/
abstract void insertAndNotify(Image imageContent);
/**
* Insert the given {@link List} of {@link File} into the system clipboard
* and notify the user about the new contents within the clipboard
*
* @param fileContent The given {@link List} of {@link File} to insert
* @see WindowsOrUnixClipboard#insertAndNotify(List)
* @see MacClipboard#insertAndNotify(List)
*/
abstract void insertAndNotify(List<File> fileContent);
/**
* Returns the current clipboard contents, {@code null} if clipboard has no contents
*
* @return {@link ClipboardContent} containing either {@code String}, {@code BufferedImage} or {@code List<File>}
*/
ClipboardContent getContents() {
Transferable clipboardContents = clipboard.getContents(null);
return clipboardContents == null ? null : clipboardUtils.getClipboardContent(clipboardContents);
}
/**
* Toggles the current value of text monitoring, the default value is set to {@code True}
*/
void toggleTextMonitored() {
this.textMonitored = !textMonitored;
}
/**
* Toggles the current value of image monitoring, the default value is set to {@code True}
*/
void toggleImageMonitored() {
this.imageMonitored = !imageMonitored;
}
/**
* Toggles the current value of file monitoring, the default value is set to {@code True}
*/
void toggleFileMonitored() {
this.fileMonitored = !fileMonitored;
}
Clipboard getClipboard() {
return clipboard;
}
void setClipboard(Clipboard clipboard) {
this.clipboard = clipboard;
}
Logger getLogger() {
return logger;
}
void setLogger(Logger logger) {
this.logger = logger;
}
EventManager getEventManager() {
return eventManager;
}
void setEventManager(EventManager eventManager) {
this.eventManager = eventManager;
}
ClipboardUtils getClipboardUtils() {
return clipboardUtils;
}
void setClipboardUtils(ClipboardUtils clipboardUtils) {
this.clipboardUtils = clipboardUtils;
}
boolean isImageMonitored() {
return imageMonitored;
}
void setImageMonitored(boolean imageMonitored) {
this.imageMonitored = imageMonitored;
}
boolean isTextMonitored() {
return textMonitored;
}
void setTextMonitored(boolean textMonitored) {
this.textMonitored = textMonitored;
}
boolean isFileMonitored() {
return fileMonitored;
}
void setFileMonitored(boolean fileMonitored) {
this.fileMonitored = fileMonitored;
}
}

@ -0,0 +1,15 @@
package me.goudham;
class SystemUtils {
boolean isMac() {
return org.apache.commons.lang3.SystemUtils.IS_OS_MAC;
}
boolean isUnix() {
return org.apache.commons.lang3.SystemUtils.IS_OS_UNIX || org.apache.commons.lang3.SystemUtils.IS_OS_LINUX;
}
boolean isWindows() {
return org.apache.commons.lang3.SystemUtils.IS_OS_WINDOWS;
}
}

@ -0,0 +1,177 @@
package me.goudham;
import java.awt.Image;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import me.goudham.domain.ClipboardContent;
import me.goudham.domain.MyBufferedImage;
import me.goudham.domain.TransferableFile;
import me.goudham.domain.TransferableImage;
import static java.lang.Thread.sleep;
import static me.goudham.Contents.FILE;
import static me.goudham.Contents.IMAGE;
import static me.goudham.Contents.TEXT;
/**
* Clipboard for Windows and Unix operating systems
*/
class WindowsOrUnixClipboard extends SystemClipboard implements Runnable, ClipboardOwner {
private ExecutorService executorService = Executors.newSingleThreadExecutor();
private boolean listening = false;
WindowsOrUnixClipboard() {
super();
}
@Override
public void lostOwnership(Clipboard oldClipboard, Transferable oldClipboardContents) {
try {
sleep(200);
Transferable newClipboardContents = oldClipboard.getContents(null);
processContents(oldClipboard, oldClipboardContents, newClipboardContents);
regainOwnership(oldClipboard, newClipboardContents);
} catch (IllegalStateException | InterruptedException exp) {
logger.error("Exception Thrown When Processing Clipboard Changes", exp);
executorService.submit(this);
}
}
/**
* Detect changes from the given {@link Clipboard} and send event notifications to all users listening
*
* @param oldClipboard The clipboard that is no longer owned
* @param oldClipboardContents The old contents of the clipboard
* @param newClipboardContents The new contents of the clipboard
*/
void processContents(Clipboard oldClipboard, Transferable oldClipboardContents, Transferable newClipboardContents) {
ClipboardContent clipboardContent = clipboardUtils.getClipboardContent(oldClipboardContents);
if (isTextMonitored()) {
if (TEXT.isAvailable(oldClipboard) && !FILE.isAvailable(oldClipboard)) {
String stringContent = clipboardUtils.getStringContent(newClipboardContents);
if (!stringContent.equals(clipboardContent.getText())) {
eventManager.notifyTextEvent(clipboardContent, stringContent);
}
}
}
if (isImageMonitored()) {
if (IMAGE.isAvailable(oldClipboard)) {
MyBufferedImage bufferedImage = clipboardUtils.getImageContent(newClipboardContents);
MyBufferedImage oldBufferedImage = new MyBufferedImage(clipboardContent.getImage());
if (!bufferedImage.equals(oldBufferedImage)) {
eventManager.notifyImageEvent(clipboardContent, bufferedImage.getBufferedImage());
}
}
}
if (isFileMonitored()) {
if (FILE.isAvailable(oldClipboard) && !IMAGE.isAvailable(oldClipboard)) {
List<File> fileList = clipboardUtils.getFileContent(newClipboardContents);
if (!fileList.equals(clipboardContent.getFiles())) {
eventManager.notifyFilesEvent(clipboardContent, fileList);
}
}
}
}
void regainOwnership(Clipboard clipboard, Transferable newClipboardContents) {
clipboard.setContents(newClipboardContents, this);
}
@Override
void startListening() {
if (!listening) {
listening = true;
execute();
}
}
@Override
void stopListening() {
if (listening) {
executorService.shutdown();
executorService = Executors.newSingleThreadExecutor();
try {
sleep(200);
} catch (InterruptedException ie) {
logger.error("Exception Thrown As Thread Cannot Sleep", ie);
}
listening = false;
}
}
@Override
void insert(String stringContent) {
setContents(new StringSelection(stringContent));
}
@Override
void insert(Image imageContent) {
setContents(new TransferableImage(imageContent));
}
@Override
void insert(List<File> fileContent) {
setContents(new TransferableFile(fileContent));
}
@Override
void insertAndNotify(String stringContent) {
Transferable currentClipboardContents = clipboard.getContents(null);
insert(stringContent);
lostOwnership(clipboard, currentClipboardContents);
}
@Override
void insertAndNotify(Image imageContent) {
Transferable currentClipboardContents = clipboard.getContents(null);
insert(imageContent);
lostOwnership(clipboard, currentClipboardContents);
}
@Override
void insertAndNotify(List<File> fileContent) {
Transferable currentClipboardContents = clipboard.getContents(null);
insert(fileContent);
lostOwnership(clipboard, currentClipboardContents);
}
void setContents(Transferable contents) {
try {
clipboard.setContents(contents, this);
} catch (IllegalStateException ise) {
logger.error("Exception Thrown As Clipboard Cannot Be Accessed", ise);
executorService.submit(this);
}
}
@Override
public void run() {
try {
Transferable currentClipboardContents = clipboard.getContents(null);
regainOwnership(clipboard, currentClipboardContents);
} catch (IllegalStateException ise) {
logger.error("Exception Thrown When Retrieving Clipboard Contents", ise);
executorService.submit(this);
}
}
/**
* Entry point for {@link WindowsOrUnixClipboard}
* <p>Retrieves a thread from {@link Executors#newSingleThreadExecutor()} and executes code in the background</p>
*/
@Override
public void execute() {
executorService.submit(this);
}
}

@ -1,72 +0,0 @@
package me.goudham;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.image.BufferedImage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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) {
if (isTextMonitored()) {
if (STRING.isAvailable(oldClipboard)) {
String stringContent = getStringContent(newClipboardContents);
notifyStringEvent(stringContent);
}
}
if (isImagesMonitored()) {
if (IMAGE.isAvailable(oldClipboard)) {
BufferedImage bufferedImage = getImageContent(newClipboardContents);
notifyImageEvent(bufferedImage);
}
}
}
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);
}
}

@ -0,0 +1,48 @@
package me.goudham.domain;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.List;
/**
* Contains potential clipboard contents returned from the clipboard. Supported types are currently {@link String},
* {@link BufferedImage} and {@link List} of {@link File}
*/
public class ClipboardContent {
private String text;
private BufferedImage image;
private List<File> files;
public ClipboardContent(String text) {
this.text = text;
}
public ClipboardContent(BufferedImage image) {
this.image = image;
}
public ClipboardContent(List<File> files) {
this.files = files;
}
public BufferedImage getImage() {
return image;
}
public String getText() {
return text;
}
public List<File> getFiles() {
return files;
}
@Override
public String toString() {
return "ClipboardContent{" +
"text='" + text + '\'' +
", image=" + image +
", files=" + files +
'}';
}
}

@ -0,0 +1,36 @@
package me.goudham.domain;
import java.util.Objects;
/**
* Contains clipboard contents as generics
* @param <T> Type of data given by the {@link java.awt.datatransfer.Clipboard}
*/
public class GenericClipboardContent<T> {
private T oldContent;
public GenericClipboardContent(Object oldContent) {
this.oldContent = (T) oldContent;
}
public void setOldContent(Object oldContent) {
this.oldContent = (T) oldContent;
}
public T getOldContent() {
return oldContent;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GenericClipboardContent<?> that = (GenericClipboardContent<?>) o;
return Objects.equals(oldContent, that.oldContent);
}
@Override
public int hashCode() {
return Objects.hash(oldContent);
}
}

@ -0,0 +1,53 @@
package me.goudham.domain;
import java.awt.image.BufferedImage;
import java.util.Objects;
/**
* Wrapper class surrounding {@link MyBufferedImage} to ensure image equality is properly evaluated
*/
public class MyBufferedImage {
private BufferedImage bufferedImage;
public MyBufferedImage(BufferedImage bufferedImage) {
this.bufferedImage = bufferedImage;
}
public BufferedImage getBufferedImage() {
return bufferedImage;
}
public void setBufferedImage(BufferedImage bufferedImage) {
this.bufferedImage = bufferedImage;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyBufferedImage that = (MyBufferedImage) o;
if (that.getBufferedImage() == null) return false;
return equals(that.bufferedImage);
}
@Override
public int hashCode() {
return Objects.hash(bufferedImage);
}
private boolean equals(BufferedImage secondBufferedImage) {
if (bufferedImage.getHeight() != secondBufferedImage.getHeight() && bufferedImage.getHeight() != secondBufferedImage.getHeight()) {
return false;
}
for (int xPixel = 0; xPixel < bufferedImage.getWidth(); xPixel++) {
for (int yPixel = 0; yPixel < bufferedImage.getHeight(); yPixel++) {
if (bufferedImage.getRGB(xPixel, yPixel) != secondBufferedImage.getRGB(xPixel, yPixel)) {
return false;
}
}
}
return true;
}
}

@ -1,16 +0,0 @@
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;
}
}

@ -0,0 +1,42 @@
package me.goudham.domain;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.util.List;
/**
* A {@link Transferable} which implements the capability required to transfer a
* {@link List} of {@link File}
* <p>
* This {@link Transferable} properly supports {@link DataFlavor#javaFileListFlavor}
* @see DataFlavor#javaFileListFlavor
*/
public class TransferableFile implements Transferable {
private final List<File> files;
public TransferableFile(List<File> files) {
this.files = files;
}
@Override
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
if (flavor.equals(DataFlavor.javaFileListFlavor)) {
return files;
} else {
throw new UnsupportedFlavorException(flavor);
}
}
@Override
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { DataFlavor.javaFileListFlavor };
}
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor == DataFlavor.javaFileListFlavor;
}
}

@ -6,6 +6,13 @@ import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import org.jetbrains.annotations.NotNull;
/**
* A {@link Transferable} which implements the capability required to transfer a
* {@link java.awt.image.BufferedImage}
* <p>
* This {@link Transferable} properly supports {@link DataFlavor#imageFlavor}
* @see DataFlavor#imageFlavor
*/
public class TransferableImage implements Transferable {
private final Image image;
@ -30,13 +37,6 @@ public class TransferableImage implements Transferable {
@Override
public boolean isDataFlavorSupported(DataFlavor flavor) {
DataFlavor[] flavors = getTransferDataFlavors();
for (DataFlavor dataFlavor : flavors) {
if (flavor.equals(dataFlavor)) {
return true;
}
}
return false;
return flavor == DataFlavor.imageFlavor;
}
}

@ -0,0 +1,7 @@
package me.goudham.event;
/**
* Common interface for all events to extend (Not used yet)
*/
interface ClipboardEvent {
}

@ -0,0 +1,12 @@
package me.goudham.event;
import java.io.File;
import java.util.List;
import me.goudham.domain.ClipboardContent;
/**
* Interface for notifying clipboard changes that happen to be {@link File}
*/
public interface FileEvent extends ClipboardEvent {
void onCopyFiles(ClipboardContent oldContent, List<File> newContent);
}

@ -0,0 +1,12 @@
package me.goudham.event;
import java.awt.image.BufferedImage;
import me.goudham.domain.ClipboardContent;
/**
* Interface for notifying clipboard changes that happen to be {@link BufferedImage}
*/
public interface ImageEvent extends ClipboardEvent {
void onCopyImage(ClipboardContent oldContent, BufferedImage newContent);
}

@ -0,0 +1,10 @@
package me.goudham.event;
import me.goudham.domain.ClipboardContent;
/**
* Interface for notifying clipboard changes that happen to be {@link String}
*/
public interface TextEvent extends ClipboardEvent {
void onCopyText(ClipboardContent oldContent, String newContent);
}

@ -0,0 +1,10 @@
package me.goudham.exception;
/**
* Thrown when {@link java.awt.datatransfer.Clipboard} is unavailable to be accessed
*/
public class CannotAccessClipboardException extends Throwable {
public CannotAccessClipboardException(String exceptionMessage) {
super(exceptionMessage);
}
}

@ -1,8 +0,0 @@
package me.goudham.listener;
import java.awt.image.BufferedImage;
public interface ClipboardEventListener {
void onCopyString(String stringContent);
void onCopyImage(BufferedImage imageContent);
}

@ -0,0 +1,49 @@
package me.goudham;
import java.awt.image.BufferedImage;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import static org.junit.jupiter.api.Assertions.assertEquals;
class BufferedImageMatcher implements Matcher<BufferedImage> {
private final BufferedImage expected;
public BufferedImageMatcher(BufferedImage expected) {
this.expected = expected;
}
@Override
public boolean matches(Object argument) {
if (argument == null) return true;
BufferedImage actual = (BufferedImage) argument;
assertEquals(expected.getWidth(), actual.getWidth());
assertEquals(expected.getHeight(), actual.getHeight());
for (int x = 0; x < actual.getWidth(); x++) {
for (int y = 0; y < actual.getHeight(); y++) {
assertEquals(expected.getRGB(x, y), actual.getRGB(x, y));
}
}
return true;
}
@Override
public void describeMismatch(Object o, Description description) {
}
@Override
public void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
}
@Override
public void describeTo(Description description) {
}
}

@ -0,0 +1,207 @@
package me.goudham;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.stream.Stream;
import me.goudham.domain.ClipboardContent;
import me.goudham.domain.GenericClipboardContent;
import me.goudham.domain.MyBufferedImage;
import me.goudham.domain.TransferableFile;
import me.goudham.domain.TransferableImage;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import static me.goudham.Contents.FILE;
import static me.goudham.Contents.IMAGE;
import static me.goudham.Contents.TEXT;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class ClipboardUtilsTest {
@Mock
private Transferable transferableMock;
@Mock
private Logger logger;
private ClipboardUtils sut;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
sut = new ClipboardUtils();
ClipboardUtils.setLogger(logger);
}
@Test
void successfullyGetStringContent() {
String expectedStringContent = "testString";
Transferable expectedTransferable = new StringSelection(expectedStringContent);
String actualStringContent = sut.getStringContent(expectedTransferable);
assertThat(actualStringContent, is(expectedStringContent));
verifyNoInteractions(logger);
}
@Test
void failToGetStringContent() throws IOException, UnsupportedFlavorException {
String expectedStringContent = null;
String expectedExceptionMessage = "Exception Thrown When Retrieving String Content";
Throwable expectedException = new UnsupportedFlavorException(TEXT.getDataFlavor());
when(transferableMock.isDataFlavorSupported(TEXT.getDataFlavor())).thenReturn(true);
when(transferableMock.getTransferData(TEXT.getDataFlavor())).thenThrow(expectedException);
String actualStringContent = sut.getStringContent(transferableMock);
verify(logger, times(1)).error(expectedExceptionMessage, expectedException);
assertThat(actualStringContent, is(expectedStringContent));
}
@Test
void successfullyGetImageContent() {
BufferedImage expectedBufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
MyBufferedImage expectedImageContent = new MyBufferedImage(expectedBufferedImage);
Transferable expectedTransferable = new TransferableImage(expectedBufferedImage);
MyBufferedImage actualImageContent = sut.getImageContent(expectedTransferable);
assertThat(actualImageContent, is(expectedImageContent));
verifyNoInteractions(logger);
}
@Test
void failToGetImageContent() throws IOException, UnsupportedFlavorException {
MyBufferedImage expectedImageContent = null;
String expectedExceptionMessage = "Exception Thrown When Retrieving Image Content";
Throwable expectedException = new UnsupportedFlavorException(IMAGE.getDataFlavor());
when(transferableMock.isDataFlavorSupported(IMAGE.getDataFlavor())).thenReturn(true);
when(transferableMock.getTransferData(IMAGE.getDataFlavor())).thenThrow(expectedException);
MyBufferedImage actualImageContent = sut.getImageContent(transferableMock);
verify(logger, times(1)).error(expectedExceptionMessage, expectedException);
assertThat(actualImageContent, is(expectedImageContent));
}
@Test
void successfullyGetFileContent() {
List<File> expectedFileContent = List.of(new File("testFile"));
Transferable expectedTransferable = new TransferableFile(expectedFileContent);
List<File> actualFileContent = sut.getFileContent(expectedTransferable);
assertThat(actualFileContent, is(expectedFileContent));
verifyNoInteractions(logger);
}
@Test
void failToGetFileContent() throws IOException, UnsupportedFlavorException {
List<File> expectedFileContent = null;
String expectedExceptionMessage = "Exception Thrown When Retrieving File Content";
Throwable expectedException = new UnsupportedFlavorException(FILE.getDataFlavor());
when(transferableMock.isDataFlavorSupported(FILE.getDataFlavor())).thenReturn(true);
when(transferableMock.getTransferData(FILE.getDataFlavor())).thenThrow(expectedException);
List<File> actualFileContent = sut.getFileContent(transferableMock);
verify(logger, times(1)).error(expectedExceptionMessage, expectedException);
assertThat(actualFileContent, is(expectedFileContent));
}
@ParameterizedTest
@MethodSource("provideArgumentsForRetrievingGenericClipboardContents")
void successfullyRetrieveOldClipboardContents(GenericClipboardContent<?> expectedGenericClipboardContent, Object expectedContent, DataFlavor dataFlavor) throws IOException, UnsupportedFlavorException {
when(transferableMock.isDataFlavorSupported(dataFlavor)).thenReturn(true);
when(transferableMock.getTransferData(dataFlavor)).thenReturn(expectedContent);
GenericClipboardContent<?> actualGenericClipboardContent = sut.getGenericClipboardContents(transferableMock);
assertThat(actualGenericClipboardContent.getOldContent(), is(expectedGenericClipboardContent.getOldContent()));
verifyNoInteractions(logger);
}
@ParameterizedTest
@MethodSource("provideArgumentsForOldClipboardContentsWhenContentIsTransferable")
void successfullyMarshallClipboardContentsIntoOldClipboardContentWhenContentIsTransferable(Object expectedContent, DataFlavor dataFlavor, String expectedString, BufferedImage expectedImage, List<File> expectedFiles) throws IOException, UnsupportedFlavorException {
when(transferableMock.isDataFlavorSupported(dataFlavor)).thenReturn(true);
when(transferableMock.getTransferData(dataFlavor)).thenReturn(expectedContent);
ClipboardContent actualClipboardContent = sut.getClipboardContent(transferableMock);
assertThat(actualClipboardContent.getText(), is(expectedString));
assertThat(actualClipboardContent.getFiles(), is(expectedFiles));
assertThat(actualClipboardContent.getImage(), is(new BufferedImageMatcher(expectedImage)));
verifyNoInteractions(logger);
}
@ParameterizedTest
@MethodSource("provideArgumentsForOldClipboardContents")
void successfullyMarshallClipboardContentsIntoOldClipboardContent(Object expectedOldContent, String expectedString, BufferedImage expectedImage, List<File> expectedFiles) {
ClipboardContent actualClipboardContent = sut.getClipboardContent(expectedOldContent);
assertThat(actualClipboardContent.getText(), is(expectedString));
assertThat(actualClipboardContent.getImage(), is(expectedImage));
assertThat(actualClipboardContent.getFiles(), is(expectedFiles));
verifyNoInteractions(logger);
}
static Stream<Arguments> provideArgumentsForOldClipboardContentsWhenContentIsTransferable() {
String string = "testString";
BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
List<File> files = List.of(new File("testFile"));
return Stream.of(
Arguments.of(string, TEXT.getDataFlavor(), string, null, null),
Arguments.of(bufferedImage, IMAGE.getDataFlavor(), null, bufferedImage, null),
Arguments.of(files, FILE.getDataFlavor(), null, null, files)
);
}
static Stream<Arguments> provideArgumentsForOldClipboardContents() {
String string = "testString";
BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
MyBufferedImage myBufferedImage = new MyBufferedImage(bufferedImage);
List<File> files = List.of(new File("testFile"));
return Stream.of(
Arguments.of(string, string, null, null),
Arguments.of(myBufferedImage, null, bufferedImage, null),
Arguments.of(files, null, null, files)
);
}
static Stream<Arguments> provideArgumentsForRetrievingGenericClipboardContents() {
String string = "testString";
BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
MyBufferedImage myBufferedImage = new MyBufferedImage(bufferedImage);
List<File> files = List.of(new File("testFile"));
return Stream.of(
Arguments.of(new GenericClipboardContent<>(string), string, TEXT.getDataFlavor()),
Arguments.of(new GenericClipboardContent<>(myBufferedImage), bufferedImage, IMAGE.getDataFlavor()),
Arguments.of(new GenericClipboardContent<>(files), files, FILE.getDataFlavor())
);
}
}

@ -0,0 +1,67 @@
package me.goudham;
import java.util.stream.Stream;
import me.goudham.exception.UnsupportedSystemException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
class MyClipboardTest {
@Mock
private SystemUtils systemUtilsMock;
private MyClipboard sut;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
MyClipboard.setSystemUtils(systemUtilsMock);
}
@ParameterizedTest
@MethodSource("provideArgumentsForSuccessfullyRetrievingCorrectSystemClipboard")
void successfullyRetrieveCorrectSystemClipboard(boolean isMac, boolean isWindows, boolean isUnix, Class<?> expectedListener) throws UnsupportedSystemException {
when(systemUtilsMock.isMac()).thenReturn(isMac);
when(systemUtilsMock.isWindows()).thenReturn(isWindows);
when(systemUtilsMock.isUnix()).thenReturn(isUnix);
MyClipboard actualMyClipboard = MyClipboard.getSystemClipboard();
assertThat(actualMyClipboard.getClipboardListener(), instanceOf(expectedListener));
}
@Test
void failToRetrieveCorrectSystemClipboard() {
String expectedOperatingSystem = "unknown";
System.setProperty("os.name", expectedOperatingSystem);
Throwable expectedException = new UnsupportedSystemException("Your Operating System: '" + System.getProperty("os.name") + "' is not supported");
when(systemUtilsMock.isMac()).thenReturn(false);
when(systemUtilsMock.isWindows()).thenReturn(false);
when(systemUtilsMock.isUnix()).thenReturn(false);
Throwable actualException = assertThrows(UnsupportedSystemException.class, MyClipboard::getSystemClipboard);
assertThat(actualException, instanceOf(expectedException.getClass()));
assertThat(actualException.getMessage(), is(expectedException.getMessage()));
}
static Stream<Arguments> provideArgumentsForSuccessfullyRetrievingCorrectSystemClipboard() {
return Stream.of(
Arguments.of(false, true, false, WindowsOrUnixClipboard.class),
Arguments.of(false, false, true, WindowsOrUnixClipboard.class),
Arguments.of(true, false, false, MacClipboard.class)
);
}
}
Loading…
Cancel
Save