/* $Id: e2_view_dialog.c 866 2008-05-06 01:05:28Z tpgww $

Copyright (C) 2004-2008 tooar <tooar@gmx.net>

This file is part of emelFM2.
emelFM2 is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

emelFM2 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with emelFM2; see the file GPL. If not, contact the Free Software
Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "emelfm2.h"
#include <string.h>
#include "e2_dialog.h"
#include "e2_view_dialog.h"
#include "e2_textiter.h"
#include "e2_task.h"

//stores for session-static and/or cached variables
gboolean case_sensitive;
gboolean search_backward;
gboolean whole_words;
gboolean search_wrap;
GList *find_history = NULL;
GList *view_history = NULL;
//keycodes corresponding to mnemonics for translated labels
guint find_keycode;
guint hide_keycode;
//we need this one because of widget-sharing with the edit dialog
//value set to 0 for a view dialog
extern guint replace_keycode;

static void _e2_view_dialog_response_cb
	(GtkDialog *dialog, gint response, E2_ViewDialogRuntime *rt);
static gboolean _e2_view_dialog_reviewQ (E2_ActionTaskData *qed);
static gboolean _e2_view_dialog_viewatQ (E2_ActionTaskData *qed);

  /**************************/
 /**** search functions ****/
/**************************/

#ifdef E2_MARK_FINDS
/**
@brief buffer-changed callback
This is to turn off all highlighting of matched strings
@param textbuffer the textbuffer which was changed
@param rt dialog runtime data struct

@return
*/
static void _e2_view_dialog_buffer_changed_cb (GtkTextBuffer *textbuffer,
	E2_ViewDialogRuntime *rt)
{
	e2_view_dialog_clear_hilites (rt);
}
/**
@brief setup to highlight all occurrences of a string in the buffer
@param rt dialog runtime data struct
@return
*/
void e2_view_dialog_init_hilites (E2_ViewDialogRuntime *rt)
{
	gtk_text_buffer_create_tag (rt->textbuffer, "matched", "background",
		e2_option_str_get ("color-highlight"), NULL);
	rt->is_lit = FALSE;
}
/**
@brief by forward scan, highlight all occurrences of a string in the buffer
The current state of the case-insensitive flag is used when searching
@param searchstr the string to find
@param rt dialog runtime data struct
@return
*/
static void _e2_view_dialog_set_hilites (const gchar *searchstr,
	E2_ViewDialogRuntime *rt)
{
	static gchar *prior_search = NULL;
	//when some search-options are changed, need to cleanup first, then re-search
	if (rt->research)
		rt->research = FALSE;
	//avoid repeated scans for the same string
	else if (rt->is_lit && g_str_equal (searchstr, prior_search))
		return;
	//clear anything from last scan
	e2_view_dialog_clear_hilites (rt);

	if (prior_search != NULL)
		g_free (prior_search);
	prior_search = g_strdup (searchstr);

	GtkTextIter start, end, iter;
	gtk_text_buffer_get_bounds (rt->textbuffer, &start, &end);

	E2TextSearchFlags flags = E2_SEARCH_TEXT_ONLY;	//superset of GtkTextSearchFlags
	if (!rt->case_sensitive)
		flags |= E2_SEARCH_CASE_INSENSITIVE;
	if (rt->whole_words)
		flags |= E2_SEARCH_WHOLE_WORD;

	gboolean found = FALSE;
	gboolean any = !rt->whole_words;
	iter = start;
	while (e2_iter_forward_search (&iter, searchstr, flags, &start, &end, NULL))
	{
		//as we only check for word-starts in the search func, ensure end is ok if relevant
		if (any || gtk_text_iter_ends_word (&end))
		{
			found = TRUE;
			gtk_text_buffer_apply_tag_by_name (rt->textbuffer, "matched", &start, &end);
		}
		iter = end;
	}
	rt->is_lit = found;
	if (found && rt->replacebar != NULL)	//in edit mode
		g_signal_connect (G_OBJECT (rt->textbuffer), "changed",
			G_CALLBACK(_e2_view_dialog_buffer_changed_cb), rt);
}
/**
@brief extinguish highlighting of matched strings
@param rt dialog runtime data struct

@return
*/
void e2_view_dialog_clear_hilites (E2_ViewDialogRuntime *rt)
{
	if (rt->is_lit)
	{
		GtkTextIter start, end;
		gtk_text_buffer_get_bounds (rt->textbuffer, &start, &end);
		gtk_text_buffer_remove_tag_by_name (rt->textbuffer, "matched", &start, &end);
		rt->is_lit = FALSE;
		if (rt->replacebar != NULL)	//in edit mode
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2_view_dialog_buffer_changed_cb, rt);
	}
}
#endif	//def E2_MARK_FINDS
/**
@brief update display of entry for @a combo, when it is (re)displayed

@param combo combobox widget to be processed

@return
*/
void e2_view_dialog_update_combo (GtkWidget *combo)
{
	if (e2_option_bool_get ("dialog-search-show-last")
		&& e2_combobox_has_history (GTK_COMBO_BOX (combo)))
			gtk_editable_select_region (GTK_EDITABLE (GTK_BIN (combo)->child), 0, -1);
	else
		gtk_entry_set_text (GTK_ENTRY (GTK_BIN (combo)->child), "");
}
/**
@brief set textbuffer marks related to searching

@param buffer textbuffer to be searched
@param start pointer to textiter at location for start-mark
@param end pointer to textiter at location for end-mark

@return
*/
static void _e2_view_dialog_attach_search_iters (GtkTextBuffer *buffer,
	GtkTextIter *start, GtkTextIter *end)
{
	GtkTextMark *mark = gtk_text_buffer_get_mark (buffer, "e2-search-start");
	if (mark == NULL)
		gtk_text_buffer_create_mark (buffer, "e2-search-start", start, FALSE);
	else
		gtk_text_buffer_move_mark (buffer, mark, start);
	mark = gtk_text_buffer_get_mark (buffer, "e2-search-end");
	if (mark == NULL)
		gtk_text_buffer_create_mark (buffer, "e2-search-end", end, FALSE);
	else
		gtk_text_buffer_move_mark (buffer, mark, end);
}
/**
@brief clear textbuffer marks related to searching, if they exist

@param buffer textbuffer which has been searched

@return
*/
static void _e2_view_dialog_unattach_search_iters (GtkTextBuffer *buffer)
{
	GtkTextMark *mark = gtk_text_buffer_get_mark (buffer, "e2-search-start");
	if (mark != NULL)
		gtk_text_buffer_delete_mark (buffer, mark);
	mark = gtk_text_buffer_get_mark (buffer, "e2-search-end");
	if (mark != NULL)
		gtk_text_buffer_delete_mark (buffer, mark);
}
/**
@brief initiate search process after setting relevant flags

@param search_iter pointer to iter where the search is to start
@param start pointer to iter to store the start of a matched string
@param end pointer to iter to store the end of a matched string
@param find the string to search for, may include 1 or more '\n'
@param incremental TRUE when performing an incremental search after a search-string keypress
@param rt pointer to dialog runtime data struct

@return TRUE if a match was found
*/
static gboolean _e2_view_dialog_dosearch (GtkTextIter *search_iter,
	GtkTextIter *start, GtkTextIter *end, const gchar *find, gboolean incremental,
	E2_ViewDialogRuntime *rt)
{
	E2TextSearchFlags flags = E2_SEARCH_TEXT_ONLY;	//superset of GtkTextSearchFlags
	if (!rt->case_sensitive)
		flags |= E2_SEARCH_CASE_INSENSITIVE;
	if (rt->whole_words)
		flags |= E2_SEARCH_WHOLE_WORD;
	if (rt->search_backward)
	{
		//try for incremental match of a current string in same place
		//FIXME find a more elegant way of doing this
		GtkTextIter next = *search_iter;
		if (incremental && !gtk_text_iter_forward_line (&next))
			next = *search_iter;	//revert after failure
		return (e2_iter_backward_search (&next, find, flags, start, end, NULL));
	}
	else
		return (e2_iter_forward_search (search_iter, find, flags, start, end, NULL));
}
/**
@brief try to find string and update GUI accordingly

@param first TRUE to initiate scanning from start or end of text, FALSE to "find next"
@param incremental TRUE when performing an incremental scan after a match-string keypress
@param rt pointer to dialog runtime data struct

@return TRUE if a match was found
*/
gboolean e2_view_dialog_search (gboolean first, gboolean incremental,
	E2_ViewDialogRuntime *rt)
{
	GtkTextIter iter;
	// get the find string
	GtkWidget *entry = GTK_BIN (rt->combo)->child;
	const gchar *find = gtk_entry_get_text (GTK_ENTRY (entry));
	if (*find == '\0')
	{
#ifdef E2_MARK_FINDS
		e2_view_dialog_clear_hilites (rt);
#endif
		//hide any selection
		if (gtk_text_buffer_get_selection_bounds (rt->textbuffer, &iter, NULL))
			gtk_text_buffer_select_range (rt->textbuffer, &iter, &iter);
		return FALSE;
	}
	if (!incremental)
	{
#ifdef E2_MARK_FINDS
		_e2_view_dialog_set_hilites (find, rt);
#endif
		e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
		e2_list_update_history (find, &find_history, NULL, 30, FALSE);
	}
#ifdef E2_MARK_FINDS
	else
		//turn off any highlihts
		e2_view_dialog_clear_hilites (rt);
#endif

	//check if the TextView's buffer has changed and get new TextIters
	GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rt->textview));
