view src/gtkspell.c @ 2906:538c58b43eff

[gaim-migrate @ 2919] save save me from this wandered around the town all the thousand things i might miss and you think we'll suffer much think we'll close our eyes just to see the light pass us by with tomorrow coming hope that i don't let you down again said i'm so glad to be here does it mean a thing if only i could breathe what you breathe if only i could see what you see sit we'll take our time watching the flowers grow all the friends we've known say goodbye and you did you suffer much did you close your eyes just to see the night rush on by gathered all around you hope that we don't let you down again i said i'm so glad to be here does it mean a thing if only i could breathe what you breathe if only i could see what you see i said i'm so glad to be here does it mean a thing if only i could breathe what you breathe if only i could see what you see if only i could just believe a thing --Moist, "Breathe" (as transcribed by http://www.veddma.com/veddma/moist.htm) Patches from: Ari Pollak Ben Miller Mark Doliner Sean Egan Vincas Ciziunas Thanks everyone. Somewhere in the middle of all of this it started to get really tedious and annoying. I think it's getting close to the point where I quit. committer: Tailor Script <tailor@pidgin.im>
author Eric Warmenhoven <eric@warmenhoven.org>
date Fri, 21 Dec 2001 10:23:04 +0000
parents 4c6ad3d9db3f
children da324fe688b3
line wrap: on
line source

/* gtkspell - a spell-checking addon for GtkText
 * Copyright (c) 2000 Evan Martin.
 * vim: ts=4 sw=4
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

#include <gtk/gtk.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <unistd.h>   
#include <stdio.h>    
#include <signal.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>

/* TODO:
 * handle dictionary changes
 * asynchronous lookups
 */

/* size of the text buffer used in various word-processing routines. */
/* #define BUFSIZE 1024 */
/* number of suggestions to display on each menu. */
#define MENUCOUNT 10
#define BUGEMAIL "gtkspell-devel@lists.sourceforge.net"

/* because we keep only one copy of the spell program running, 
 * all ispell-related variables can be static.  
 */
static pid_t spell_pid = -1;
static int fd_write[2] = {0}, fd_read[2] = {0};
static int signal_set_up = 0;

/* FIXME? */
static GdkColor highlight = { 0, 255*256, 0, 0 };

static void entry_insert_cb(GtkText *gtktext, 
		gchar *newtext, guint len, guint *ppos, gpointer d);
static void set_up_signal();

int gtkspell_running() {
	return (spell_pid > 0);
}

/*
static void error_print(const char *fmt, ...) {
	va_list ap;
	va_start(ap, fmt);
	fprintf(stderr, "gtkspell: ");
	vfprintf(stderr, fmt, ap);
	va_end(ap);
}
*/
extern void debug_printf(char *, ...);
#define error_print debug_printf

/* functions to interface with pipe */
static void writetext(char *text) {
	write(fd_write[1], text, strlen(text));
}

static char *readline() {
	static gchar *buf = NULL;
	char *end;
	char *ret;
	char *tmp;

	/* read until we get a newline */
	while (!buf || (end = strchr(buf, '\n')) == NULL) {
		char space[1024];
		int ret = read(fd_read[0], space, 1023);
		if (ret <= 0) {
			error_print("read: %s\n", strerror(errno));
			return NULL;
		}
		space[ret] = 0;
		if (buf) {
			tmp = buf;
			buf = g_strconcat(tmp, space, NULL);
			g_free(tmp);
		} else
			buf = g_strdup(space);
	}

	/* we got a newline, and end points to it.
	 * copy out the data, reset buf, return */

	if (end[1] == 0) {
		/* only one line is in the buffer */
		ret = buf;
		buf = NULL;
		return ret;
	}

	ret = g_strndup(buf, end - buf + 1);
	tmp = buf;
	buf = g_strdup(end + 1);
	g_free(tmp);

	return ret;
}

static char *readresponse() {
	char *r1, *r2, *result;

	r1 = readline();
	if (!r1)
		return NULL;
	if (*r1 == '\n') {
		g_strchomp(r1);
		return r1;
	}
	r2 = readline();
	if (!r2) {
		g_free(r1);
		return NULL;
	}

	while (r2 && *r2 != '\n') {
		char *tmp = r1;
		r1 = g_strconcat(tmp, r2, NULL);
		g_free(tmp);
		g_free(r2);
		r2 = readline();
	}

	if (!r2) {
		g_free(r1);
		return NULL;
	}

	result = g_strconcat(r1, r2, NULL);
	g_free(r1);
	g_free(r2);
	g_strchomp(result);
	return result;
}


