2 * Copyright (c) 2006 Teodor Sigaev <teodor@sigaev.ru>
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of the author nor the names of any co-contributors
14 * may be used to endorse or promote products derived from this software
15 * without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
18 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
25 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
26 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
27 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38 * Max nmumber of items
43 * max length in characters of item's name
45 #define MaxItemName 32
50 #define KeyToPopup "B"
51 #define MaskKeyToPopup ControlMask
56 #define StartupItem "--(EMPTY)--"
59 * Color of locked items
61 #define LockedColor "red"
64 * Struct describing item
66 typedef struct ClipboardMenuItem {
67 GtkWidget *widget; /* pointer to GtkMenuItem */
68 gchar *buffer; /* value and it's length */
72 #define CMI_LOCKED 0x0001
73 #define CMI_INIT 0x0002
76 * Describe all data needed to work clipboard history
78 typedef struct ClipboardDescr {
79 guint key; /* key and msk to describe hotkey */
82 GtkWidget *widget; /* GtkMenu */
84 GtkWidget *activator; /* GtkMenuItem for manage active state */
87 ClipboardMenuItem *items[NHistItem]; /* histories item's */
88 gint nitems; /* number of items */
89 gint nlocked; /* number of locked items */
91 GtkStyle *style_normal; /* default style for item */
92 GtkStyle *style_locked; /* style for locked item */
97 * Assign menu's popup to hotkey
100 assignKey( ClipboardDescr *key )
102 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
103 key->mask, GDK_ROOT_WINDOW(),
104 True, GrabModeAsync, GrabModeAsync);
105 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
106 key->mask | LockMask, GDK_ROOT_WINDOW(),
107 True, GrabModeAsync, GrabModeAsync);
108 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
109 key->mask | Mod2Mask, GDK_ROOT_WINDOW(),
110 True, GrabModeAsync, GrabModeAsync);
111 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
112 key->mask | Mod2Mask | LockMask, GDK_ROOT_WINDOW(),
113 True, GrabModeAsync, GrabModeAsync);
117 * Catch key press signal handler
120 catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data)
122 ClipboardDescr *key = (ClipboardDescr*)data;
123 XEvent *xevent = (XEvent *)gdk_xevent;
125 if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) )
127 if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) )
129 gtk_menu_popup(GTK_MENU(key->widget),
137 return GDK_FILTER_REMOVE;
140 return GDK_FILTER_CONTINUE;
144 * Check existing and returns number of item corresponding to
145 * buffer and -1 if it isn't found
148 existMenuItem(ClipboardDescr *descr, const gchar *buffer)
152 for(i=0;i<descr->nitems;i++)
153 if ( strcmp( descr->items[i]->buffer, buffer ) == 0 )
160 * Move n-th item to the head of history
163 moveMenuItemFirst( ClipboardDescr *descr, gint n )
165 ClipboardMenuItem *item;
167 if ( n == 0 ) /* nothing to do */
170 item = descr->items[n];
171 gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0);
173 memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n );
174 descr->items[0] = item;
178 * Finds history item by it's GtkMenuItem
181 lookupItem( ClipboardDescr *desc, GtkWidget *widget)
185 for(i=0;i<desc->nitems;i++)
186 if ( desc->items[i]->widget == widget )
193 * Set history item to the head and put its value
197 itemActivate(GtkWidget *widget, gpointer user_data)
199 ClipboardDescr *descr = (ClipboardDescr*)user_data;
202 n = lookupItem(descr, widget);
203 g_assert( n>=0 && n<descr->nitems);
206 * do not activate item with init value
208 if ( (descr->items[n]->flags & CMI_INIT) == 0 )
210 moveMenuItemFirst( descr, n );
211 gtk_clipboard_set_text(
212 gtk_clipboard_get(GDK_SELECTION_PRIMARY),
213 descr->items[0]->buffer, -1 );
223 itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data)
225 ClipboardDescr *descr = (ClipboardDescr*)user_data;
226 GdkEventButton *bevent = (GdkEventButton *)event;
227 ClipboardMenuItem *item;
232 * Only right click should be assigned
234 if ( bevent->button != 3 )
237 n = lookupItem(descr, widget);
238 g_assert( n>=0 && n<descr->nitems);
239 item = descr->items[n];
241 if ( item->flags & CMI_LOCKED )
246 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal);
247 item->flags &= ~CMI_LOCKED;
248 g_assert( descr->nlocked>0 );
251 else if ( descr->nlocked == descr->nitems-1 )
256 * It's impossible to lock all items to prevent "no room"
257 * ambiguity to add new value, but skip message for init
261 if ( item->flags & CMI_INIT )
264 dialog = gtk_message_dialog_new( NULL,
265 GTK_DIALOG_DESTROY_WITH_PARENT,
268 "Can not lock all items");
270 gtk_dialog_run(GTK_DIALOG(dialog));
271 gtk_widget_destroy(dialog);
273 else if ( (item->flags & CMI_INIT) == 0 )
276 * lock item with no-init value
278 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked);
279 item->flags |= CMI_LOCKED;
287 * Makes new history item, creates corresponding GtkMenuItem
290 static ClipboardMenuItem*
291 newClipboardMenuItem(ClipboardDescr *descr)
293 ClipboardMenuItem *item;
295 item = g_new0( ClipboardMenuItem, 1 );
296 item->widget = gtk_menu_item_new_with_label(StartupItem);
297 gtk_label_set_max_width_chars(
298 GTK_LABEL( gtk_bin_get_child(GTK_BIN(item->widget)) ),
302 g_signal_connect(item->widget,
304 G_CALLBACK(itemActivate),
307 g_signal_connect(item->widget,
308 "button-release-event",
309 G_CALLBACK(itemToggleLock),
312 gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget);
313 gtk_widget_show( item->widget );
319 * adds new value to lis of history items, but before checks its
323 addMenuItem( ClipboardDescr *descr, gchar *buffer )
326 static gchar itemname[5*MaxItemName + 1];
327 gchar *ptrname, *ptrbuffer;
330 * if such value already exists just move to to head
332 if ( (i=existMenuItem(descr, buffer)) >= 0 )
334 moveMenuItemFirst( descr, i );
335 g_free( buffer ); /* free buffer if is not needed */
339 if ( descr->nitems == NHistItem )
342 * List of items as already full, so find oldest non-locked
343 * item and move it to head
345 for(i=descr->nitems - 1; i>= 0; i--)
346 if ( (descr->items[i]->flags & CMI_LOCKED) == 0 )
351 moveMenuItemFirst( descr, i );
356 * add new item. But if list has only one element and it's a
357 * init value then reuse it.
359 if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) )
361 memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems );
364 descr->items[0] = newClipboardMenuItem(descr);
368 if ( descr->items[0]->buffer )
369 g_free( descr->items[0]->buffer );
371 descr->items[0]->buffer = buffer;
372 descr->items[0]->flags = 0;
375 * make item's name, we should remember that buffer
376 * is in UTF-8 encoding, ie multibyte characters
380 ptrbuffer = (gchar*)buffer;
382 for(i=0; *ptrbuffer && i < MaxItemName; i++)
384 int charlen = g_utf8_offset_to_pointer(ptrbuffer, 1) - ptrbuffer;
390 widechar = g_utf8_get_char_validated( ptrbuffer, charlen );
392 if (!g_unichar_isdefined(widechar))
395 if ( g_unichar_isprint( widechar ) )
397 memmove( ptrname, ptrbuffer, charlen );
406 ptrbuffer += charlen;
412 * setting up GtkMenuItem's title
414 gtk_widget_set_style( GTK_BIN(descr->items[0]->widget)->child, descr->style_normal);
416 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->items[0]->widget)) ),
422 appToggleActivate(GtkWidget *widget, gpointer user_data)
424 ClipboardDescr *descr = (ClipboardDescr*)user_data;
426 if ( descr->is_active == TRUE )
428 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_locked);
430 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
433 descr->is_active = FALSE;
437 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_normal);
439 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
442 descr->is_active = TRUE;
449 * Initialize main struct
452 initHistMenu( ClipboardDescr *descr )
454 GtkWidget *separator;
456 * set up hotkey to call menu
458 descr->key = gdk_keyval_from_name(KeyToPopup);
459 descr->mask = MaskKeyToPopup;
460 gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr);
466 descr->widget = gtk_menu_new();
467 gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history");
469 separator = gtk_separator_menu_item_new();
470 gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), separator);
472 descr->activator = gtk_menu_item_new_with_label("Deactivate");
473 gtk_label_set_max_width_chars(
474 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
477 g_signal_connect(descr->activator,
479 G_CALLBACK(appToggleActivate),
481 gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), descr->activator);
483 gtk_widget_show(descr->activator);
484 gtk_widget_show(separator);
485 gtk_widget_show(descr->widget);
491 * make styles for locked and normal items
493 descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget));
495 descr->style_locked = gtk_style_copy(gtk_widget_get_style(descr->widget));
496 gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_NORMAL]) );
497 gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_PRELIGHT]) );
500 * Create first item and mark it as INIT due to
501 * void menu is showed very bad
503 addMenuItem( descr, g_strdup(StartupItem) );
504 descr->items[0]->flags = CMI_INIT;
506 descr->is_active = TRUE;
510 * Receive text content of buffer
512 * Some applications ( OpenOffice at least ) sets owner at
513 * begining of selection instead of after finishing selection. And
514 * gtk_clipboard_wait_for_text() will fix state of X-Selection. So,
515 * GTK clipboard will see only small part of real selection. For preventing
516 * from this misbehaviour we will not try to get contents of selection -
517 * just skip it as image etc. However, it's possible to delay
518 * request of contents - but I don't know way to count correct
519 * timeout or to get finalizing event (may be yet :) )
522 receiveTarget(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer data)
526 static struct /* just an unnamed struct */
528 gchar *target_prefix;
529 gint target_prefix_len;
530 } ignore_targets[] = {
531 { "application/x-openoffice", -1 },
532 { NULL, 0 } /* mark end */
538 * If there is not text in targets then return
540 if ( gtk_targets_include_text( atoms, n_atoms ) == FALSE )
544 * init ignore target if it's needed
546 if ( ignore_targets->target_prefix_len < 0 )
548 ptr = ignore_targets;
550 while( ptr->target_prefix )
552 ptr->target_prefix_len = strlen( ptr->target_prefix );
558 * checks all avaliable targets for presence of ignorable
559 * targets ( which come from "wrong" applications )
561 for(i=0;i<n_atoms;i++)
563 gchar *target_name = gdk_atom_name( atoms[i] );
568 ptr = ignore_targets;
570 while( ptr->target_prefix )
572 if ( strncmp( target_name, ptr->target_prefix, ptr->target_prefix_len ) == 0 )
575 * It's found a ignorable targets - we will not accept text to
576 * prevent unexpected and surprised behaviour of X-selection
588 * Ok - selection has text, selection is not owned to strange application -
591 text = gtk_clipboard_wait_for_text(clipboard);
592 /* be carefull - world has a lot of unexpected events :) */
594 addMenuItem( (ClipboardDescr*)data, text );
598 * Clipboard change signal handler. It requests avaliable
599 * targets to make checks about contents of selection
602 ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data)
605 * Do real work if and only if application is active
607 if ( ((ClipboardDescr*)data)->is_active )
608 gtk_clipboard_request_targets( clipboard, receiveTarget, data );
612 main( int argc, char *argv[] ) {
613 ClipboardDescr descr;
616 gtk_init(&argc, &argv);
618 initHistMenu(&descr);
621 * every time cliboard's content changing ClipboardChange function
624 g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
626 G_CALLBACK(ClipboardChange),