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
21 #include <sys/types.h>
32 #define GTKSPELL_USE_GNOME
34 #ifdef GTKSPELL_USE_GNOME
36 #endif /* GTKSPELL_USE_GNOME */
41 * handle dictionary changes
42 * asynchronous lookups
45 /* size of the text buffer used in various word-processing routines. */
47 /* number of suggestions to display on each menu. */
49 #define BUGEMAIL "gtkspell-devel@lists.sourceforge.net"
51 /* because we keep only one copy of the spell program running,
52 * all ispell-related variables can be static.
54 static pid_t spell_pid = -1;
55 static int fd_write[2] = {0}, fd_read[2] = {0};
56 static int signal_set_up = 0;
59 static GdkColor highlight = { 0, 255*256, 0, 0 };
61 static void entry_insert_cb(GtkText *gtktext,
62 gchar *newtext, guint len, guint *ppos, gpointer d);
63 static void set_up_signal();
65 int gtkspell_running() {
66 return (spell_pid > 0);
69 static void error_print(const char *fmt, ...) {
72 fprintf(stderr, "gtkspell: ");
73 vfprintf(stderr, fmt, ap);
77 /* functions to interface with pipe */
78 static void writetext(char *text) {
79 write(fd_write[1], text, strlen(text));
81 static int readpipe(char *buf, int bufsize) {
83 len = read(fd_read[0], buf, bufsize-1);
85 error_print("read: %s\n", strerror(errno));
87 } else if (len == 0) {
88 error_print("pipe closed.\n");
90 } else if (len == bufsize-1) {
91 error_print("buffer overflowed?\n");
97 static int readline(char *buf) {
98 return readpipe(buf, BUFSIZE);
101 static int readresponse(char *buf) {
103 len = readpipe(buf, BUFSIZE);
105 /* all ispell responses of any reasonable length should end in \n\n.
106 * depending on the speed of the spell checker, this may require more
108 if (len >= 2 && (buf[len-1] != '\n' || buf[len-2] != '\n')) {
109 len += readpipe(buf+len, BUFSIZE-len);
112 /* now we can remove all of the the trailing newlines. */
113 while (len > 0 && buf[len-1] == '\n')
120 void gtkspell_stop() {
121 if (gtkspell_running()) {
122 kill(spell_pid, SIGHUP);
129 int gtkspell_start(char *path, char * args[]) {
133 if (gtkspell_running()) {
134 error_print("gtkspell_start called while already running.\n");
138 if (!signal_set_up) {
149 error_print("fork: %s\n", strerror(errno));
151 } else if (spell_pid == 0) {
152 dup2(fd_write[0], 0);
160 dup2(fd_error[1], 2);
165 if (execvp(args[0], args) < 0)
166 error_print("execvp('%s'): %s\n", args[0], strerror(errno));
168 if (execv(path, args) < 0)
169 error_print("execv('%s'): %s\n", path, strerror(errno));
171 /* if we get here, we failed.
172 * send some text on the pipe to indicate status.
174 write(0, "!", 1); /* stdout _is_ the pipe. */
178 /* there are at least two ways to fail:
179 * - the exec() can fail
180 * - the exec() can succeed, but the program can dump the help screen
181 * we must check for both.
190 FD_SET(fd_error[0], &rfds);
191 FD_SET(fd_read[0], &rfds);
195 if (select(MAX(fd_error[0], fd_read[0])+1,
196 &rfds, NULL, NULL, &tv) < 0) {
197 /* FIXME: is this needed? */
198 error_print("Timed out waiting for spell command.\n");
203 if (FD_ISSET(fd_error[0], &rfds)) { /* stderr readable? */
204 error_print("Spell command printed on stderr -- probably failed.\n");
209 /* we're done with stderr, now. */
213 /* otherwise, fd_read[0] is set. */
216 /* ispell should print something like this:
217 * @(#) International Ispell Version 3.1.20 10/10/95
218 * if it doesn't, it's an error. */
225 /* put ispell into terse mode.
226 * this makes it not respond on correctly spelled words. */
232 static GList* misspelled_suggest(char *word) {
238 sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
242 switch (buf[0]) { /* first char is ispell command. */
243 case 0: /* no response: word is ok. */
245 case '&': /* misspelled, with suggestions */
246 /* & <orig> <count> <ofs>: <miss>, <miss>, <guess>, ... */
247 strtok(buf, " "); /* & */
248 newword = strtok(NULL, " "); /* orig */
249 l = g_list_append(l, g_strdup(newword));
250 newword = strtok(NULL, " "); /* count */
251 count = atoi(newword);
252 strtok(NULL, " "); /* ofs: */
254 while ((newword = strtok(NULL, ",")) != NULL) {
255 int len = strlen(newword);
256 if (newword[len-1] == ' ' || newword[len-1] == '\n')
259 g_list_append(l, NULL); /* signal the "suggestions" */
261 /* add it to the list, skipping the initial space. */
263 g_strdup(newword[0] == ' ' ? newword+1 : newword));
269 case '#': /* misspelled, no suggestions */
270 case '?': /* ispell is guessing. */
272 strtok(buf, " "); /* & */
273 newword = strtok(NULL, " "); /* orig */
274 l = g_list_append(l, g_strdup(newword));
277 error_print("Unsupported spell command '%c'.\n"
278 "This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
279 error_print("Input [%s]\nOutput [%s]\n", word, buf);
285 static int misspelled_test(char *word) {
287 sprintf(buf, "^%s\n", word); /* guard against ispell control chars */
293 } else if (buf[0] == '&' || buf[0] == '#' || buf[0] == '?') {
297 error_print("Unsupported spell command '%c'.\n"
298 "This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
299 error_print("Input [%s]\nOutput [%s]\n", word, buf);
303 static gboolean iswordsep(char c) {
304 return !isalpha(c) && c != '\'';
307 static gboolean get_word_from_pos(GtkText* gtktext, int pos, char* buf,
308 int *pstart, int *pend) {
311 if (iswordsep(GTK_TEXT_INDEX(gtktext, pos))) return FALSE;
313 for (start = pos; start >= 0; --start) {
314 if (iswordsep(GTK_TEXT_INDEX(gtktext, start))) break;
318 for (end = pos; end <= gtk_text_get_length(gtktext); end++) {
319 if (iswordsep(GTK_TEXT_INDEX(gtktext, end))) break;
323 for (pos = start; pos < end; pos++)
324 buf[pos-start] = GTK_TEXT_INDEX(gtktext, pos);
328 if (pstart) *pstart = start;
329 if (pend) *pend = end;
334 static gboolean get_curword(GtkText* gtktext, char* buf,
335 int *pstart, int *pend) {
336 int pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
337 return get_word_from_pos(gtktext, pos, buf, pstart, pend);
340 static void change_color(GtkText *gtktext,
341 int start, int end, GdkColor *color) {
342 char *newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
343 gtk_text_freeze(gtktext);
344 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
345 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
347 gtk_text_set_point(gtktext, start);
348 gtk_text_forward_delete(gtktext, end-start);
350 if (newtext && end-start > 0)
351 gtk_text_insert(gtktext, NULL, color, NULL, newtext, end-start);
353 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
354 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
355 gtk_text_thaw(gtktext);
359 static gboolean check_at(GtkText *gtktext, int from_pos) {
363 if (!get_word_from_pos(gtktext, from_pos, buf, &start, &end)) {
367 if (misspelled_test(buf)) {
368 if (highlight.pixel == 0) {
369 /* add an entry for the highlight in the color map. */
370 GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
371 gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);;
373 change_color(gtktext, start, end, &highlight);
376 change_color(gtktext, start, end,
377 &(GTK_WIDGET(gtktext)->style->fg[0]));
382 void gtkspell_check_all(GtkText *gtktext) {
388 if (!gtkspell_running()) return;
390 len = gtk_text_get_length(gtktext);
392 adj_value = gtktext->vadj->value;
393 gtk_text_freeze(gtktext);
394 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
396 while (pos < len && iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
398 while (pos < len && !iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
401 check_at(gtktext, pos-1);
403 gtk_text_thaw(gtktext);
404 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
407 static void entry_insert_cb(GtkText *gtktext,
408 gchar *newtext, guint len, guint *ppos, gpointer d) {
411 if (!gtkspell_running()) return;
413 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
414 GTK_SIGNAL_FUNC(entry_insert_cb),
416 gtk_text_insert(GTK_TEXT(gtktext), NULL,
417 &(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
418 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
419 GTK_SIGNAL_FUNC(entry_insert_cb),
421 gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
424 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
426 if (iswordsep(newtext[0])) {
427 /* did we just end a word? */
428 if (*ppos >= 2) check_at(gtktext, *ppos-2);
430 /* did we just split a word? */
431 if (*ppos < gtk_text_get_length(gtktext))
432 check_at(gtktext, *ppos+1);
434 /* check as they type, *except* if they're typing at the end (the most
437 if (*ppos < gtk_text_get_length(gtktext) &&
438 !iswordsep(GTK_TEXT_INDEX(gtktext, *ppos)))
439 check_at(gtktext, *ppos-1);
442 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
443 gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
446 static void entry_delete_cb(GtkText *gtktext,
447 gint start, gint end, gpointer d) {
450 if (!gtkspell_running()) return;
452 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
453 check_at(gtktext, start-1);
454 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
455 gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
456 /* this is to *UNDO* the selection, in case they were holding shift
457 * while hitting backspace. */
460 static void replace_word(GtkWidget *w, gpointer d) {
465 /* we don't save their position,
466 * because the cursor is moved by the click. */
468 gtk_text_freeze(GTK_TEXT(d));
470 gtk_label_get(GTK_LABEL(GTK_BIN(w)->child), &newword);
471 get_curword(GTK_TEXT(d), buf, &start, &end);
473 gtk_text_set_point(GTK_TEXT(d), end);
474 gtk_text_backward_delete(GTK_TEXT(d), end-start);
475 gtk_text_insert(GTK_TEXT(d), NULL, NULL, NULL, newword, strlen(newword));
477 gtk_text_thaw(GTK_TEXT(d));
480 static GtkMenu *make_menu(GList *l, GtkText *gtktext) {
481 GtkWidget *menu, *item;
483 menu = gtk_menu_new(); {
484 caption = g_strdup_printf("Not in dictionary: %s", (char*)l->data);
485 item = gtk_menu_item_new_with_label(caption);
486 /* I'd like to make it so this item is never selectable, like
487 * the menu titles in the GNOME panel... unfortunately, the GNOME
488 * panel creates their own custom widget to do this! */
489 gtk_widget_show(item);
490 gtk_menu_append(GTK_MENU(menu), item);
492 item = gtk_menu_item_new();
493 gtk_widget_show(item);
494 gtk_menu_append(GTK_MENU(menu), item);
498 item = gtk_menu_item_new_with_label("(no suggestions)");
499 gtk_widget_show(item);
500 gtk_menu_append(GTK_MENU(menu), item);
502 GtkWidget *curmenu = menu;
505 if (l->data == NULL && l->next != NULL) {
507 curmenu = gtk_menu_new();
508 item = gtk_menu_item_new_with_label("Other Possibilities...");
509 gtk_widget_show(item);
510 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
511 gtk_menu_append(GTK_MENU(curmenu), item);
513 } else if (count > MENUCOUNT) {
515 item = gtk_menu_item_new_with_label("More...");
516 gtk_widget_show(item);
517 gtk_menu_append(GTK_MENU(curmenu), item);
518 curmenu = gtk_menu_new();
519 gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
521 item = gtk_menu_item_new_with_label((char*)l->data);
522 gtk_signal_connect(GTK_OBJECT(item), "activate",
523 GTK_SIGNAL_FUNC(replace_word), gtktext);
524 gtk_widget_show(item);
525 gtk_menu_append(GTK_MENU(curmenu), item);
527 } while ((l = l->next) != NULL);
530 return GTK_MENU(menu);
533 static void popup_menu(GtkText *gtktext, GdkEventButton *eb) {
537 get_curword(gtktext, buf, NULL, NULL);
539 list = misspelled_suggest(buf);
541 gtk_menu_popup(make_menu(list, gtktext), NULL, NULL, NULL, NULL,
542 eb->button, eb->time);
543 for (l = list; l != NULL; l = l->next)
549 /* ok, this is pretty wacky:
550 * we need to let the right-mouse-click go through, so it moves the cursor,
551 * but we *can't* let it go through, because GtkText interprets rightclicks as
552 * weird selection modifiers.
554 * so what do we do? forge rightclicks as leftclicks, then popup the menu.
557 static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d) {
561 if (!gtkspell_running()) return FALSE;
563 if (e->type != GDK_BUTTON_PRESS) return FALSE;
564 eb = (GdkEventButton*) e;
566 if (eb->button != 3) return FALSE;
568 /* forge the leftclick */
571 gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
572 GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
573 gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
575 gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
576 GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
577 gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");
579 /* now do the menu wackiness */
580 popup_menu(gtktext, eb);
584 void gtkspell_uncheck_all(GtkText *gtktext) {
589 adj_value = gtktext->vadj->value;
590 gtk_text_freeze(gtktext);
591 origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
592 text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
593 gtk_text_set_point(gtktext, 0);
594 gtk_text_forward_delete(gtktext, gtk_text_get_length(gtktext));
595 gtk_text_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
596 gtk_text_thaw(gtktext);
598 gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
599 gtk_adjustment_set_value(gtktext->vadj, adj_value);
602 void gtkspell_attach(GtkText *gtktext) {
603 gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
604 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
605 gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
606 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
607 gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
608 GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
611 void gtkspell_detach(GtkText *gtktext) {
612 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
613 GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
614 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
615 GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
616 gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
617 GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
619 gtkspell_uncheck_all(gtktext);
622 static void sigchld(int param) {
623 if (gtkspell_running() &&
624 (waitpid(spell_pid, NULL, WNOHANG) == spell_pid)) {
627 /* a default SIGCHLD handler.
628 * what else to do here? */
629 waitpid(-1, NULL, WNOHANG);
633 static void set_up_signal() {
634 struct sigaction sigact;
635 memset(&sigact, 0, sizeof(struct sigaction));
637 sigact.sa_handler = sigchld;
638 sigaction(SIGCHLD, &sigact, NULL);