/*	if (buffer != rt->textbuffer)
	{
		rt->textbuffer = buffer;
		unattach_iters (buffer);
		search_iter = NULL;
	}
*/
	GtkTextMark *mark = NULL;
	if (first)
	{	//searching starts from cursor, so move that
		_e2_view_dialog_unattach_search_iters (buffer);	//(re)start searching
		if (rt->search_backward)
			gtk_text_buffer_get_end_iter (buffer, &iter);
		else
			gtk_text_buffer_get_start_iter (buffer, &iter);
		gtk_text_buffer_place_cursor (buffer, &iter);
	}
	else
	{	//searching from cursor, not from start or end of buffer
		gboolean fromstart = (incremental) ? TRUE : rt->search_backward;
		if (fromstart)
			mark = gtk_text_buffer_get_mark (buffer, "e2-search-start");
		else
			mark = gtk_text_buffer_get_mark (buffer, "e2-search-end");
	}

	GtkTextIter *search_iter = NULL;
	if (mark != NULL)
	{
		gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
		search_iter = &iter;
	}
	if (search_iter == NULL)
	{
		//search from cursor
		mark = gtk_text_buffer_get_mark (buffer, "insert");
		gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
		search_iter = &iter;
/* MAYBE search from top/bottom of window if cursor is off-window ?
		GdkRectangle visible;
		gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (rt->textview),
			&visible);
		if (rt->opt_search_backward)
			//search backward from end of screen
			gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (rt->textview),
				&iter, visible.width, visible.height);
		else
			//search forward from top of screen
			gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (rt->textview),
				&iter, visible.x, visible.y); */
	}

	// search for the string
	GtkTextIter start, end;
	gboolean found = _e2_view_dialog_dosearch (search_iter, &start, &end, find,
		incremental, rt);

	if (!found && rt->search_wrap)
	{
		_e2_view_dialog_unattach_search_iters (buffer);
		if (rt->search_backward)
			gtk_text_buffer_get_end_iter (buffer, &iter);
		else
			gtk_text_buffer_get_start_iter (buffer, &iter);
		found = _e2_view_dialog_dosearch (&iter, &start, &end, find, incremental, rt);
	}
	if (found)
	{
		gtk_widget_hide (rt->info_label);
		gtk_text_buffer_select_range (buffer, &start, &end);	//always show where we are now
		_e2_view_dialog_attach_search_iters (buffer, &start, &end);
		//scroll found position into window, if not there already
		GdkRectangle visible_rect;	//, iter_rect;
		gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (rt->textview), &visible_rect);

		gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (rt->textview), &iter,
			visible_rect.x, visible_rect.y);
		gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (rt->textview), &end,
			visible_rect.x + visible_rect.width, visible_rect.y + visible_rect.height);
		if (!gtk_text_iter_in_range (&start, &iter, &end))
/* alternative approach, using vertical check only
		gtk_text_view_get_iter_location (GTK_TEXT_VIEW (rt->textview), &start, &iter_rect);
		if ((iter_rect.y + iter_rect.height) > (visible_rect.y + visible_rect.height)
			|| iter_rect.y < visible_rect.y)
*/
			gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (rt->textview), &start,
				0.0, TRUE, 0.1, (rt->search_backward) ? 0.8 : 0.2);
		return TRUE;
	}
	gtk_widget_show (rt->info_label);
	return FALSE;
}

  /*****************/
 /***** utils *****/
/*****************/

/**
@brief read text file to be viewed
This is used by both view and edit dialogs
Assumes the item being read is in curr_view i.e. no cd race from queueing
Character encoding is performed if necessary and possible.
@a rt ->filepath has the localised name of the file to be read. It will be
replaced by a newly-allocated string, to be freed elsewhere
Error messages and any macro-instigated dialog expect BGL open
@param localfile name of item to read, localised text, not neccessarily absolute
@param rt dialog runtime data struct

@return TRUE if the read and conversion was successful
*/
gboolean e2_view_dialog_read_text (VPATH *localfile, E2_ViewDialogRuntime *rt)
{
	E2_ERR_DECLARE
	gchar *localpath;
	if (!g_path_is_absolute (VPSTR (localfile)))
		localpath = g_build_filename (curr_view->dir, VPSTR (localfile), NULL);
	else
		localpath = g_strdup (VPSTR (localfile));	//copy in case we want to keep it
#ifdef E2_VFS
	VPATH ddata = { localpath, localfile->spacedata };
	if (e2_fs_access (&ddata, R_OK E2_ERR_PTR()))	//traverse a link
#else
	if (e2_fs_access (localpath, R_OK E2_ERR_PTR()))	//traverse a link
#endif
	{
		e2_fs_error_local (_("Cannot read '%s'"),
#ifdef E2_VFS
		&ddata E2_ERR_MSGL());
#else
		localpath E2_ERR_MSGL());
#endif
		E2_ERR_CLEAR
		g_free (localpath);
		return FALSE;
	}
#ifdef E2_VFS
	if (e2_fs_is_dir3 (&ddata E2_ERR_NONE()))
#else
	if (e2_fs_is_dir3 (localpath E2_ERR_NONE()))
#endif
	{
		e2_fs_error_simple (_("'%s' is a directory"),
#ifdef E2_VFS
		&ddata);
#else
		localpath);
#endif
		g_free (localpath);
		return FALSE;
	}

	gboolean usable;
	gpointer contents;
	gchar *converter = NULL;
	if (e2_option_bool_get ("use-external-encoder"))
	{
		converter = e2_option_str_get ("command-encoder");
		if (*converter != '\0')
		{
			gchar *command;
#ifdef E2_VFSTMP
	//can't sniff non-local files
#endif
			//sniff the file to check if conversion is really needed
			//this is not too good for files with mixed encoding !
			command = g_strconcat ("cat ", localpath, NULL);
			if (e2_fs_sniff_command_output (command, &contents, 1024))
			{
				usable = e2_utf8_detect_charset ((guchar *)contents, &rt->charset);
				g_free (contents);
			}
			else
				usable = FALSE;
			g_free (command);

			if (!usable)
			{	//we need to do a conversion
				//substitute file for any %f or %p in command string, get language etc
				gdk_threads_enter ();	//in case there's a dialog-macro in the command
				command = e2_utils_expand_macros (converter, localpath);
				gdk_threads_leave ();
				if (command == NULL || command == GINT_TO_POINTER(1))
				{
					g_free (localpath);
					return FALSE;
				}
				else
				{
					rt->charset = "UNKNOWN";	//no translation
					if (!e2_fs_get_command_output (command, &contents))
					{
						gchar *msg = g_strdup_printf
						(_("Encoding conversion command '%s' failed"), converter);
						gdk_threads_enter ();
						e2_output_print_error (msg, TRUE);
						gdk_threads_leave ();
						converter = NULL;	//set flag for internal conversion fallback
					}
					g_free (command);
				}
			}
			else	//no external conversion needed
				converter = NULL;
		}
		else
			converter = NULL;
	}

	if (converter == NULL	//not externally converted
#ifdef E2_VFS
		&& !e2_fs_get_file_contents (&ddata, &contents, NULL, TRUE E2_ERR_PTR()))
#else
		&& !e2_fs_get_file_contents (localpath, &contents, NULL, TRUE E2_ERR_PTR()))
#endif
	{
		e2_fs_error_local (_("Error reading file %s"),
#ifdef E2_VFS
		 &ddata E2_ERR_MSGL());
#else
		 localpath E2_ERR_MSGL());
