Add activate/deactivate menu item, wordaround for OO
[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  * Assign menu's popup to hotkey
97  */
98 static void
99 assignKey( ClipboardDescr *key ) 
100 {
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);
113 }
114
115 /*
116  * Catch key press signal handler
117  */
118 GdkFilterReturn
119 catchKey(GdkXEvent *gdk_xevent, GdkEvent *event, gpointer data) 
120 {
121         ClipboardDescr *key = (ClipboardDescr*)data;
122         XEvent  *xevent = (XEvent *)gdk_xevent;
123
124         if ( key && xevent->type == KeyPress && (xevent->xkey.state & key->mask) ) 
125         {
126                 if ( xevent->xkey.keycode == XKeysymToKeycode(GDK_DISPLAY(), key->key) ) 
127                 { 
128                         gtk_menu_popup(GTK_MENU(key->widget),
129                                                         NULL, NULL,
130                                                         NULL, NULL,
131                                                         0,
132                                                         GDK_CURRENT_TIME);
133                         
134                 }
135
136                 return GDK_FILTER_REMOVE;
137         }
138
139         return GDK_FILTER_CONTINUE;
140 }
141
142 /*
143  * Check existing and returns number of item corresponding to 
144  * buffer and -1 if it isn't found
145  */
146 static gint
147 existMenuItem(ClipboardDescr *descr, const gchar *buffer)
148 {
149         gint i;
150
151         for(i=0;i<descr->nitems;i++)
152                 if ( strcmp( descr->items[i]->buffer, buffer ) == 0 )
153                         return i;
154
155         return -1;
156 }
157
158 /*
159  * Move n-th item to the head of history
160  */
161 static void
162 moveMenuItemFirst( ClipboardDescr *descr, gint  n )
163 {
164         ClipboardMenuItem       *item;
165
166         if ( n == 0 ) /* nothing to do */
167                 return;
168
169         item = descr->items[n];
170         gtk_menu_reorder_child( GTK_MENU(descr->widget), item->widget, 0);
171
172         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * n );
173         descr->items[0] = item;
174 }
175
176 /*
177  * Finds history item by it's GtkMenuItem
178  */
179 static gint
180 lookupItem( ClipboardDescr *desc, GtkWidget *widget)
181 {
182         gint i;
183
184         for(i=0;i<desc->nitems;i++)
185                 if ( desc->items[i]->widget == widget )
186                         return i;
187
188         return -1;
189 }
190
191 /*
192  * Set history item to the head and put its value
193  * to the clipboard
194  */
195 static gboolean
196 itemActivate(GtkWidget *widget, gpointer user_data)
197 {
198         ClipboardDescr  *descr = (ClipboardDescr*)user_data;
199         gint    n;
200
201         n = lookupItem(descr, widget);
202         g_assert( n>=0 && n<descr->nitems);
203
204         /*
205          * do not activate item with init value
206          */
207         if ( (descr->items[n]->flags & CMI_INIT) == 0 )
208         {
209                 moveMenuItemFirst( descr, n );
210                 gtk_clipboard_set_text(
211                         gtk_clipboard_get(GDK_SELECTION_PRIMARY),
212                         descr->items[0]->buffer, -1 );
213         }
214         
215         return TRUE;
216 }
217
218 /*
219  * Lock/unlock item
220  */
221 static gboolean
222 itemToggleLock( GtkWidget *widget, GdkEvent *event, gpointer user_data)
223 {
224         ClipboardDescr          *descr = (ClipboardDescr*)user_data;
225         GdkEventButton          *bevent = (GdkEventButton *)event;
226         ClipboardMenuItem       *item;
227
228         gint    n;
229
230         /*
231          * Only right click should be assigned
232          */
233         if ( bevent->button != 3 )
234                 return  FALSE;
235
236         n = lookupItem(descr, widget);
237         g_assert( n>=0 && n<descr->nitems);
238         item = descr->items[n];
239
240         if ( item->flags & CMI_LOCKED )
241         {
242                 /*
243                  * just unlock item
244                  */
245                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_normal);
246                 item->flags &= ~CMI_LOCKED;
247                 g_assert( descr->nlocked>0 );
248                 descr->nlocked--;
249         }
250         else if ( descr->nlocked == descr->nitems-1 )
251         {
252                 GtkWidget       *dialog;
253
254                 /*
255                  * It's impossible to lock all items to prevent "no room"
256                  * ambiguity to add new value, but skip message for init 
257                  * item
258                  */
259
260                 if ( item->flags & CMI_INIT )
261                         return  FALSE;
262
263                 dialog = gtk_message_dialog_new( NULL,
264                                                         GTK_DIALOG_DESTROY_WITH_PARENT,
265                                                         GTK_MESSAGE_WARNING,
266                                                         GTK_BUTTONS_CLOSE,
267                                                         "Can not lock all items");
268
269                 gtk_dialog_run(GTK_DIALOG(dialog));
270                 gtk_widget_destroy(dialog);
271         }
272         else if ( (item->flags & CMI_INIT) == 0 )
273         {
274                 /*
275                  * lock item with no-init value
276                  */
277                 gtk_widget_set_style( GTK_BIN(item->widget)->child, descr->style_locked);
278                 item->flags |= CMI_LOCKED;
279                 descr->nlocked++;
280         }
281
282         return TRUE;
283 }
284
285 /*
286  * Makes new history item, creates corresponding GtkMenuItem
287  * and initializes it
288  */
289 static ClipboardMenuItem*
290 newClipboardMenuItem(ClipboardDescr *descr)
291 {
292         ClipboardMenuItem       *item;
293
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)) ),
298                 MaxItemName
299         );
300
301         g_signal_connect(item->widget,
302                                                 "activate",
303                                                 G_CALLBACK(itemActivate),
304                                                 (gpointer)descr);
305
306         g_signal_connect(item->widget,
307                                                 "button-release-event",
308                                                 G_CALLBACK(itemToggleLock),
309                                                 (gpointer)descr);
310
311         gtk_menu_shell_prepend(GTK_MENU_SHELL(descr->widget), item->widget);
312         gtk_widget_show( item->widget );
313
314         return item;
315 }
316
317 /*
318  * adds new value to lis of history items, but before checks its
319  * existing
320  */
321 static void
322 addMenuItem( ClipboardDescr *descr, gchar *buffer )
323 {
324         gint    i;
325         static  gchar itemname[5*MaxItemName + 1];
326         gchar   *ptrname, *ptrbuffer;
327
328         /*
329          * if such value already exists just move to to head
330          */
331         if ( (i=existMenuItem(descr, buffer)) >= 0 )
332         {
333                 moveMenuItemFirst( descr, i );
334                 return;
335         }
336
337         if ( descr->nitems == NHistItem )
338         {
339                 /*
340                  * List of items as already full, so find oldest non-locked
341                  * item and move it to head
342                  */
343                 for(i=descr->nitems - 1; i>= 0; i--)
344                         if ( (descr->items[i]->flags & CMI_LOCKED) == 0 )
345                                 break;
346
347                 g_assert( i>= 0 );
348                 if ( i!= 0 )
349                         moveMenuItemFirst( descr, i );  
350         } 
351         else 
352         {
353                 /*
354                  * add new item. But if list has only one element and it's a 
355                  * init value then reuse it.
356                  */
357                 if ( !(descr->nitems == 1 && (descr->items[0]->flags & CMI_INIT)) )
358                 {
359                         memmove( descr->items+1, descr->items, sizeof(ClipboardMenuItem*) * descr->nitems );
360                         descr->nitems++;
361
362                         descr->items[0] = newClipboardMenuItem(descr);
363                 }
364         }
365
366         if ( descr->items[0]->buffer )
367                 g_free( descr->items[0]->buffer );
368
369         descr->items[0]->buffer = buffer;
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 && 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 static gboolean
420 appToggleActivate(GtkWidget *widget, gpointer user_data)
421 {
422         ClipboardDescr  *descr = (ClipboardDescr*)user_data;
423
424         if ( descr->is_active == TRUE )
425         {
426                 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_locked);
427                 gtk_label_set_text(
428                         GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
429                         "Activate"
430                 );
431                 descr->is_active = FALSE;
432         }
433         else
434         {
435                 gtk_widget_set_style( GTK_BIN(descr->activator)->child, descr->style_normal);
436                 gtk_label_set_text(
437                         GTK_LABEL( gtk_bin_get_child(GTK_BIN(descr->activator)) ),
438                         "Deactivate"
439                 );
440                 descr->is_active = TRUE;
441         }
442         
443         return TRUE;
444 }
445
446 /*
447  * Initialize main struct
448  */
449 static void
450 initHistMenu( ClipboardDescr *descr )
451 {
452         GtkWidget       *separator;
453         /*
454          * set up hotkey to call menu
455          */
456         descr->key = gdk_keyval_from_name(KeyToPopup);
457         descr->mask = MaskKeyToPopup;
458         gdk_window_add_filter(GDK_ROOT_PARENT(), catchKey, descr);
459         assignKey(descr);
460
461         /*
462          * Create menu
463          */
464         descr->widget = gtk_menu_new();
465         gtk_menu_set_title(GTK_MENU(descr->widget), "Clipboard history");
466
467         separator = gtk_separator_menu_item_new();
468         gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), separator);
469
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)) ),
473                 MaxItemName
474         );
475         g_signal_connect(descr->activator,
476                                                 "activate",
477                                                 G_CALLBACK(appToggleActivate),
478                                                 (gpointer)descr);
479         gtk_menu_shell_append(GTK_MENU_SHELL(descr->widget), descr->activator);
480
481         gtk_widget_show(descr->activator);
482         gtk_widget_show(separator);
483         gtk_widget_show(descr->widget);
484
485         descr->nitems = 0;
486         descr->nlocked = 0;
487
488         /*
489          * make styles for locked and normal items
490          */
491         descr->style_normal = gtk_style_copy(gtk_widget_get_style(descr->widget));
492
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]) );
496
497         /*
498          * Create first item and mark it as INIT due to
499          * void menu is showed very bad
500          */
501         addMenuItem( descr, g_strdup(StartupItem) );
502         descr->items[0]->flags = CMI_INIT;
503
504         descr->is_active = TRUE;
505 }
506
507 /*
508  * Receive text content of buffer
509  *
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...
513  */
514 static void
515 receiveTarget(GtkClipboard *clipboard, GdkAtom *atoms, gint n_atoms, gpointer data)
516 {
517         gint i;
518         gboolean has_text = FALSE,
519                          has_multiple = FALSE,
520                          has_targets = FALSE;
521
522         /*
523          * checks all avaliable targets
524          */
525         for(i=0;i<n_atoms;i++)
526         {
527                 if ( atoms[i] == gdk_atom_intern_static_string("UTF8_STRING") ||
528                           atoms[i] == gdk_atom_intern_static_string("COMPOUND_TEXT") )
529                 {
530                         has_text = TRUE;
531                 }
532                 else if ( atoms[i] == gdk_atom_intern_static_string("MULTIPLE") )
533                 {
534                         has_multiple = TRUE;
535                 } 
536                 else if ( atoms[i] == gdk_atom_intern_static_string("TARGETS") )
537                 {
538                         has_targets = TRUE;
539                 } 
540         }
541
542         /*
543          * check for workaround
544          */
545         if ( has_text == TRUE && ( has_multiple==FALSE || has_targets == TRUE ) )
546         {
547                 gchar *text = gtk_clipboard_wait_for_text(clipboard);
548
549                 if (text)
550                         addMenuItem( (ClipboardDescr*)data, text );
551         }
552 }
553
554 /*
555  * Clipboard change signal handler. It requests new content of
556  * clipboard in a text form
557  */
558 static void
559 ClipboardChange(GtkClipboard *clipboard, GdkEvent *event, gpointer data)
560 {
561         /*
562          * Do real work if and only if application is active 
563          */
564         if ( ((ClipboardDescr*)data)->is_active ) 
565                 gtk_clipboard_request_targets( clipboard, receiveTarget, data );
566 }
567
568 int 
569 main( int argc, char *argv[] ) {
570         ClipboardDescr  descr;
571         
572         gtk_set_locale();
573     gtk_init(&argc, &argv);
574
575         initHistMenu(&descr);   
576
577         /*
578          * every time cliboard's content changing ClipboardChange function
579          * will be called
580          */
581         g_signal_connect(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
582                                         "owner-change",
583                                         G_CALLBACK(ClipboardChange),
584                                         (gpointer)&descr);
585
586     gtk_main ();
587      
588     return(0);
589 }
590