/* * 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 */ 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 */ GtkWidget *activator; /* GtkMenuItem for manage active state */ gboolean is_active; 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) { gint i; for(i=0;initems;i++) if ( 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, -1 ); } 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", G_CALLBACK(itemActivate), (gpointer)descr); g_signal_connect(item->widget, "button-release-event", G_CALLBACK(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, gchar *buffer ) { 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)) >= 0 ) { moveMenuItemFirst( descr, i ); g_free( buffer ); /* free buffer if is not needed */ 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 = buffer; 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 && 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 ); } static gboolean appToggleActivate(GtkWidget *widget, gpointer user_data) { ClipboardDescr *descr = (ClipboardDescr*)user_data; if ( descr->is_active == TRUE ) { gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_locked); gtk_label_set_text( GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ), "Activate" ); descr->is_active = FALSE; } else { gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_normal); gtk_label_set_text( GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ), "Deactivate" ); descr->is_active = TRUE; } return TRUE; } /* * Initialize main struct */ static void initHistMenu( ClipboardDescr *descr ) { GtkWidget *separator; /* * 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"); separator = gtk_separator_menu_item_new(); gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), separator); descr->activator = gtk_menu_item_new_with_label("Deactivate"); gtk_label_set_max_width_chars( GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ), MaxItemName ); g_signal_connect(descr->activator, "activate", G_CALLBACK(appToggleActivate), (gpointer)descr); gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), descr->activator); gtk_widget_show(descr->activator); gtk_widget_show(separator); 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, g_strdup(StartupItem) ); descr->items[0]->flags = CMI_INIT; descr->is_active = TRUE; } /* * Receive text content of buffer * * Some applications ( OpenOffice at least ) sets owner at * begining of selection instead of after finishing selection. And * gtk_clipboard_wait_for_text() will fix state of X-Selection. So, * GTK clipboard will see only small part of real selection. For preventing * from this misbehaviour we will not try to get contents of selection - * just skip it as image etc. However, it's possible to delay * request of contents - but I don't know way to count correct * timeout or to get finalizing event (may be yet :) ) */ static void receiveTarget(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer data) { gint i; gchar *text; static struct /* just an unnamed struct */ { gchar *target_prefix; gint target_prefix_len; } ignore_targets[] = { { "application/x-openoffice", -1 }, { NULL, 0 } /* mark end */ }, *ptr ; /* * If there is not text in targets then return */ if ( gtk_targets_include_text( atoms, n_atoms ) == FALSE ) return; /* * init ignore target if it's needed */ if ( ignore_targets->target_prefix_len < 0 ) { ptr = ignore_targets; while( ptr->target_prefix ) { ptr->target_prefix_len = strlen( ptr->target_prefix ); ptr++; } } /* * checks all avaliable targets for presence of ignorable * targets ( which come from "wrong" applications ) */ for(i=0;itarget_prefix ) { if ( strncmp( target_name, ptr->target_prefix, ptr->target_prefix_len ) == 0 ) { /* * It's found a ignorable targets - we will not accept text to * prevent unexpected and surprised behaviour of X-selection */ g_free(target_name); return; } g_free(target_name); ptr++; } } /* * Ok - selection has text, selection is not owned to strange application - * request contents */ text = gtk_clipboard_wait_for_text(clipboard); /* be carefull - world has a lot of unexpected events :) */ if (text) addMenuItem( (ClipboardDescr*)data, text ); } /* * Clipboard change signal handler. It requests avaliable * targets to make checks about contents of selection */ static void ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data) { /* * Do real work if and only if application is active */ if ( ((ClipboardDescr*)data)->is_active ) gtk_clipboard_request_targets( clipboard, receiveTarget, 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", G_CALLBACK(ClipboardChange), (gpointer)&descr); gtk_main (); return(0); }