#endif
		E2_ERR_CLEAR
		g_free (localpath);
		return FALSE;
	}
	//fix CR's in the text if necessary
	rt->linebreak = e2_utils_LF_line_ends ((gchar *)contents);
	//fix character encoding if necessary and possible
	if (converter == NULL)	//not externally converted
	{
		if (*((gchar *)contents) != '\0')
		{
			gchar *format = _("Conversion from %s encoding failed: \"%s\"");
			gchar *msg, *utf;
			GError *utf_error;
			usable = e2_utf8_detect_charset ((guchar *)contents, &rt->charset);
			if (!usable)
			{	//convert using the identified encoding if we can
				//but no point in 'non-conversion' ...
				if (strstr (rt->charset, "UTF-8") == NULL)
				{
					utf_error = NULL;
					utf = g_convert ((gchar *)contents, -1, "UTF-8", rt->charset, NULL, NULL, &utf_error);
					if (utf != NULL)
					{
						printd (DEBUG, "converted content from %s to UTF8", rt->charset);
						usable = TRUE;
						g_free (contents);	//need free() if file buffer allocated by malloc()
						contents = utf;
					}
					else
					{
						printd (DEBUG, "FAILED conversion of content from %s to UTF8", rt->charset);
						if (!g_str_equal (rt->charset, "CP1252"))	//that's generally a default when we can't find the real value
						{
							msg = g_strdup_printf (format, rt->charset, utf_error->message);
							gdk_threads_enter ();
							e2_output_print_error (msg, TRUE);
							gdk_threads_leave ();
						}
						g_error_free (utf_error);
					}
				}
			}
			if (!usable)
			{	//try default coding as a fallback
				const gchar *defset;
				e2_utils_get_charset (&defset);
				//again, no point in 'non-conversion'
				if (strstr (defset, "UTF-8") != NULL)
					defset = e2_cl_options.fallback_encoding;
				if (strstr (defset, "UTF-8") != NULL)
					defset = "ISO-8859-1";
				printd (DEBUG, "Conversion from %s encoding to be attempted", defset);
				utf_error = NULL;
				utf = g_convert ((gchar *)contents, -1, "UTF-8", defset, NULL, NULL, &utf_error);
				if (utf == NULL)
				{
					msg = g_strdup_printf (format, defset, utf_error->message);
					gdk_threads_enter ();
					e2_output_print_error (msg, TRUE);
					gdk_threads_leave ();
					g_error_free (utf_error);
					g_free (contents);	//need free() if file buffer allocated by malloc()
					return FALSE;
				}
				else if (!g_utf8_validate (utf, -1, NULL))
				{
					g_free (utf);
					g_free (contents);	//need free() if file buffer allocated by malloc()
					return FALSE;
				}
				rt->charset = "UNKNOWN";	//prevent attempts at saving to the unusable charset
				g_free (contents);	//need free() if file buffer allocated by malloc()
				contents = utf;
			}
		}
		else //assume default encoding for empty files
		{
			e2_utils_get_charset (&rt->charset);
//			printd (DEBUG, "empty file, assume default charset %s", rt->charset);
		}
	}
	if (rt->localpath != NULL)
		g_free (rt->localpath);
	rt->localpath = localpath;	//we're ok, so remember the real path
#ifdef E2_VFS
	rt->spacedata = localfile->spacedata;
