1 /* gtkspell - a spell-checking addon for GtkText
2 * Copyright (c) 2000 Evan Martin.
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 Stuphead: (C) 2000,2001 Grigroy Bakunov, Sergey Pinaev
23 * Adapted for Sylpheed (Claws) (c) 2001 by Hiroyuki Yamamoto &
24 * The Sylpheed Claws Team.
27 #if defined(HAVE_CONFIG_H)
34 #include <sys/types.h>
47 #include <prefs_common.h>
52 #include <gtk/gtkoptionmenu.h>
53 #include <gtk/gtkmenu.h>
54 #include <gtk/gtkmenuitem.h>
60 /* size of the text buffer used in various word-processing routines. */
63 /* number of suggestions to display on each menu. */
66 /* because we keep only one copy of the spell program running,
67 * all ispell-related variables can be static.
69 volatile pid_t spell_pid = -1;
70 static int sp_fd_write[2], sp_fd_read[2];
71 static int signal_set_up = 0;
73 static GdkColor highlight = { 0, 255 * 256, 0, 0 };
75 static void entry_insert_cb(GtkXText *gtktext,
76 gchar *newtext, guint len, guint *ppos, gpointer d);
77 static void set_up_signal();
79 int gtkspell_running()
81 return (spell_pid > 0);
84 /* functions to interface with pipe */
85 static void writetext(unsigned char *text_ccc)
87 write(sp_fd_write[1], text_ccc, strlen(text_ccc));
90 static int readpipe(unsigned char *buf, int bufsize)
93 len = read(sp_fd_read[0], buf, bufsize - 1);
95 debug_print(_("*** readpipe: read: %s\n"), strerror(errno));
97 } else if (len == 0) {
98 debug_print(_("*** readpipe: pipe closed.\n"));
100 } else if (len == bufsize - 1) {
101 debug_print(_("*** readpipe: buffer overflowed?\n"));
108 static int readline(unsigned char *buf)
110 return readpipe(buf, BUFSIZE);
113 static int readresponse(unsigned char *buf)
116 len = readpipe(buf, BUFSIZE);
118 /* all ispell responses of any reasonable length should end in \n\n.
119 * depending on the speed of the spell checker, this may require more
121 if (len >= 2 && (buf[len - 1] != '\n' || buf[len - 2] != '\n')) {
122 len += readpipe(buf + len, BUFSIZE - len);
125 /* now we can remove all of the the trailing newlines. */
126 while (len > 0 && buf[len - 1] == '\n')
134 if (gtkspell_running()) {
135 kill(spell_pid, SIGTERM);
136 debug_print(_("*** Kill pid[%i] returned: %s\n"), spell_pid, strerror(errno));
137 while (spell_pid != -1);
141 static int poller(int buffer)
149 FD_SET(buffer, &rfds);
150 memset(&tv, 0, sizeof(tv));
153 return select(buffer + 1, &rfds, NULL, NULL, &tv);
156 int gtkspell_start(unsigned char *path, char * args[])
159 FILE *sav_stdin, *sav_stdout, *sav_stderr;
163 if (gtkspell_running()) {
164 debug_print(_("*** gtkspell_start called while already running.\n"));
168 if (!signal_set_up) {
179 debug_print(_("*** fork: %s\n"), strerror(errno));
181 } else if (spell_pid == 0) {
182 sav_stdin = fdopen(dup(fileno(stdin)), "r");
183 sav_stdout = fdopen(dup(fileno(stdout)), "w");
184 sav_stderr = fdopen(dup(fileno(stderr)), "w");
185 dup2(sp_fd_write[0], 0);
186 dup2(sp_fd_read[1], 1);
187 dup2(fd_error[1], 2);
188 close(sp_fd_read[0]);
190 close(sp_fd_write[1]);
193 if (execvp(args[0], args) < 0)
194 //DONT call debug_print here, because stdout is closed at this moment
195 fprintf(sav_stderr, _("*** execvp('%s'): %s\n"), args[0], strerror(errno));
197 if (execv(path, args) < 0)
198 //DONT call debug_print here, because stdout is closed at this moment
199 fprintf(sav_stderr, _("*** execv('%s'): %s\n"), path, strerror(errno));
201 /* if we get here, we failed.
202 * send some text on the pipe to indicate status.
204 write(sp_fd_read[1], "!", 1);
208 retncode = poller(sp_fd_read[1]);
210 debug_print(_("*** Spell comand failed: %s.\n"), strerror(errno));
215 /* ispell should print something like this:
216 * @(#) International Ispell Version 3.1.20 10/10/95
217 * if it doesn't, it's an error. */
219 debug_print(_("*** ispell didnt print '@'\n"));
225 /* put ispell into terse mode.
226 * this makes it not respond on correctly spelled words. */
232 static GList* misspelled_suggest(unsigned char *word)
234 unsigned char buf[BUFSIZE];
235 unsigned char *newword;
238 sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
241 switch (buf[0]) { /* first char is ispell command. */
242 case 0: /* no response: word is ok. */
244 case 10: /* just enter word is ok. */
246 case '&': /* misspelled, with suggestions */
247 /* & <orig> <count> <ofs>: <miss>, <miss>, <guess>, ... */
248 strtok(buf, " "); /* & */
249 newword = strtok(NULL, " "); /* orig */
250 l = g_list_append(l, g_strdup(newword));
251 newword = strtok(NULL, " "); /* count */
252 count = atoi(newword);
253 strtok(NULL, " "); /* ofs: */
255 while ((newword = strtok(NULL, ",")) != NULL) {
256 int len = strlen(newword);
257 if (newword[len - 1] == ' ' || newword[len - 1] == '\n')
258 newword[len - 1] = 0;
260 g_list_append(l, NULL); /* signal the "suggestions" */
262 /* add it to the list, skipping the initial space. */
264 g_strdup(newword[0] == ' ' ? newword + 1 : newword));
269 case '?': /* ispell is guessing. */
270 case '#': /* misspelled, no suggestions */
272 strtok(buf, " "); /* & */
273 newword = strtok(NULL, " "); /* orig */
274 l = g_list_append(l, g_strdup(newword));
277 debug_print(_("*** Unsupported spell command '%c'.\n"), buf[0]);
282 static int misspelled_test(unsigned char *word)
284 unsigned char buf[BUFSIZE];
285 sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
291 } else if (buf[0] == '&' || buf[0] == '#' || buf[0] == '?') {
295 debug_print(_("*** Unsupported spell command '%c'.\n"), buf[0]);
300 static gboolean iswordsep(unsigned char c)
302 return !isalpha(c) && c != '\'';
305 static guchar get_text_index_whar(GtkXText *gtktext, int pos)
309 text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), pos, pos + 1);
310 if (text == NULL) return 0;
316 static gboolean get_word_from_pos(GtkXText *gtktext, int pos, unsigned char* buf,
317 int *pstart, int *pend)
320 if (iswordsep(get_text_index_whar(gtktext, pos))) return FALSE;
322 for (start = pos; start >= 0; --start) {
323 if (iswordsep(get_text_index_whar(gtktext, start))) break;
327 for (end = pos; end < gtk_xtext_get_length(gtktext); end++) {
328 if (iswordsep(get_text_index_whar(gtktext, end))) break;
332 for (pos = start; pos < end; pos++) buf[pos - start] = get_text_index_whar(gtktext, pos);
333 buf[pos - start] = 0;
336 if (pstart) *pstart = start;
337 if (pend) *pend = end;
342 static gboolean get_curword(GtkXText *gtktext, unsigned char* buf,
343 int *pstart, int *pend)
345 int pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
346 return get_word_from_pos(gtktext, pos, buf, pstart, pend);
349 static void change_color(GtkXText *gtktext, int start, int end, GdkColor *color)
355 gtk_xtext_freeze(gtktext);
356 newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
357 // if (prefs_common.auto_makeispell) {
358 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
359 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
361 gtk_xtext_set_point(gtktext, start);
362 gtk_xtext_forward_delete(gtktext, end - start);
364 if (newtext && end - start > 0)
365 gtk_xtext_insert(gtktext, NULL, color, NULL, newtext, end - start);
366 // if (prefs_common.auto_makeispell) {
367 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
368 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
370 gtk_xtext_thaw(gtktext);
373 static gboolean check_at(GtkXText *gtktext, int from_pos)
376 unsigned char buf[BUFSIZE];
377 if (from_pos < 0) return FALSE;
378 if (!get_word_from_pos(gtktext, from_pos, buf, &start, &end)) {
381 if (misspelled_test(buf)) {
382 if (highlight.pixel == 0) {
383 /* add an entry for the highlight in the color map. */
384 GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
385 gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);
388 change_color(gtktext, start, end, &highlight);
391 change_color(gtktext, start, end,
392 &(GTK_WIDGET(gtktext)->style->fg[0]));
397 void gtkspell_check_all(GtkXText *gtktext)
404 if (!gtkspell_running()) return ;
406 len = gtk_xtext_get_length(gtktext);
408 adj_value = gtktext->vadj->value;
409 gtk_xtext_freeze(gtktext);
410 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
412 while (pos < len && iswordsep(get_text_index_whar(gtktext, pos)))
414 while (pos < len && !iswordsep(get_text_index_whar(gtktext, pos)))
417 check_at(gtktext, pos - 1);
419 gtk_xtext_thaw(gtktext);
420 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
421 gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
425 static void entry_insert_cb(GtkXText *gtktext,
426 gchar *newtext, guint len, guint *ppos, gpointer d)
429 if (!gtkspell_running()) return ;
431 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
432 GTK_SIGNAL_FUNC(entry_insert_cb),
434 gtk_xtext_insert(GTK_XTEXT(gtktext), NULL,
435 &(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
437 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
438 GTK_SIGNAL_FUNC(entry_insert_cb),
440 gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
442 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
444 if (iswordsep(newtext[0])) {
445 /* did we just end a word? */
446 if (*ppos >= 2) check_at(gtktext, *ppos - 2);
448 /* did we just split a word? */
449 if (*ppos < gtk_xtext_get_length(gtktext))
450 check_at(gtktext, *ppos + 1);
452 /* check as they type, *except* if they're typing at the end (the most
455 if (*ppos < gtk_xtext_get_length(gtktext) &&
456 !iswordsep(get_text_index_whar(gtktext, *ppos)))
457 check_at(gtktext, *ppos - 1);
460 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
463 static void entry_delete_cb(GtkXText *gtktext,
464 gint start, gint end, gpointer d)
467 if (!gtkspell_running()) return ;
469 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
470 check_at(gtktext, start - 1);
471 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
472 gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
473 /* this is to *UNDO* the selection, in case they were holding shift
474 * while hitting backspace. */
477 static void replace_word(GtkWidget *w, gpointer d)
480 unsigned char *newword;
481 unsigned char buf[BUFSIZE];
482 gtk_xtext_freeze(GTK_XTEXT(d));
484 gtk_label_get(GTK_LABEL(GTK_BIN(w)->child), (gchar**) &newword);
485 get_curword(GTK_XTEXT(d), buf, &start, &end);
487 gtk_xtext_set_point(GTK_XTEXT(d), end);
488 gtk_xtext_backward_delete(GTK_XTEXT(d), end - start);
489 gtk_xtext_insert(GTK_XTEXT(d), NULL, NULL, NULL, newword, strlen(newword));
491 gtk_xtext_thaw(GTK_XTEXT(d));
494 static GtkMenu *make_menu(GList *l, GtkXText *gtktext)
496 GtkWidget *menu, *item;
497 unsigned char *caption;
498 menu = gtk_menu_new();
500 caption = g_strdup_printf(_("Not in dictionary: %s"), (unsigned char*)l->data);
501 item = gtk_menu_item_new_with_label(caption);
502 /* I'd like to make it so this item is never selectable, like
503 * the menu titles in the GNOME panel... unfortunately, the GNOME
504 * panel creates their own custom widget to do this! */
505 gtk_widget_show(item);
506 gtk_menu_append(GTK_MENU(menu), item);
508 item = gtk_menu_item_new();
509 gtk_widget_show(item);
510 gtk_menu_append(GTK_MENU(menu), item);
514 item = gtk_menu_item_new_with_label(_("(no suggestions)"));
515 gtk_widget_show(item);
516 gtk_menu_append(GTK_MENU(menu), item);
518 GtkWidget *curmenu = menu;
521 if (l->data == NULL && l->next != NULL) {
523 curmenu = gtk_menu_new();
524 item = gtk_menu_item_new_with_label(_("Others..."));
525 gtk_widget_show(item);
526 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
527 gtk_menu_append(GTK_MENU(curmenu), item);
529 } else if (count > MENUCOUNT) {
531 item = gtk_menu_item_new_with_label(_("More..."));
532 gtk_widget_show(item);
533 gtk_menu_append(GTK_MENU(curmenu), item);
534 curmenu = gtk_menu_new();
535 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
537 item = gtk_menu_item_new_with_label((unsigned char*)l->data);
538 gtk_signal_connect(GTK_OBJECT(item), "activate",
539 GTK_SIGNAL_FUNC(replace_word), gtktext);
540 gtk_widget_show(item);
541 gtk_menu_append(GTK_MENU(curmenu), item);
543 } while ((l = l->next) != NULL);
545 return GTK_MENU(menu);
548 static void popup_menu(GtkXText *gtktext, GdkEventButton *eb)
550 unsigned char buf[BUFSIZE];
552 if (!get_curword(gtktext, buf, NULL, NULL)) return ;
553 if (buf == NULL) return ;
554 list = misspelled_suggest(buf);
556 gtk_menu_popup(make_menu(list, gtktext), NULL, NULL, NULL, NULL,
557 eb->button, eb->time);
558 for (l = list; l != NULL; l = l->next)
564 /* ok, this is pretty wacky:
565 * we need to let the right-mouse-click go through, so it moves the cursor,
566 * but we *can't* let it go through, because GtkText interprets rightclicks as
567 * weird selection modifiers.
569 * so what do we do? forge rightclicks as leftclicks, then popup the menu.
572 static gint button_press_intercept_cb(GtkXText *gtktext, GdkEvent *e, gpointer d)
577 if (!gtkspell_running()) return FALSE;
579 if (e->type != GDK_BUTTON_PRESS) return FALSE;
580 eb = (GdkEventButton*) e;
582 if (eb->button != 3) return FALSE;
584 /* forge the leftclick */
587 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
588 GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
589 gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
591 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
592 GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
593 gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");
595 /* now do the menu wackiness */
596 popup_menu(gtktext, eb);
600 void gtkspell_uncheck_all(GtkXText *gtktext)
605 adj_value = gtktext->vadj->value;
606 gtk_xtext_freeze(gtktext);
607 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
608 text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
609 gtk_xtext_set_point(gtktext, 0);
610 gtk_xtext_forward_delete(gtktext, gtk_xtext_get_length(gtktext));
611 gtk_xtext_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
612 gtk_xtext_thaw(gtktext);
614 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
615 gtk_adjustment_set_value(gtktext->vadj, adj_value);
618 void gtkspell_attach(GtkXText *gtktext)
620 // if (prefs_common.auto_makeispell) {
621 gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
622 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
623 gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
624 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
626 gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
627 GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
631 void gtkspell_detach(GtkXText *gtktext)
633 // if (prefs_common.auto_makeispell) {
634 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
635 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
636 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
637 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
639 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
640 GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
642 gtkspell_uncheck_all(gtktext);
645 static void sigchld(int param)
649 debug_print(_("*** SIGCHLD called. (ispell pid: %i)\n"), spell_pid);
650 while ((retval = waitpid( -1, &retstat, WNOHANG)) > 0) {
651 if (retval == spell_pid) {
652 debug_print(_("*** SIGCHLD called for ispell. (pid: %i)\n"), spell_pid);
658 static void set_up_signal()
660 /* RETSIGTYPE is found in autoconf's config.h */
662 typedef RETSIGTYPE (*sighandler)(int);
663 signal(SIGCHLD, (sighandler)sigchld);
665 /* otherwise, just hope it works */
666 signal(SIGCHLD, sigchld);
670 /*** Sylpheed (Claws) ***/
672 static GSList *create_empty_dictionary_list(void)
677 dict = g_new0(Dictionary, 1);
678 dict->name = g_strdup(_("None"));
680 return g_slist_append(list, dict);
683 /* gtkspell_get_dictionary_list() - returns list of dictionary names and the full
684 * path of file names. */
685 GSList *gtkspell_get_dictionary_list(const gchar *ispell_path)
688 gchar *dict_path, *tmp, *prevdir;
696 /* ASSUME: ispell_path is full path */
697 dict_path = strstr(ispell_path + 1, G_DIR_SEPARATOR_S);
698 tmp = g_strndup(ispell_path, dict_path - ispell_path);
700 /* ASSUME: ispell dictionaries in PREFIX/lib/ispell */
701 dict_path = g_strconcat(tmp, G_DIR_SEPARATOR_S, "lib", G_DIR_SEPARATOR_S, "ispell", NULL);
705 #warning TODO: no directory change
708 prevdir = g_get_current_dir();
709 if (chdir(dict_path) <0) {
710 FILE_OP_ERROR(dict_path, "chdir");
713 return create_empty_dictionary_list();
716 debug_print(_("Checking for dictionaries in %s\n"), dict_path);
718 if (NULL != (dir = opendir("."))) {
719 while (NULL != (ent = readdir(dir))) {
720 /* search for hash table */
721 if (NULL != (tmp = strstr(ent->d_name, ".hash"))) {
722 dict = g_new0(Dictionary, 1);
723 dict->name = g_strndup(ent->d_name, tmp - ent->d_name);
724 dict->path = g_strconcat(dict_path, G_DIR_SEPARATOR_S, ent->d_name, NULL);
725 debug_print(_("Found dictionary %s\n"), dict->path);
726 list = g_slist_append(list, dict);
732 FILE_OP_ERROR(dict_path, "opendir");
733 debug_print(_("No dictionary found\n"));
734 list = create_empty_dictionary_list();
742 void gtkspell_free_dictionary_list(GSList *list)
746 for (walk = list; walk != NULL; walk = g_slist_next(walk))
748 dict = (Dictionary *) walk->data;
758 static void dictionary_option_menu_item_data_destroy(gpointer data)
760 gchar *str = (gchar *) data;
766 GtkWidget *gtkspell_dictionary_option_menu_new(const gchar *ispell_path)
768 GSList *dict_list, *tmp;
773 dict_list = gtkspell_get_dictionary_list(ispell_path);
774 g_return_val_if_fail(dict_list, NULL);
776 menu = gtk_menu_new();
778 for (tmp = dict_list; tmp != NULL; tmp = g_slist_next(tmp)) {
779 dict = (Dictionary *) tmp->data;
780 item = gtk_menu_item_new_with_label(dict->name);
782 gtk_object_set_data_full(GTK_OBJECT(item), "full_path",
783 g_strdup(dict->path),
784 dictionary_option_menu_item_data_destroy);
785 gtk_menu_append(GTK_MENU(menu), item);
786 gtk_widget_show(item);
789 gtk_widget_show(menu);
791 gtkspell_free_dictionary_list(dict_list);
796 gchar *gtkspell_get_dictionary_menu_active_item(GtkWidget *menu)
801 g_return_val_if_fail(GTK_IS_MENU(menu), NULL);
802 menuitem = gtk_menu_get_active(GTK_MENU(menu));
804 result = gtk_object_get_data(GTK_OBJECT(menuitem), "full_path");
805 g_return_val_if_fail(result, NULL);
807 return g_strdup(result);