Initial revision
[hclip.git] / hclip.c
1 /*
2  * Copyright (c) 2006 Teodor Sigaev <teodor@sigaev.ru>
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
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.
16  *
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.
28  */
29
30 #include <stdlib.h>
31 #include <string.h>
32
33 #include <gtk/gtk.h>
34 #include <gdk/gdk.h>
35 #include <gdk/gdkx.h>
36
37 /*
38  * Max nmumber of items
39  */
40 #define NHistItem 16
41
42 /*
43  * max length in characters of item's name
44  */
45 #define MaxItemName 32
46
47 /*
48  * Key binding
49  */
50 #define KeyToPopup              "B"
51 #define MaskKeyToPopup  ControlMask
52
53 /*
54  * Init item's name
55  */
56 #define StartupItem "--(EMPTY)--"
57
58 /*
59  * Color of locked items
60  */
61 #define LockedColor     "red"
62
63 /*
64  * Struct describing item
65  */
66 typedef struct ClipboardMenuItem {
67         GtkWidget                               *widget; /* pointer to GtkMenuItem */
68         gchar                                   *buffer; /* value  and it's length */
69         size_t                                  buflen;  
70         gint                                    flags;
71 } ClipboardMenuItem;
72
73 #define CMI_LOCKED              0x0001
74 #define CMI_INIT                0x0002
75
76 /*
77  * Describe all data needed to work clipboard history
78  */
79 typedef struct ClipboardDescr {
80         guint                           key;    /* key and msk to describe hotkey */
81         guint                           mask;
82
83         GtkWidget                       *widget; /* GtkMenu */
84
85         ClipboardMenuItem       *items[NHistItem];  /* histories item's */
86         gint                            nitems;                         /* number of items */
87         gint                            nlocked;                        /* number of locked items */
88
89         GtkStyle                        *style_normal;          /* default style for item */
90         GtkStyle                        *style_locked;          /* style for locked item */
91 } ClipboardDescr;
92
93 /*
94  * Assign menu's popup to hotkey
95  */
96 static void
97 assignKey( ClipboardDescr *key ) 
98 {
99         XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
100                          key->mask, GDK_ROOT_WINDOW(), 
101                          True, GrabModeAsync, GrabModeAsync);
102         XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
103                          key->mask | LockMask, GDK_ROOT_WINDOW(), 
104                          True, GrabModeAsync, GrabModeAsync);
105         XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
106                          key->mask | Mod2Mask, GDK_ROOT_WINDOW(), 
107                          True, GrabModeAsync, GrabModeAsync);
108         XGrabKey(GDK_DISPLAY(), XKeysymToKeycode(GDK_DISPLAY(), key->key),
109                          key->mask | Mod2Mask | LockMask, GDK_ROOT_WINDOW(), 
110                          True, GrabModeAsync, GrabModeAsync);
111 }
112
113 /*
114  * Catch key press signal handler
115  */
116 GdkFilterReturn
117 catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data) 
118 {
119         ClipboardDescr *key = (ClipboardDescr*)data;
120         XEvent  *xevent = (XEvent *)gdk_xevent;
121
122         if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) ) 
123         {
124                 if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) ) 
125                 { 
126                         gtk_menu_popup(GTK_MENU(key->widget),
127                                                         NULL, NULL,
128                                                         NULL, NULL,
129                                                         0,
130                                                         GDK_CURRENT_TIME);
131                         
132                 }
133
134                 return GDK_FILTER_REMOVE;
135         }
136
137         return GDK_FILTER_CONTINUE;
138 }
139
140 /*
141  * Check existing and returns number of item corresponding to 
142  * buffer and -1 if it isn't found
143  */
144 static gint
145 existMenuItem(ClipboardDescr *descr, const gchar *buffer, size_t buflen)
146 {
147         gint i;
148
149         for(i=0;i<descr->nitems;i++)
150                 if ( descr->items[i]->buflen == buflen && strcmp( descr->items[i]->buffer, buffer ) == 0 )
151                         return i;
152
153         return -1;
154 }
155
156 /*
157  * Move n-th item to the head of history
158  */
159 static void
160 moveMenuItemFirst( ClipboardDescr *descr, gint  n )
161 {
162         ClipboardMenuItem       *item;
163
164         if ( n == 0 ) /* nothing to do */
165                 return;
166
167         item = descr->items[n];
168         gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0);
169
170         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n );
171         descr->items[0] = item;
172 }
173
174 /*
175  * Finds history item by it's GtkMenuItem
176  */
177 static gint
178 lookupItem( ClipboardDescr *desc, GtkWidget *widget)
179 {
180         gint i;
181
182         for(i=0;i<desc->nitems;i++)
183                 if ( desc->items[i]->widget == widget )
184                         return i;
185
186         return -1;
187 }
188
189 /*
190  * Set history item to the head and put its value
191  * to the clipboard
192  */
193 static gboolean
194 itemActivate(GtkWidget *widget, gpointer user_data)
195 {
196         ClipboardDescr  *descr = (ClipboardDescr*)user_data;
197         gint    n;
198
199         n = lookupItem(descr, widget);
200         g_assert( n>=0 && n<descr->nitems);
201
202         /*
203          * do not activate item with init value
204          */
205         if ( (descr->items[n]->flags & CMI_INIT) == 0 )
206         {
207                 moveMenuItemFirst( descr, n );
208                 gtk_clipboard_set_text(
209                         gtk_clipboard_get(GDK_SELECTION_PRIMARY),
210                         descr->items[0]->buffer,
211                         descr->items[0]->buflen);
212         }
213         
214         return TRUE;
215 }
216
217 /*
218  * Lock/unlock item
219  */
220 static gboolean
221 itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data)
222 {
223         ClipboardDescr          *descr = (ClipboardDescr*)user_data;
224         GdkEventButton          *bevent = (GdkEventButton *)event;
225         ClipboardMenuItem       *item;
226
227         gint    n;
228
229         /*
230          * Only right click should be assigned
231          */
232         if ( bevent->button != 3 )
233                 return  FALSE;
234
235         n = lookupItem(descr, widget);
236         g_assert( n>=0 && n<descr->nitems);
237         item = descr->items[n];
238
239         if ( item->flags & CMI_LOCKED )
240         {
241                 /*
242                  * just unlock item
243                  */
244                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal);
245                 item->flags &= ~CMI_LOCKED;
246                 g_assert( descr->nlocked>0 );
247                 descr->nlocked--;
248         }
249         else if ( descr->nlocked == descr->nitems-1 )
250         {
251                 GtkWidget       *dialog;
252
253                 /*
254                  * It's impossible to lock all items to prevent "no room"
255                  * ambiguity to add new value, but skip message for init 
256                  * item
257                  */
258
259                 if ( item->flags & CMI_INIT )
260                         return  FALSE;
261
262                 dialog = gtk_message_dialog_new( NULL,
263                                                         GTK_DIALOG_DESTROY_WITH_PARENT,
264                                                         GTK_MESSAGE_WARNING,
265                                                         GTK_BUTTONS_CLOSE,
266                                                         "Can not lock all items");
267
268                 gtk_dialog_run(GTK_DIALOG(dialog));
269                 gtk_widget_destroy(dialog);
270         }
271         else if ( (item->flags & CMI_INIT) == 0 )
272         {
273                 /*
274                  * lock item with no-init value
275                  */
276                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked);
277                 item->flags |= CMI_LOCKED;
278                 descr->nlocked++;
279         }
280
281         return TRUE;
282 }
283
284 /*
285  * Makes new history item, creates corresponding GtkMenuItem
286  * and initializes it
287  */
288 static ClipboardMenuItem*
289 newClipboardMenuItem(ClipboardDescr *descr)
290 {
291         ClipboardMenuItem       *item;
292
293         item = g_new0( ClipboardMenuItem, 1 );
294         item->widget = gtk_menu_item_new_with_label(StartupItem);
295         gtk_label_set_max_width_chars(
296                 GTK_LABEL( gtk_bin_get_child(GTK_BIN(item->widget)) ),
297                 MaxItemName
298         );
299
300         g_signal_connect(item->widget,
301                                                 "activate",
302                                                 (GCallback)itemActivate,
303                                                 (gpointer)descr);
304
305         g_signal_connect(item->widget,
306                                                 "button-release-event",
307                                                 (GCallback)itemToggleLock,
308                                                 (gpointer)descr);
309
310         gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget);
311         gtk_widget_show( item->widget );
312
313         return item;
314 }
315
316 /*
317  * adds new value to lis of history items, but before checks its
318  * existing
319  */
320 static void
321 addMenuItem( ClipboardDescr *descr, const gchar *buffer, size_t buflen )
322 {
323         gint    i;
324         static  gchar itemname[5*MaxItemName + 1];
325         gchar   *ptrname, *ptrbuffer;
326
327         /*
328          * if such value already exists just move to to head
329          */
330         if ( (i=existMenuItem(descr, buffer, buflen)) >= 0 )
331         {
332                 moveMenuItemFirst( descr, i );
333                 return;
334         }
335
336         if ( descr->nitems == NHistItem )
337         {
338                 /*
339                  * List of items as already full, so find oldest non-locked
340                  * item and move it to head
341                  */
342                 for(i=descr->nitems - 1; i>= 0; i--)
343                         if ( (descr->items[i]->flags & CMI_LOCKED) == 0 )
344                                 break;
345
346                 g_assert( i>= 0 );
347                 if ( i!= 0 )
348                         moveMenuItemFirst( descr, i );  
349         } 
350         else 
351         {
352                 /*
353                  * add new item. But if list has only one element and it's a 
354                  * init value then reuse it.
355                  */
356                 if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) )
357                 {
358                         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems );
359                         descr->nitems++;
360
361                         descr->items[0] = newClipboardMenuItem(descr);
362                 }
363         }
364
365         if ( descr->items[0]->buffer )
366                 g_free( descr->items[0]->buffer );
367
368         descr->items[0]->buffer = g_memdup(buffer, buflen+1);
369         descr->items[0]->buflen = buflen;
370         descr->items[0]->flags = 0;
371
372         /*
373          * make item's name, we should remember that buffer
374          * is in UTF-8 encoding, ie multibyte characters
375          */
376
377         ptrname = itemname;
378         ptrbuffer = (gchar*)buffer;
379
380         for(i=0; *ptrbuffer && (ptrbuffer - buffer)<buflen && i < MaxItemName; i++)
381         {
382                 int     charlen = g_utf8_offset_to_pointer(ptrbuffer, 1) - ptrbuffer; 
383                 gunichar         widechar;
384
385                 if ( charlen <= 0 )
386                         break;
387
388                 widechar = g_utf8_get_char_validated( ptrbuffer, charlen );
389
390                 if (!g_unichar_isdefined(widechar))
391                         break;
392
393                 if ( g_unichar_isprint( widechar ) )
394                 {
395                         memmove( ptrname, ptrbuffer, charlen );
396                         ptrname += charlen;
397                 }
398                 else
399                 {
400                         *ptrname = '_';
401                         ptrname++;
402                 }
403
404                 ptrbuffer += charlen;
405         }
406
407         *ptrname = '\0';
408
409         /*
410          * setting up GtkMenuItem's title
411          */
412         gtk_widget_set_style( GTK_BIN(descr->items[0]->widget)->child, descr->style_normal);
413         gtk_label_set_text(
414                 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->items[0]->widget)) ),
415                 itemname
416         );
417 }
418
419 /*
420  * Initialize main struct
421  */
422 static void
423 initHistMenu( ClipboardDescr *descr )
424 {
425         /*
426          * set up hotkey to call menu
427          */
428         descr->key = gdk_keyval_from_name(KeyToPopup);
429         descr->mask = MaskKeyToPopup;
430         gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr);
431         assignKey(descr);
432
433         /*
434          * Create menu
435          */
436         descr->widget = gtk_menu_new();
437         gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history");
438         gtk_widget_show(descr->widget);
439
440         descr->nitems = 0;
441         descr->nlocked = 0;
442
443         /*
444          * make styles for locked and normal items
445          */
446         descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget));
447
448         descr->style_locked = gtk_style_copy(gtk_widget_get_style(descr->widget));
449         gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_NORMAL]) );
450         gdk_color_parse(LockedColor, &(descr->style_locked->fg[GTK_STATE_PRELIGHT]) );
451
452         /*
453          * Create first item and mark it as INIT due to
454          * void menu is showed very bad
455          */
456         addMenuItem( descr, StartupItem, strlen(StartupItem) );
457         descr->items[0]->flags = CMI_INIT;
458 }
459
460 /*
461  * Retrieves content of clipoard in a text form
462  */
463 static void
464 receiveText(GtkClipboard *cpb, const gchar *text, gpointer data) 
465 {
466         if ( text != NULL ) 
467                 addMenuItem( (ClipboardDescr*)data, text, strlen(text) ); 
468 }
469
470 /*
471  * Clipboard change signal handler. It requests new content of
472  * clipboard in a text form
473  */
474 static void
475 ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data)
476 {
477         gtk_clipboard_request_text( clipboard, receiveText, data );     
478 }
479
480 int 
481 main( int argc, char *argv[] ) {
482         ClipboardDescr  descr;
483         
484         gtk_set_locale();
485     gtk_init(&argc, &argv);
486
487         initHistMenu(&descr);   
488
489         /*
490          * every time cliboard's content changing ClipboardChange function
491          * will be called
492          */
493         g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
494                                         "owner-change",
495                                         (GCallback)ClipboardChange,
496                                         (gpointer)&descr);
497
498     gtk_main ();
499      
500     return(0);
501 }
502