#endif

	//for some reason, the buffer sometimes takes a while to be created
	while (!GTK_IS_TEXT_BUFFER (rt->textbuffer))
		rt->textbuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (rt->textview));

	gtk_text_buffer_set_text (rt->textbuffer, (gchar *)contents, -1);
	g_free (contents);

	//put cursor at start of buffer, for searching etc
	GtkTextIter start;
	gtk_text_buffer_get_start_iter (rt->textbuffer, &start);
	gtk_text_buffer_place_cursor (rt->textbuffer, &start);

	printd (DEBUG, "Read text file charset is %s", rt->charset);

	return TRUE;
}
/**
@brief set dialog font and get size of a typical character in that font

@param char_width store for character width for font
@param char_height store for character height for font
@param rt pointer to dialog data struct

@return
*/
void e2_view_dialog_set_font (gint *char_width, gint *char_height, E2_ViewDialogRuntime *rt)
{
	gchar *fntname;
	if (e2_option_bool_get ("dialog-view-use-font"))
	{
		fntname = e2_option_str_get ("dialog-view-font");
		if (*fntname == '\0')
			fntname = NULL;
	}
	else
		fntname = NULL;
	if (fntname == NULL)
	{
		GtkSettings* defs = gtk_settings_get_default ();
		g_object_get (G_OBJECT (defs), "gtk-font-name", &fntname, NULL);
		if (fntname == NULL)	//CHECKME needed ?
		{
			printd (WARN, "No default font detected");
			fntname = "Sans 10";
		}
	}
	e2_widget_set_font (rt->textview, fntname);
	e2_widget_get_font_pixels (rt->textview, char_width, char_height);
}
/**
@brief set popup menu position

This function is supplied when calling gtk_menu_popup(), to position
the displayed menu.
Set @a push_in to TRUE for menu completely inside the screen,
FALSE for menu clamped to screen size

@param menu the GtkMenu to be positioned
@param x	place to store gint representing the menu left
@param y  place to store gint representing the menu top
@param push_in place to store pushin flag
@param rt data struct for dialog in focus when the menu key was pressed

@return
*/
void e2_view_dialog_set_menu_position (GtkWidget *menu,
	gint *x, gint *y, gboolean *push_in, E2_ViewDialogRuntime *rt)
{
	gint left, top;
	gtk_window_get_position (GTK_WINDOW (rt->dialog), &left, &top);
	GtkAllocation alloc = rt->textview->allocation;
	*x = left + alloc.x + alloc.width/2;
	*y = top + alloc.y + alloc.height/2;
	*push_in = FALSE;
}
#ifdef USE_GTK2_10
/**
@brief callback for "begin-print" signal on print-operation for selected or all text

@param op the print operation
@param context print context for the operation
@param rt runtime data struct for the dialog

@return
*/
static void _e2_view_dialog_begin_print_cb (GtkPrintOperation *op,
		GtkPrintContext *context, E2_ViewDialogRuntime *rt)
{
	GtkTextIter start, end;
	if (gtk_text_buffer_get_has_selection (rt->textbuffer))
		gtk_text_buffer_get_selection_bounds (rt->textbuffer, &start, &end);
	else
		gtk_text_buffer_get_bounds (rt->textbuffer, &start, &end);

	gchar *seltext = gtk_text_buffer_get_text (rt->textbuffer, &start, &end, FALSE);
	g_strstrip (seltext);
	if (*seltext != '\0')
	{
		PangoLayout *layout = gtk_print_context_create_pango_layout (context);
		//remember it for other uses
		g_object_set_data_full (G_OBJECT (context), "e2-print-layout", layout,
			(GDestroyNotify) g_object_unref);	//cleanup when context is destroyed
		PangoFontDescription *font_desc = gtk_widget_get_style (rt->textview)->font_desc;
		pango_layout_set_font_description (layout, font_desc);
		gdouble page_width = gtk_print_context_get_width (context); //in pixels
		//don't assume printer device-unit is one point
		gdouble dpi = gtk_print_context_get_dpi_x (context);
		page_width = page_width * 72 / dpi; //now in points
		pango_layout_set_width (layout, (gint) page_width * PANGO_SCALE);
		pango_layout_set_text (layout, seltext, -1);

		gdouble page_height = gtk_print_context_get_height (context); //in pixels
		if (!pango_font_description_get_size_is_absolute (font_desc))
		{
			dpi = gtk_print_context_get_dpi_y (context);
			page_height = page_height * 72 / dpi; //now in points
		}
		gint text_height = pango_font_description_get_size (font_desc) / PANGO_SCALE; //in points or pixels
		gint lines_per_page = page_height / text_height;
		gint line_count = pango_layout_get_line_count (layout);
		gint page_count = (line_count - 1) / lines_per_page + 1;	//round upwards
		gtk_print_operation_set_n_pages (op, page_count);

		//setup page header with filename
		PangoLayout *layout_name = gtk_print_context_create_pango_layout (context);
		g_object_set_data_full (G_OBJECT (context), "e2-print-name", layout_name,
			(GDestroyNotify) g_object_unref);	//cleanup when context is destroyed
		pango_layout_set_font_description (layout_name, font_desc);
		pango_layout_set_width (layout_name, (gint) page_width * PANGO_SCALE);
		gchar *base = g_path_get_basename (rt->localpath);
		gchar *page_title = F_FILENAME_FROM_LOCALE (base);
		pango_layout_set_text (layout_name, page_title, -1);
		g_free (base);
		F_FREE (page_title);
		//and for page counter, if > 1 page
		if (page_count > 1)
		{
			PangoLayout *layout_num = gtk_print_context_create_pango_layout (context);
			pango_layout_set_font_description (layout_num, font_desc);
//			pango_layout_set_alignment (layout_right, PANGO_ALIGN_RIGHT);
			g_object_set_data_full (G_OBJECT (context), "e2-print-page", layout_num,
				(GDestroyNotify) g_object_unref);	//cleanup when context is destroyed
		}
	}
	g_free (seltext);
}
/* *
@brief callback for "end-print" signal on print-operation for selected or all text

@param op UNUSDED the print operation
@param context print context for the operation
@param data UNUSED data specified when callback was connected

@return
*/
/*static void _e2_view_dialog_end_print_cb (GtkPrintOperation *op,
	GtkPrintContext *context, gpointer data)
{
} */
/**
@brief callback for "draw-page" signal on print-operation for selected or all text

@param op UNUSDED the print operation
@param context print context for the operation
@param page_num 0-based index of printed pages count
@param rt runtime data struct for the dialog

@return
*/
static void _e2_view_dialog_draw_page_cb (GtkPrintOperation *op,
	GtkPrintContext *context, gint page_num, E2_ViewDialogRuntime *rt)
{
//	PangoLayoutLine *line, *layout_lh, *layout_rh;
	cairo_t *cr = gtk_print_context_get_cairo_context (context);
	if (cr != NULL)
	{
		gdouble dpi;
		//print header, comprising filename, and page-number if > 1 page
		PangoFontDescription *font_desc = gtk_widget_get_style (rt->textview)->font_desc;
		//CHECKME dpi_v instead of 72 ?
		cairo_move_to (cr, 0, - 10 * 72 / 25.4);	//left margin, up 10 mm from top of text
		PangoLayout *layout = g_object_get_data (G_OBJECT (context), "e2-print-name");
		pango_cairo_show_layout (cr, layout);

		gint n_pages;
		g_object_get (G_OBJECT (op), "n-pages", &n_pages, NULL);
		if (n_pages > 1)
		{
			PangoLayout *layout_right = g_object_get_data (G_OBJECT (context),
				"e2-print-page");
			gchar *page_text = g_strdup_printf ("%d / %d", page_num + 1, n_pages);
			pango_layout_set_text (layout_right, page_text, -1);
			g_free (page_text);
			gdouble page_width = gtk_print_context_get_width (context); //in pixels
			dpi = gtk_print_context_get_dpi_x (context);
			page_width = page_width * 72 / dpi; //in points
			gint layout_width;
			pango_layout_get_size (layout_right, &layout_width, NULL);
			//CHECKME dpi_v instead of 72 ?
			cairo_move_to (cr,
				page_width - layout_width / PANGO_SCALE, - 10 * 72 / 25.4); //up 10 mm from top of text
			pango_cairo_show_layout (cr, layout_right);
		}

		layout = g_object_get_data (G_OBJECT (context), "e2-print-layout");
		gdouble page_height = gtk_print_context_get_height (context);	//in pixels
		if (!pango_font_description_get_size_is_absolute (font_desc))
		{
			dpi = gtk_print_context_get_dpi_y (context);
			page_height = page_height / dpi * 72; //now in points
		}
		gint text_height = pango_font_description_get_size (font_desc) / PANGO_SCALE; //in pixels or points
		guint lines_per_page = page_height / text_height;
		gint line_count = pango_layout_get_line_count (layout);
		guint last_line = (line_count > lines_per_page * (page_num + 1)) ?
			lines_per_page * (page_num + 1) : line_count;
		guint pageline, fileline;
		for (pageline = 1, fileline = lines_per_page * page_num;	//CHECKME pageline=0 (move to tops of lines?)
				fileline < last_line; pageline++, fileline++)
		{
			PangoLayoutLine *linelay = pango_layout_get_line (layout, fileline);
			cairo_move_to (cr, 0, pageline * text_height);
			pango_cairo_show_layout_line (cr, linelay);
		}
	}
}
/**
@brief callback for "status-changed" signal on print-operation for selected or all text

@param op the print operation
@param data UNUSED data specified when callback was connected

@return
*/
static void _e2_view_dialog_print_status_cb (GtkPrintOperation *op,
	gpointer data)
{
	GtkPrintStatus pstat = gtk_print_operation_get_status (op);
	switch (pstat)
	{
		case GTK_PRINT_STATUS_PENDING_ISSUE:
		case GTK_PRINT_STATUS_FINISHED:	//also for end of preview
		case GTK_PRINT_STATUS_FINISHED_ABORTED: //BAD for sync, maybe OK for async
			g_object_unref (G_OBJECT (op));	//cleanup, ref => 2
		default:
			break;
	}
}
/**
@brief asynchronously print currently-selected text or all text if no selection

@param menuitem UNUSED the selected widget, or NULL
@param rt runtime struct to work on

@return
*/
void e2_view_dialog_print_cb (GtkWidget *menuitem, E2_ViewDialogRuntime *rt)
{
//	static GtkPrintSettings *settings = NULL;
	static GtkPageSetup *page_setup = NULL;

	GtkPrintOperation *op = gtk_print_operation_new ();
/*	too hard to properly manage refcount in all usage cases
	if (settings != NULL)
		//re-use the settings from a former print operation
		gtk_print_operation_set_print_settings (op, settings);
*/

	if (page_setup == NULL)
	{
		page_setup = gtk_page_setup_new ();	//never cleaned
		gtk_page_setup_set_top_margin (page_setup, 25.0, GTK_UNIT_MM);
		gtk_page_setup_set_bottom_margin (page_setup, 20.0, GTK_UNIT_MM);
		gtk_page_setup_set_left_margin (page_setup, 20.0, GTK_UNIT_MM);
		gtk_page_setup_set_right_margin (page_setup, 20.0, GTK_UNIT_MM);
	}

	gtk_print_operation_set_default_page_setup (op, page_setup);
	gtk_print_operation_set_allow_async (op, TRUE);

	g_signal_connect (G_OBJECT (op), "begin-print",
		G_CALLBACK (_e2_view_dialog_begin_print_cb), rt);
//	g_signal_connect (G_OBJECT (op), "end-print",
//		G_CALLBACK(_e2_view_dialog_end_print_cb), NULL);
	g_signal_connect (G_OBJECT (op), "draw-page",
		G_CALLBACK (_e2_view_dialog_draw_page_cb), rt);
	g_signal_connect (op, "status-changed",
		G_CALLBACK (_e2_view_dialog_print_status_cb), NULL);
	GError *error = NULL;
	GtkPrintOperationResult result = gtk_print_operation_run (op,
		GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG, GTK_WINDOW (rt->dialog), &error);

	switch (result)
	{
//		GObject *debug2;
		case GTK_PRINT_OPERATION_RESULT_ERROR:
			e2_output_print_error (error->message, FALSE);
			g_error_free (error);
/*		case GTK_PRINT_OPERATION_RESULT_APPLY:	//e.g. after sync preview
		case GTK_PRINT_OPERATION_RESULT_IN_PROGRESS:
	TOO HARD TO PROPERLY MANAGE SETTINGS REFCOUNT IN ALL USAGE CASES
*/
		default:
//			if (result != GTK_PRINT_OPERATION_RESULT_APPLY)//this for after sync preview
//				debug2 = G_OBJECT (op);	//refcount 2 in async mode
			if (!(result == GTK_PRINT_OPERATION_RESULT_IN_PROGRESS
				|| result == GTK_PRINT_OPERATION_RESULT_APPLY))	//e.g. after a sync preview cancelled
				g_object_unref (G_OBJECT (op));
			break;
	}
}
#endif //def USE_GTK2_10
/**
@brief perform copy

@param menuitem UNUSED the selected widget, or NULL
@param rt runtime struct to work on

@return
*/
static void _e2_view_dialog_copy_cb (GtkWidget *menuitem,
	E2_ViewDialogRuntime *rt)
{
	GtkClipboard *cb = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
	gtk_text_buffer_copy_clipboard (rt->textbuffer, cb);
}
/**
@brief construct and show dialog context menu

@param textview the textview widget where the click happened
@param event_button which mouse button was clicked (0 for a menu key)
@param event_time time that the event happened (0 for a menu key)
@param rt runtime struct to work on

@return
*/
static void _e2_view_dialog_show_context_menu (GtkWidget *textview,
	guint event_button, gint event_time, E2_ViewDialogRuntime *rt)
{
	gchar *item_name;
	GtkWidget *menu = gtk_menu_new ();

/*	GtkWidget *item = e2_menu_add (menu, _("_Find"), GTK_STOCK_FIND,
		NULL,  _e2_view_dialog_menu_find_cb, rt);
#ifdef USE_GTK2_12TIPS
	e2_widget_set_toggletip (
#else
	gtk_tooltips_set_tip (app.tooltips,
#endif
		item,
		_("Show the search options bar"), _("Find the first/next occurrence"));

	GtkWidget *submenu = e2_menu_add_submenu (menu, _("_Settings"), GTK_STOCK_PREFERENCES);
	e2_option_create_menu (GTK_WIDGET (textview), submenu,
		opt_view_wrap, NULL, NULL,
 etc
		app.output.opt_show_on_new, NULL, NULL,
		app.output.opt_show_on_focus_in, NULL, NULL,
		NULL);
*/
	GtkWidget *item = e2_menu_add (menu, _("_Copy"), GTK_STOCK_COPY,
		_("Copy selected text"), _e2_view_dialog_copy_cb, rt);
	gtk_widget_set_sensitive (item,
		gtk_text_buffer_get_selection_bounds (rt->textbuffer, NULL, NULL));
#ifdef USE_GTK2_10
	gchar *tip = (gtk_text_buffer_get_has_selection (rt->textbuffer)) ?
		_("Print selected text") : _("Print file");
	e2_menu_add (menu, _("_Print.."), GTK_STOCK_PRINT, tip,
		e2_view_dialog_print_cb, rt);
#endif
	e2_menu_add_separator (menu);
	item_name = g_strconcat (_A(2),".",_A(32),NULL);
	e2_menu_add_action (menu, _("_Settings"), GTK_STOCK_PREFERENCES,
		_("Open the configuration dialog at the options page"),
		item_name, _C(11), //_("dialogs")
		NULL);
	g_free(item_name);

	g_signal_connect (G_OBJECT (menu), "selection-done",
		G_CALLBACK (e2_menu_destroy_cb), NULL);

	if (event_button == 0)
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			(GtkMenuPositionFunc) e2_view_dialog_set_menu_position,
			rt, event_button, event_time);
	else
		//this was a button-3 click
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			NULL, NULL, event_button, event_time);
}
/**
@brief construct and show hbox with search-bar items

@param rt runtime struct to work on

@return hbox for the searchbar
*/
GtkWidget *e2_view_dialog_create_searchbar (E2_ViewDialogRuntime *rt)
{
	rt->is_hidden = TRUE;	//searchbar is not displayed until a search is started
	rt->release_blocked = FALSE;

	GtkWidget *hbox = gtk_hbox_new (FALSE, 0);
	//add search-string combo
	rt->combo = e2_combobox_add (hbox, TRUE, 0, NULL, NULL, &find_history,
		E2_COMBOBOX_HAS_ENTRY | E2_COMBOBOX_FOCUS_ON_CHANGE);	//| E2_COMBOBOX_NO_AUTO_HISTORY);
	gtk_widget_set_size_request (rt->combo, 230, -1);

	GtkWidget *entry = GTK_BIN (rt->combo)->child;
	//any signal applied to this widget will also apply to the dialog window
	//as a whole and to any other element of it!!!
	g_signal_connect_after (G_OBJECT (entry), "key-release-event",
		G_CALLBACK (e2_view_dialog_combokey_cb), rt);
#ifdef USE_GTK2_12TIPS
	gtk_widget_set_tooltip_text (
#else
	e2_widget_set_tooltip (NULL,
#endif
		entry, _("Finds"));
	//add search-option buttons
	//if also replacing, to make expandable entries the same width,
	//the buttons need to be in a specific hbox in a size group
	GtkWidget *hbox2;
	if (rt->sgroup != NULL)	//this is setup in edit dialog code
	{
		hbox2 = e2_widget_add_box (hbox, FALSE, 0, FALSE, FALSE, 0);
		gtk_size_group_add_widget (rt->sgroup, hbox2);
	}
	else
		hbox2 = hbox;

	GtkWidget *btn = e2_button_add_toggle (hbox2, TRUE, rt->case_sensitive,
		_("_match case"), _("If activated, text case does matter when searching"),
		FALSE, E2_PADDING_XSMALL, e2_view_dialog_toggled, &rt->case_sensitive);
	//flag that extra processing is needed in the callback
	g_object_set_data (G_OBJECT (btn), "e2_dlg_runtime", rt);
	btn = e2_button_add_toggle (hbox2, TRUE, rt->whole_words,
		_("wh_ole words"),_("If activated, matches must be surrounded by word-separator characters"),
		FALSE, E2_PADDING_XSMALL, e2_view_dialog_toggled, &rt->whole_words);
	g_object_set_data (G_OBJECT (btn), "e2_dlg_runtime", rt);
	e2_button_add_toggle (hbox2, TRUE, rt->search_backward,
		_("_backward"),_("If activated, searching proceeds toward document start"),
		FALSE, E2_PADDING_XSMALL, e2_view_dialog_toggled, &rt->search_backward);
	e2_button_add_toggle (hbox2, TRUE, rt->search_wrap,
		_("_loop"), _("If activated, searching cycles from either end to the other"),
		FALSE, E2_PADDING_XSMALL, e2_view_dialog_toggled, &rt->search_wrap);

	gtk_widget_show (hbox);
	return hbox;
}
/**
@brief setup initial view position, when re-viewing/editing a file

@param filepath utf8 string with full path of item being re-opened
@param rt runtime struct to work on

@return hbox for the searchbar
*/
void e2_view_dialog_show_atlast (VPATH *localpath, E2_ViewDialogRuntime *rt)
{
	GtkTextIter top;
	GList *iterator;
	E2_ViewHistory *viewed;
	for (iterator = view_history; iterator != NULL; iterator = iterator->next)
	{
		viewed = (E2_ViewHistory *) iterator->data;
#ifdef E2_VFSTMP
		compare spacedata ?
#endif
		if (g_str_equal (viewed->localpath, VPSTR (localpath)))
			break;
	}
	if (iterator != NULL)
	{
		//cannot use buffer coordinates here (bad for all but smallest files)
		gtk_text_buffer_get_iter_at_line (rt->textbuffer, &top, viewed->topline);
		//need to add a mark to get the scroll to work properly
		GtkTextMark *mark = gtk_text_buffer_create_mark
			(rt->textbuffer, NULL, &top, FALSE);
		gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (rt->textview), mark,
			0.0, TRUE, 0.0, 0.0);
	}
	else
		gtk_text_buffer_get_start_iter (rt->textbuffer, &top);

	gtk_text_buffer_place_cursor (rt->textbuffer, &top);

	e2_dialog_show (rt->dialog, app.main_window, E2_DIALOG_DONT_SHOW_ALL,
		NULL);
	gtk_window_present (GTK_WINDOW (rt->dialog));
}
/**
@brief cleanup when ending a view dialog

@param rt runtime struct to work on

@return
*/
void e2_view_dialog_destroy (E2_ViewDialogRuntime *rt)
{
	//in case we last performed an incremental search ...
	if (!rt->is_hidden)
	{
		GtkWidget *entry = GTK_BIN (rt->combo)->child;
		const gchar *find = gtk_entry_get_text (GTK_ENTRY (entry));
		if (*find != '\0')
		{
			e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
			e2_list_update_history (find, &find_history, NULL, 30, FALSE);
		}
	}
	//backup some dialog-specific options
	case_sensitive = rt->case_sensitive;
	whole_words = rt->whole_words;
	search_backward = rt->search_backward;
	search_wrap = rt->search_wrap;
	//in case we want to re-view, remember where we are now
	//(need to use line no. as buffer coords are no use when reloading)
	GList *iterator;
	E2_ViewHistory *viewed;
	for (iterator = view_history; iterator != NULL; iterator = iterator->next)
	{
		viewed = (E2_ViewHistory *) iterator->data;
		if (g_str_equal (viewed->localpath, rt->localpath))
			break;
	}
	if (iterator == NULL)
	{
		viewed = MALLOCATE (E2_ViewHistory);	//too small for slice, never deallocated
		CHECKALLOCATEDWARN (viewed, );
		if (viewed != NULL)
		{
#ifdef E2_VFSTMP
			spacedata needed ?
#endif
			viewed->localpath = g_strdup (rt->localpath);
			view_history = g_list_append (view_history, viewed);
		}
	}

	GdkRectangle visible_rect;
	GtkTextIter start;
	gtk_text_view_get_visible_rect (GTK_TEXT_VIEW (rt->textview), &visible_rect);
	gtk_text_view_get_line_at_y (GTK_TEXT_VIEW (rt->textview), &start,
		visible_rect.y, NULL);
//	if ()
//	{
		//when editing the output buffer, we don't want to change it
		GtkTextIter scan;
		guint i = 0;
		do
		{
			gtk_text_buffer_get_iter_at_line (rt->textbuffer, &scan, i++);
		} while (gtk_text_iter_compare (&scan, &start) < 0);
		viewed->topline = i-1;
/*	}
	else
	{
		//get gtk to count the preceding lines by deleting the rest of the conents
		GtkTextIter end;
		gtk_text_buffer_get_end_iter (rt->textbuffer, &end);
		gtk_text_buffer_delete (rt->textbuffer, &start, &end);
		viewed->topline = gtk_text_buffer_get_line_count (rt->textbuffer);
		if (viewed->topline > 0)
			viewed->topline--;
	} */

#ifdef E2_TRANSIENTKEYS
	e2_keybinding_unregister (rt->key_binding, GTK_WIDGET (rt->textview));
	g_free (rt->key_binding);
#endif
	gtk_widget_destroy (rt->dialog);
	gtk_widget_grab_focus (curr_view->treeview);	//CHECKME consistency ok ?
	g_free (rt->localpath);
//	if (rt->idle_id != 0)
//		g_source_remove (rt->idle_id);
	DEALLOCATE (E2_ViewDialogRuntime, rt);
}

  /*********************/
 /***** callbacks *****/