void gtkspell_stop() {
	if (gtkspell_running()) {
		kill(spell_pid, SIGHUP); 
		spell_pid = 0;
		close(fd_read[0]);
		close(fd_write[1]);
	}
}

int gtkspell_start(char *path, char * args[]) {
	int fd_error[2];

	if (gtkspell_running()) {
		error_print("gtkspell_start called while already running.\n");
		gtkspell_stop();
	}

	if (!signal_set_up) {
		set_up_signal();
		signal_set_up = 1;
	}

	pipe(fd_write);
	pipe(fd_read);
	pipe(fd_error);

	spell_pid = fork();
	if (spell_pid < 0) {
		error_print("fork: %s\n", strerror(errno));
		return -1;
	} else if (spell_pid == 0) {
		dup2(fd_write[0], 0);
		close(fd_write[0]);
		close(fd_write[1]);

		dup2(fd_read[1], 1);
		close(fd_read[0]);
		close(fd_read[1]);

		dup2(fd_error[1], 2);
		close(fd_error[0]);
		close(fd_error[1]);

		if (path == NULL) {
			if (execvp(args[0], args) < 0) 
				error_print("execvp('%s'): %s\n", args[0], strerror(errno));
		} else {
			if (execv(path, args) < 0) 
				error_print("execv('%s'): %s\n", path, strerror(errno));
		}
		/* if we get here, we failed.
		 * send some text on the pipe to indicate status.
		 */
		write(0, "!", 1); /* stdout _is_ the pipe. */

		_exit(0);
	} else {
		/* there are at least two ways to fail:
		 * - the exec() can fail
		 * - the exec() can succeed, but the program can dump the help screen
		 * we must check for both.
		 */
		fd_set rfds;
		struct timeval tv;
		char *buf;
		
		close(fd_write[0]);
		close(fd_read[1]);

		FD_ZERO(&rfds);
		FD_SET(fd_error[0], &rfds);
		FD_SET(fd_read[0], &rfds);
		tv.tv_sec = 2;
		tv.tv_usec = 0;

		if (select(MAX(fd_error[0], fd_read[0])+1, 
					&rfds, NULL, NULL, &tv) < 0) {
			/* FIXME: is this needed? */
			error_print("Timed out waiting for spell command.\n");
			gtkspell_stop();
			return -1;
		}

		if (FD_ISSET(fd_error[0], &rfds)) { /* stderr readable? */
			error_print("Spell command printed on stderr -- probably failed.\n");
			gtkspell_stop();
			return -1;
		}

		/* we're done with stderr, now. */
		close(fd_error[0]);
		close(fd_error[1]);

		/* otherwise, fd_read[0] is set. */
		buf = readline();

		/* ispell should print something like this:
		 * @(#) International Ispell Version 3.1.20 10/10/95
		 * if it doesn't, it's an error. */
		if (!buf || buf[0] != '@') {
			if (buf)
				g_free(buf);
			gtkspell_stop();
			return -1;
		}
		g_free(buf);
	}

	/* put ispell into terse mode.  
	 * this makes it not respond on correctly spelled words. */
	writetext("!\n");
	return 0;
}

