--- /dev/null
+/*
+ * 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);
+}
+