Fix compilation
[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         gint                                    flags;
70 } ClipboardMenuItem;
71
72 #define CMI_LOCKED              0x0001
73 #define CMI_INIT                0x0002
74
75 /*
76  * Describe all data needed to work clipboard history
77  */
78 typedef struct ClipboardDescr {
79         guint                           key;    /* key and msk to describe hotkey */
80         guint                           mask;
81
82         GtkWidget                       *widget; /* GtkMenu */
83
84         GtkWidget                       *activator; /* GtkMenuItem for manage active state */
85         gboolean                        is_active;
86
87         ClipboardMenuItem       *items[NHistItem];  /* histories item's */
88         gint                            nitems;                         /* number of items */
89         gint                            nlocked;                        /* number of locked items */
90
91         GtkStyle                        *style_normal;          /* default style for item */
92         GtkStyle                        *style_locked;          /* style for locked item */
93 } ClipboardDescr;
94
95
96 /*
97  * Assign menu's popup to hotkey
98  */
99 static void
100 assignKey( ClipboardDescr *key ) 
101 {
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);
114 }
115
116 /*
117  * Catch key press signal handler
118  */
119 GdkFilterReturn
120 catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data) 
121 {
122         ClipboardDescr *key = (ClipboardDescr*)data;
123         XEvent  *xevent = (XEvent *)gdk_xevent;
124
125         if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) ) 
126         {
127                 if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) ) 
128                 { 
129                         gtk_menu_popup(GTK_MENU(key->widget),
130                                                         NULL, NULL,
131                                                         NULL, NULL,
132                                                         0,
133                                                         GDK_CURRENT_TIME);
134                         
135                 }
136
137                 return GDK_FILTER_REMOVE;
138         }
139
140         return GDK_FILTER_CONTINUE;
141 }
142
143 /*
144  * Check existing and returns number of item corresponding to 
145  * buffer and -1 if it isn't found
146  */
147 static gint
148 existMenuItem(ClipboardDescr *descr, const gchar *buffer)
149 {
150         gint i;
151
152         for(i=0;i<descr->nitems;i++)
153                 if ( strcmp( descr->items[i]->buffer, buffer ) == 0 )
154                         return i;
155
156         return -1;
157 }
158
159 /*
160  * Move n-th item to the head of history
161  */
162 static void
163 moveMenuItemFirst( ClipboardDescr *descr, gint  n )
164 {
165         ClipboardMenuItem       *item;
166
167         if ( n == 0 ) /* nothing to do */
168                 return;
169
170         item = descr->items[n];
171         gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0);
172
173         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n );
174         descr->items[0] = item;
175 }
176
177 /*
178  * Finds history item by it's GtkMenuItem
179  */
180 static gint
181 lookupItem( ClipboardDescr *desc, GtkWidget *widget)
182 {
183         gint i;
184
185         for(i=0;i<desc->nitems;i++)
186                 if ( desc->items[i]->widget == widget )
187                         return i;
188
189         return -1;
190 }
191
192 /*
193  * Set history item to the head and put its value
194  * to the clipboard
195  */
196 static gboolean
197 itemActivate(GtkWidget *widget, gpointer user_data)
198 {
199         ClipboardDescr  *descr = (ClipboardDescr*)user_data;
200         gint    n;
201
202         n = lookupItem(descr, widget);
203         g_assert( n>=0 && n<descr->nitems);
204
205         /*
206          * do not activate item with init value
207          */
208         if ( (descr->items[n]->flags & CMI_INIT) == 0 )
209         {
210                 moveMenuItemFirst( descr, n );
211                 gtk_clipboard_set_text(
212                         gtk_clipboard_get(GDK_SELECTION_PRIMARY),
213                         descr->items[0]->buffer, -1 );
214         }
215         
216         return TRUE;
217 }
218
219 /*
220  * Lock/unlock item
221  */
222 static gboolean
223 itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data)
224 {
225         ClipboardDescr          *descr = (ClipboardDescr*)user_data;
226         GdkEventButton          *bevent = (GdkEventButton *)event;
227         ClipboardMenuItem       *item;
228
229         gint    n;
230
231         /*
232          * Only right click should be assigned
233          */
234         if ( bevent->button != 3 )
235                 return  FALSE;
236
237         n = lookupItem(descr, widget);
238         g_assert( n>=0 && n<descr->nitems);
239         item = descr->items[n];
240
241         if ( item->flags & CMI_LOCKED )
242         {
243                 /*
244                  * just unlock item
245                  */
246                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal);
247                 item->flags &= ~CMI_LOCKED;
248                 g_assert( descr->nlocked>0 );
249                 descr->nlocked--;
250         }
251         else if ( descr->nlocked == descr->nitems-1 )
252         {
253                 GtkWidget       *dialog;
254
255                 /*
256                  * It's impossible to lock all items to prevent "no room"
257                  * ambiguity to add new value, but skip message for init 
258                  * item
259                  */
260
261                 if ( item->flags & CMI_INIT )
262                         return  FALSE;
263
264                 dialog = gtk_message_dialog_new( NULL,
265                                                         GTK_DIALOG_DESTROY_WITH_PARENT,
266                                                         GTK_MESSAGE_WARNING,
267                                                         GTK_BUTTONS_CLOSE,
268                                                         "Can not lock all items");
269
270                 gtk_dialog_run(GTK_DIALOG(dialog));
271                 gtk_widget_destroy(dialog);
272         }
273         else if ( (item->flags & CMI_INIT) == 0 )
274         {
275                 /*
276                  * lock item with no-init value
277                  */
278                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked);
279                 item->flags |= CMI_LOCKED;
280                 descr->nlocked++;
281         }
282
283         return TRUE;
284 }
285
286 /*
287  * Makes new history item, creates corresponding GtkMenuItem
288  * and initializes it
289  */
290 static ClipboardMenuItem*
291 newClipboardMenuItem(ClipboardDescr *descr)
292 {
293         ClipboardMenuItem       *item;
294
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)) ),
299                 MaxItemName
300         );
301
302         g_signal_connect(item->widget,
303                                                 "activate",
304                                                 G_CALLBACK(itemActivate),
305                                                 (gpointer)descr);
306
307         g_signal_connect(item->widget,
308                                                 "button-release-event",
309                                                 G_CALLBACK(itemToggleLock),
310                                                 (gpointer)descr);
311
312         gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget);
313         gtk_widget_show( item->widget );
314
315         return item;
316 }
317
318 /*
319  * adds new value to lis of history items, but before checks its
320  * existing
321  */
322 static void
323 addMenuItem( ClipboardDescr *descr, gchar *buffer )
324 {
325         gint    i;
326         static  gchar itemname[5*MaxItemName + 1];
327         gchar   *ptrname, *ptrbuffer;
328
329         /*
330          * if such value already exists just move to to head
331          */
332         if ( (i=existMenuItem(descr, buffer)) >= 0 )
333         {
334                 moveMenuItemFirst( descr, i );
335                 g_free( buffer ); /* free buffer if is not needed */
336                 return;
337         }
338
339         if ( descr->nitems == NHistItem )
340         {
341                 /*
342                  * List of items as already full, so find oldest non-locked
343                  * item and move it to head
344                  */
345                 for(i=descr->nitems - 1; i>= 0; i--)
346                         if ( (descr->items[i]->flags & CMI_LOCKED) == 0 )
347                                 break;
348
349                 g_assert( i>= 0 );
350                 if ( i!= 0 )
351                         moveMenuItemFirst( descr, i );  
352         } 
353         else 
354         {
355                 /*
356                  * add new item. But if list has only one element and it's a 
357                  * init value then reuse it.
358                  */
359                 if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) )
360                 {
361                         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems );
362                         descr->nitems++;
363
364                         descr->items[0] = newClipboardMenuItem(descr);
365                 }
366         }
367
368         if ( descr->items[0]->buffer )
369                 g_free( descr->items[0]->buffer );
370
371         descr->items[0]->buffer = buffer;
372         descr->items[0]->flags = 0;
373
374         /*
375          * make item's name, we should remember that buffer
376          * is in UTF-8 encoding, ie multibyte characters
377          */
378
379         ptrname = itemname;
380         ptrbuffer = (gchar*)buffer;
381
382         for(i=0; *ptrbuffer && i < MaxItemName; i++)
383         {
384                 int     charlen = g_utf8_offset_to_pointer(ptrbuffer, 1) - ptrbuffer; 
385                 gunichar         widechar;
386
387                 if ( charlen <= 0 )
388                         break;
389
390                 widechar = g_utf8_get_char_validated( ptrbuffer, charlen );
391
392                 if (!g_unichar_isdefined(widechar))
393                         break;
394
395                 if ( g_unichar_isprint( widechar ) )
396                 {
397                         memmove( ptrname, ptrbuffer, charlen );
398                         ptrname += charlen;
399                 }
400                 else
401                 {
402                         *ptrname = '_';
403                         ptrname++;
404                 }
405
406                 ptrbuffer += charlen;
407         }
408
409         *ptrname = '\0';
410
411         /*
412          * setting up GtkMenuItem's title
413          */
414         gtk_widget_set_style( GTK_BIN(descr->items[0]->widget)->child, descr->style_normal);
415         gtk_label_set_text(
416                 GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->items[0]->widget)) ),
417                 itemname
418         );
419 }
420
421 static gboolean
422 appToggleActivate(GtkWidget *widget, gpointer user_data)
423 {
424         ClipboardDescr  *descr = (ClipboardDescr*)user_data;
425
426         if ( descr->is_active == TRUE )
427         {
428                 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_locked);
429                 gtk_label_set_text(
430                         GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
431                         "Activate"
432                 );
433                 descr->is_active = FALSE;
434         }
435         else
436         {
437                 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_normal);
438                 gtk_label_set_text(
439                         GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
440                         "Deactivate"
441                 );
442                 descr->is_active = TRUE;
443         }
444         
445         return TRUE;
446 }
447
448 /*
449  * Initialize main struct
450  */
451 static void
452 initHistMenu( ClipboardDescr *descr )
453 {
454         GtkWidget       *separator;
455         /*
456          * set up hotkey to call menu
457          */
458         descr->key = gdk_keyval_from_name(KeyToPopup);
459         descr->mask = MaskKeyToPopup;
460         gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr);
461         assignKey(descr);
462
463         /*
464          * Create menu
465          */
466         descr->widget = gtk_menu_new();
467         gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history");
468
469         separator = gtk_separator_menu_item_new();
470         gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), separator);
471
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)) ),
475                 MaxItemName
476         );
477         g_signal_connect(descr->activator,
478                                                 "activate",
479                                                 G_CALLBACK(appToggleActivate),
480                                                 (gpointer)descr);
481         gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), descr->activator);
482
483         gtk_widget_show(descr->activator);
484         gtk_widget_show(separator);
485         gtk_widget_show(descr->widget);
486
487         descr->nitems = 0;
488         descr->nlocked = 0;
489
490         /*
491          * make styles for locked and normal items
492          */
493         descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget));
494
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]) );
498
499         /*
500          * Create first item and mark it as INIT due to
501          * void menu is showed very bad
502          */
503         addMenuItem( descr, g_strdup(StartupItem) );
504         descr->items[0]->flags = CMI_INIT;
505
506         descr->is_active = TRUE;
507 }
508
509 /*
510  * Receive text content of buffer
511  *
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 :) )
520  */
521 static void
522 receiveTarget(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer data)
523 {
524         gint                    i;
525         gchar                   *text;
526         static struct   /* just an unnamed struct */ 
527         {               
528                 gchar   *target_prefix;
529                 gint     target_prefix_len;
530         }                               ignore_targets[] = {
531                                                 { "application/x-openoffice", -1 },
532                                                 { NULL, 0 } /* mark end */
533                                         },
534                                         *ptr ;
535
536
537         /*
538          * If there is not text in targets then return
539          */
540         if ( gtk_targets_include_text( atoms, n_atoms ) == FALSE )
541                 return;
542
543         /*
544          * init ignore target if it's needed 
545          */
546         if ( ignore_targets->target_prefix_len < 0 )
547         {
548                 ptr = ignore_targets;
549
550                 while( ptr->target_prefix )
551                 {
552                         ptr->target_prefix_len = strlen( ptr->target_prefix );
553                         ptr++;
554                 }
555         }
556
557         /*
558          * checks all avaliable targets for presence of ignorable
559          * targets ( which come from "wrong" applications )
560          */
561         for(i=0;i<n_atoms;i++)
562         {
563                 gchar   *target_name = gdk_atom_name( atoms[i] );
564
565                 if (!target_name)
566                         continue;
567
568                 ptr = ignore_targets;
569
570                 while( ptr->target_prefix )
571                 {
572                         if ( strncmp( target_name, ptr->target_prefix, ptr->target_prefix_len ) == 0 )
573                         {
574                                 /*
575                                  * It's found a ignorable targets - we will not accept text to
576                                  * prevent unexpected and surprised behaviour of X-selection
577                                  */
578                                  g_free(target_name);
579                                  return;
580                         }
581
582                         g_free(target_name);
583                         ptr++;
584                 }
585         }
586
587         /*
588          * Ok - selection has text, selection is not owned to strange application -
589          * request contents
590          */
591         text = gtk_clipboard_wait_for_text(clipboard);
592         /* be carefull - world has a lot of unexpected events :) */
593         if (text)
594                 addMenuItem( (ClipboardDescr*)data, text );
595 }
596
597 /*
598  * Clipboard change signal handler. It requests avaliable 
599  * targets to make checks about contents of selection
600  */
601 static void
602 ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data)
603 {
604         /*
605          * Do real work if and only if application is active 
606          */
607         if ( ((ClipboardDescr*)data)->is_active ) 
608                 gtk_clipboard_request_targets( clipboard, receiveTarget, data );
609 }
610
611 int 
612 main( int argc, char *argv[] ) {
613         ClipboardDescr  descr;
614         
615         gtk_set_locale();
616     gtk_init(&argc, &argv);
617
618         initHistMenu(&descr);   
619
620         /*
621          * every time cliboard's content changing ClipboardChange function
622          * will be called
623          */
624         g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
625                                         "owner-change",
626                                         G_CALLBACK(ClipboardChange),
627                                         (gpointer)&descr);
628
629     gtk_main ();
630      
631     return(0);
632 }
633