/*********************/

/**
@brief menu-button press callback

@param textview the textview widget where the press happened
@param rt dialog runtime data struct

@return TRUE always
*/
static gboolean _e2_view_dialog_popup_menu_cb (GtkWidget *textview,
	E2_ViewDialogRuntime *rt)
{
	gint event_time = gtk_get_current_event_time ();
	_e2_view_dialog_show_context_menu (textview, 0, event_time, rt);
	return TRUE;
}
/**
@brief mouse button press callback

@param textview the widget where the button was pressed
@param event gdk event data
@param rt rt data for the dialog

@return TRUE (stop other handlers) for btn 3 press, else FALSE (allow other handlers)
*/
static gboolean _e2_view_dialog_button_press_cb (GtkWidget *textview,
	GdkEventButton *event, E2_ViewDialogRuntime *rt)
{
	if (event->button == 3)
	{
		_e2_view_dialog_show_context_menu (textview, 3, event->time, rt);
		return TRUE;
	}
	return FALSE;
}
/**
@brief check-button state change callback

@param button the button widget that changed
@param store pointer to store for the new state of @a button

@return
*/
void e2_view_dialog_toggled (GtkWidget *button, gboolean *store)
{
	*store = GTK_TOGGLE_BUTTON (button)->active;
	//whole-word and case-match buttons need extra handling
	E2_ViewDialogRuntime *rt = g_object_get_data (G_OBJECT (button), "e2_dlg_runtime");
	if (rt != NULL)
		rt->research = TRUE;	//trigger re-highlighting of matches
}
/**
@brief general dialog key press callback

@param textview UNUSED the widget where the button was pressed
@param event gdk event data
@param rt rt data for the dialog

@return TRUE (stop other handlers) for recognised keys
*/
static gboolean _e2_view_dialog_key_press_cb (GtkWidget *textview, GdkEventKey *event,
	E2_ViewDialogRuntime *rt)
{
	printd (DEBUG, "_e2_view_dialog_key_press_cb textview: %x event: %x data: %x, key: %x",
		textview, event, rt, event->keyval);
	guint mask = gtk_accelerator_get_default_mod_mask () & event->state;
	guint lower = (gdk_keyval_is_upper (event->keyval)) ?
		gdk_keyval_to_lower (event->keyval) : event->keyval;
	if (//g_ascii_isalpha (event->keyval) //&& (event->keyval < 0xF000 || event->keyval > 0xFFFF) //&&
		(mask & GDK_CONTROL_MASK || mask == GDK_MOD1_MASK))
	{	//the key is a letter and the modifier is Alt or a modifier is Ctrl (Shift+Ctrl reverses direction)
		if (lower == find_keycode || lower == GDK_g) //special case, again
		{
			rt->release_blocked = TRUE;	//block anomalous key-release-event searches

			_e2_view_dialog_response_cb (GTK_DIALOG (rt->dialog),
				(lower == find_keycode) ? E2_RESPONSE_FIND : E2_RESPONSE_USER2, rt);
			return TRUE;
		}
		else if (lower == hide_keycode)
		{
			_e2_view_dialog_response_cb (GTK_DIALOG (rt->dialog),
				E2_RESPONSE_USER3, rt);
			return TRUE;
		}
	}
	printd (DEBUG, "_e2_view_dialog_key_press_cb returns FALSE");
	return FALSE;
}
/**
@brief key-release callback
This is intended for the search combo entry, but actually applies to any key
release for any widget in the dialog (and we block spurious ones with a flag)
This generally implements non-incremental searches, except when various
special cases are detected.
@param entry UNUSED the entry widget where the key was pressed
@param event pointer to event data struct
@param rt pointer to data struct for the search

@return FALSE always
*/
gboolean e2_view_dialog_combokey_cb (GtkWidget *entry, GdkEventKey *event,
	E2_ViewDialogRuntime *rt)
{
	if (event->keyval == GDK_BackSpace || event->keyval == GDK_Delete)
		e2_view_dialog_search (FALSE, TRUE, rt);
	else
	{
		guint mask = event->state & gtk_accelerator_get_default_mod_mask ();
		//except for <Ctrl><Return> release, incremental search is ineffective
		if (event->keyval == GDK_Return)
		{
			if (mask == GDK_SHIFT_MASK)
			{	//temporary direction change
				rt->search_backward = !rt->search_backward;
				e2_view_dialog_search (FALSE, FALSE, rt);
				rt->search_backward = !rt->search_backward;
			}
			else
				//non-incremental search, from start if <Control> is pressed
				e2_view_dialog_search ((mask == GDK_CONTROL_MASK), FALSE, rt);
		}
		else if (event->keyval < 0xF000 || event->keyval > 0xFFFF)	//only interested in "text" keyreleases
		{	//we recognise the equivalent of button presses here
			guint lower = (gdk_keyval_is_upper (event->keyval)) ?
				gdk_keyval_to_lower (event->keyval) : event->keyval;
			if (mask & GDK_CONTROL_MASK || mask == GDK_MOD1_MASK)
			{	//the modifier is Alt or a modifier is Ctrl (Shift+Ctrl reverses direction)
				if (lower == find_keycode || lower == GDK_g)	//special case, not in button label
				{
					if (rt->release_blocked)	//this is an event after a textview keypress
					{
						rt->release_blocked = FALSE;
						return FALSE;
					}
					else
					{
						printd (DEBUG, "e2_view_dialog_combokey_cb - going to response cb");
						_e2_view_dialog_response_cb (GTK_DIALOG (rt->dialog),
							(lower == find_keycode) ? E2_RESPONSE_FIND : E2_RESPONSE_USER2,
							rt);
						return TRUE;
					}
				}
				else if (lower == hide_keycode)
				{
					if (rt->replacebar != NULL)	//actually in edit mode
						return (e2_edit_dialog_key_press_cb (rt->textview, event, rt));
					else
					{
						_e2_view_dialog_response_cb (GTK_DIALOG (rt->dialog),
							E2_RESPONSE_USER3, rt);
						return TRUE;
					}
				}
				else if (lower == replace_keycode)
				{	//this combo is used for the edit dialog too
					if (rt->replacebar != NULL)	//actually in edit mode
						return (e2_edit_dialog_key_press_cb (rt->textview, event, rt));
				}
			}
			// by default, do an incremental search
			e2_view_dialog_search (FALSE, TRUE, rt);
		}
	}
	return FALSE;
}
/**
@brief view dialog response callback
This can also be called directly, from other mechanisms to initiate a search
The trigger may have been a <Ctrl>f or <Ctrl>g keypress
@param dialog UNUSED the dialog where the response was triggered
@param response the number assigned the activated widget
@param view rt data for the dialog

@return
*/
static void _e2_view_dialog_response_cb (GtkDialog *dialog, gint response,
	E2_ViewDialogRuntime *rt)
{
	printd (DEBUG, "_e2_view_dialog_response_cb (dialog:_,response:%d,rt:_)", response);
	switch (response)
	{
		case E2_RESPONSE_USER1:  //text wrap
			rt->textwrap = !rt->textwrap;
			gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (rt->textview),
				rt->textwrap ? GTK_WRAP_WORD: GTK_WRAP_NONE);
			break;
		case E2_RESPONSE_USER2:	//find next (after <Ctrl>g press)
		case E2_RESPONSE_FIND:
			if (!rt->is_hidden) //search panel is visible
			{
				//these tests are irrelevant for an actual response callback,
				//as no callback happens then
				//if a mod key is pressed, treat it specially
				//Ctrl = first, Shift = temp reverse
				GdkModifierType mask = e2_utils_get_modifiers () &
					gtk_accelerator_get_default_mod_mask ();
				if (mask & GDK_SHIFT_MASK)
				{	//temporary direction change
					rt->search_backward = !rt->search_backward;
					e2_view_dialog_search (response == E2_RESPONSE_FIND, FALSE, rt);
					rt->search_backward = !rt->search_backward;
				}
				else
					//perform non-incremental search, from start|end if <Control> is pressed
					e2_view_dialog_search (response == E2_RESPONSE_FIND, FALSE, rt);
			}
			else
			{	//show the search panel
				gtk_widget_show (rt->panel);
				rt->is_hidden = FALSE;
				if (rt->hidebtn != NULL)
					gtk_widget_show (rt->hidebtn);

				GtkTextIter start_iter, end_iter;
				if (gtk_text_buffer_get_selection_bounds (rt->textbuffer,
						&start_iter, &end_iter))
				{
					gchar *find_string = gtk_text_buffer_get_text
						(rt->textbuffer, &start_iter, &end_iter, FALSE);
					gtk_entry_set_text (GTK_ENTRY (GTK_BIN (rt->combo)->child),
						find_string);
					gtk_editable_select_region (GTK_EDITABLE (GTK_BIN (rt->combo)->child), 0, -1);
					e2_view_dialog_search (response == E2_RESPONSE_FIND, FALSE, rt); //CHECKME never search for first when first showing the bar
					g_free (find_string);
				}
				else
					e2_view_dialog_update_combo (rt->combo);

				//focus on the text entry
				gtk_widget_grab_focus (GTK_BIN (rt->combo)->child);
				//swap find btn tip
				if (rt->findbtn != NULL)
					e2_widget_swap_tooltip (rt->findbtn);
			}
			break;
/*		case E2_RESPONSE_USER2:	//find next
			_e2_view_dialog_search (FALSE, FALSE, rt);
			break; */
		case E2_RESPONSE_USER3:	//hide
		{
			//in case we last performed an incremental search ...
			GtkWidget *entry = GTK_BIN (rt->combo)->child;
			const gchar *find = gtk_entry_get_text (GTK_ENTRY (entry));
			if (*find != '\0')
			{
				e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
				e2_list_update_history (find, &find_history, NULL, 30, FALSE);
			}
			gtk_widget_hide (rt->panel);
			rt->is_hidden = TRUE;
//			gtk_widget_hide (rt->nextbtn);
			if (rt->hidebtn != NULL)	//in view mode
				gtk_widget_hide (rt->hidebtn);
			gtk_widget_hide (rt->info_label);
//			gtk_widget_show (rt->refreshbtn);
#ifdef E2_MARK_FINDS
			//turn off any highlihts
			e2_view_dialog_clear_hilites (rt);
#endif
			//hide any selection
			GtkTextIter iter;
			if (gtk_text_buffer_get_selection_bounds (rt->textbuffer, &iter, NULL))
				gtk_text_buffer_select_range (rt->textbuffer, &iter, &iter);
			//revert find btn tip
			if (rt->findbtn != NULL)
				e2_widget_swap_tooltip (rt->findbtn);
			gtk_widget_grab_focus (rt->textview);
		}
			break;
		default:
			e2_view_dialog_destroy (rt);
			break;
	}
}

