/*
** 1998-05-19 -	A text viewer window. Is it simple? Yep, it worked in under well 100 lines.
** 1998-09-10 -	Sooner or later, all old code gets rewritten, it seems. This one did last
**		a long while, though.
** 1998-09-16 -	Added a command (cmd_viewtext). Sorry about the name, but it feels better.
** 1998-12-15 -	Added a GTK+ widget name to the text widget.
** 1998-12-18 -	Rewrote text reader to use mmap(), and implemented a hex viewer.
** 1999-01-05 -	Extended. Added more buttons (mainly for navigation), and a neat Goto dialog.
** 1999-01-07 -	Added search capabilities. Rather limited and perhaps not uber-smooth to use,
**		but better than nothing IMO.
** 1999-03-02 -	Revamped interface, now sort of semi-opaque. Better.
** 1999-03-13 -	Changed for the new dialog module.
*/

#include "gentoo.h"

#include <ctype.h>
#include <fcntl.h>
#include <regex.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>

#include <gdk/gdkkeysyms.h>

#include "errors.h"
#include "fileutil.h"
#include "dirpane.h"
#include "dialog.h"
#include "guiutil.h"
#include "strutil.h"
#include "textview.h"

#define	HEX_ROW_SIZE	(16)		/* Must be multiple of four! */
#define	HEX_UNPRINTABLE	('.')

/* ----------------------------------------------------------------------------------------- */

typedef enum { MVE_UP = 0, MVE_DOWN, MVE_TOP, MVE_BOTTOM } MoveID;

typedef struct {		/* Used by the Goto-dialog. */
	Dialog		*dlg;			/* Must be first, for initialization below! */
	GtkWidget	*vbox;
	GtkWidget	*label;
	GtkWidget	*entry;
} DlgGoto;

typedef struct {
	Dialog		*dlg;			/* Must be first, see init of static instance below. */
	GtkWidget	*vbox;
	GtkWidget	*label;
	GtkWidget	*entry;
	GtkWidget	*hbox;
	GtkWidget	*fnocase;
	GtkWidget	*fnolf;
} DlgSrch;

/* As of gentoo 0.9.24, this structure is internal to this module. */
typedef struct {
	MainInfo	*min;			/* Incredibly handy. */
	GtkWidget	*win;
	GtkWidget	*text;
	GtkWidget	*vbar;
	GtkWidget	*bhbox;			/* Horizontal box with action buttons. */
	GtkWidget	*blabel;		/* A label that is in the action box. */
	GtkWidget	*bplabel;		/* This label shows file position. */
	guint		search_pos;		/* Offset into text to start next search at. */
	guint		keypress_handler;	/* Handler ID for the keypress event. */
} TxvInfo;

/* ----------------------------------------------------------------------------------------- */

static DlgGoto	goto_dialog = { NULL };		/* Make sure the 'dlg' member is NULL. */
static DlgSrch	srch_dialog = { NULL };		/* Same here. */

#define	SEARCH_RE_MAX	(256)			/* Far more than *I* would need... ;^) */

static gchar	last_re[SEARCH_RE_MAX] = "";	/* Buffer the RE between searches. */
static gint	last_fnocase = 0, last_fnolf = 0;

/* ----------------------------------------------------------------------------------------- */

