Added SILC Thread Queue API
[runtime.git] / apps / silcer / src / gtkurl.c
1 /* GtkUrl - A addon for GtkText that enables colored and clickable URLs
2  * Copyright (C) 2001 Benedikt Roth <Benedikt.Roth@bratislav.de>  
3  *   Based on code from 
4  *   gtkspell - a spell-checking addon for GtkText
5  *   Copyright (c) 2000 Evan Martin.
6  * 
7  * This library is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2 of the License, or (at your option) any later version.
11  * 
12  * This library is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
20  */
21
22 #define GTKURL_USE_GNOME
23
24 #include <gtk/gtk.h>
25 #ifdef GTKURL_USE_GNOME
26 #include <gnome.h>
27 #endif /* GTKURL_USE_GNOME */
28 #include <stdio.h>
29 #include <string.h>
30 #include <ctype.h>
31 #include <unistd.h>
32
33 #include "gtkurl.h"
34
35 /* FIXME? */
36 static GdkColor highlight = { 0, 0, 0, 255*256 };
37
38 enum {
39   GTKURL_NO_URL,
40   GTKURL_URL,
41   GTKURL_HOST
42 };
43
44
45 static void entry_insert_cb(GtkText *gtktext, gchar *newtext, guint len, guint *ppos, gpointer d);
46 static void entry_delete_cb(GtkText *gtktext, gint start, gint end, gpointer d);
47 static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d);
48
49 static void popup_menu(GtkText *gtktext, GdkEventButton *eb);
50 static GtkMenu *make_menu(gchar *url);
51
52 static gboolean visit_url_gnome_cb( GtkWidget *widget, gpointer *data);
53 static int my_poptParseArgvString(const char * s, int * argcPtr, char *** argvPtr);
54 static gboolean visit_url_cmd_cb( GtkWidget *widget, gpointer *data);
55
56 static gboolean check_at(GtkText *gtktext, gint from_pos);
57 static gchar *get_word_from_pos(GtkText* gtktext, gint pos, gint *pstart, gint *pend);
58 static gchar *get_curword(GtkText* gtktext, gint *pstart, gint *pend);
59
60 static void change_color(GtkText *gtktext, gint start, gint end, GdkColor *color);
61
62 static gboolean iswordsep(gchar c);
63 static gint is_url(gchar* word);
64
65
66
67 void gtkurl_attach(GtkText *gtktext)
68 {
69    gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
70                      GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
71    gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
72                             GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
73    gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
74                       GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
75 }
76
77
78 void gtkurl_detach(GtkText *gtktext)
79 {
80   gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
81                                 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
82   gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
83                                 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
84   gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext), 
85                                 GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
86   
87   gtkurl_uncheck_all(gtktext);
88 }
89
90
91 void gtkurl_check_all(GtkText *gtktext)
92 {
93   guint origpos;
94   guint pos = 0;
95   guint len;
96   float adj_value;
97
98   len = gtk_text_get_length(gtktext);
99   
100   adj_value = gtktext->vadj->value;
101   gtk_text_freeze(gtktext);
102   origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
103
104   while (pos < len) 
105     { 
106       while (pos < len && iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
107         pos++;
108       while (pos < len && !iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
109         pos++;
110       if (pos > 0)
111         check_at(gtktext, pos-1);
112     }
113
114   gtk_text_thaw(gtktext);
115   gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
116 }
117
118
119 void gtkurl_uncheck_all(GtkText *gtktext)
120 {
121   gint origpos;
122   gchar *text;
123   gfloat adj_value;
124
125   adj_value = gtktext->vadj->value;
126   gtk_text_freeze(gtktext);
127   origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
128   text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
129   gtk_text_set_point(gtktext, 0);
130   gtk_text_forward_delete(gtktext, gtk_text_get_length(gtktext));
131   gtk_text_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
132   gtk_text_thaw(gtktext);
133
134   gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
135   gtk_adjustment_set_value(gtktext->vadj, adj_value);
136 }
137
138
139 static void entry_insert_cb(GtkText *gtktext, gchar *newtext, guint len, guint *ppos, gpointer d)
140 {
141   gint origpos;
142
143   gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
144                                    GTK_SIGNAL_FUNC(entry_insert_cb), 
145                                    NULL );
146   
147   gtk_text_insert(GTK_TEXT(gtktext), NULL,
148                   &(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
149
150   gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
151                                      GTK_SIGNAL_FUNC(entry_insert_cb),
152                                      NULL);
153   
154   gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
155   *ppos += len;
156
157   origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
158
159   if (iswordsep(newtext[0])) 
160     {
161       /* did we just end a word? */
162       if (*ppos >= 2) check_at(gtktext, *ppos-2);
163       
164       /* did we just split a word? */
165       if (*ppos < gtk_text_get_length(gtktext))
166         check_at(gtktext, *ppos+1);
167     } 
168   else 
169     {
170       /* check as they type, *except* if they're typing at the end (the most
171        * common case.
172        */
173       if (*ppos < gtk_text_get_length(gtktext) && !iswordsep(GTK_TEXT_INDEX(gtktext, *ppos)))
174         check_at(gtktext, *ppos-1);
175     }
176
177   gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
178   gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
179 }
180
181
182 static void entry_delete_cb(GtkText *gtktext, gint start, gint end, gpointer d)
183 {
184   gint origpos;
185   
186   origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
187   check_at(gtktext, start-1);
188   gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
189   gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
190   /* this is to *UNDO* the selection, in case they were holding shift
191    * while hitting backspace. */
192 }
193
194
195 /* ok, this is pretty wacky:
196  * we need to let the right-mouse-click go through, so it moves the cursor, 
197  * but we *can't* let it go through, because GtkText interprets rightclicks as
198  * weird selection modifiers.
199  *
200  * so what do we do?  forge rightclicks as leftclicks, then popup the menu. 
201  * HACK HACK HACK. 
202  */
203 static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d)
204 {
205   GdkEventButton *eb;
206   gboolean retval;
207   
208   if (e->type != GDK_BUTTON_PRESS) return FALSE;
209   eb = (GdkEventButton*) e;
210
211   if (eb->button != 3)
212     return FALSE;
213
214   /* forge the leftclick */
215   eb->button = 1;
216
217   gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
218                                    GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
219   gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
220                           e, &retval);
221   gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
222                                      GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
223   gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");
224
225   /* now do the menu wackiness */
226   popup_menu(gtktext, eb);
227   return TRUE;
228 }
229
230
231 static void popup_menu(GtkText *gtktext, GdkEventButton *eb)
232 {
233   gchar *buf;
234   
235   buf = get_curword(gtktext, NULL, NULL);
236   
237   gtk_menu_popup(make_menu(buf), NULL, NULL, NULL, NULL,
238                  eb->button, eb->time);
239 }
240
241
242 static GtkMenu *make_menu(gchar *url)
243 {
244   GtkWidget *menu, *item;
245   gchar *caption;
246   gchar *s = "http://";
247   gchar *cmd;
248
249   switch( is_url(url) )
250     {
251     case GTKURL_URL:
252       url = g_strdup_printf("%s", url);
253       break;
254     case GTKURL_HOST: 
255       url = g_strdup_printf("%s%s", s, url);
256       break;
257     }     
258
259   menu = gtk_menu_new(); 
260   
261   caption = g_strdup_printf("%s", url);
262   item = gtk_menu_item_new_with_label(caption);
263   g_free(caption);
264   gtk_widget_set_sensitive( GTK_WIDGET(item), FALSE);
265   /* I'd like to make it so this item is never selectable, like
266    * the menu titles in the GNOME panel... unfortunately, the GNOME
267    * panel creates their own custom widget to do this! */
268   gtk_widget_show(item);
269   gtk_menu_append(GTK_MENU(menu), item);
270   
271   item = gtk_menu_item_new();
272   gtk_widget_show(item);
273   gtk_menu_append(GTK_MENU(menu), item);
274
275 #ifdef GTKURL_USE_GNOME
276   item = gtk_menu_item_new_with_label(_("Open with GNOME URL Handler"));
277   gtk_signal_connect(GTK_OBJECT(item), "activate", 
278                      GTK_SIGNAL_FUNC(visit_url_gnome_cb), g_strdup(url) );
279   gtk_menu_append(GTK_MENU(menu), item);
280   gtk_widget_show(item);
281 #endif /* GTKURL_USE_GNOME */
282     
283   item = gtk_menu_item_new_with_label(_("Open with Netscape (Existing)"));
284   cmd = g_strdup_printf("netscape -remote 'openURL(%s)'", url);
285   gtk_signal_connect(GTK_OBJECT(item), "activate",
286                      GTK_SIGNAL_FUNC(visit_url_cmd_cb), g_strdup(cmd) );
287   g_free(cmd);
288   gtk_menu_append(GTK_MENU(menu), item);
289   gtk_widget_show(item);
290
291   item = gtk_menu_item_new_with_label(_("Open with Netscape (New Window)"));
292   cmd = g_strdup_printf("netscape -remote 'openURL(%s,new-window)'", url);
293   gtk_signal_connect(GTK_OBJECT(item), "activate",
294                      GTK_SIGNAL_FUNC(visit_url_cmd_cb), g_strdup(cmd) );
295   g_free(cmd);
296   gtk_menu_append(GTK_MENU(menu), item);
297   gtk_widget_show(item);
298
299   item = gtk_menu_item_new_with_label(_("Open with Netscape (Run New)"));
300   cmd = g_strdup_printf("netscape %s", url);
301   gtk_signal_connect(GTK_OBJECT(item), "activate",
302                      GTK_SIGNAL_FUNC(visit_url_cmd_cb), g_strdup(cmd) );
303   g_free(cmd);
304   gtk_menu_append(GTK_MENU(menu), item);
305   gtk_widget_show(item);
306
307   g_free(url);
308   
309   return GTK_MENU(menu);
310 }
311
312
313 #ifdef GTKURL_USE_GNOME
314 static gboolean visit_url_gnome_cb( GtkWidget *widget, gpointer *data)
315 {
316   gnome_url_show((gchar *) data);
317   g_free(data);
318   return(TRUE);
319 }
320 #endif /* GTKURL_USE_GNOME */
321
322
323 /* this is taken from gnome-libs 1.2.4 */
324 #define POPT_ARGV_ARRAY_GROW_DELTA 5
325
326 static int my_poptParseArgvString(const char * s, int * argcPtr, char *** argvPtr)
327 {
328     char * buf, * bufStart, * dst;
329     const char * src;
330     char quote = '\0';
331     int argvAlloced = POPT_ARGV_ARRAY_GROW_DELTA;
332     char ** argv = malloc(sizeof(*argv) * argvAlloced);
333     const char ** argv2;
334     int argc = 0;
335     int i, buflen;
336
337     buflen = strlen(s) + 1;
338     bufStart = buf = alloca(buflen);
339     memset(buf, '\0', buflen);
340
341     src = s;
342     argv[argc] = buf;
343
344     while (*src) {
345         if (quote == *src) {
346             quote = '\0';
347         } else if (quote) {
348             if (*src == '\\') {
349                 src++;
350                 if (!*src) {
351                     free(argv);
352                     return 1;
353                 }
354                 if (*src != quote) *buf++ = '\\';
355             }
356             *buf++ = *src;
357         } else if (isspace(*src)) {
358             if (*argv[argc]) {
359                 buf++, argc++;
360                 if (argc == argvAlloced) {
361                     argvAlloced += POPT_ARGV_ARRAY_GROW_DELTA;
362                     argv = realloc(argv, sizeof(*argv) * argvAlloced);
363                 }
364                 argv[argc] = buf;
365             }
366         } else switch (*src) {
367           case '"':
368           case '\'':
369             quote = *src;
370             break;
371           case '\\':
372             src++;
373             if (!*src) {
374                 free(argv);
375                 return 1;
376             }
377             /* fallthrough */
378           default:
379             *buf++ = *src;
380         }
381
382         src++;
383     }
384
385     if (strlen(argv[argc])) {
386         argc++, buf++;
387     }
388
389     dst = malloc((argc + 1) * sizeof(*argv) + (buf - bufStart));
390     argv2 = (void *) dst;
391     dst += (argc + 1) * sizeof(*argv);
392     memcpy(argv2, argv, argc * sizeof(*argv));
393     argv2[argc] = NULL;
394     memcpy(dst, bufStart, buf - bufStart);
395
396     for (i = 0; i < argc; i++) {
397         argv2[i] = dst + (argv[i] - bufStart);
398     }
399
400     free(argv);
401
402     *argvPtr = (char **)argv2;  /* XXX don't change the API */
403     *argcPtr = argc;
404
405     return 0;
406 }
407
408
409 static gboolean visit_url_cmd_cb( GtkWidget *widget, gpointer *data)
410 {
411   int pid;
412   char **argv;
413   int argc;
414
415   if (my_poptParseArgvString ( (const char *)data, &argc, &argv) != 0)
416     return -1;
417
418   pid = fork ();
419   if (pid == -1)
420     return -1;
421   if (pid == 0)
422     {
423       execvp (argv[0], argv);
424       _exit (0);
425     } else
426       {
427         free (argv);
428         return pid;
429       }
430   
431   g_free(data);
432
433   return(TRUE);
434 }
435
436
437 static gboolean check_at(GtkText *gtktext, gint from_pos)
438 {
439   gint start, end;
440   gchar *buf;
441   
442   if ( ! (buf = get_word_from_pos(gtktext, from_pos, &start, &end)) ) 
443       return FALSE;
444   
445   if ( is_url(buf) ) 
446     {
447       if (highlight.pixel == 0) 
448         {
449           /* add an entry for the highlight in the color map. */
450           GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
451           gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);;
452         }
453       change_color(gtktext, start, end, &highlight);
454       return(TRUE);
455     } 
456   else 
457     { 
458       change_color(gtktext, start, end, &(GTK_WIDGET(gtktext)->style->fg[0]));
459       return(FALSE);
460     }
461 }
462
463
464 static gchar *get_word_from_pos(GtkText* gtktext, gint pos, gint *pstart, gint *pend)
465 {
466   GString *word = g_string_new("");
467   gint start, end;
468   gchar ch;
469   
470   if (iswordsep(GTK_TEXT_INDEX(gtktext, pos))) 
471     return(NULL);
472
473   /* Get start and end position from the word */
474   for (start = pos; start >= 0; --start) 
475     if (iswordsep(GTK_TEXT_INDEX(gtktext, start))) 
476       break;
477   start++;
478   
479   for (end = pos; end < gtk_text_get_length(gtktext); end++) 
480     if (iswordsep(GTK_TEXT_INDEX(gtktext, end)) )
481       break;
482
483   /* Be sure to not include punctation marks etc. */
484   for ( ;end>start; end-- )
485     {
486       ch = GTK_TEXT_INDEX(gtktext, end-1); 
487       if( isalpha(ch) || isdigit(ch) || ch == ':' )
488         break;
489     }
490
491   /* Get the word (everyting between start and end */
492   for (pos = start; pos < end; pos++)
493     g_string_append_c( word, GTK_TEXT_INDEX(gtktext, pos) );
494
495   if (pstart) 
496     *pstart = start;
497   if (pend) 
498     *pend = end;
499   
500   return(word->str);
501 }
502
503
504 static gchar *get_curword(GtkText* gtktext, gint *pstart, gint *pend)
505 {
506   gint pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
507   return(get_word_from_pos(gtktext, pos, pstart, pend));
508 }
509
510
511 static void change_color(GtkText *gtktext, gint start, gint end, GdkColor *color)
512 {
513   gchar *newtext;
514
515   /* So we don't need spaces at the very end of the text */
516   if ( end == gtk_text_get_length(GTK_TEXT(gtktext))+1 )
517     end--;
518
519   newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
520     
521   gtk_text_freeze(gtktext);
522   gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),  
523                                    GTK_SIGNAL_FUNC(entry_insert_cb), NULL); 
524         
525   gtk_text_set_point(gtktext, start);
526   gtk_text_forward_delete(gtktext, end-start);
527
528   if (newtext && end-start > 0)
529     gtk_text_insert(gtktext, NULL, color, NULL, newtext, end-start); 
530
531   gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
532                                      GTK_SIGNAL_FUNC(entry_insert_cb), NULL); 
533   gtk_text_thaw(gtktext);
534   g_free(newtext);
535 }
536
537
538 static gboolean iswordsep(gchar c)
539 {
540 /*      return !isalpha(c) && c != '\''; */
541   return( isspace(c) );
542 }
543
544
545 static gint is_url(gchar* word)
546 {
547      gint len;
548      if (!word)
549           return GTKURL_NO_URL;
550
551    len = strlen (word);
552
553    if (!strncasecmp (word, "irc://", 6))
554       return GTKURL_URL;
555
556    if (!strncasecmp (word, "irc.", 4))
557       return GTKURL_URL;
558
559    if (!strncasecmp (word, "ftp.", 4))
560       return GTKURL_URL;
561
562    if (!strncasecmp (word, "ftp:", 4))
563       return GTKURL_URL;
564
565    if (!strncasecmp (word, "www.", 4))
566       return GTKURL_URL;
567
568    if (!strncasecmp (word, "http:", 5))
569       return GTKURL_URL;
570
571    if (!strncasecmp (word, "https:", 6))
572       return GTKURL_URL;
573
574    if (!strncasecmp (word + len - 4, ".org", 4))
575       return GTKURL_HOST;
576
577    if (!strncasecmp (word + len - 4, ".net", 4))
578       return GTKURL_HOST;
579
580    if (!strncasecmp (word + len - 4, ".com", 4))
581       return GTKURL_HOST;
582
583    if (!strncasecmp (word + len - 4, ".edu", 4))
584       return GTKURL_HOST;
585
586    return GTKURL_NO_URL;
587 }