static GList* misspelled_suggest(char *word) {
	char *buf;
	char *newword;
	GList *l = NULL;
	int count;

	if (!word)
		return NULL;

	buf = g_strdup_printf("^%s\n", word); /* guard against ispell control chars */
	writetext(buf);
	g_free(buf);
	buf = readresponse();

	if (!buf)
		return NULL;

	switch (buf[0]) { /* first char is ispell command. */
		case 0: /* no response: word is ok. */
			g_free(buf);
			return NULL;
		case '&': /* misspelled, with suggestions */
			/* & <orig> <count> <ofs>: <miss>, <miss>, <guess>, ... */
			strtok(buf, " "); /* & */
			newword = strtok(NULL, " "); /* orig */
			l = g_list_append(l, g_strdup(newword));
			newword = strtok(NULL, " "); /* count */
			count = atoi(newword);
			strtok(NULL, " "); /* ofs: */

			while ((newword = strtok(NULL, ",")) != NULL) {
				int len = strlen(newword);
				if (newword[len-1] == ' ' || newword[len-1] == '\n') 
					newword[len-1] = 0;
				if (count == 0) {
					g_list_append(l, NULL); /* signal the "suggestions" */
				}
				/* add it to the list, skipping the initial space. */
				l = g_list_append(l, 
						g_strdup(newword[0] == ' ' ? newword+1 : newword));

				count--;
			}
			g_free(buf);
			return l;

		case '#': /* misspelled, no suggestions */
		case '?': /* ispell is guessing. */
			/* # <orig> <ofs> */
			strtok(buf, " "); /* & */
			newword = strtok(NULL, " "); /* orig */
			l = g_list_append(l, g_strdup(newword));
			g_free(buf);
			return l;
		default:
			error_print("Unsupported spell command '%c'.\n"
					"This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
			error_print("Input [%s]\nOutput [%s]\n", word, buf);

	}
	g_free(buf);
	return NULL;
}

static int misspelled_test(char *word) {
	char *buf;

	if (word == NULL)
		return 0;

	buf = g_strdup_printf("^%s\n", word); /* guard against ispell control chars */
	writetext(buf);
	g_free(buf);
	buf = readresponse();

	if (!buf)
		return 0;

	if (buf[0] == 0) {
		g_free(buf);
		return 0;
	} else if (buf[0] == '&' || buf[0] == '#' || buf[0] == '?') {
		g_free(buf);
		return 1;
	}
	
	error_print("Unsupported spell command '%c'.\n"
			"This is a bug; mail " BUGEMAIL " about it.\n", buf[0]);
	error_print("Input [%s]\nOutput [%s]\n", word, buf);
	g_free(buf);
	return -1;
}

static gboolean iswordsep(char c) {
	return !isalpha(c) && c != '\'';
}

static gboolean get_word_from_pos(GtkText* gtktext, int pos, char** buf, 
		int *pstart, int *pend) {
	gint start, end;

	if (iswordsep(GTK_TEXT_INDEX(gtktext, pos))) return FALSE;

	for (start = pos; start >= 0; --start) {
		if (iswordsep(GTK_TEXT_INDEX(gtktext, start))) break;
	}
	start++;

	for (end = pos; end <= gtk_text_get_length(gtktext); end++) {
		if (iswordsep(GTK_TEXT_INDEX(gtktext, end))) break;
	}

	if (buf && (end - start + 1 > 0)) {
		*buf = g_malloc(end - start + 1);
		for (pos = start; pos < end; pos++) 
			(*buf)[pos-start] = GTK_TEXT_INDEX(gtktext, pos);
		(*buf)[pos-start] = 0;
	}

	if (pstart) *pstart = start;
	if (pend) *pend = end;

	return TRUE;
}

static gboolean get_curword(GtkText* gtktext, char** buf, 
		int *pstart, int *pend) {
	int pos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	return get_word_from_pos(gtktext, pos, buf, pstart, pend);
}

static void change_color(GtkText *gtktext, 
		int start, int end, GdkColor *color) {
	char *newtext = gtk_editable_get_chars(GTK_EDITABLE(gtktext), start, end);
	gtk_text_freeze(gtktext);
	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	
	gtk_text_set_point(gtktext, start);
	gtk_text_forward_delete(gtktext, end-start);

	if (newtext && end-start > 0)
		gtk_text_insert(gtktext, NULL, color, NULL, newtext, end-start);

	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_text_thaw(gtktext);
	g_free(newtext);
}

static gboolean check_at(GtkText *gtktext, int from_pos) {
	int start, end;
	char *buf = NULL;

	if (!get_word_from_pos(gtktext, from_pos, &buf, &start, &end)) {
		if (buf)
			g_free(buf);
		return FALSE;
	}

	if (misspelled_test(buf)) {
		if (highlight.pixel == 0) {
			/* add an entry for the highlight in the color map. */
			GdkColormap *gc = gtk_widget_get_colormap(GTK_WIDGET(gtktext));
			gdk_colormap_alloc_color(gc, &highlight, FALSE, TRUE);
		}
		change_color(gtktext, start, end, &highlight);
		if (buf)
			g_free(buf);
		return TRUE;
	} else { 
		change_color(gtktext, start, end, 
				&(GTK_WIDGET(gtktext)->style->fg[0]));
		if (buf)
			g_free(buf);
		return FALSE;
	}
}

void gtkspell_check_all(GtkText *gtktext) {
	guint origpos;
	guint pos = 0;
	guint len;
	float adj_value;
	
	if (!gtkspell_running()) return;
	
	len = gtk_text_get_length(gtktext);

	adj_value = gtktext->vadj->value;
	gtk_text_freeze(gtktext);
	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	while (pos < len) {
		while (pos < len && iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
			pos++;
		while (pos < len && !iswordsep(GTK_TEXT_INDEX(gtktext, pos)))
			pos++;
		if (pos > 0)
			check_at(gtktext, pos-1);
	}
	gtk_text_thaw(gtktext);
	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
}

static void entry_insert_cb(GtkText *gtktext, 
		gchar *newtext, guint len, guint *ppos, gpointer d) {
	int origpos;

	if (!gtkspell_running()) return;

	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext),
									 GTK_SIGNAL_FUNC(entry_insert_cb),
									 NULL);
	gtk_text_insert(GTK_TEXT(gtktext), NULL,
			&(GTK_WIDGET(gtktext)->style->fg[0]), NULL, newtext, len);
	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext),
									 GTK_SIGNAL_FUNC(entry_insert_cb),
									 NULL);
	gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "insert-text");
	*ppos += len;

	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));

	if (iswordsep(newtext[0])) {
		/* did we just end a word? */
		if (*ppos >= 2) check_at(gtktext, *ppos-2);

		/* did we just split a word? */
		if (*ppos < gtk_text_get_length(gtktext))
			check_at(gtktext, *ppos+1);
	} else {
		/* check as they type, *except* if they're typing at the end (the most
		 * common case.
		 */
		if (*ppos < gtk_text_get_length(gtktext) && 
				!iswordsep(GTK_TEXT_INDEX(gtktext, *ppos)))
			check_at(gtktext, *ppos-1);
	}

	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
	gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
}

