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 */
96 * Assign menu's popup to hotkey
99 assignKey( ClipboardDescr *key )
101 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
102 key->mask, GDK_ROOT_WINDOW(),
103 True, GrabModeAsync, GrabModeAsync);
104 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
105 key->mask | LockMask, GDK_ROOT_WINDOW(),
106 True, GrabModeAsync, GrabModeAsync);
107 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
108 key->mask | Mod2Mask, GDK_ROOT_WINDOW(),
109 True, GrabModeAsync, GrabModeAsync);
110 XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
111 key->mask | Mod2Mask | LockMask, GDK_ROOT_WINDOW(),
112 True, GrabModeAsync, GrabModeAsync);
116 * Catch key press signal handler
119 catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data)
121 ClipboardDescr *key = (ClipboardDescr*)data;
122 XEvent *xevent = (XEvent *)gdk_xevent;
124 if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) )
126 if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) )
128 gtk_menu_popup(GTK_MENU(key->widget),
136 return GDK_FILTER_REMOVE;
139 return GDK_FILTER_CONTINUE;
143 * Check existing and returns number of item corresponding to
144 * buffer and -1 if it isn't found
147 existMenuItem(ClipboardDescr *descr, const gchar *buffer)
151 for(i=0;i<descr->nitems;i++)
152 if ( strcmp( descr->items[i]->buffer, buffer ) == 0 )
159 * Move n-th item to the head of history
162 moveMenuItemFirst( ClipboardDescr *descr, gint n )
164 ClipboardMenuItem *item;
166 if ( n == 0 ) /* nothing to do */
169 item = descr->items[n];
170 gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0);
172 memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n );
173 descr->items[0] = item;
177 * Finds history item by it's GtkMenuItem
180 lookupItem( ClipboardDescr *desc, GtkWidget *widget)
184 for(i=0;i<desc->nitems;i++)
185 if ( desc->items[i]->widget == widget )
192 * Set history item to the head and put its value
196 itemActivate(GtkWidget *widget, gpointer user_data)
198 ClipboardDescr *descr = (ClipboardDescr*)user_data;
201 n = lookupItem(descr, widget);
202 g_assert( n>=0 && n<descr->nitems);
205 * do not activate item with init value
207 if ( (descr->items[n]->flags & CMI_INIT) == 0 )
209 moveMenuItemFirst( descr, n );
210 gtk_clipboard_set_text(
211 gtk_clipboard_get(GDK_SELECTION_PRIMARY),
212 descr->items[0]->buffer, -1 );
222 itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data)
224 ClipboardDescr *descr = (ClipboardDescr*)user_data;
225 GdkEventButton *bevent = (GdkEventButton *)event;
226 ClipboardMenuItem *item;
231 * Only right click should be assigned
233 if ( bevent->button != 3 )
236 n = lookupItem(descr, widget);
237 g_assert( n>=0 && n<descr->nitems);
238 item = descr->items[n];
240 if ( item->flags & CMI_LOCKED )
245 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal);
246 item->flags &= ~CMI_LOCKED;
247 g_assert( descr->nlocked>0 );
250 else if ( descr->nlocked == descr->nitems-1 )
255 * It's impossible to lock all items to prevent "no room"
256 * ambiguity to add new value, but skip message for init
260 if ( item->flags & CMI_INIT )
263 dialog = gtk_message_dialog_new( NULL,
264 GTK_DIALOG_DESTROY_WITH_PARENT,
267 "Can not lock all items");
269 gtk_dialog_run(GTK_DIALOG(dialog));
270 gtk_widget_destroy(dialog);
272 else if ( (item->flags & CMI_INIT) == 0 )
275 * lock item with no-init value
277 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked);
278 item->flags |= CMI_LOCKED;
286 * Makes new history item, creates corresponding GtkMenuItem
289 static ClipboardMenuItem*
290 newClipboardMenuItem(ClipboardDescr *descr)
292 ClipboardMenuItem *item;
294 item = g_new0( ClipboardMenuItem, 1 );
295 item->widget = gtk_menu_item_new_with_label(StartupItem);
296 gtk_label_set_max_width_chars(
297 GTK_LABEL( gtk_bin_get_child(GTK_BIN(item->widget)) ),
301 g_signal_connect(item->widget,
303 G_CALLBACK(itemActivate),
306 g_signal_connect(item->widget,
307 "button-release-event",
308 G_CALLBACK(itemToggleLock),
311 gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget);
312 gtk_widget_show( item->widget );
318 * adds new value to lis of history items, but before checks its
322 addMenuItem( ClipboardDescr *descr, gchar *buffer )
325 static gchar itemname[5*MaxItemName + 1];
326 gchar *ptrname, *ptrbuffer;
329 * if such value already exists just move to to head
331 if ( (i=existMenuItem(descr, buffer)) >= 0 )
333 moveMenuItemFirst( descr, i );
337 if ( descr->nitems == NHistItem )
340 * List of items as already full, so find oldest non-locked
341 * item and move it to head
343 for(i=descr->nitems - 1; i>= 0; i--)
344 if ( (descr->items[i]->flags & CMI_LOCKED) == 0 )
349 moveMenuItemFirst( descr, i );
354 * add new item. But if list has only one element and it's a
355 * init value then reuse it.
357 if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) )
359 memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems );
362 descr->items[0] = newClipboardMenuItem(descr);
366 if ( descr->items[0]->buffer )
367 g_free( descr->items[0]->buffer );
369 descr->items[0]->buffer = buffer;
370 descr->items[0]->flags = 0;
373 * make item's name, we should remember that buffer
374 * is in UTF-8 encoding, ie multibyte characters
378 ptrbuffer = (gchar*)buffer;
380 for(i=0; *ptrbuffer && i < MaxItemName; i++)
382 int charlen = g_utf8_offset_to_pointer(ptrbuffer, 1) - ptrbuffer;
388 widechar = g_utf8_get_char_validated( ptrbuffer, charlen );
390 if (!g_unichar_isdefined(widechar))
393 if ( g_unichar_isprint( widechar ) )
395 memmove( ptrname, ptrbuffer, charlen );
404 ptrbuffer += charlen;
410 * setting up GtkMenuItem's title
412 gtk_widget_set_style( GTK_BIN(descr->items[0]->widget)->child, descr->style_normal);
414 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->items[0]->widget)) ),
420 appToggleActivate(GtkWidget *widget, gpointer user_data)
422 ClipboardDescr *descr = (ClipboardDescr*)user_data;
424 if ( descr->is_active == TRUE )
426 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_locked);
428 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
431 descr->is_active = FALSE;
435 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_normal);
437 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
440 descr->is_active = TRUE;
447 * Initialize main struct
450 initHistMenu( ClipboardDescr *descr )
452 GtkWidget *separator;
454 * set up hotkey to call menu
456 descr->key = gdk_keyval_from_name(KeyToPopup);
457 descr->mask = MaskKeyToPopup;
458 gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr);
464 descr->widget = gtk_menu_new();
465 gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history");
467 separator = gtk_separator_menu_item_new();
468 gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), separator);
470 descr->activator = gtk_menu_item_new_with_label("Deactivate");
471 gtk_label_set_max_width_chars(
472 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
475 g_signal_connect(descr->activator,
477 G_CALLBACK(appToggleActivate),
479 gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), descr->activator);
481 gtk_widget_show(descr->activator);
482 gtk_widget_show(separator);
483 gtk_widget_show(descr->widget);
489 * make styles for locked and normal items
491 descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget));
493 descr->style_locked = gtk_style_copy(gtk_widget_get_style(descr->widget));
494 gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_NORMAL]) );
495 gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_PRELIGHT]) );
498 * Create first item and mark it as INIT due to
499 * void menu is showed very bad
501 addMenuItem( descr, g_strdup(StartupItem) );
502 descr->items[0]->flags = CMI_INIT;
504 descr->is_active = TRUE;
508 * Receive text content of buffer
510 * it seems to me, that there is some problem in gtk+
511 * when targets MULTIPLE and TARGETS are set (at least with russian
512 * characters in OO ). So just ignore such cases...
515 receiveTarget(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer data)
518 gboolean has_text = FALSE,
519 has_multiple = FALSE,
523 * checks all avaliable targets
525 for(i=0;i<n_atoms;i++)
527 if ( atoms[i] == gdk_atom_intern_static_string("UTF8_STRING") ||
528 atoms[i] == gdk_atom_intern_static_string("COMPOUND_TEXT") )
532 else if ( atoms[i] == gdk_atom_intern_static_string("MULTIPLE") )
536 else if ( atoms[i] == gdk_atom_intern_static_string("TARGETS") )
543 * check for workaround
545 if ( has_text == TRUE && ( has_multiple==FALSE || has_targets == TRUE ) )
547 gchar *text = gtk_clipboard_wait_for_text(clipboard);
550 addMenuItem( (ClipboardDescr*)data, text );
555 * Clipboard change signal handler. It requests new content of
556 * clipboard in a text form
559 ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data)
562 * Do real work if and only if application is active
564 if ( ((ClipboardDescr*)data)->is_active )
565 gtk_clipboard_request_targets( clipboard, receiveTarget, data );
569 main( int argc, char *argv[] ) {
570 ClipboardDescr descr;
573 gtk_init(&argc, &argv);
575 initHistMenu(&descr);
578 * every time cliboard's content changing ClipboardChange function
581 g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
583 G_CALLBACK(ClipboardChange),