From: teodor Date: Mon, 25 Dec 2006 15:54:14 +0000 (+0000) Subject: Initial revision X-Git-Tag: start~1 X-Git-Url: http://sigaev.ru/git/gitweb.cgi?a=commitdiff_plain;h=0f33952e3a5f99f5dc516805b7d15cf66412234d;p=hclip.git Initial revision --- 0f33952e3a5f99f5dc516805b7d15cf66412234d diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2da78d9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2006 Teodor Sigaev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the author nor the names of any co-contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY CONTRIBUTORS ``AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..479f9c8 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +CC=gcc +INCLUDE=`pkg-config gtk+-2.0 --cflags` +CFLAGS=-Wall -O2 +LIB=`pkg-config gtk+-2.0 --libs` + +STRIP=strip + + +OBJ=hclip.o + +.SUFFIXES: .o.c + +.c.o: + $(CC) $(CFLAGS) $(INCLUDE) -c $< + +all: hclip + +hclip: $(OBJ) + $(CC) -o $@ $(OBJ) $(LIB) + $(STRIP) $@ + +clean: + rm -rf *core *.o hclip + diff --git a/README b/README new file mode 100644 index 0000000..a4c9ad6 --- /dev/null +++ b/README @@ -0,0 +1,27 @@ +hclip - Simple clipboard history application that keeps history of clipboard +operations and allows you to put previously copied items back to the clipboard +for pasting to other applications. + +Aplications works withouit main window and it's very simple, all configurable +options are configured at compile time as defines at begining of source file. + +Item in list may be locked (and unlocked) by right click. Locked items stay +forever in list. + +hclip works only with text content of clipborad and simply ignore all other +types. hclip correctly works with encodings because internally it works with +UTF-8 encoding which is provided by GTK 2 clipboard interface. + +NHistItem - maximum number of remembering clipboard's items +MaxItemName - maximum length in characters of displayed name of items +KeyToPopup - key which should be pressed to call menu of item's list +MaskKeyToPopup - key's modificator of KeyToPopup + +Default values: +NHistItem 16 +MaxItemName 32 +Key to call list Ctrl+B + +Tested on FreeBSD with GTK 2.10.6 + +Author: Teodor Sigaev diff --git a/hclip.c b/hclip.c new file mode 100644 index 0000000..c4cdaaa --- /dev/null +++ b/hclip.c @@ -0,0 +1,502 @@ +/* + * Copyright (c) 2006 Teodor Sigaev + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the author nor the names of any co-contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY CONTRIBUTORS ``AS IS'' AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include +#include +#include + +/* + * Max nmumber of items + */ +#define NHistItem 16 + +/* + * max length in characters of item's name + */ +#define MaxItemName 32 + +/* + * Key binding + */ +#define KeyToPopup "B" +#define MaskKeyToPopup ControlMask + +/* + * Init item's name + */ +#define StartupItem "--(EMPTY)--" + +/* + * Color of locked items + */ +#define LockedColor "red" + +/* + * Struct describing item + */ +typedef struct ClipboardMenuItem { + GtkWidget *widget; /* pointer to GtkMenuItem */ + gchar *buffer; /* value and it's length */ + size_t buflen; + gint flags; +} ClipboardMenuItem; + +#define CMI_LOCKED 0x0001 +#define CMI_INIT 0x0002 + +/* + * Describe all data needed to work clipboard history + */ +typedef struct ClipboardDescr { + guint key; /* key and msk to describe hotkey */ + guint mask; + + GtkWidget *widget; /* GtkMenu */ + + ClipboardMenuItem *items[NHistItem]; /* histories item's */ + gint nitems; /* number of items */ + gint nlocked; /* number of locked items */ + + GtkStyle *style_normal; /* default style for item */ + GtkStyle *style_locked; /* style for locked item */ +} ClipboardDescr; + +/* + * Assign menu's popup to hotkey + */ +static void +assignKey( ClipboardDescr *key ) +{ + XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key), + key->mask, GDK_ROOT_WINDOW(), + True, GrabModeAsync, GrabModeAsync); + XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key), + key->mask | LockMask, GDK_ROOT_WINDOW(), + True, GrabModeAsync, GrabModeAsync); + XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key), + key->mask | Mod2Mask, GDK_ROOT_WINDOW(), + True, GrabModeAsync, GrabModeAsync); + XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key), + key->mask | Mod2Mask | LockMask, GDK_ROOT_WINDOW(), + True, GrabModeAsync, GrabModeAsync); +} + +/* + * Catch key press signal handler + */ +GdkFilterReturn +catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data) +{ + ClipboardDescr *key = (ClipboardDescr*)data; + XEvent *xevent = (XEvent *)gdk_xevent; + + if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) ) + { + if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) ) + { + gtk_menu_popup(GTK_MENU(key->widget), + NULL, NULL, + NULL, NULL, + 0, + GDK_CURRENT_TIME); + + } + + return GDK_FILTER_REMOVE; + } + + return GDK_FILTER_CONTINUE; +} + +/* + * Check existing and returns number of item corresponding to + * buffer and -1 if it isn't found + */ +static gint +existMenuItem(ClipboardDescr *descr, const gchar *buffer, size_t buflen) +{ + gint i; + + for(i=0;initems;i++) + if ( descr->items[i]->buflen == buflen && strcmp( descr->items[i]->buffer, buffer ) == 0 ) + return i; + + return -1; +} + +/* + * Move n-th item to the head of history + */ +static void +moveMenuItemFirst( ClipboardDescr *descr, gint n ) +{ + ClipboardMenuItem *item; + + if ( n == 0 ) /* nothing to do */ + return; + + item = descr->items[n]; + gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0); + + memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n ); + descr->items[0] = item; +} + +/* + * Finds history item by it's GtkMenuItem + */ +static gint +lookupItem( ClipboardDescr *desc, GtkWidget *widget) +{ + gint i; + + for(i=0;initems;i++) + if ( desc->items[i]->widget == widget ) + return i; + + return -1; +} + +/* + * Set history item to the head and put its value + * to the clipboard + */ +static gboolean +itemActivate(GtkWidget *widget, gpointer user_data) +{ + ClipboardDescr *descr = (ClipboardDescr*)user_data; + gint n; + + n = lookupItem(descr, widget); + g_assert( n>=0 && nnitems); + + /* + * do not activate item with init value + */ + if ( (descr->items[n]->flags & CMI_INIT) == 0 ) + { + moveMenuItemFirst( descr, n ); + gtk_clipboard_set_text( + gtk_clipboard_get(GDK_SELECTION_PRIMARY), + descr->items[0]->buffer, + descr->items[0]->buflen); + } + + return TRUE; +} + +/* + * Lock/unlock item + */ +static gboolean +itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data) +{ + ClipboardDescr *descr = (ClipboardDescr*)user_data; + GdkEventButton *bevent = (GdkEventButton *)event; + ClipboardMenuItem *item; + + gint n; + + /* + * Only right click should be assigned + */ + if ( bevent->button != 3 ) + return FALSE; + + n = lookupItem(descr, widget); + g_assert( n>=0 && nnitems); + item = descr->items[n]; + + if ( item->flags & CMI_LOCKED ) + { + /* + * just unlock item + */ + gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal); + item->flags &= ~CMI_LOCKED; + g_assert( descr->nlocked>0 ); + descr->nlocked--; + } + else if ( descr->nlocked == descr->nitems-1 ) + { + GtkWidget *dialog; + + /* + * It's impossible to lock all items to prevent "no room" + * ambiguity to add new value, but skip message for init + * item + */ + + if ( item->flags & CMI_INIT ) + return FALSE; + + dialog = gtk_message_dialog_new( NULL, + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_WARNING, + GTK_BUTTONS_CLOSE, + "Can not lock all items"); + + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); + } + else if ( (item->flags & CMI_INIT) == 0 ) + { + /* + * lock item with no-init value + */ + gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked); + item->flags |= CMI_LOCKED; + descr->nlocked++; + } + + return TRUE; +} + +/* + * Makes new history item, creates corresponding GtkMenuItem + * and initializes it + */ +static ClipboardMenuItem* +newClipboardMenuItem(ClipboardDescr *descr) +{ + ClipboardMenuItem *item; + + item = g_new0( ClipboardMenuItem, 1 ); + item->widget = gtk_menu_item_new_with_label(StartupItem); + gtk_label_set_max_width_chars( + GTK_LABEL( gtk_bin_get_child(GTK_BIN(item->widget)) ), + MaxItemName + ); + + g_signal_connect(item->widget, + "activate", + (GCallback)itemActivate, + (gpointer)descr); + + g_signal_connect(item->widget, + "button-release-event", + (GCallback)itemToggleLock, + (gpointer)descr); + + gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget); + gtk_widget_show( item->widget ); + + return item; +} + +/* + * adds new value to lis of history items, but before checks its + * existing + */ +static void +addMenuItem( ClipboardDescr *descr, const gchar *buffer, size_t buflen ) +{ + gint i; + static gchar itemname[5*MaxItemName + 1]; + gchar *ptrname, *ptrbuffer; + + /* + * if such value already exists just move to to head + */ + if ( (i=existMenuItem(descr, buffer, buflen)) >= 0 ) + { + moveMenuItemFirst( descr, i ); + return; + } + + if ( descr->nitems == NHistItem ) + { + /* + * List of items as already full, so find oldest non-locked + * item and move it to head + */ + for(i=descr->nitems - 1; i>= 0; i--) + if ( (descr->items[i]->flags & CMI_LOCKED) == 0 ) + break; + + g_assert( i>= 0 ); + if ( i!= 0 ) + moveMenuItemFirst( descr, i ); + } + else + { + /* + * add new item. But if list has only one element and it's a + * init value then reuse it. + */ + if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) ) + { + memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems ); + descr->nitems++; + + descr->items[0] = newClipboardMenuItem(descr); + } + } + + if ( descr->items[0]->buffer ) + g_free( descr->items[0]->buffer ); + + descr->items[0]->buffer = g_memdup(buffer, buflen+1); + descr->items[0]->buflen = buflen; + descr->items[0]->flags = 0; + + /* + * make item's name, we should remember that buffer + * is in UTF-8 encoding, ie multibyte characters + */ + + ptrname = itemname; + ptrbuffer = (gchar*)buffer; + + for(i=0; *ptrbuffer && (ptrbuffer - buffer)items[0]->widget)->child, descr->style_normal); + gtk_label_set_text( + GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->items[0]->widget)) ), + itemname + ); +} + +/* + * Initialize main struct + */ +static void +initHistMenu( ClipboardDescr *descr ) +{ + /* + * set up hotkey to call menu + */ + descr->key = gdk_keyval_from_name(KeyToPopup); + descr->mask = MaskKeyToPopup; + gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr); + assignKey(descr); + + /* + * Create menu + */ + descr->widget = gtk_menu_new(); + gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history"); + gtk_widget_show(descr->widget); + + descr->nitems = 0; + descr->nlocked = 0; + + /* + * make styles for locked and normal items + */ + descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget)); + + descr->style_locked = gtk_style_copy(gtk_widget_get_style(descr->widget)); + gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_NORMAL]) ); + gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_PRELIGHT]) ); + + /* + * Create first item and mark it as INIT due to + * void menu is showed very bad + */ + addMenuItem( descr, StartupItem, strlen(StartupItem) ); + descr->items[0]->flags = CMI_INIT; +} + +/* + * Retrieves content of clipoard in a text form + */ +static void +receiveText(GtkClipboard *cpb, const gchar *text, gpointer data) +{ + if ( text != NULL ) + addMenuItem( (ClipboardDescr*)data, text, strlen(text) ); +} + +/* + * Clipboard change signal handler. It requests new content of + * clipboard in a text form + */ +static void +ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data) +{ + gtk_clipboard_request_text( clipboard, receiveText, data ); +} + +int +main( int argc, char *argv[] ) { + ClipboardDescr descr; + + gtk_set_locale(); + gtk_init(&argc, &argv); + + initHistMenu(&descr); + + /* + * every time cliboard's content changing ClipboardChange function + * will be called + */ + g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY), + "owner-change", + (GCallback)ClipboardChange, + (gpointer)&descr); + + gtk_main (); + + return(0); +} +