static void entry_delete_cb(GtkText *gtktext,
		gint start, gint end, gpointer d) {
	int origpos;

	if (!gtkspell_running()) return;

	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	check_at(gtktext, start-1);
	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
	gtk_editable_select_region(GTK_EDITABLE(gtktext), origpos, origpos);
	/* this is to *UNDO* the selection, in case they were holding shift
	 * while hitting backspace. */
}

static void replace_word(GtkWidget *w, gpointer d) {
	int start, end;
	char *newword;

	/* we don't save their position, 
	 * because the cursor is moved by the click. */

	gtk_text_freeze(GTK_TEXT(d));

	gtk_label_get(GTK_LABEL(GTK_BIN(w)->child), &newword);
	get_curword(GTK_TEXT(d), NULL, &start, &end);

	gtk_text_set_point(GTK_TEXT(d), end);
	gtk_text_backward_delete(GTK_TEXT(d), end-start);
	gtk_text_insert(GTK_TEXT(d), NULL, NULL, NULL, newword, strlen(newword));

	gtk_text_thaw(GTK_TEXT(d));
}

static GtkMenu *make_menu(GList *l, GtkText *gtktext) {
	GtkWidget *menu, *item;
	char *caption;
	menu = gtk_menu_new(); {
		caption = g_strdup_printf("Not in dictionary: %s", (char*)l->data);
		item = gtk_menu_item_new_with_label(caption);
		g_free(caption);
		/* I'd like to make it so this item is never selectable, like
		 * the menu titles in the GNOME panel... unfortunately, the GNOME
		 * panel creates their own custom widget to do this! */
		gtk_widget_show(item);
		gtk_menu_append(GTK_MENU(menu), item);

		item = gtk_menu_item_new();
		gtk_widget_show(item);
		gtk_menu_append(GTK_MENU(menu), item);

		l = l->next;
		if (l == NULL) {
			item = gtk_menu_item_new_with_label("(no suggestions)");
			gtk_widget_show(item);
			gtk_menu_append(GTK_MENU(menu), item);
		} else {
			GtkWidget *curmenu = menu;
			int count = 0;
			do {
				if (l->data == NULL && l->next != NULL) {
					count = 0;
					curmenu = gtk_menu_new();
					item = gtk_menu_item_new_with_label("Other Possibilities...");
					gtk_widget_show(item);
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
					gtk_menu_append(GTK_MENU(curmenu), item);
					l = l->next;
				} else if (count > MENUCOUNT) {
					count -= MENUCOUNT;
					item = gtk_menu_item_new_with_label("More...");
					gtk_widget_show(item);
					gtk_menu_append(GTK_MENU(curmenu), item);
					curmenu = gtk_menu_new();
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), curmenu);
				}
				item = gtk_menu_item_new_with_label((char*)l->data);
				gtk_signal_connect(GTK_OBJECT(item), "activate",
						GTK_SIGNAL_FUNC(replace_word), gtktext);
				gtk_widget_show(item);
				gtk_menu_append(GTK_MENU(curmenu), item);
				count++;
			} while ((l = l->next) != NULL);
		}
	}
	return GTK_MENU(menu);
}

