Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanSweet committed Oct 28, 2014
1 parent d6862fb commit b1c8eb3
Show file tree
Hide file tree
Showing 29 changed files with 2,551 additions and 15 deletions.
12 changes: 12 additions & 0 deletions .classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="assets"/>
<classpathentry kind="lib" path="libs/minlog-1.2.jar"/>
<classpathentry kind="lib" path="libs/jna-4.1.0-win32-x86-64.jar"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
<classpathentry kind="lib" path="libs/jna-4.1.0-min.jar"/>
<classpathentry kind="lib" path="libs/h2-1.4.181.jar"/>
<classpathentry kind="lib" path="libs/jsonbeans-0.7a.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
14 changes: 2 additions & 12 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
*.class

# Mobile Tools for Java (J2ME)
.mtj.tmp/

# Package Files #
*.jar
*.war
*.ear

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
bin
clippy.log
17 changes: 17 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>clippy</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
291 changes: 291 additions & 0 deletions .settings/org.eclipse.jdt.core.prefs

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions .settings/org.eclipse.jdt.ui.prefs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
eclipse.preferences.version=1
formatter_profile=_libgdx
formatter_settings_version=12
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,43 @@
clippy
======
# Clippy

Clipboard history for Windows
Clippy is a clipboard history tool for Windows. It uses a database to stores any text item that is copied to the clipboard. The stored text items can be easily retrieved by browsing or searching.

Clippy is a powerful tool, especially for programmers:

* It is very common to need to copy and paste a few different items. Since the operating system clipboard can only hold a single item, this is a tedious task without Clippy.

* Clippy can be used simply to keep text safe, just in case it is needed later. While writing an email or before refactoring a large piece of code, you can select all the text and copy it, knowing that Clippy has stored the text should it ever be needed in the future.

* Text pasted from Clippy's popup is always plain text. This means Clippy can be used to strip unwanted formatting from text.

* When files are copied, Clippy stores the paths to the files as text. This makes it easy to copy a file and paste its path to the command line, a file dialog, etc.

## Download

Download the latest version of Clippy from the releases page:

