view src/gtkspell.c @ 3725:dd48b1ac5bd8

[gaim-migrate @ 3861] This is better because Duffman says so, oh yeah! No, but seriously... before these changes, if you got new email on an account, but didn't read it, gaim would pop up a little "read yo email, sucka!" notice every once in a while, because AIM sends you a little email status thing every once in a while. This should alleviate that problem (by attempting to keep track of the number of unread emails in your account). committer: Tailor Script <tailor@pidgin.im>
author Mark Doliner <mark@kingant.net>
date Thu, 17 Oct 2002 05:06:15 +0000
parents 9682c0e022c6
children
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>

#ifndef _WIN32
#include <sys/wait.h>
#include <sys/time.h>
#include <unistd.h>
#else
#include <io.h>
#endif   

#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};

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

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

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

/*
 * Set to "gtkspell not running" status
 *
 * May seem a bit silly, but it allows us to keep the file-global
 * variable from going program-global.  And if we need to do
 * something else additional later, well...
 */
void gtkspell_notrunning() {
    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() {
#ifndef _WIN32
	if (gtkspell_running()) {
		kill(spell_pid, SIGHUP); 
		spell_pid = 0;
		close(fd_read[0]);
		close(fd_write[1]);
	}
#endif
}

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

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

	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");
#endif /*!_WIN32*/
	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) {
	static GtkWidget *menu = NULL;
	GtkWidget *item;
	char *caption;

	/*
	 * If a menu already exists, destroy it before creating a new one,
	 * thus freeing-up the memory it occupied.
	 */
	if(menu)
		gtk_widget_destroy(menu);

	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);
}