static void popup_menu(GtkText *gtktext, GdkEventButton *eb) {
	char *buf = NULL;
	GList *list, *l;

	get_curword(gtktext, &buf, NULL, NULL);

	list = misspelled_suggest(buf);
	if (buf)
		g_free(buf);
	if (list != NULL) {
		gtk_menu_popup(make_menu(list, gtktext), NULL, NULL, NULL, NULL,
				eb->button, eb->time);
		for (l = list; l != NULL; l = l->next)
			g_free(l->data);
		g_list_free(list);
	}
}

/* ok, this is pretty wacky:
 * we need to let the right-mouse-click go through, so it moves the cursor, 
 * but we *can't* let it go through, because GtkText interprets rightclicks as
 * weird selection modifiers.
 *
 * so what do we do?  forge rightclicks as leftclicks, then popup the menu. 
 * HACK HACK HACK. 
 */
static gint button_press_intercept_cb(GtkText *gtktext, GdkEvent *e, gpointer d) {
	GdkEventButton *eb;
	gboolean retval;

	if (!gtkspell_running()) return FALSE;

	if (e->type != GDK_BUTTON_PRESS) return FALSE;
	eb = (GdkEventButton*) e;

	if (eb->button != 3) return FALSE;

	/* forge the leftclick */
	eb->button = 1;

	gtk_signal_handler_block_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
	gtk_signal_emit_by_name(GTK_OBJECT(gtktext), "button-press-event",
			e, &retval);
	gtk_signal_handler_unblock_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), d);
	gtk_signal_emit_stop_by_name(GTK_OBJECT(gtktext), "button-press-event");

	/* now do the menu wackiness */
	popup_menu(gtktext, eb);
	return TRUE;
}

void gtkspell_uncheck_all(GtkText *gtktext) {
	int origpos;
	char *text;
	float adj_value;

	adj_value = gtktext->vadj->value;
	gtk_text_freeze(gtktext);
	origpos = gtk_editable_get_position(GTK_EDITABLE(gtktext));
	text = gtk_editable_get_chars(GTK_EDITABLE(gtktext), 0, -1);
	gtk_text_set_point(gtktext, 0);
	gtk_text_forward_delete(gtktext, gtk_text_get_length(gtktext));
	gtk_text_insert(gtktext, NULL, NULL, NULL, text, strlen(text));
	gtk_text_thaw(gtktext);
	g_free(text);

	gtk_editable_set_position(GTK_EDITABLE(gtktext), origpos);
	gtk_adjustment_set_value(gtktext->vadj, adj_value);
}

void gtkspell_attach(GtkText *gtktext) {
	gtk_signal_connect(GTK_OBJECT(gtktext), "insert-text",
		GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_signal_connect_after(GTK_OBJECT(gtktext), "delete-text",
		GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
	gtk_signal_connect(GTK_OBJECT(gtktext), "button-press-event",
			GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);
}

void gtkspell_detach(GtkText *gtktext) {
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
		GTK_SIGNAL_FUNC(entry_insert_cb), NULL);
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext),
		GTK_SIGNAL_FUNC(entry_delete_cb), NULL);
	gtk_signal_disconnect_by_func(GTK_OBJECT(gtktext), 
			GTK_SIGNAL_FUNC(button_press_intercept_cb), NULL);

	gtkspell_uncheck_all(gtktext);
}

static void sigchld(int param) {
	if (gtkspell_running() &&
		(waitpid(spell_pid, NULL, WNOHANG) == spell_pid)) {
		spell_pid = 0;
	} else {
		/* a default SIGCHLD handler.
		 * what else to do here? */
		waitpid(-1, NULL, WNOHANG);
	}
}

static void set_up_signal() {
	struct sigaction sigact;
	memset(&sigact, 0, sizeof(struct sigaction));

	sigact.sa_handler = sigchld;
	sigaction(SIGCHLD, &sigact, NULL);
}