#ifdef E2_TRANSIENTKEYS
/**
@brief function to setup default key-bindings for view dialog
This is just to provide placeholders, the actual bindings are meaningless
@param set pointer to option data struct

@return
*/
static void _e2_view_dialog_keybindings (E2_OptionSet *set)
{
	//the key name strings are parsed by gtk, and no translation is possible
	e2_option_tree_setup_defaults (set,
	g_strdup("keybindings=<"),  //internal name
	//the category string(s) here need to match the binding name
//	g_strconcat(_C(17),"||||",NULL),  //_("general"
	g_strconcat(_C(11),"||||",NULL),  //_("dialogs"
	g_strconcat("\t",_A(101),"||||",NULL),  //_("view"
	g_strconcat("\t\t|<Control>j","||",_A(119),".",_A(120),"|<Control>a",NULL),
	g_strconcat("\t\t|<Control>k","||",_A(119),".",_A(120),"|<Control>c",NULL),
	g_strdup(">"),
	NULL);
}
#endif

  /**********************/
 /**** dialog setup ****/
/**********************/

/**
@brief create and show view dialog

Expects BGL closed when we arrive here

@param localpath localised string which has path & name of item to be processed
@param srt pointer to store for returning dialog data, or NULL

@return the dialog widget, or NULL if there's a problem
*/
static GtkWidget *_e2_view_dialog_create (VPATH *localpath,
	E2_ViewDialogRuntime **srt)
{
	//init view runtime object (0's to ensure edit-dialog things are NULL)
	E2_ViewDialogRuntime *rt = ALLOCATE0 (E2_ViewDialogRuntime);
	CHECKALLOCATEDWARN (rt, )
	if (rt == NULL)
	{
		if (srt != NULL)
			*srt = NULL;
		return NULL;
	}
	rt->textview = gtk_text_view_new ();
	// add the item's content
	gdk_threads_leave ();	//any downstream error message does its own mutex management
	if (!e2_view_dialog_read_text (localpath, rt))
	{
		gdk_threads_enter ();
		gtk_widget_destroy (rt->textview);
		DEALLOCATE (E2_ViewDialogRuntime, rt);
		if (srt != NULL)
			*srt = NULL;
		return NULL;
	}
	gdk_threads_enter ();
	rt->case_sensitive = case_sensitive;
	rt->whole_words = whole_words;
	rt->search_backward = search_backward;
	rt->search_wrap = search_wrap;
#ifdef E2_MARK_FINDS
	rt->research = FALSE;
	e2_view_dialog_init_hilites (rt);
#endif

	gchar *utf = F_FILENAME_FROM_LOCALE (rt->localpath);
	rt->dialog = e2_dialog_create (NULL, utf, _("displaying file"),
		_e2_view_dialog_response_cb, rt);
	F_FREE (utf);
	//override some default label properties
	GtkWidget *label = g_object_get_data (G_OBJECT (rt->dialog),
		"e2-dialog-label");
//	gtk_label_set_line_wrap (GTK_LABEL (label), FALSE);
	gtk_label_set_selectable (GTK_LABEL (label), TRUE);

	gtk_window_set_type_hint (GTK_WINDOW (rt->dialog),
			    GDK_WINDOW_TYPE_HINT_NORMAL);

	e2_widget_add_separator (GTK_DIALOG (rt->dialog)->vbox, FALSE, 0);
	//create the view
	GtkWidget *sw = e2_widget_add_sw (GTK_DIALOG (rt->dialog)->vbox,
			GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, TRUE, E2_PADDING);
	gtk_container_add (GTK_CONTAINER (sw), rt->textview);
	gtk_widget_show (rt->textview);
	// set view defaults
	gtk_text_view_set_editable (GTK_TEXT_VIEW (rt->textview), FALSE);
	gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (rt->textview), FALSE);
	rt->textwrap = e2_option_bool_get ("dialog-view-wrap");
	gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (rt->textview),
		rt->textwrap ? GTK_WRAP_WORD : GTK_WRAP_NONE);
	gint char_width, char_height;
	e2_view_dialog_set_font (&char_width, &char_height, rt);
	rt->window_width = e2_option_int_get ("dialog-view-width");
	rt->window_height = e2_option_int_get ("dialog-view-height");;