[Download Clippy](https://github.com/EsotericSoftware/clippy/releases)

When run, Clippy stores any text items that are placed on the clipboard. These items can be retrieved by pressing the hotkey to show Clippy's popup (see below). Clippy can be exited by clicking the icon in the system tray.

## Popup

The default hotkey to show the popup is `ctrl+shift+insert`. The popup shows the most recent clipboard items. Items can be selected by pressing `up` or `down` and are put on the clipboard and pasted by pressing `enter` or by clicking. Pressing 'home' or 'end' selects the first or last item. Pressing `page up` or `page down` shows additional pages of items. The popup can be hidden by pressing `escape` or clicking outside the popup.

### Search

Items can be searched simply by typing while the popup is open. All items containing any of the typed text are shown.

### Menu

The popup menu is shown by pressing `alt`. The `Lock items` checkbox prevents the order of items from changing when an existing item is chosen.

## Configuration

Clippy stores its configuration file and the database in user folder, under the `.clippy` subfolder. The `config.json` file can be edited to configure Clippy. Eg, change the popup hotkey, the number of items shown on the popup, and more.

## Development details

Clippy's codebase is clean and straightforward. It is written in Java and uses [JNA](https://github.com/twall/jna/) to access the Windows APIs necessary to monitor and interact with the [clipboard](https://github.com/EsotericSoftware/clippy/blob/master/com/esotericsoftware/clippy/Clipboard.java), system-wide [hotkeys](https://github.com/EsotericSoftware/clippy/blob/master/com/esotericsoftware/clippy/Keyboard.java), the [system tray](https://github.com/EsotericSoftware/clippy/blob/master/com/esotericsoftware/clippy/Tray.java), etc.

Text clips are stored in an [H2](http://www.h2database.com) relational database. A small [data store](https://github.com/EsotericSoftware/clippy/blob/master/com/esotericsoftware/clippy/ClipDataStore.java) API simplifies interaction with the database.
Binary file added assets/icon.ico
Binary file not shown.
22 changes: 22 additions & 0 deletions build/launch4j.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<launch4jConfig>
<dontWrapJar>false</dontWrapJar>
<headerType>gui</headerType>
<jar>C:\Dev\oss\target\clippy\dist\onejar\clippy-all.jar</jar>
<outfile>C:\Dev\oss\clippy\build\Clippy.exe</outfile>
<errTitle></errTitle>
<cmdLine></cmdLine>
<chdir></chdir>
<priority>normal</priority>
<downloadUrl>http://java.com/download</downloadUrl>
<supportUrl></supportUrl>
<stayAlive>false</stayAlive>
<manifest></manifest>
<icon>C:\Dev\oss\clippy\assets\icon.ico</icon>
<jre>
<path></path>
<bundledJre64Bit>false</bundledJre64Bit>
<minVersion>1.6.0</minVersion>
<maxVersion></maxVersion>
<jdkPreference>preferJre</jdkPreference>
</jre>
</launch4jConfig>
Binary file added libs/h2-1.4.181.jar
Binary file not shown.
Binary file added libs/jna-4.1.0-min.jar
Binary file not shown.
Binary file added libs/jna-4.1.0-win32-x86-64.jar
Binary file not shown.
Binary file added libs/jsonbeans-0.7a.jar
Binary file not shown.
Binary file added libs/minlog-1.2.jar
Binary file not shown.
4 changes: 4 additions & 0 deletions project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
main: com.esotericsoftware.clippy.Clippy
---
Build.build(project);
Build.oneJAR(project);
84 changes: 84 additions & 0 deletions src/com/esotericsoftware/clippy/ClipDataStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@

package com.esotericsoftware.clippy;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;

import com.esotericsoftware.clippy.util.DataStore;

public class ClipDataStore extends DataStore<ClipDataStore.ClipConnection> {
public ClipDataStore () throws SQLException {
super("~/.clippy/db/db", "clips");
if (System.getProperty("dev") != null)
setInMemory(true);
else
setSocketLocking(true);
addColumn("id INTEGER IDENTITY");
addColumn("text VARCHAR_IGNORECASE");
open();
addIndex("text");
createIndexes();
getThreadConnection().execute("SET LOG 0"); // Disable transaction log.
}

public ClipConnection newConnection () throws SQLException {
return new ClipConnection();
}

public final class ClipConnection extends DataStore.DataStoreConnection {
private final PreparedStatement addClip, removeClip, contains, makeLast, search, last;

ClipConnection () throws SQLException {
addClip = prepareStatement("INSERT INTO :table: SET text=?");
removeClip = prepareStatement("DELETE FROM :table: WHERE text=?");
contains = prepareStatement("SELECT COUNT(*) FROM :table: WHERE text=? LIMIT 1");
makeLast = prepareStatement("UPDATE :table: SET id=(SELECT MAX(id) + 1 FROM :table:) WHERE text=? LIMIT 1");
last = prepareStatement("SELECT text FROM clips ORDER BY id DESC LIMIT ? OFFSET ?");
search = prepareStatement("SELECT text FROM clips WHERE text LIKE ? ORDER BY id DESC LIMIT ?");
}

public void addClip (String text) throws SQLException {
addClip.setString(1, text);
addClip.executeUpdate();
}

public void removeClip (String text) throws SQLException {
removeClip.setString(1, text);
removeClip.executeUpdate();
}

public boolean contains (String text) throws SQLException {
contains.setString(1, text);
ResultSet set = contains.executeQuery();
if (!set.next()) return false;
return set.getInt(1) != 0;
}

public void makeLast (String text) throws SQLException {
makeLast.setString(1, text);
makeLast.executeUpdate();
}

public ArrayList<String> search (ArrayList<String> results, String text, int max) throws SQLException {
search.setString(1, text);
search.setInt(2, max);
ResultSet set = search.executeQuery();
results.clear();
while (set.next())
results.add(set.getString(1));
return results;
}

public ArrayList<String> last (ArrayList<String> results, int max, int start) throws SQLException {
last.setInt(1, max);
last.setInt(2, start);
ResultSet set = last.executeQuery();
results.clear();
while (set.next())
results.add(set.getString(1));
return results;
}
}
}
179 changes: 179 additions & 0 deletions src/com/esotericsoftware/clippy/Clipboard.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@

package com.esotericsoftware.clippy;

import static com.esotericsoftware.clippy.Win.Kernel32.*;
import static com.esotericsoftware.clippy.Win.Shell32.*;
import static com.esotericsoftware.clippy.Win.User32.*;
import static com.esotericsoftware.minlog.Log.*;

import java.util.concurrent.CyclicBarrier;

import com.esotericsoftware.clippy.Win.MSG;
import com.sun.jna.Pointer;
import com.sun.jna.WString;

public class Clipboard {
Pointer hwnd;
final char[] chars = new char[2048];

public Clipboard () {
final CyclicBarrier barrier = new CyclicBarrier(2);

new Thread("Clipboard") {
public void run () {
if (TRACE) trace("Entered clipboard thread.");

hwnd = CreateWindowEx(0, new WString("STATIC"), new WString(""), 0, 0, 0, 0, 0, 0, 0, 0, 0);
if (hwnd == null) {
if (ERROR) error("Unable to create clipboard window.");
System.exit(0);
}

if (!AddClipboardFormatListener(hwnd)) {
if (ERROR) error("Unable to install clipboard listener.");
System.exit(0);
}

try {
barrier.await();
} catch (Exception ignored) {
}

MSG msg = new MSG();
while (GetMessage(msg, null, WM_CLIPBOARDUPDATE, WM_CLIPBOARDUPDATE)) {
if (msg.message != WM_CLIPBOARDUPDATE) continue;
if (hwnd.equals(GetClipboardOwner())) {
if (TRACE) trace("Clipboard changed (own event).");
continue;
}
if (DEBUG) debug("Clipboard changed.");
changed();
}

if (TRACE) trace("Exited clipboard thread.");
}
}.start();

try {
barrier.await();
} catch (Exception ignored) {
}
}

protected void changed () {
}

private boolean open (int millis) {
int i = 0;
while (!OpenClipboard(hwnd)) {
try {
Thread.sleep(5);
} catch (InterruptedException ex) {
}
i += 5;
if (i > millis) {
if (WARN) warn("Unable to open clipboard.");
return false;
}
}
return true;
}

/** @return May be null. */
public String getContents () {
if (!open(500)) return null;
try {
int format;
if (IsClipboardFormatAvailable(CF_TEXT)) {
format = CF_TEXT;
} else if (IsClipboardFormatAvailable(CF_HDROP)) {
format = CF_HDROP;
if (TRACE) trace("Files clipboard item.");
} else {
if (TRACE) trace("Non-text clipboard item.");
return null;
}

Pointer globalData = GetClipboardData(format);
if (globalData == null) {
if (WARN) warn("Unable to retrieve clipboard data.");
return null;
}

Pointer data = GlobalLock(globalData);
if (data == null) {
if (WARN) warn("Unable to lock clipboard buffer.");
return null;
}

String text;
if (format == CF_TEXT) {
text = data.getString(0);
} else {
int fileCount = DragQueryFile(data, -1, null, 0);
if (fileCount == 0) {
if (WARN) warn("Unable to query file count.");
return null;
}
StringBuilder buffer = new StringBuilder(512);
for (int i = 0; i < fileCount; i++) {
int charCount = DragQueryFile(data, i, chars, chars.length);
if (charCount == 0) {
if (WARN) warn("Unable to query file name.");
return null;
}
buffer.append(chars, 0, charCount);
buffer.append('\n');
}
buffer.setLength(buffer.length() - 1);
text = buffer.toString();
}

GlobalUnlock(globalData);

return text;
} finally {
if (!CloseClipboard(hwnd)) {
if (WARN) warn("Unable to close clipboard.");
return null;
}
}
}

public boolean setContents (String text) {
if (TRACE) trace("Setting clipboard text: " + text.trim());

if (!open(150)) return false;

try {
if (!EmptyClipboard()) {
if (WARN) warn("Unable to empty clipboard.");
return false;
}

Pointer data = GlobalAlloc(GMEM_MOVEABLE, text.length() + 1);
if (data == null) {
if (WARN) warn("Unable to allocate data.");
return false;
}

Pointer buffer = GlobalLock(data);
if (buffer == null) {
if (WARN) warn("Unable to lock buffer.");
return false;
}
buffer.setString(0, text);
GlobalUnlock(data);

if (SetClipboardData(CF_TEXT, buffer) == null) {
if (WARN) warn("Unable to set clipboard data.");
return false;
}
return true;
} finally {
if (!CloseClipboard(hwnd)) {
if (WARN) warn("Unable to close clipboard.");
}
}
}
}
Loading

0 comments on commit b1c8eb3

Please sign in to comment.