Initial revision
authorteodor <teodor>
Mon, 25 Dec 2006 15:54:14 +0000 (15:54 +0000)
committerteodor <teodor>
Mon, 25 Dec 2006 15:54:14 +0000 (15:54 +0000)
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
hclip.c [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..2da78d9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2006 Teodor Sigaev <teodor@sigaev.ru>
+ * 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 (file)
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 (file)
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 <teodor@sigaev.ru>
diff --git a/hclip.c b/hclip.c
new file mode 100644 (file)
index 0000000..c4cdaaa
--- /dev/null
+++ b/hclip.c
@@ -0,0 +1,502 @@
+/*
+ * Copyright (c) 2006 Teodor Sigaev <teodor@sigaev.ru>
+ * 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 <stdlib.h>
+#include <string.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkx.h>
+
+/*
+ * 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;i<descr->nitems;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;i<desc->nitems;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 && n<descr->nitems);
+
+       /*
+        * 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 && n<descr->nitems);
+       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)<buflen && i < MaxItemName; i++)
+       {
+               int     charlen = g_utf8_offset_to_pointer(ptrbuffer, 1) - ptrbuffer; 
+               gunichar         widechar;
+
+               if ( charlen <= 0 )
+                       break;
+
+               widechar = g_utf8_get_char_validated( ptrbuffer, charlen );
+
+               if (!g_unichar_isdefined(widechar))
+                       break;
+
+               if ( g_unichar_isprint( widechar ) )
+               {
+                       memmove( ptrname, ptrbuffer, charlen );
+                       ptrname += charlen;
+               }
+               else
+               {
+                       *ptrname = '_';
+                       ptrname++;
+               }
+
+               ptrbuffer += charlen;
+       }
+
+       *ptrname = '\0';
+
+       /*
+        * setting up GtkMenuItem's title
+        */
+       gtk_widget_set_style( GTK_BIN(descr->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);
+}
+