static void really_destroy(GtkWidget *wid)
{
	TxvInfo	*txi;

	if((wid != NULL) && ((txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL))
	{
		if(goto_dialog.dlg != NULL)		/* Goto dialog window open? Then close it. */
		{
			dlg_dialog_destroy(goto_dialog.dlg);
			goto_dialog.dlg = NULL;
		}
		if(srch_dialog.dlg != NULL)		/* Close down search window, if open. */
		{
			dlg_dialog_destroy(srch_dialog.dlg);
			srch_dialog.dlg = NULL;
		}
		g_free(txi);
	}
}

/* 1998-05-19 -	This simplistic thing causes the textviewing window to close when needed. */
static gint evt_delete(GtkWidget *wid, GdkEvent *evt, gpointer data)
{
	really_destroy(wid);

	return FALSE;
}

/* 1999-02-23 -	An attempt to support ESCape for closing down the text viewer. Shouldn't
**		be too harmful. I hope.
*/
static gint evt_keypress(GtkWidget *wid, GdkEventKey *event, gpointer user)
{
	if(event->keyval == GDK_Escape)
	{
		really_destroy(wid);
		gtk_widget_destroy(wid);
	}
	return FALSE;
}

/* 1999-03-02 -	Users of this module no longer have access to the GtkText widget, so
**		this function is needed for them to be able to actually display text.
*/
void txv_put_text(GtkWidget *wid, const char *text, int length)
{
	TxvInfo	*txi;

	if((wid == NULL) || ((txi = gtk_object_get_user_data(GTK_OBJECT(wid))) == NULL) || length < 1)
		return;

	gtk_text_insert(GTK_TEXT(txi->text), NULL, NULL, NULL, text, length);
}

/* 1998-12-18 -	Rewrote this a couple of times today. Note how this version really doesn't
**		care about the size of its input file; it just reads until reading fails.
**		This makes it possible to look at /proc files, which have size 0.
** 1998-12-20 -	Now uses mmap() if file has a non-zero size. Best of both worlds.
** 1999-02-03 -	Changed prototype slightly, since it got public. Added the <use_mmap> flag,
**		since we don't want to read config options at this level.
** 1999-02-20 -	Now checks the return value of mmap() correctly (I assumed it returned
**		NULL on failure, don't know how I got that idea). Also now falls back on
**		using normal file I/O if mmap() fails, which can happen on certain filesystems.
** 1999-03-02 -	New interface, renamed.
** 1999-04-06 -	Added the <buf_size> argument, which is used when mmap() isn't.
*/
int txv_load_text(GtkWidget *wid, const char *name, gboolean use_mmap, gsize buf_size)
{
	char	*buf;
	int	fd, ret = 0;
	TxvInfo	*txi;

	if(wid == NULL || (txi = gtk_object_get_user_data(GTK_OBJECT(wid))) == NULL || name == NULL)
		return 0;

	txv_set_label(wid, name);

	err_clear(txi->min);
	if((fd = open(name, O_RDONLY)) >= 0)
	{
		gsize	size;

		if(use_mmap && fut_size(name, &size) && (size > 0) &&
		   (buf = mmap(NULL, size, PROT_READ, MAP_PRIVATE, fd, 0)) != MAP_FAILED)
		{
			gtk_text_insert(GTK_TEXT(txi->text), NULL, NULL, NULL, buf, (gint) size);
			munmap(buf, size);
		}
		else
		{
			gssize	got;

			buf = g_malloc(buf_size);
			while((got = read(fd, buf, buf_size)) > 0)
				gtk_text_insert(GTK_TEXT(txi->text), NULL, NULL, NULL, buf, (gint) got);
			g_free(buf);
		}
		close(fd);
	}
	if(errno != 0)
		err_set(txi->min, errno, "File reading", (char *) name);

	return ret;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-12-18 -	Build "groups" of actual hex data, from <data> and <bytes> bytes on. */
static void build_hex_group(char *data, char *group, gsize bytes)
{
	gchar	*hex = "0123456789ABCDEF", *ptr = group;
	guint8	here;
	gsize	i;

	for(i = 0; i < bytes; i++)
	{
		if(i && !(i & 3))
			ptr++;
		here = (guint8) *data++;
		*ptr++ = hex[here >> 4];
		*ptr++ = hex[here & 15];
	}
}

/* 1998-12-18 -	Build text representation of stuff found at <data>, <bytes> bytes worth. */
static void build_hex_text(char *data, char *text, gsize bytes)
{
	gsize	i;

	for(i = 0; i < bytes; i++, data++)
		*text++ = isprint((gint) *data) ? *data : HEX_UNPRINTABLE;
}

/* 1998-12-18 -	Add contents of file <name> into GtkText widget <wid>, as a hex dump.
** 1999-02-03 -	New prototype, now public.
** 1999-02-20 -	Fixed use of mmap(), and added fall-back to ordinary read() I/O if
**		the wanted file couldn't be mapped. This might be suboptimal for huge
**		files...
** 1999-03-02 -	New interface, renamed.
** 1999-04-06 -	Cleaned up. Since the data-to-ASCII-hex conversion is only done on a small
**		number of bytes at a time, it was grossly inefficient to load the entire file.
**		Now we do plenty of small read()s instead, which I'm sure is slower, but at
**		least doesn't waste memory.
*/
int txv_load_text_hex(GtkWidget *win, const char *name)
{
	gchar		line[8 + 1 + (HEX_ROW_SIZE / 4) * 9 + 1 + HEX_ROW_SIZE + 1 + 1],
			group[9 * (HEX_ROW_SIZE / 4) + 1], text[HEX_ROW_SIZE + 1],
			buf[HEX_ROW_SIZE];
	gint		fd;
	gsize		chunk, offset = 0;
	TxvInfo		*txi;

	if((win == NULL) || ((txi = gtk_object_get_user_data(GTK_OBJECT(win))) == NULL) || (name == NULL))
		return 0;

	txv_set_label(win, name);

	err_clear(txi->min);

	memset(group, ' ', sizeof group - 1);
	group[sizeof group - 1] = '\0';
	memset(text, ' ', sizeof text - 1);
	text[sizeof text - 1]   = '\0';

	gtk_text_freeze(GTK_TEXT(txi->text));
	if((fd = open(name, O_RDONLY)) >= 0)
	{
		while((chunk = read(fd, buf, sizeof buf)) > 0)
		{
			if(chunk < sizeof buf)
			{
				memset(group, ' ', sizeof group - 1);
				memset(text, ' ', sizeof text - 1);
			}
			build_hex_group(buf, group, chunk);
			build_hex_text(buf, text, chunk);
			g_snprintf(line, sizeof line, "%08lX %s %s\n", (unsigned long) offset, group, text);
			gtk_text_insert(GTK_TEXT(txi->text), NULL, NULL, NULL, line, strlen(line));
			offset += chunk;
		}
		close(fd);
	}
	gtk_text_thaw(GTK_TEXT(txi->text));

	if(errno != 0)
		err_set(txi->min, errno, "File reading", (char *) name);

	return errno != 0;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-07 -	Reset the search, so that the next one starts at the beginning. */
static void reset_search(TxvInfo *txi)
{
	if(txi != NULL)
	{
		gtk_editable_select_region(GTK_EDITABLE(txi->text), 0, 0);
		txi->search_pos = 0;
	}
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-07 -	This handler gets called when the vertical range (scrollbar) changes. This
**		should probably also include the number of lines available, but that is a
**		rather fuzzy quantity, so I'll just leave that for now.
*/
static gint evt_pos_changed(GtkWidget *wid, gpointer data)
{
	TxvInfo		*txi = (TxvInfo *) data;
	GtkAdjustment	*adj = (GtkAdjustment *) wid;
	GtkStyle	*stl;
	char		buf[64];
	gfloat		p;
	gint		fh = 10;

	if((stl = gtk_widget_get_style(txi->text)) != NULL)
		fh = stl->font->ascent + stl->font->descent;

	if(adj->upper - adj->page_size > 0.0f)
		p = 100 * (adj->value / (adj->upper - adj->page_size));
	else
		p = 0.0f;
	g_snprintf(buf, sizeof buf, "Line %d (%.0f%%)", (int) adj->value / fh, p);
	gtk_label_set_text(GTK_LABEL(txi->bplabel), buf);

	return TRUE;
}

/* 1999-01-05 -	User clicked on one of the four basic movement buttons. Determine which, and
**		make the text move accordingly.
*/
static gint evt_move_clicked(GtkWidget *wid, gpointer data)
{
	TxvInfo		*txi = (TxvInfo *) data;
	GtkAdjustment	*adj;
	GtkStyle	*stl;
	gint		fh = 10;

	if((stl = gtk_widget_get_style(txi->text)) != NULL)
		fh = stl->font->ascent + stl->font->descent;
	if((adj = gtk_range_get_adjustment(GTK_RANGE(txi->vbar))) != NULL)
	{
		switch((MoveID) gtk_object_get_user_data(GTK_OBJECT(wid)))
		{
			case MVE_UP:
				gtk_adjustment_set_value(adj, adj->value - fh);
				break;
			case MVE_DOWN:
				if(adj->value < adj->upper - adj->page_size)
					gtk_adjustment_set_value(adj, adj->value + fh);
				break;
			case MVE_TOP:
				gtk_adjustment_set_value(adj, adj->lower);
				reset_search(txi);
				break;
			case MVE_BOTTOM:
				gtk_adjustment_set_value(adj, adj->upper - adj->page_size);
				reset_search(txi);
				break;
			default:
				;
		}
	}
	return TRUE;
}

/* 1999-01-05 -	User hit the "Goto..." button. Pop up a dialog asking for destination. */
static gint evt_goto_clicked(GtkWidget *wid, gpointer data)
{
	TxvInfo		*txi = (TxvInfo *) data;
	char		*txt, unit;
	GtkAdjustment	*adj;
	GtkStyle	*stl;
	gfloat		pos = -1.0f;
	gint		value, fh = 10;

	goto_dialog.vbox  = gtk_vbox_new(FALSE, 0);
	goto_dialog.label = gtk_label_new("Enter Line Number or Percentage:");
	gtk_box_pack_start(GTK_BOX(goto_dialog.vbox), goto_dialog.label, FALSE, FALSE, 0);
	gtk_widget_show(goto_dialog.label);
	goto_dialog.entry = gtk_entry_new();
	gtk_box_pack_start(GTK_BOX(goto_dialog.vbox), goto_dialog.entry, FALSE, FALSE, 0);
	gtk_widget_show(goto_dialog.entry);
	if((goto_dialog.dlg = dlg_dialog(goto_dialog.vbox, "Goto", "OK|Cancel|", NULL, NULL)) != NULL)
	{
		dlg_dialog_set_keep(goto_dialog.dlg, TRUE);
		gtk_widget_grab_focus(goto_dialog.entry);
		if(!dlg_dialog_wait(goto_dialog.dlg) && (adj = gtk_range_get_adjustment(GTK_RANGE(txi->vbar))) != NULL)
		{
			txt = gtk_entry_get_text(GTK_ENTRY(goto_dialog.entry));
			if((stl = gtk_widget_get_style(txi->text)) != NULL)
				fh = stl->font->ascent + stl->font->descent;
			if(sscanf(txt, "0x%x", &value) == 1)			/* Hexadecimal offset? */
				pos = (gfloat) (fh * value / 16);
			else if(sscanf(txt, "%d%c", &value, &unit) == 2)
			{
				if(unit == '%')					/* Percentage? */
					pos = value * (adj->upper - adj->page_size) / 100.0;
				if(unit == 'K')					/* Kilobytes? Silly if not hex-view. */
					pos = (gfloat) fh * value * (1024 / 16);
			}
			else if(sscanf(txt, "%d", &value) == 1)			/* Line number? */
				pos = (gfloat) fh * value;
			if(pos >= 0.0 && pos <= (adj->upper - adj->page_size))
				gtk_adjustment_set_value(adj, pos);
		}
		dlg_dialog_destroy(goto_dialog.dlg);
		goto_dialog.dlg = NULL;		/* Indicate that the dialog is no longer open. */
	}
	return TRUE;
}

static gint evt_close_clicked(GtkWidget *wid, gpointer data)
{
	TxvInfo		*txi = (TxvInfo *) data;
	GtkWidget	*win;

	if(txi != NULL)
	{
		win = txi->win;
		really_destroy(win);
		gtk_widget_destroy(win);
	}

	return TRUE;
}

/* ----------------------------------------------------------------------------------------- */

/* 1999-01-07 -	Scroll the text so that the character at offset <pos> from the beginning
**		becomes visible. This search disregards wrapped lines, for reasons of
**		programmer sanity preservation. This obviously sucks, but I really don't
**		want to dig *that* deep into the GtkText widget. There is an easy way to
**		accomplish this that eliminates this problem: use a insert/delete pair.
**		However, that causes the widget to *scroll* to the required position. Yawn.
*/
static void show_text(TxvInfo *txi, int pos)
{
	GtkAdjustment	*adj;
	GtkStyle	*stl;
	gchar		*text;
	gfloat		nv;
	gint		line, fh = 10;

	if((text = gtk_editable_get_chars(GTK_EDITABLE(txi->text), 0, -1)) != NULL)
	{
		for(line = 0; *text && pos; pos--, text++)
		{
			if(*text == '\n')		/* Probably breaks on non-Unix systems. So? */
				line++;
		}
		if((stl = gtk_widget_get_style(txi->text)) != NULL)
		{
			fh = stl->font->ascent + stl->font->descent;
			if((adj = gtk_range_get_adjustment(GTK_RANGE(txi->vbar))) != NULL)
			{
				nv = fh * line;
				if(nv >= 0 && nv <= (adj->upper - adj->page_size))
 					gtk_adjustment_set_value(adj, nv);
			}
		}
	}
}

/* 1999-01-07 -	Do the actual search, using <re_src> as the regular expression source. This
**		should really begin with '(', and with ')', and don't contain any non-escaped
**		parentheses within!
*/
static void do_search(TxvInfo *txi, char *re_src, int nocase, int nolf)
{
	regex_t		re;
	regmatch_t	rm[1];
	char		*text, ebuf[1024];
	int		cerr, cflags = REG_EXTENDED;

	if(nocase)
		cflags |= REG_ICASE;
	if(nolf)
		cflags |= REG_NEWLINE;

	if((cerr = regcomp(&re, re_src, cflags)) == 0)
	{
		if((text = gtk_editable_get_chars(GTK_EDITABLE(txi->text), txi->search_pos, -1)) != NULL)
		{
			if(regexec(&re, text, sizeof rm / sizeof rm[0], rm, 0) == 0)
			{
				show_text(txi, txi->search_pos + rm[0].rm_so);
				gtk_editable_select_region(GTK_EDITABLE(txi->text), txi->search_pos + rm[0].rm_so,
								txi->search_pos + rm[0].rm_eo);
				txi->search_pos += rm[0].rm_eo;
			}
		}
		regfree(&re);
	}
	else
	{
		str_strncpy(ebuf, "Malformed regular expression:\n", sizeof ebuf);
		regerror(cerr, &re, ebuf + strlen(ebuf), sizeof ebuf - strlen(ebuf) - 1);
		dlg_dialog_error(ebuf);
	}
}

/* 1999-01-07 -	User hit the "Search" button, so let's pop up something to request parameters. */
static gint evt_search_clicked(GtkWidget *wid, gpointer data)
{
	TxvInfo	*txi = (TxvInfo *) data;

	srch_dialog.vbox  = gtk_vbox_new(FALSE, 0);
	srch_dialog.label = gtk_label_new("Enter Text to Search For (RE)");
	gtk_box_pack_start(GTK_BOX(srch_dialog.vbox), srch_dialog.label, FALSE, FALSE, 0);
	gtk_widget_show(srch_dialog.label);
	srch_dialog.entry = gtk_entry_new_with_max_length(SEARCH_RE_MAX - 1);
	gtk_entry_set_text(GTK_ENTRY(srch_dialog.entry), last_re);
	gtk_editable_select_region(GTK_EDITABLE(srch_dialog.entry), 0, -1);
	gtk_box_pack_start(GTK_BOX(srch_dialog.vbox), srch_dialog.entry, FALSE, FALSE, 0);
	gtk_widget_show(srch_dialog.entry);
	srch_dialog.hbox  = gtk_hbox_new(FALSE, 0);
	srch_dialog.fnocase = gtk_check_button_new_with_label("Ignore Case?");
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(srch_dialog.fnocase), last_fnocase);
	gtk_box_pack_start(GTK_BOX(srch_dialog.hbox), srch_dialog.fnocase, TRUE, TRUE, 0);
	gtk_widget_show(srch_dialog.fnocase);
	srch_dialog.fnolf = gtk_check_button_new_with_label("Don't Span Newlines?");
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(srch_dialog.fnolf), last_fnolf);
	gtk_box_pack_start(GTK_BOX(srch_dialog.hbox), srch_dialog.fnolf, TRUE, TRUE, 0);
	gtk_widget_show(srch_dialog.fnolf);
	gtk_box_pack_start(GTK_BOX(srch_dialog.vbox), srch_dialog.hbox, FALSE, FALSE, 0);
	gtk_widget_show(srch_dialog.hbox);

	if((srch_dialog.dlg = dlg_dialog(srch_dialog.vbox, "Search", "OK|Cancel", NULL, NULL)) != NULL)
	{
		dlg_dialog_set_keep(srch_dialog.dlg, TRUE);
		gtk_widget_grab_focus(srch_dialog.entry);
		if(dlg_dialog_wait(srch_dialog.dlg) == DLG_POSITIVE)
		{
			gchar	*txt;
			GString	*tmp;
			txt = gtk_entry_get_text(GTK_ENTRY(srch_dialog.entry));
			str_strncpy(last_re, txt, sizeof last_re);
			last_fnocase = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(srch_dialog.fnocase));
			last_fnolf   = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(srch_dialog.fnolf));
			if(txt != NULL && (tmp = g_string_new("(")) != NULL)
			{
				for(; *txt; txt++)		/* Quote any parentheses in user's string. */
				{
					if(*txt == '(')
						g_string_append(tmp, "\\(");
					else if(*txt == ')')
						g_string_append(tmp, "\\)");
					else
						g_string_append_c(tmp, *txt);
				}
				g_string_append_c(tmp, ')');
				do_search(txi, tmp->str, last_fnocase, last_fnolf);
				g_string_free(tmp, TRUE);
			}
		}
		dlg_dialog_destroy(srch_dialog.dlg);
		srch_dialog.dlg = NULL;		/* Indicate that the dialog is no longer open. */
	}
	return TRUE;
}