//	rt->idle_id = 0;
	//init search runtime data
	rt->history = e2_list_copy_with_data (find_history);

	//action area is a GtkHButtonBox packed at the end of the dialog's vbox
	//ditto for dialog->separator
	//locate find-bar between those 2
	rt->panel = e2_widget_get_box (TRUE, FALSE, 0);
	gtk_box_pack_end (GTK_BOX (GTK_DIALOG (rt->dialog)->vbox), rt->panel,
		FALSE, TRUE, E2_PADDING_XSMALL);
	gtk_box_reorder_child (GTK_BOX (GTK_DIALOG (rt->dialog)->vbox), rt->panel, 1);

	//add handlebox
	GtkWidget *hndlbox = gtk_handle_box_new ();
	gtk_handle_box_set_shadow_type (GTK_HANDLE_BOX (hndlbox),
		GTK_SHADOW_NONE);
	gtk_handle_box_set_handle_position (GTK_HANDLE_BOX (hndlbox),
		GTK_POS_LEFT);
	gtk_container_add (GTK_CONTAINER (rt->panel), hndlbox);
	gtk_widget_show (hndlbox);
	GtkWidget *hbox = e2_view_dialog_create_searchbar (rt);
	gtk_container_add (GTK_CONTAINER (hndlbox), hbox);
//now add things to the action-area
	//the "not found" warning is created but not displayed
	gchar *labeltext = g_strconcat ("<span weight=\"bold\" foreground=\"",
		e2_option_str_get ("color-negative"), "\">", _("not found"), "</span>", NULL);
	rt->info_label =
		e2_widget_add_mid_label (GTK_DIALOG (rt->dialog)->action_area, labeltext, 0.0, TRUE, 0);
	//left-align the label
	gtk_button_box_set_child_secondary (
		GTK_BUTTON_BOX (GTK_DIALOG (rt->dialog)->action_area), rt->info_label, TRUE);
	gtk_widget_hide (rt->info_label);
	//search and/or view buttons
	labeltext = _("_Hide");
	rt->hidebtn = e2_dialog_add_undefined_button
		(rt->dialog, GTK_STOCK_ZOOM_FIT, labeltext, E2_RESPONSE_USER3);