/* ----------------------------------------------------------------------------------------- */

/* 1998-09-10 -	A complete rewrite of the textview widget creation stuff. Now supports just creating
**		the necessary widgetry, and then returning it to the caller for custom manipulation
**		elsewhere. Very useful when capturing output of commands. Consistency is nice.
** 1998-09-11 -	Added the <with_save> boolean, which when TRUE enables a "Save" button.
** 1999-01-05 -	Removed the <with_save> boolean, added loads of other fun buttons that are always
**		there instead. :)
** 1999-02-03 -	Added the <label> argument, which will be set if non-NULL.
** 1999-03-02 -	Redid interface.
*/
GtkWidget * txv_open(MainInfo *min, const gchar *label)
{
	GtkWidget	*vbox, *tbox, *btn, *algn, *bhbox;
	TxvInfo		*txi;

	txi = g_malloc(sizeof *txi);

	txi->min = min;
	txi->search_pos  = 0;		/* Start searching from the beginning. */
	txi->keypress_handler = 0;

	if((txi->win = gtk_window_new(GTK_WINDOW_DIALOG)) != NULL)
	{
		gtk_object_set_user_data(GTK_OBJECT(txi->win), (gpointer) txi);

		gtk_signal_connect(GTK_OBJECT(txi->win), "delete_event", GTK_SIGNAL_FUNC(evt_delete), (gpointer) txi);
		txv_connect_keypress(txi->win, NULL, NULL);
/*		txv_connect_keypress(txi->win, GTK_SIGNAL_FUNC(evt_keypress), txi->win);*/
		vbox = gtk_vbox_new(FALSE, 0);
		tbox = gtk_hbox_new(FALSE, 0);
		if((txi->text = gtk_text_new(NULL, NULL)) != NULL)
		{
			gtk_widget_set_name(txi->text, "txvText");
			gtk_box_pack_start(GTK_BOX(tbox), txi->text, TRUE, TRUE, 0);
			gtk_widget_show(txi->text);
			gtk_signal_connect(GTK_OBJECT(GTK_TEXT(txi->text)->vadj), "value_changed", GTK_SIGNAL_FUNC(evt_pos_changed), (gpointer) txi);
			if((txi->vbar = gtk_vscrollbar_new(GTK_TEXT(txi->text)->vadj)) != NULL)
			{
				gtk_box_pack_start(GTK_BOX(tbox), txi->vbar, FALSE, FALSE, 0);
				gtk_widget_show(txi->vbar);
				gtk_text_set_editable(GTK_TEXT(txi->text), FALSE);
				gtk_window_set_default_size(GTK_WINDOW(txi->win), 640, 480);
				gtk_text_set_word_wrap(GTK_TEXT(txi->text), FALSE);
				gtk_box_pack_start(GTK_BOX(vbox), tbox, TRUE, TRUE, 0);
				gtk_widget_show(tbox);

				txi->bhbox = gtk_hbox_new(FALSE, 0);
				algn = gtk_alignment_new(0.01, 0.5, 0, 0);
				if(label != NULL)
					txi->blabel = gtk_label_new(label);
				else
					txi->blabel = gtk_label_new("");
				gtk_container_add(GTK_CONTAINER(algn), txi->blabel);
				gtk_widget_show(txi->blabel);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), algn, TRUE, TRUE, 10);
				gtk_widget_show(algn);

				algn = gtk_alignment_new(0.99, 0.5, 0, 0);
				txi->bplabel = gtk_label_new("");
				gtk_container_add(GTK_CONTAINER(algn), txi->bplabel);
				gtk_widget_show(txi->bplabel);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), algn, TRUE, TRUE, 0);
				gtk_widget_show(algn);

				btn = gui_arrow_button_new(GTK_ARROW_UP);
				gtk_object_set_user_data(GTK_OBJECT(btn), (gpointer) MVE_UP);
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_move_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);
				btn = gui_arrow_button_new(GTK_ARROW_DOWN);
				gtk_object_set_user_data(GTK_OBJECT(btn), (gpointer) MVE_DOWN);
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_move_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);
				btn = gtk_button_new_with_label("Top");
				gtk_object_set_user_data(GTK_OBJECT(btn), (gpointer) MVE_TOP);
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_move_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);
				btn = gtk_button_new_with_label("Bottom");
				gtk_object_set_user_data(GTK_OBJECT(btn), (gpointer) MVE_BOTTOM);
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_move_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);

				bhbox = gtk_hbox_new(FALSE, 0);
				btn = gtk_button_new_with_label("Goto...");
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_goto_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);
				btn = gtk_button_new_with_label("Search...");
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_search_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(bhbox), btn, FALSE, FALSE, 0);
				gtk_widget_show(btn);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), bhbox, FALSE, FALSE, 5);
				gtk_widget_show(bhbox);

				btn = gtk_button_new_with_label("Close");
				gtk_signal_connect(GTK_OBJECT(btn), "clicked", GTK_SIGNAL_FUNC(evt_close_clicked), (gpointer) txi);
				gtk_box_pack_start(GTK_BOX(txi->bhbox), btn, FALSE, TRUE, 0);
				gtk_widget_show(btn);
				gtk_widget_set_sensitive(txi->bhbox, FALSE);
				gtk_box_pack_start(GTK_BOX(vbox), txi->bhbox, FALSE, FALSE, 0);
				gtk_widget_show(txi->bhbox);
				gtk_container_add(GTK_CONTAINER(txi->win), vbox);
				gtk_widget_show(vbox);

				gtk_widget_grab_focus(txi->text);
				return txi->win;
			}
			gtk_widget_destroy(txi->text);
		}
		gtk_widget_destroy(txi->win);
		g_free(txi);
	}
	return NULL;
}

/* 1999-03-02 -	Install a keypress handler on the textviewing window. This cannot be
**		done directly by the module user, since we have an internal handler
**		and they don't seem to chain properly... Odd.
*/
guint txv_connect_keypress(GtkWidget *wid, GtkSignalFunc func, gpointer user)
{
	TxvInfo	*txi;

	if((wid == NULL) || ((txi = gtk_object_get_user_data(GTK_OBJECT(wid))) == NULL))
		return 0;
	if(txi->keypress_handler > 0)
	{
		gtk_signal_disconnect(GTK_OBJECT(wid), txi->keypress_handler);
		txi->keypress_handler = 0;
	}
	if(func == NULL)		/* Reset internal handler? */
	{
		func = GTK_SIGNAL_FUNC(evt_keypress);
		user = wid;
	}
	return txi->keypress_handler = gtk_signal_connect(GTK_OBJECT(wid), "key_press_event", func, user);
}

/* 1999-01-05 -	Set the label. This is kind of redundant, but true to the original Opus look. :)
** 1999-03-02 -	New interface.
*/
void txv_set_label(GtkWidget *wid, const char *text)
{
	TxvInfo	*txi;

	if(wid != NULL && (txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL && text != NULL)
	{
		gtk_window_set_title(GTK_WINDOW(wid), text);
		gtk_label_set_text(GTK_LABEL(txi->blabel), text);
	}
}

/* 1999-04-03 -	Freeze the text viewing part of the txv. Handy before massive insertions. */
void txv_freeze(GtkWidget *wid)
{
	TxvInfo	*txi;

	if((wid != NULL) && (txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL)
		gtk_text_freeze(GTK_TEXT(txi->text));
}

/* 1999-04-03 -	Thaw the text viewer up. */
void txv_thaw(GtkWidget *wid)
{
	TxvInfo	*txi;

	if((wid != NULL) && (txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL)
		gtk_text_thaw(GTK_TEXT(txi->text));
}

/* 1999-03-02 -	Enable the text widget for viewing. Called once all insertions are done. */
void txv_enable(GtkWidget *wid)
{
	TxvInfo	*txi;

	if((wid != NULL) && ((txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL))
	{
		gtk_widget_show(wid);	/* Just in case. */
		gtk_adjustment_set_value(GTK_ADJUSTMENT(GTK_TEXT(txi->text)->vadj), 0.0f);
		gtk_widget_set_sensitive(txi->bhbox, TRUE);
	}
	else
		fprintf(stderr, "TEXTVIEW: Couldn't enable\n");
}

/* 1999-02-23 -	Close down a text viewer. Handy for use from cmdgrab.
** 1999-03-02 -	New interface.
*/
void txv_close(GtkWidget *wid)
{
	TxvInfo	*txi;

	if(wid != NULL && (txi = gtk_object_get_user_data(GTK_OBJECT(wid))) != NULL)
	{
		GtkWidget	*win = txi->win;

		really_destroy(win);		/* This makes the TxvInfo go away. */
		gtk_widget_destroy(win);
	}
}