#ifdef USE_GTK2_12TIPS
	gtk_widget_set_tooltip_text (
#else
	e2_widget_set_tooltip (NULL,
#endif
		rt->hidebtn, _("Hide the search options bar"));
	hide_keycode = e2_utils_get_mnemonic_keycode (labeltext);
	//this one not seen yet
	gtk_widget_hide (rt->hidebtn);

	e2_dialog_add_check_button (rt->dialog, rt->textwrap, _("_wrap"),
		_("If activated, text in the window will be word-wrapped"),
		E2_RESPONSE_USER1);

	labeltext = _("_Find");
	rt->findbtn = e2_dialog_add_undefined_button
		(rt->dialog, GTK_STOCK_FIND, labeltext, E2_RESPONSE_FIND);
#ifdef USE_GTK2_12TIPS
	e2_widget_set_toggletip (
#else
	gtk_tooltips_set_tip (app.tooltips,
#endif
		rt->findbtn,
		_("Show the search options bar"), _("Find the next match"));
	find_keycode = e2_utils_get_mnemonic_keycode (labeltext);

	//for a view dialog, we never want to match replace keypresses
	replace_keycode = 0;

	e2_dialog_add_defined_button (rt->dialog, &E2_BUTTON_CLOSE);
//	e2_dialog_set_responses (rt->dialog, E2_RESPONSE_FIND, GTK_RESPONSE_CLOSE);
	e2_dialog_set_negative_response (rt->dialog, GTK_RESPONSE_CLOSE);
	//this prevents a check button from being activated by keyboard
//	gtk_dialog_set_default_response (GTK_DIALOG (rt->dialog), E2_RESPONSE_FIND);

	gtk_window_resize (GTK_WINDOW (rt->dialog), char_width * rt->window_width,
				(char_height+3) * rt->window_height);

#ifdef E2_TRANSIENTKEYS
	//add dialog-specific key bindings, before the key-press callback
	//group name (must be freeable)
//	rt->key_binding = g_strconcat (_C(17), ".", _A(101), NULL);	//_(general.view
	rt->key_binding = g_strconcat (_C(11), ".", _A(101), NULL);	//_(dialogs.view
	e2_keybinding_register_transient (rt->key_binding, GTK_WIDGET (rt->textview),
		_e2_view_dialog_keybindings);
#endif

	g_signal_connect (G_OBJECT (rt->textview), "popup-menu",
		G_CALLBACK (_e2_view_dialog_popup_menu_cb), rt);
	g_signal_connect (G_OBJECT (rt->textview), "button-press-event",
		G_CALLBACK (_e2_view_dialog_button_press_cb), rt);
	g_signal_connect (G_OBJECT (rt->textview), "key-press-event",
		G_CALLBACK (_e2_view_dialog_key_press_cb), rt);

	if (srt != NULL)
		*srt = rt;	//make rt available to openat func

	return rt->dialog;
}

  /*****************/
 /**** actions ****/
/*****************/

/**
@brief re-open file for viewing, at the last-used location in the file
This returns immediately, so if run in a Q-thread, that will end
@param from the widget that was activated to initiate the action
@param art runtime data for the action
@return TRUE if the dialog was opened
*/
static gboolean _e2_view_dialog_reopen (gpointer from, E2_ActionRuntime *art)
{
	return (e2_task_do_task (E2_TASK_VIEW, art, from,
		_e2_view_dialog_reviewQ, NULL));
}
static gboolean _e2_view_dialog_reviewQ (E2_ActionTaskData *qed)
{
	//FIXME also allow specification of a name
	//process the 1st selected item in active pane
	GPtrArray *names = qed->names;
	E2_SelectedItemInfo **iterator = (E2_SelectedItemInfo **) names->pdata;
	//".." entries filtered when names compiled
	//for name comparisons, we need the full path
	gchar *localpath = e2_utils_strcat (qed->currdir, (*iterator)->filename);
#ifdef E2_VFS
	VPATH ddata = { localpath, qed->currspace };
#endif
	E2_ViewDialogRuntime *vrt;

	gdk_threads_enter ();
	GtkWidget *dialog = _e2_view_dialog_create
#ifdef E2_VFS
		(&ddata, &vrt);
#else
		(localpath, &vrt);
#endif
	gdk_threads_leave ();
	if (dialog != NULL)
	{
		WAIT_FOR_EVENTS_UNLOCKED_SLOWLY
		gdk_threads_enter ();
#ifdef E2_VFS
		e2_view_dialog_show_atlast (&ddata, vrt);
#else
		e2_view_dialog_show_atlast (localpath, vrt);
#endif
		gdk_threads_leave ();
		g_free (localpath);
		return TRUE;
	}
	g_free (localpath);
	return FALSE;
}
/**
@brief open file for viewing, at a location where there is a specified string
@a art includes a string with filepath and an argument that is a text string
to find in the file. Separator between path and find-target is the first unquoted
unescaped ' ' char (not a \t)
This returns immediately, so if run in a Q-thread, that will end
@param from the widget that was activated to initiate the action
@param art runtime data for the action
@return TRUE if dialog was created
*/
static gboolean _e2_view_dialog_openat (gpointer from, E2_ActionRuntime *art)
{
	return (e2_task_do_task (E2_TASK_VIEW, art, from,
		_e2_view_dialog_viewatQ, NULL));
}
static gboolean _e2_view_dialog_viewatQ (E2_ActionTaskData *qed)
{
	//for a help doc, has localised doc path, and a "[title]" arg in undefined encoding
	gchar *view_this = (gchar *) qed->rt_data;
//	gchar c;
	//strip off any arg FIXME doc path may have whitespace
	gchar *target = e2_utils_bare_strchr (view_this, ' ');	//always ascii ' ', don't need g_utf8_strchr()
	if (target != NULL)
	{
//		c = *target;
		*target = '\0';
	}
#ifdef E2_VFS
	VPATH ddata = { view_this, qed->currspace };
#endif
	E2_ViewDialogRuntime *vrt;
	gdk_threads_enter ();	//dialog setup wants BGL closed
	GtkWidget *dialog = _e2_view_dialog_create
#ifdef E2_VFS
 		(&ddata, &vrt);
#else
		(view_this, &vrt);
#endif
	gdk_threads_leave ();

	if (target != NULL)
//		*target = c;	//revert the argument, ready for next time
		*target = ' ';	//revert the argument, ready for next time

	if (dialog != NULL)
	{
		if (target != NULL)
			target = e2_utils_pass_whitespace (target+1);
		if (target != NULL && *(target+1) != '\0')
		{	//find the target string and open there
			GtkTextIter iter, start, end;
			gtk_text_buffer_get_start_iter (vrt->textbuffer, &iter);
			if (gtk_text_iter_forward_search (&iter, target, 0, &start, &end, NULL))
			{	//need to add a mark to get the scroll to work properly
				//FIXME sometimes it scrolls a bit too far
				GtkTextMark *mark = gtk_text_buffer_create_mark
					(vrt->textbuffer, NULL, &start, FALSE);
				WAIT_FOR_EVENTS_UNLOCKED
				gdk_threads_enter ();
				//to support incremental searching, move the cursor
				//to the start of the located string
				gtk_text_buffer_place_cursor (vrt->textbuffer, &start);
				gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (vrt->textview), mark,
					0.0, TRUE, 0.0, 0.2);
				gdk_threads_leave ();
			}
		}
		gdk_threads_enter ();
		e2_dialog_show (dialog, app.main_window, E2_DIALOG_DONT_SHOW_ALL,
			NULL);
		gtk_window_present (GTK_WINDOW (dialog));
		gdk_threads_leave ();
		return TRUE;
	}
	return FALSE;
}

  /******************/
 /***** public *****/
/******************/

/**
@brief create and show view dialog
@a localpath may not have any path, or an absolute path. In those cases,
curr_view->dir will be prepended
This returns immediately, so if run in a Q-thread, that will end
@param localpath localised string which has at least the name of item to be processed

@return TRUE if the dialog was created
*/
gboolean e2_view_dialog_create (VPATH *localpath)
{
	//there is no async thread protection here or downstream,, as this func can
	//be called from different contexts. Such protection, if needed, must be done by the caller
	GtkWidget *dialog = _e2_view_dialog_create (localpath, NULL);
	if (dialog != NULL)
	{
		WAIT_FOR_EVENTS_SLOWLY
		e2_dialog_show (dialog, app.main_window, E2_DIALOG_DONT_SHOW_ALL, NULL);
		gtk_window_present (GTK_WINDOW (dialog));
		return TRUE;
	}
	return FALSE;
}
/**
@brief create and show non-queued, non-logged view dialog, for user help
Expects BGL on/closed
@param view_this string with localised path of help doc, AND a heading name
 (in undefined encoding) as its argument
@return TRUE if the dialog was created
*/
gboolean e2_view_dialog_create_immediate (VPATH *view_this)
{
	E2_ActionTaskData qed;
	qed.rt_data = VPSTR (view_this);
#ifdef E2_VFS
	qed.currspace = view_this->spacedata;
#endif
	gdk_threads_leave ();	//turn off the BGL
	gboolean retval = _e2_view_dialog_viewatQ (&qed);
	gdk_threads_enter ();
	return retval;
}
/**
@brief register actions related to view dialog, and some other initialisation
@return
*/
void e2_view_dialog_actions_register (void)
{
	//HACK while we're at it, init some session parameters
	//these options are static for the session
	case_sensitive = e2_option_bool_get ("dialog-search-case-sensitive");

	if (e2_option_bool_get ("dialog-search-history"))
		e2_cache_list_register ("search-history", &find_history);

	gchar *action_name = g_strconcat(_A(5),".",_A(102),NULL);
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_view_dialog_reopen, NULL, FALSE);
	action_name = g_strconcat(_A(5),".",_A(103),NULL);
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_view_dialog_openat, NULL, TRUE);
}
/**
@brief register config options related to view dialog
@return
*/
void e2_view_dialog_options_register (void)
{
	gchar *group_name = g_strconcat(_C(11),":",_C(40),NULL); //_("dialogs:view"
	//first some options that may, but probably won't, change during the session
	e2_option_bool_register ("dialog-view-wrap",
		group_name, _("wrap text"),
		_("This causes the view window to open with text-wrapping enabled"),
		NULL, TRUE,
		E2_OPTION_FLAG_BASIC | E2_OPTION_FLAG_FREEGROUP);
	e2_option_int_register ("dialog-view-width",
		group_name, _("window width"),
		_("The view window will default to showing this many characters per line (but the the displayed buttons may make it wider than this)")
		, NULL, 84, 20, 1000,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_int_register ("dialog-view-height",
		group_name, _("window height"),
		_("The view window will default to showing this many lines of text"), NULL, 30, 10, 1000,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register ("dialog-view-use-font",
		group_name, _("use custom font"),
		_("If activated, the font specified below will be used, instead of the theme default"),
		NULL, FALSE,
		E2_OPTION_FLAG_BASIC);
	e2_option_font_register ("dialog-view-font", group_name, _("custom font for viewing files"),
		_("This is the font used for text in each view dialog"), "dialog-view-use-font", "Sans 10", 	//_I( font name
		E2_OPTION_FLAG_BASIC);
	e2_option_bool_register
		("dialog-search-case-sensitive", group_name, _("case sensitive searches"),
		_("This causes the view window search-bar to first open with case-sensitive searching enabled"),
		NULL, FALSE,
		E2_OPTION_FLAG_BASIC);
	e2_option_bool_register ("dialog-search-show-last",
		group_name, _("show last search string"),
		_("This shows the last search-string in the entry field, when the view window search-bar is displayed"),
		NULL, TRUE,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register
		("dialog-search-history", group_name, _("keep search history"),
		_("This causes search strings to be remembered between sessions"),
		NULL, FALSE,
		E2_OPTION_FLAG_ADVANCED);
}
