/* $Id: e2_edit_dialog.c 2074 2010-05-16 03:20:46Z tpgww $

Copyright (C) 2006-2010 tooar <tooar@emelfm2.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, see http://www.gnu.org/licenses.
*/

#include "emelfm2.h"
#include <string.h>
#include "e2_dialog.h"
#include "e2_view_dialog.h"
#ifdef USE_GTK2_10
# include "e2_print.h"
#endif
#include "e2_textiter.h"
#include "e2_task.h"
#ifdef E2_MOUSECUSTOM
# include "e2_mousebinding.h"
#endif

//rates around the default blink rate will never actually blink! dunno why ...
//so: MULTIPLIER >= 1.5, MULTIPLIER/DIVIDER <= 0.5
#define OVERWR_CURSOR_MULTIPLIER 2.0
#define INSERT_CURSOR_DIVIDER 4.0
#ifndef CURSOR_ON_MULTIPLIER
#define CURSOR_ON_MULTIPLIER 0.66
#endif
#ifndef CURSOR_OFF_MULTIPLIER
#define CURSOR_OFF_MULTIPLIER 0.34
#endif

//these variables can all be shared among all concurrent and sequential
//view and edit dialogs
extern gboolean case_sensitive;
extern gboolean whole_words;
extern gboolean search_backward;
extern gboolean search_wrap;
extern GList *find_history;
static gboolean replace_all;
static gboolean confirm_before;
static gboolean do_next;
static GList *replace_history = NULL;
#ifdef E2_SPELLCHECK
static GList *lang_history = NULL;
#endif
gint blinkmsec;	//blink-cycle duration (msec)
//keycodes corresponding to mnemonics for translated labels
//these ones are shared with a view dialog
extern guint find_keycode;
extern guint findnext_keycode;
extern guint hide_keycode;
guint replace_keycode;
static guint save_keycode;
static guint undo_keycode;
static guint redo_keycode;

static void _e2_edit_dialog_dirty_change_cb (GtkTextBuffer *textbuffer,
	E2_ViewDialogRuntime *rt);
static gboolean _e2_edit_dialog_reeditQ (E2_ActionTaskData *qed);

  /****************************/
 /**** undo functionality ****/
/****************************/

#define INCLUDED_IN_PARENT
#include "e2_edit_dialog_undo.c"
#undef INCLUDED_IN_PARENT

  /***************************/
 /**** editing functions ****/
/***************************/

#ifdef E2_SPELLCHECK
static void _e2_edit_dialog_cancel_spell (E2_ViewDialogRuntime *rt)
{
	gtkspell_detach (rt->spelldata);	//this destroys the GtkSpell
	rt->spelldata = NULL;
}
#endif

/**
@brief save text selected in @a buffer
This func is also used to save output pane selection
@param buffer text buffer from which the text will be saved
#ifdef E2_VFS
@param spacedata pointer to namspace data
#endif
@param parent parent widget, dialog or main window

@return
*/
void e2_edit_dialog_save_selected (GtkTextBuffer *buffer,
#ifdef E2_VFS
	PlaceInfo *spacedata,
#endif
	GtkWidget *parent)
{
#ifdef E2_VFSTMP
	check assumption that selector only works for current namespace
#endif
	GtkTextIter start, end;
	//get this first, as the dialog unselects the text
	if (gtk_text_buffer_get_selection_bounds (buffer, &start, &end))
	{
		GtkWidget *dialog = gtk_file_chooser_dialog_new (NULL,
			GTK_WINDOW (parent), GTK_FILE_CHOOSER_ACTION_SAVE,
			GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
			GTK_STOCK_SAVE, GTK_RESPONSE_OK,
			NULL);

		e2_dialog_setup_chooser (dialog,
			_("file save as"),
			//API doco is silent on whether g_get_home_dir() gets a localised string
			g_get_home_dir (),
			GTK_FILE_CHOOSER_ACTION_SAVE,
			FALSE,	//hide-hidden
			FALSE,	//single-selection
			GTK_RESPONSE_OK);	//default response

		gint response;
		while ((response = e2_dialog_run_simple (dialog, parent)) == E2_RESPONSE_USER1)
		{}

		if (response == GTK_RESPONSE_OK)
		{
			gchar *local = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
#ifdef E2_VFS
			VPATH ddata = { local, spacedata };
#endif
#ifndef USE_GTK2_8
			//check for o/w
			if (e2_option_bool_get ("confirm-overwrite")
#ifdef E2_VFS
				&& e2_fs_access2 (&ddata E2_ERR_NONE()) == 0)
#else
				&& e2_fs_access2 (local E2_ERR_NONE()) == 0)
#endif
			{
				gdk_threads_leave ();
				DialogButtons choice = e2_dialog_ow_check (NULL,
#ifdef E2_VFS
					&ddata,
#else
					local,
#endif
					NONE);
				gdk_threads_enter ();
				if (choice != OK)
				{
					gtk_widget_destroy (dialog);
					gtk_text_buffer_select_range (buffer, &start, &end);
					g_free (local);
					return;
				}
			}
#endif
			gchar *text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
			e2_fs_set_file_contents (
#ifdef E2_VFS
			&ddata, text, strlen (text), 0644 E2_ERR_NONE());
#else
			local, text, strlen (text), 0644 E2_ERR_NONE());
#endif
			g_free (text);
			g_free (local);
		}
		gtk_widget_destroy (dialog);
		gtk_text_buffer_select_range (buffer, &start, &end);
	}
}
/**
@brief save currently-selected text

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

@return
*/
static void _e2_edit_dialog_savesel_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	e2_edit_dialog_save_selected (rt->textbuffer,
#ifdef E2_VFS
	rt->spacedata,
#endif
	rt->dialog);
}
/**
@brief save file

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

@return
*/
static void _e2_edit_dialog_save_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	GtkTextIter start, end;
	gtk_text_buffer_get_bounds (rt->textbuffer, &start, &end);
	gchar *text = gtk_text_buffer_get_text (rt->textbuffer, &start, &end, FALSE);

	//restore line ends
	gint extras;
	if (rt->linebreak == CR+LF)
		extras = gtk_text_buffer_get_line_count (rt->textbuffer) + 1;	//+1 = possible incomplete line
	else
		extras = 0; //irrelevant
	text = e2_utils_revert_line_ends (text, extras, rt->linebreak);

	//after external encoding conversion, or some fallback conversion,
	//we can't know the original encoding, so just save as UTF-8, and warn the user
	if (strcmp (rt->charset, "UNKNOWN")
		&& strstr (rt->charset, "UTF-8") == NULL)	//conversion is actually needed
	{
		gsize bytes_old, bytes_new;
		GError *error = NULL;
		gchar *newtext;
		newtext = g_convert (text, -1, rt->charset, "UTF-8", &bytes_old, &bytes_new, &error);
		if (error == NULL)
		{
			g_free (text);
			text = newtext;
		}
		else
		{
			gchar *msg;
			if (newtext != NULL)
				g_free (newtext);
			switch (error->code)
			{
				case G_CONVERT_ERROR_ILLEGAL_SEQUENCE:
					msg = g_strdup_printf (
					_("Cannot save %s in its original encoding %s. Reverting to UTF-8"),
					rt->localpath, rt->charset);
					e2_output_print_error (msg, TRUE);
					g_error_free (error);
					break;
				default:
					msg = g_strdup_printf (
					_("Encoding conversion failed for %s"), rt->localpath);
					e2_output_print_error (msg, TRUE);
					e2_output_print_error (error->message, FALSE);
					g_error_free (error);
					rt->saved_ok = FALSE;
					return;
			}
		}
	}

	const gchar *usepath;
	//rt->localpath may be a replacement name, when "saving as"
	usepath = (rt->saveas) ? rt->newlocalpath : rt->localpath;

#ifdef E2_VFS
	VPATH ddata = { usepath, rt->spacedata };
#endif
	if (e2_option_bool_get ("edit-save-backup")
		&& !rt->saveas
		&& !e2_fs_access (
#ifdef E2_VFS
		&ddata, W_OK E2_ERR_NONE()))
#else
		usepath, W_OK E2_ERR_NONE()))
#endif
	{
		gchar *saved = e2_utils_strcat (usepath, "~");
#ifdef E2_VFS
		VPATH sdata = { saved, rt->spacedata };
#endif
		gdk_threads_leave ();	//downstream errors invoke local mutex locking
#ifdef E2_VFS
		e2_task_backend_rename (&ddata, &sdata);	//ignore any warning !!
#else
		e2_task_backend_rename (usepath, saved);	//ignore any warning !!
#endif
		gdk_threads_enter ();
		g_free (saved);
	}

	//allow new file permissions to be the same as an old one, if any
	struct stat sb;
	gboolean replaced = !rt->saveas &&
#ifdef E2_VFS
		!e2_fs_stat (&ddata, &sb E2_ERR_NONE());	//through a link
#else
		!e2_fs_stat (usepath, &sb);
#endif

	rt->saved_ok = e2_fs_set_file_contents (
#ifdef E2_VFS
		&ddata, text, strlen (text), 0600 E2_ERR_NONE());
#else
		usepath, text, strlen (text), 0600);
#endif

	g_free (text);
	if (rt->saved_ok)
	{
		mode_t perms = (replaced) ? sb.st_mode & ALLPERMS : 0644;
#ifdef E2_VFS
		e2_fs_chmod (&ddata, perms E2_ERR_NONE());
#else
		e2_fs_chmod (usepath, perms);
#endif
	//	uid_t owner_id = ?;
	//	gid_t group_id = ?;
	//	e2_fs_chown (local, owner_id, group_id);

		if (!strcmp (rt->charset, "UNKNOWN"))
		{
			e2_fs_error_simple (
			_("Original encoding of '%s' is unknown, now saved as UTF-8"),
#ifdef E2_VFS
			&ddata);
#else
			usepath);
#endif
		}
		gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (rt->textbuffer), FALSE);
	}
}
/**
@brief save file with new name

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

@return
*/
static void _e2_edit_dialog_saveas_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
#ifdef E2_VFSTMP
	check assumption that selector only works for current namespace
#endif
	GtkWidget *dialog = gtk_file_chooser_dialog_new (NULL,
		GTK_WINDOW (rt->dialog), GTK_FILE_CHOOSER_ACTION_SAVE,
		GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
		GTK_STOCK_SAVE, GTK_RESPONSE_OK,
		NULL);

	gchar *utf = F_FILENAME_FROM_LOCALE (rt->localpath);
	e2_dialog_setup_chooser (dialog,
		_("file save as"),
		utf,
		GTK_FILE_CHOOSER_ACTION_SAVE,
		FALSE, 	//hide-hidden
		FALSE,	//single-selection
		GTK_RESPONSE_OK);	//default response
	F_FREE (utf, rt->localpath);

	gint response;
	while ((response = e2_dialog_run_simple (dialog, rt->dialog)) == E2_RESPONSE_USER1)
	{}

	if (response == GTK_RESPONSE_OK)
	{
		gchar *local = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
#ifndef USE_GTK2_8
		//check for O/W if not done by the dialog
#ifdef E2_VFS
		VPATH ddata = { local, rt->spacedata };
#endif
		if (e2_option_bool_get ("confirm-overwrite")
#ifdef E2_VFS
			&& e2_fs_access2 (&ddata E2_ERR_NONE()) == 0)
#else
			&& e2_fs_access2 (local E2_ERR_NONE()) == 0)
#endif
		{
			gdk_threads_leave ();
			DialogButtons choice = e2_dialog_ow_check (NULL,
#ifdef E2_VFS
				&ddata,
#else
				local,
#endif
				NONE);
			gdk_threads_enter ();
			if (choice != OK)
			{
				gtk_widget_destroy (dialog);
				g_free (local);
				return;
			}
		}
#endif
		rt->saveas = TRUE;
		rt->newlocalpath = local;
		_e2_edit_dialog_save_cb (NULL, rt);
		rt->saveas = FALSE;

		if (rt->saved_ok)
		{
			g_free (rt->localpath);
			rt->localpath = local;
			//update the dialog label
			GtkWidget *label = g_object_get_data (G_OBJECT (rt->dialog), "e2-dialog-label");
			gchar *utf = F_FILENAME_FROM_LOCALE (rt->localpath);
			gtk_label_set_text (GTK_LABEL (label), utf);
			F_FREE (utf, rt->localpath);
		}
	}
	gtk_widget_destroy (dialog);
}
/**
@brief reload edited file

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

@return
*/
static void _e2_edit_dialog_refresh_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	if (!gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (rt->textbuffer)))
		return;	//no point in reloading
	DialogButtons choice = e2_dialog_warning (
	_("Reverting to saved version cannot be undone"), _("Re_vert"));
	if (choice == OK)
	{
#ifdef E2_VFS
		VPATH ddata = { rt->localpath, rt->spacedata };
#endif
		gdk_threads_leave ();
#ifdef E2_VFS
		e2_view_dialog_read_text (&ddata, rt);	//no failure test when refreshing
#else
		e2_view_dialog_read_text (rt->localpath, rt);	//no failure test when refreshing
#endif
		gdk_threads_enter ();
		gtk_text_buffer_set_modified (rt->textbuffer, FALSE);
	}
	gtk_widget_grab_focus (rt->textview);
}
/**
@brief undo change

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

@return
*/
static void _e2_edit_dialog_undo_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	_e2edtdlg_undo_undo_one (rt);
}
#ifdef E2_REDO_ENABLED
/**
@brief redo change

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

@return
*/
static void _e2_edit_dialog_redo_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	_e2edtdlg_undo_redo_one (rt);
}
#endif
#ifdef E2_SPELLCHECK
static GList *lang_history;
/**
@brief recheck all buffer spelling

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

@return
*/
static void _e2_edit_dialog_spellall_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	gchar *lang = (lang_history == NULL) ? NULL : 	//start speller with default language
					(gchar *) lang_history->data;  //or with last-chosen language
	GError *error = NULL;
	rt->spelldata = gtkspell_new_attach (GTK_TEXT_VIEW (rt->textview), lang, &error);
	if (rt->spelldata == NULL)
	{
		gchar *msg = g_strdup_printf (
			_("Cannot initialize spell checking: %s"), error->message);
		e2_output_print_error (msg, TRUE);
		g_error_free (error);
	}
	else
	{
		//disable all gtkspell signal-callbacks
		g_signal_handlers_block_matched (G_OBJECT (rt->textview),
			G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, rt->spelldata);
	}
}
/**
@brief cancel buffer spell checking

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

@return
*/
static void _e2_edit_dialog_unspellall_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	gtkspell_detach (rt->spelldata);	//this destroys the GtkSpell
	rt->spelldata = NULL;
}
/**
@brief set spell-checker language

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

@return
*/
static void _e2_edit_dialog_spell_language_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	gchar *lang;
	if (lang_history == NULL)
	{
		lang = (gchar *)g_getenv ("LANG");	//ok if NULL
		if (lang != NULL && !strcmp (lang, "C"))
			lang = NULL;
		//CHECKME default used by speller backends
	}
	else
		lang = (gchar *)lang_history->data;

	DialogButtons choice = e2_dialog_combo_input (_("choose language"),
		_("Enter the spell-checker language (like en_CA):"), lang,
		0, &lang_history, &lang);	//re-use for entered value

	if (choice == OK)
	{
		GError *error = NULL;
		gtkspell_set_language (rt->spelldata, lang, &error);
		if (error != NULL)
		{
			gchar *msg = g_strdup_printf (
				_("Cannot set speller language to %s (%s)"), lang, error->message);
			e2_output_print_error (msg, TRUE);
			g_error_free (error);
		}
		g_free (lang);
	}
}
#endif
/**
@brief toggle text wrapping

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

@return
*/
static void _e2_edit_dialog_wrap_cb (GtkMenuItem *menuitem,
	E2_ViewDialogRuntime *rt)
{
	rt->textwrap = !rt->textwrap;
	gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (rt->textview),
		rt->textwrap ? GTK_WRAP_WORD: GTK_WRAP_NONE);
}
/**
@brief hide search/replace bar

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

@return
*/
static void _e2_edit_dialog_hide_cb (GtkWidget *menuitem,
	E2_ViewDialogRuntime *rt)
{
	gtk_widget_hide (rt->panel);
	rt->is_hidden = TRUE;
	gtk_widget_hide (rt->info_label);
	gtk_widget_hide (rt->findbtn);
	gtk_widget_hide (rt->replacebtn);
	//in case we last performed an incremental search ...
	GtkWidget *entry =
#ifdef USE_GTK2_14
		gtk_bin_get_child (GTK_BIN (rt->combo));
#else
		GTK_BIN (rt->combo)->child;
#endif
	const gchar *choice = gtk_entry_get_text (GTK_ENTRY (entry));
	if (*choice != '\0')
	{
		e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
		e2_list_update_history (choice, &find_history, NULL, 30, FALSE);
	}
#ifdef E2_MARK_FINDS
	//turn off any highlihts
	e2_view_dialog_clear_hilites (rt);
#endif
	//hide any selection NAH, might want it for editing
//	GtkTextIter iter;
//	if (gtk_text_buffer_get_selection_bounds (rt->textbuffer, &iter, NULL))
//		gtk_text_buffer_select_range (rt->textbuffer, &iter, &iter);
	//now for the replacements ...
	entry =
#ifdef USE_GTK2_14
		gtk_bin_get_child (GTK_BIN (rt->combo2));
#else
		GTK_BIN (rt->combo2)->child;
#endif
	choice = gtk_entry_get_text (GTK_ENTRY (entry));
	if (*choice != '\0')
	{
		e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
		e2_list_update_history (choice, &replace_history, NULL, 30, FALSE);
	}
	//focus on the text
	gtk_widget_grab_focus (rt->textview);
}
/**
@brief perform find
This is a menu-selection callback, and can also be called directly
@param menuitem the activated menu widget, or NULL or 0x1 when called directly
@param rt runtime data for the dialog

@return
*/
static void _e2_edit_dialog_find_cb (GtkWidget *menuitem,
	E2_ViewDialogRuntime *rt)
{
	if (!rt->is_hidden) //search panel is visible
	{
		//if a mod key is pressed, treat it specially
		//Ctrl = find first, Shift = temp reverse direction
		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 (FALSE, FALSE, rt);
			rt->search_backward = !rt->search_backward;
		}
		else
			//perform non-incremental search, from start|end if <Control> is pressed
			//in menu-initiated search, or direct-call with menuitem = 0x1
			e2_view_dialog_search (
				(menuitem != NULL && mask == GDK_CONTROL_MASK),
				FALSE, rt);
	}
	else
	{	//show the search panel & related things
#ifdef USE_GTK2_18
		if (gtk_widget_get_visible (rt->replacebar))
#else
		if (GTK_WIDGET_VISIBLE (rt->replacebar))
#endif
			gtk_widget_hide (rt->replacebar);
		gtk_widget_show (rt->panel);
		rt->is_hidden = FALSE;
		gtk_widget_show (rt->findbtn);

		GtkWidget *child =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (rt->combo));
#else
			GTK_BIN (rt->combo)->child;
#endif
		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 (child), find_string);
			gtk_editable_select_region (GTK_EDITABLE (child), 0, -1);
			printd (DEBUG, "_e2_edit_dialog_find_cb - non-incremental search");
			e2_view_dialog_search (FALSE, FALSE, rt);
			g_free (find_string);
		}
		else
			e2_view_dialog_update_combo (rt->combo);
		//focus on the text entry
		gtk_widget_grab_focus (child);
	}
}
/**
@brief perform replacement

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

@return
*/
static void _e2_edit_dialog_replace_cb (GtkWidget *menuitem,
	E2_ViewDialogRuntime *rt)
{
#ifdef USE_GTK2_18
	if (!gtk_widget_get_visible (rt->replacebtn))
		gtk_widget_show (rt->replacebtn);
	if (!gtk_widget_get_visible (rt->findbtn))
		gtk_widget_show (rt->findbtn);
	if (!gtk_widget_get_visible (rt->replacebar))
		gtk_widget_show (rt->replacebar);
#else
	if (!GTK_WIDGET_VISIBLE (rt->replacebtn))
		gtk_widget_show (rt->replacebtn);
	if (!GTK_WIDGET_VISIBLE (rt->findbtn))
		gtk_widget_show (rt->findbtn);
	if (!GTK_WIDGET_VISIBLE (rt->replacebar))
		gtk_widget_show (rt->replacebar);
#endif
	if (!rt->is_hidden) //search panel is visible
	{
		GtkTextIter start, end;
		if (!gtk_text_buffer_get_selection_bounds (rt->textbuffer, &start, &end))
		{	//nothing selected, presumably after a replace that's just happened
			//lazy user, try for another match, like Ctrl F
			e2_view_dialog_search (FALSE, FALSE, rt);
			return;
		}
		GtkWidget *entry =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (rt->combo2));
#else
 			GTK_BIN (rt->combo2)->child;
#endif
		const gchar *replace = gtk_entry_get_text (GTK_ENTRY (entry));
		//do replacement(s)
		if (replace_all)
		{
			//ignore compiler warning about unitialized usage
			GtkWidget *dialog = NULL;	//assignment for complier-warning prevention only
			DialogButtons choice;
			if (confirm_before)
			{
				dialog = e2_dialog_create (GTK_STOCK_DIALOG_QUESTION,
					_("Replace this one ?"), _("confirm"), DUMMY_RESPONSE_CB, NULL);
				e2_dialog_add_defined_button (dialog, &E2_BUTTON_CANCEL);

				E2_Button local_btn;
				e2_button_derive (&local_btn, &E2_BUTTON_NO, BTN_NO_KEEP);
				e2_dialog_add_defined_button (dialog, &local_btn);

				e2_button_derive (&local_btn, &E2_BUTTON_YES, BTN_YES_DELETE);
				e2_dialog_add_defined_button (dialog, &local_btn);

				e2_dialog_setup (dialog, rt->dialog);
			}
			choice = NO_TO_ALL;	//default in case of abort
			while (e2_view_dialog_search (FALSE, FALSE, rt))
			{
				if (confirm_before)
				{
					gtk_widget_show (dialog);
					choice = e2_dialog_wait (dialog, TRUE, TRUE, FALSE, TRUE);
					if (GTK_IS_DIALOG (dialog)) //not explicitly closed by the user
						gtk_widget_hide (dialog);
					if (choice == CANCEL)
						continue;
					else if (choice == NO_TO_ALL)
						break;
				}
				gtk_text_buffer_delete_selection (rt->textbuffer, TRUE, TRUE);
				if (*replace != '\0')
					gtk_text_buffer_insert_interactive_at_cursor (rt->textbuffer,
						replace, -1, TRUE);
			}
			if (confirm_before && GTK_IS_DIALOG (dialog)) //not explicitly closed by the user
				gtk_widget_destroy (dialog);
		}
		else if (gtk_text_buffer_get_selection_bounds (rt->textbuffer, &start, &end))
		{
			gtk_text_buffer_delete_selection (rt->textbuffer, TRUE, TRUE);
			if (*replace != '\0')
				gtk_text_buffer_insert_interactive_at_cursor (rt->textbuffer,
					replace, -1, TRUE);
			if (do_next)
				e2_view_dialog_search (FALSE, FALSE, rt);
		}
	}
	else	//first time, make the search panel visible
	{
		gtk_widget_show (rt->panel);
		rt->is_hidden = FALSE;
		e2_view_dialog_update_combo (rt->combo2);	//CHECKME always empty the replace string ?

		GtkWidget *child =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (rt->combo));
#else
			GTK_BIN (rt->combo)->child;
#endif
		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 (child), find_string);
			gtk_editable_select_region (GTK_EDITABLE (child), 0, -1);
			printd (DEBUG, "_e2_edit_dialog_replace_cb - non-incremental search");
			e2_view_dialog_search (FALSE, FALSE, rt);
			g_free (find_string);
		}
		else
			e2_view_dialog_update_combo (rt->combo);
		//focus on the text entry
		gtk_widget_grab_focus (child);
	}
}
/**
@brief construct and pop up destroyable context-menu for the edit-dialog

@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_edit_dialog_show_context_menu (GtkWidget *textview,
	guint event_button, gint event_time, E2_ViewDialogRuntime *rt)
{
	gchar *item_name;
	GtkWidget *item, *menu = gtk_menu_new ();
#ifdef E2_SPELLCHECK
	if (rt->spelldata != NULL)
	{	//decide if we need a spellfix item in the menu
		GtkTextIter iter;
		gboolean atcursor = (event_button == 0); //TRUE if processing a menu-button press
		if (!atcursor)
		{
			GdkEvent *event = gtk_get_current_event ();
			if (event != NULL) //should never happen
			{
				if (event->type == GDK_BUTTON_PRESS)
				{
					gint buf_x, buf_y, x, y;
					x = ((GdkEventButton *) event)->x;
					y = ((GdkEventButton *) event)->y;
					gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (rt->textview),
						GTK_TEXT_WINDOW_TEXT, x, y, &buf_x, &buf_y);
					gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (rt->textview),
						&iter, buf_x, buf_y);
				}
				else
					atcursor = TRUE;	//revert to using cursor position
				gdk_event_free (event);
			}
			else
				atcursor = TRUE;
		}
		if (atcursor)
		{ //menu button was pressed, or error happened
			GtkTextMark *mark = gtk_text_buffer_get_insert (rt->textbuffer);
			gtk_text_buffer_get_iter_at_mark (rt->textbuffer, &iter, mark);
		}
		//use function from patched gtk-spell to get fixes menu, if any
		GtkWidget *submenu = gtkspell_get_suggestions_menu (rt->spelldata, &iter);
		if (submenu != NULL)
		{	//word is mis-spelt
			item = e2_menu_add (menu, _("Spelling suggestions"), NULL, NULL, NULL, NULL);
			gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
			e2_menu_add_separator (menu);
		}
	}
#endif
	item =
	e2_menu_add (menu, _("_Save"), GTK_STOCK_SAVE,
		_("Save the file"), _e2_edit_dialog_save_cb, rt);
	gtk_widget_set_sensitive (item, rt->is_dirty);
	e2_menu_add (menu, _("Save as.."), GTK_STOCK_SAVE_AS,	//no available valid mnemonic
		_("Save the file with a new name"), _e2_edit_dialog_saveas_cb, rt);
	item =
	e2_menu_add (menu, _("Save se_lection.."), "save_selection"E2ICOND,
		_("Save the selected text"), _e2_edit_dialog_savesel_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_dialog_print_cb, rt);
#endif
	item =
	e2_menu_add (menu, _("Re_vert"), GTK_STOCK_REVERT_TO_SAVED,
		_("Reload the file being edited"), _e2_edit_dialog_refresh_cb, rt);
	gtk_widget_set_sensitive (item, rt->is_dirty);
	e2_menu_add_separator (menu);
	e2_menu_add (menu, _("_Find.."), GTK_STOCK_FIND,
		_("Find matching text"), _e2_edit_dialog_find_cb, rt);
	e2_menu_add (menu, _("_Replace.."), GTK_STOCK_FIND_AND_REPLACE,
		_("Find and replace matching text"), _e2_edit_dialog_replace_cb, rt);
	item =
	e2_menu_add (menu, _("_Hide"), GTK_STOCK_ZOOM_FIT,
		_("Hide the search options bar"), _e2_edit_dialog_hide_cb, rt);
	gtk_widget_set_sensitive (item, !rt->is_hidden);
	item =
	e2_menu_add (menu, _("_Undo"), GTK_STOCK_UNDO,
		_("Undo last change"), _e2_edit_dialog_undo_cb, rt);
	gtk_widget_set_sensitive (item, rt->undo_enabled);
#ifdef E2_REDO_ENABLED
	item =
	e2_menu_add (menu, _("Re_do"), GTK_STOCK_REDO,
		_("Reverse last undo"), _e2_edit_dialog_redo_cb, rt);
	gtk_widget_set_sensitive (item, rt->redo_enabled);
#endif
#ifdef E2_SPELLCHECK
	if (rt->spelldata == NULL)
		e2_menu_add (menu, _("_Check spelling"), GTK_STOCK_SPELL_CHECK,
			_("Flag mis-spelt words"), _e2_edit_dialog_spellall_cb, rt);
	else
		e2_menu_add (menu, _("_Clear spellcheck"), GTK_STOCK_SPELL_CHECK,
			_("Remove all spell-check flags"), _e2_edit_dialog_unspellall_cb, rt);
#endif
	e2_menu_add_separator (menu);
#ifdef E2_SPELLCHECK
	item =
	e2_menu_add (menu, _("_Language.."), GTK_STOCK_INDEX,
		_("Set spell-checker language"), _e2_edit_dialog_spell_language_cb, rt);
	gtk_widget_set_sensitive (item, (rt->spelldata != NULL));
#endif
	item =
	e2_menu_add_check (menu, _("_Wrap"), rt->textwrap,
		 _e2_edit_dialog_wrap_cb, rt);
	e2_widget_set_safetip (item,
		_("If activated, text in the window will be word-wrapped"));

	item_name = g_strconcat (_A(3),".",_A(32),NULL);
	e2_menu_add_action (menu, _("Se_ttings"), GTK_STOCK_PREFERENCES,
		_("Open the configuration dialog at the options page"), item_name,
		_C(11)); //_("dialogs")
	g_free(item_name);

	g_signal_connect (G_OBJECT (menu), "selection-done",
		G_CALLBACK (e2_menu_selection_done_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);
}

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

static void _e2_edit_dialog_destroy (E2_ViewDialogRuntime *rt, gboolean forced)
{
	if (rt->is_dirty)
	{
		GtkWidget *dialog = e2_dialog_create (GTK_STOCK_DIALOG_QUESTION,
			_("Save modified file ?"), _("confirm"), DUMMY_RESPONSE_CB, NULL);

		//arrange for a specific retval if the user presses <Esc>
//default setting is sufficient e2_dialog_set_negative_response (dialog, GTK_RESPONSE_USER1);

		E2_Button resume_btn;
		resume_btn.label = _("_Edit");
		resume_btn.stock = GTK_STOCK_EDIT;
		resume_btn.tip = _("Resume editing");
		resume_btn.showflags = E2_BTN_TIPPED;
		resume_btn.response = E2_RESPONSE_NOTOALL;	//must be understood by the response-decoder

		E2_Button discard_btn;
		discard_btn = E2_BUTTON_DISCARD;
		discard_btn.response = E2_RESPONSE_APPLYTOALL;	//must be understood by the response-decoder

		E2_Button yes_btn;
		yes_btn.label = _("_Save");
		yes_btn.stock = GTK_STOCK_SAVE;
		yes_btn.showflags = E2_BTN_DEFAULT;
		yes_btn.response = GTK_RESPONSE_YES;

		//block until user selects something
		DialogButtons choice = e2_dialog_show (dialog, app.main_window,
			E2_DIALOG_BLOCKED | E2_DIALOG_FREE,
			&resume_btn, &discard_btn, &yes_btn, NULL);

		switch (choice)
		{
			case OK:	//save
				_e2_edit_dialog_save_cb (NULL, rt);
			case YES_TO_ALL:	//discard
				break;
			default:	//resume, close etc
				if (!forced)
					return;	//close-dialog or Escape-press just cancels the close, in effect
				break;
		}
	}

	if (rt->blink_init_id > 0)
	{
		g_source_remove (rt->blink_init_id);
		rt->blink_init_id = 0;
	}
	if (rt->blink_id > 0)
	{
		g_source_remove (rt->blink_id);
		rt->blink_id = 0;
	}

	GtkWidget *entry =
#ifdef USE_GTK2_14
		gtk_bin_get_child (GTK_BIN (rt->combo2));
#else
		GTK_BIN (rt->combo2)->child;
#endif
	const gchar *repl = gtk_entry_get_text (GTK_ENTRY (entry));
	if (*repl != '\0')
	{
		e2_combobox_activated_cb (entry, GINT_TO_POINTER (FALSE));
		e2_list_update_history (repl, &replace_history, NULL, 30, FALSE);
	}
	//if editing an output tab, need some specific cleanup
	GList *tmp;
	for (tmp = app.tabslist; tmp != NULL; tmp = tmp->next)
	{
		if (((E2_OutputTabRuntime*)tmp->data)->buffer == rt->textbuffer)
		{	//we were editing an output pane buffer
			//that stays in existence, so we need to get rid of redundant callbacks
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2edtdlg_insert_text_cb, rt);
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2edtdlg_delete_range_cb, rt);
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2edtdlg_begin_user_action_cb, rt);
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2edtdlg_end_user_action_cb, rt);
			g_signal_handlers_disconnect_by_func (G_OBJECT (rt->textbuffer),
				_e2_edit_dialog_dirty_change_cb, rt);
#ifdef E2_MARK_FINDS
			if (rt->is_lit)
				e2_view_dialog_clear_hilites (rt);	//also clears callback
#endif
			break;
		}
	}
#ifdef E2_SPELLCHECK
	if (rt->spelldata != NULL)
		gtkspell_detach (rt->spelldata);	//this frees GtkSpell data
#endif

	e2_view_dialog_destroy (rt);
}
/**
@brief toggle cursor visibility
This is a timer callback
@param textview the textview for the dialog

@return TRUE (so the timer continues) if @a textview is not focused
*/
// FORCED CURSOR BLINKING STUFFS UP LARGER VERTICAL MOVES
static gboolean _e2_edit_dialog_blink (E2_ViewDialogRuntime *rt)
{
//	printd (DEBUG, "edit dialog cursor visibility toggle cb");
//	gdk_threads_enter ();
//	gboolean blinkme = TRUE;	//GTK_WIDGET_HAS_FOCUS (rt->dialog);
//	if (blinkme)
//	{
//		g_source_remove (blink_id);
	if (GTK_IS_WIDGET (rt->textview))
	{	//the dialog hasn't been closed between callbacks
		gboolean visible = gtk_text_view_get_cursor_visible (GTK_TEXT_VIEW (rt->textview));
		gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (rt->textview), !visible);
		gint period = (visible) ?
			blinkmsec * CURSOR_OFF_MULTIPLIER:
			blinkmsec * CURSOR_ON_MULTIPLIER;
		if (rt->ow_mode)
			period /= INSERT_CURSOR_DIVIDER;
		//FIXME this timer needs to be cancellable at session-end
		rt->blink_id = g_timeout_add_full (G_PRIORITY_LOW, period,
			(GSourceFunc) _e2_edit_dialog_blink, rt, NULL);
	}
//	}
//	gdk_threads_leave ();
//	return !blinkme; //remove ourself if we're done
//	return TRUE;	//already removed
	return FALSE;	//remove ourself
}
/**
@brief initialize cursor blinking
This is an idle callback
@param rt data struct for the dialog

@return FALSE(so the idle callback is cancelled)
*/
static gboolean _e2_edit_dialog_blink_start (E2_ViewDialogRuntime *rt)
{
//	printd (DEBUG, "Cursor blinking started");
	if (rt->blink_id == 0)
		rt->blink_id = g_timeout_add_full (G_PRIORITY_HIGH_IDLE,
			blinkmsec * CURSOR_OFF_MULTIPLIER,
			(GSourceFunc) _e2_edit_dialog_blink, rt, NULL);
	rt->blink_init_id = 0;
	return FALSE;
}
/**
@brief suspend cursor blinking

@param rt data struct for the dialog

@return
*/
static void _e2_edit_dialog_blink_stop (E2_ViewDialogRuntime *rt)
{
	if (rt->blink_id > 0)
	{
		g_source_remove (rt->blink_id);
		rt->blink_id = 0;
	}
	gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (rt->textview), TRUE);
	//setup to go again when next idle
	rt->blink_init_id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
		(GSourceFunc)_e2_edit_dialog_blink_start, rt, NULL);
}

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

/**
@brief text-buffer edited callback

@param textbuffer the textbuffer which is now clean or dirty
@param rt dialog runtime data struct

@return
*/
static void _e2_edit_dialog_dirty_change_cb (GtkTextBuffer *textbuffer,
	E2_ViewDialogRuntime *rt)
{
	rt->is_dirty = !rt->is_dirty;
	gtk_widget_set_sensitive (rt->savebtn, rt->is_dirty);
}
/**
@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_edit_dialog_popup_menu_cb (GtkWidget *textview,
	E2_ViewDialogRuntime *rt)
{
	_e2_edit_dialog_blink_stop (rt);
	gint event_time = gtk_get_current_event_time ();
	_e2_edit_dialog_show_context_menu (textview, 0, event_time, rt);
	return TRUE;
}
/**
@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
*/
gboolean e2_edit_dialog_key_press_cb (GtkWidget *textview,
	GdkEventKey *event, E2_ViewDialogRuntime *rt)
{
	_e2_edit_dialog_blink_stop (rt);
	guint mask = event->state & gtk_accelerator_get_default_mod_mask ();
	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
		guint lower = gdk_keyval_to_lower (event->keyval);
		//hashtable already checked during dialog setup
		guint asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (lower)));
		if (asciicode == 0)
			asciicode = lower;
		if (lower == save_keycode) //|| asciicode == GDK_s)
		{
			if (rt->is_dirty)
				_e2_edit_dialog_save_cb (NULL, rt);
			return TRUE;
		}
		else if (lower == find_keycode || asciicode == findnext_keycode)	//special case again
		{
#ifdef E2_SPELLCHECK
			if (rt->spelldata != NULL)
				_e2_edit_dialog_cancel_spell (rt);
#endif
			rt->release_blocked = TRUE;	//block anomalous key-release-event searches
			//trick cb into doing a from-start|end search for Ctrl f
			_e2_edit_dialog_find_cb (
				(lower == find_keycode) ? NULL : GINT_TO_POINTER (1), rt);
			return TRUE;
		}
		else if (lower == replace_keycode) //|| asciicode == GDK_h)
		{
#ifdef E2_SPELLCHECK
			if (rt->spelldata != NULL)
				_e2_edit_dialog_cancel_spell (rt);
#endif
			_e2_edit_dialog_replace_cb (NULL, rt);
			return TRUE;
		}
		else if (lower == hide_keycode)
		{
			_e2_edit_dialog_hide_cb (NULL, rt);
			return TRUE;
		}
		else if (lower == undo_keycode || (asciicode == GDK_z && mask != GDK_MOD1_MASK)) //no Alt-z support
		{
			if (rt->undo_enabled)
				_e2edtdlg_undo_undo_one (rt);
			return TRUE;
		}
#ifdef E2_REDO_ENABLED
		else if (lower == redo_keycode
			 || (asciicode == GDK_z && mask == (GDK_CONTROL_MASK | GDK_SHIFT_MASK)))
		{
			if (rt->redo_enabled)
				_e2edtdlg_undo_redo_one (rt);
			return TRUE;
		}
#endif
	}
	else if (event->keyval == GDK_Insert)
	{
	//GTK_TEXT_VIEW (rt->textview)->overwrite_mode can get overwrite_mode state
		rt->ow_mode = !gtk_text_view_get_overwrite (GTK_TEXT_VIEW (rt->textview));
		gtk_text_view_set_overwrite (GTK_TEXT_VIEW (rt->textview), rt->ow_mode);
		return TRUE;
	}
//	else if (lower == GDK_Delete)
//	{	//workaround gtk behaviour
//		gtk_text_buffer_set_modified (rt->textbuffer, TRUE);
//	}
	rt->keyval = event->keyval;	//save, for undo processing
	return FALSE;
}
/**
@brief replace combo-box key press callback
This ensures that a <Return> keypress behaves like Ctrl-r, and handles some other
special cases too
@param entry UNUSED the widget where the key was pressed
@param event gdk event data
@param rt rt data for the dialog

@return TRUE if recognised key was pressed
*/
static gboolean _e2_edit_dialog_combokey_cb (GtkWidget *entry,
	GdkEventKey *event, E2_ViewDialogRuntime *rt)
{
	guint lower;
	if (event->keyval == GDK_Return
	 || event->keyval == GDK_KP_Enter
	 || event->keyval == GDK_ISO_Enter
	 || event->keyval == GDK_3270_Enter)
		lower = replace_keycode;
	else if (event->keyval < 0xF000 || event->keyval > 0xFFFF)
		lower = gdk_keyval_to_lower (event->keyval);
	else
		lower = 0; //ignore other controls
	//not all key-actions are supported in replace-widget context
	//e.g. no undo or redo
	if (lower == find_keycode
	 || lower == findnext_keycode
	 || lower == replace_keycode
	 || lower == hide_keycode)
		return (e2_edit_dialog_key_press_cb (rt->textview, event, rt));
	else
		return FALSE;
}
/**
@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_edit_dialog_button_press_cb (GtkWidget *textview,
	GdkEventButton *event, E2_ViewDialogRuntime *rt)
{
	_e2_edit_dialog_blink_stop (rt);
	if (event->button == 3
#ifdef E2_MOUSECUSTOM
		&& (event->state & E2_MODIFIER_MASK) == 0
#endif
		)
	{
		_e2_edit_dialog_show_context_menu (textview, 3, event->time, rt);
		return TRUE;
	}
	return FALSE;
}
/**
@brief edit dialog response callback

@param dialog 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_edit_dialog_response_cb (GtkDialog *dialog, gint response,
	E2_ViewDialogRuntime *rt)
{
	printd (DEBUG, "response_cb (dialog:_,response:%d,rt:_)", response);
	switch (response)
	{
		case E2_RESPONSE_FIND:
#ifdef E2_SPELLCHECK
			if (rt->spelldata != NULL)
				_e2_edit_dialog_cancel_spell (rt);
#endif
			_e2_edit_dialog_find_cb (NULL, rt);
			break;
		case E2_RESPONSE_USER1:	//replace
#ifdef E2_SPELLCHECK
			if (rt->spelldata != NULL)
				_e2_edit_dialog_cancel_spell (rt);
#endif
			_e2_edit_dialog_replace_cb (NULL, rt);
			break;
		case E2_RESPONSE_USER2:	//save
			_e2_edit_dialog_save_cb (NULL, rt);
			break;
#ifdef E2_SPELLCHECK
		case E2_RESPONSE_USER3:	//cancel spelling
			_e2_edit_dialog_cancel_spell (rt);
			break;
#endif
		default:
//		case GTK_RESPONSE_CLOSE:
			//if close button is clicked, it is not "forced"
			_e2_edit_dialog_destroy (rt, (response != GTK_RESPONSE_CLOSE));
			break;
	}
}

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

@return
*/
static void _e2_edit_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 column-0 category string(s) here need to match at least the lowest
	//treestore-iter of the full category name
//	g_strconcat(_C(17),"||||",NULL),  //_("general"
//	g_strconcat("\t",_C(11),"||||",NULL),  //_("dialogs"
	g_strconcat("\t\t",_A(43),"||||",NULL),  //_("edit"
	g_strconcat("\t\t\t|<Control>u","||","echo|\"up\"", NULL),//_A(124),".",_A(125),"|<Control>Home",NULL),
	g_strconcat("\t\t\t|<Control>n","||","echo|\"down\"", NULL),//_A(124),".",_A(125),"|<Control>End",NULL),
	g_strdup(">"),
	NULL);
}
# ifdef E2_MOUSECUSTOM
static void _e2_edit_dialog_mousebindings (E2_OptionSet *set)
{
	//the button name strings are parsed by gtk, and no translation is possible
	//if releases handled, lines must have one extra (=6) separator
	e2_option_tree_setup_defaults (set,
	g_strdup("mousebuttons=<"),  //internal name
	//the column-0 category string(s) here need to match at least the lowest
	//treestore-iter of the full category name
//	g_strconcat(_C(17),"|||||",NULL),  //_("general"
//	g_strconcat("\t",_C(11),"|||||",NULL),  //_("dialogs"
	g_strconcat("\t\t",_A(43),"|||||",NULL),  //_("edit"
#ifdef WITH_KEYFAKE
	g_strconcat("\t\t\t|<Control>4","|||",_A(124),".",_A(125),"|<Control>Home",NULL),
	g_strconcat("\t\t\t|<Control>5","|||",_A(124),".",_A(125),"|<Control>End",NULL),
#endif
	//MORE HERE
	g_strdup(">"),
	NULL);
}
#  ifdef E2_PTRGESTURES
static void _e2_edit_dialog_mousegestures (E2_OptionSet *set)
{
	e2_option_tree_setup_defaults (set,
	g_strdup("mousedrags=<"),  //internal name
	//the column-0 category string(s) here need to match at least the lowest
	//treestore-iter of the full category name
//	g_strconcat(_C(17),"||||",NULL),  //_("general"
//	g_strconcat("\t",_C(11),"||||",NULL),  //_("dialogs"
	g_strconcat("\t\t",_A(43),"||||",NULL),  //_("edit"
#ifdef WITH_KEYFAKE
	g_strconcat("\t\t\t|<Alt>3|^|8,2|",_A(124),".",_A(125),"|Page_Up",NULL), //"key.fake"
	g_strconcat("\t\t\t|<Alt>3||2,8|",_A(124),".",_A(125),"|Page_Down",NULL), //"key.fake"
#endif
	//MORE HERE
	g_strdup(">"),
	NULL);
}
#  endif
# endif
#endif

  /**********************/
 /**** dialog setup ****/
/**********************/
/**
@brief create and show edit dialog
@a localpath may be full path, or at least the name, of item to be edited.
If @a localpath is not an absolute path, curr_view->dir is prepended
If @a srt is non-NULL, and there's a problem during setup, @a srt is set to NULL
Expects BGL closed when we arrive here
@param localpath localised string, or NULL if editing a buffer
@param buf pointer to output pane tab textbuffer, or NULL id editing a file
@param srt pointer to store for returned dialog data, or NULL

@return
*/
static void _e2_edit_dialog_create (VPATH *localpath, GtkTextBuffer *buf, E2_ViewDialogRuntime **srt)
{
	//init view runtime object (0's to ensure view-dialog things are NULL)
	E2_ViewDialogRuntime *rt = ALLOCATE0 (E2_ViewDialogRuntime);
#if (CHECKALLOCATEDWARN)
	CHECKALLOCATEDWARN (rt, if (srt != NULL) *srt = NULL; return;)
#else
	if (rt == NULL)
	{
		if (srt != NULL)
			*srt = NULL;
		return;
	}
#endif
	gint char_width, char_height;
	gboolean fileedit = (localpath != NULL);
	if (fileedit)
	{	//edit file
		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;
		}
		gdk_threads_enter ();
		e2_view_dialog_set_font (&char_width, &char_height, rt);
	}
	else
	{	//edit output pane buffer
		//CHECKME any benefit from a flag to distinguish output edtis ?
		const gchar *home = g_get_home_dir ();
		gchar *name = g_strconcat (_C(28), "-", _A(43), NULL);	//_("output-edit")
		rt->localpath = g_build_filename (home, name, NULL);
		rt->charset = "UTF-8";	//the text is from our buffer
		g_free (name);
		//textview defaults to editable and cursor visible
		rt->textview = gtk_text_view_new_with_buffer (buf);
		rt->textbuffer = buf;
		//preserve appearance of output pane
		const gchar *fontname = e2_utils_get_output_font ();
		e2_widget_set_font (rt->textview, fontname);
		e2_widget_get_font_pixels (rt->textview, &char_width, &char_height);
	}
	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
	//dunno why this gets clobbered when threads used, but ...
	rt->charset = g_strdup (rt->charset);
//	gtk_text_buffer_set_modified (rt->textbuffer, FALSE);
	gtk_text_buffer_set_modified (rt->textbuffer, !fileedit);
//	rt->is_dirty = FALSE; not needed if FALSE set above
	rt->is_dirty = !fileedit;
//	rt->undo_list = NULL;
#ifdef E2_REDO_ENABLED
//	rt->redo_list = NULL;
#endif
//	rt->ui_tmp = NULL;
//	rt->seq_reserve = FALSE;	//default value for ui->seq ??

	//hard-code processing of localised <Ctrl>g for find-next
	findnext_keycode = GDK_g;

	gchar *utf = F_FILENAME_FROM_LOCALE (rt->localpath);
	rt->dialog = e2_dialog_create (NULL, utf, _("editing file"),
		(ResponseFunc)_e2_edit_dialog_response_cb, rt);
	F_FREE (utf, rt->localpath);
	GtkWidget *dialog_vbox =
#ifdef USE_GTK2_14
		gtk_dialog_get_content_area (GTK_DIALOG (rt->dialog));
#else
		GTK_DIALOG (rt->dialog)->vbox;
#endif
//	gtk_window_set_type_hint (GTK_WINDOW (rt->dialog), GDK_WINDOW_TYPE_HINT_NORMAL);
	//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);

	e2_widget_add_separator (dialog_vbox, FALSE, 0);
	//create the view
	GtkWidget *sw = e2_widget_add_sw (dialog_vbox,
			GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC, TRUE, E2_PADDING);
	gtk_container_add (GTK_CONTAINER (sw), rt->textview);
	// set view defaults
	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);
	rt->window_width = e2_option_int_get ("dialog-view-width");
	rt->window_height = e2_option_int_get ("dialog-view-height");
	if (!fileedit)
	{
		GtkTextMark *mark = gtk_text_buffer_get_insert (buf);
		gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (rt->textview),
			mark, 0.2, TRUE, 0.0, 0.5);
	}
//	rt->idle_id = 0;
	//init search runtime data
	rt->history = e2_list_copy_with_data (find_history);	//CHECKME why copy these ?
	rt->rephistory = e2_list_copy_with_data (replace_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 (dialog_vbox), rt->panel, FALSE, TRUE, E2_PADDING_XSMALL);
	gtk_box_reorder_child (GTK_BOX (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);

	GtkWidget *vbox = gtk_vbox_new (FALSE, 0);
	gtk_container_add (GTK_CONTAINER (hndlbox), vbox);
	//create search bar
	rt->sgroup = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	GtkWidget *hbox = e2_view_dialog_create_searchbar (rt);
	g_object_unref (G_OBJECT(rt->sgroup));
	gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);

	//create (hidden) replace bar, with buttons-area same size as buttons in search bar
	//(so that the corresponding expandable entries are the same size)
	rt->replacebar = hbox = gtk_hbox_new (FALSE, 0);
	//add replace-string combo
	rt->combo2 = e2_combobox_add (hbox, TRUE, 0, NULL, NULL, &replace_history,
		E2_COMBOBOX_HAS_ENTRY | E2_COMBOBOX_FOCUS_ON_CHANGE);
//	gtk_widget_set_size_request (rt->combo2, 230, -1);
//	gtk_size_group_add_widget (group, rt->combo2);
	GtkWidget *entry =
#ifdef USE_GTK2_14
		gtk_bin_get_child (GTK_BIN (rt->combo2));
#else
		GTK_BIN (rt->combo2)->child;
#endif
	g_signal_connect_after (G_OBJECT (entry), "key-release-event",
		G_CALLBACK (_e2_edit_dialog_combokey_cb), rt);
	e2_widget_set_safetip (entry, _("Replacements"));
	//add replace-option buttons (mnemonics conform to search and action buttons)
	GtkWidget *hbox2 = e2_widget_add_box (hbox, FALSE, 0, FALSE, FALSE, 0);
	e2_button_add_toggle (hbox2, TRUE, do_next,
		_("r_epeat"),_("If activated, the next match will be sought after each replacement"),
		do_next, E2_PADDING_XSMALL, e2_view_dialog_toggled_cb, &do_next);
	e2_button_add_toggle (hbox2, TRUE, replace_all,
		_("_all"), _("If activated, all matches will be replaced at once"),
		replace_all, E2_PADDING_XSMALL, e2_view_dialog_toggled_cb, &replace_all);
	e2_button_add_toggle (hbox2, TRUE, confirm_before,
		_("co_nfirm"),_("If activated, confirmation will be sought when \"replacing all\""),
		confirm_before, E2_PADDING_XSMALL, e2_view_dialog_toggled_cb, &confirm_before);
	gtk_size_group_add_widget (rt->sgroup, hbox2);
	gtk_box_pack_start (GTK_BOX (vbox), hbox, FALSE, FALSE, 0);

	//now add things to the action-area
	//produce but don't yet show the "not found" warning
	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 (
#ifdef USE_GTK2_14
		gtk_dialog_get_action_area (GTK_DIALOG (rt->dialog)),
#else
		GTK_DIALOG (rt->dialog)->action_area,
#endif
		labeltext, 0.0, TRUE, 0);
	//left-align the label
	gtk_button_box_set_child_secondary (
		GTK_BUTTON_BOX (
#ifdef USE_GTK2_14
		gtk_dialog_get_action_area (GTK_DIALOG (rt->dialog))
#else
		GTK_DIALOG (rt->dialog)->action_area
#endif
		), rt->info_label, TRUE);
	labeltext = _("_Replace");
	replace_keycode = e2_utils_get_mnemonic_keycode (labeltext);
	guint asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (replace_keycode)));
	if (asciicode != 0)
		replace_keycode = asciicode;
	rt->replacebtn =
	e2_dialog_add_simple_button (rt->dialog, GTK_STOCK_CONVERT, labeltext, E2_RESPONSE_USER1);
	e2_widget_set_safetip (rt->replacebtn, _("Replace this match"));
	labeltext = _("_Find");
	find_keycode = e2_utils_get_mnemonic_keycode (labeltext);
	asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (find_keycode)));
	if (asciicode != 0)
		find_keycode = asciicode;
	rt->findbtn =
	e2_dialog_add_simple_button (rt->dialog, GTK_STOCK_FIND, labeltext, E2_RESPONSE_FIND);
	e2_widget_set_safetip (rt->findbtn, _("Find the next match"));
	labeltext = _("_Save");
	save_keycode = e2_utils_get_mnemonic_keycode (labeltext);
	asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (save_keycode)));
	if (asciicode != 0)
		save_keycode = asciicode;
	rt->savebtn =
	e2_dialog_add_simple_button (rt->dialog, GTK_STOCK_SAVE, labeltext, E2_RESPONSE_USER2);
	//allow saving of a fresh output buffer
	gtk_widget_set_sensitive (rt->savebtn, !fileedit);
	e2_dialog_add_defined_button (rt->dialog, &E2_BUTTON_CLOSE);
//	e2_dialog_set_responses (rt->dialog, E2_RESPONSE_FIND, GTK_RESPONSE_CLOSE);
	//make sure we always exit cleanly
	e2_dialog_set_negative_response (rt->dialog, GTK_RESPONSE_CLOSE);
	//some used keycodes are related to context-menu-item labels which are not created until needed ...
	hide_keycode = e2_utils_get_mnemonic_keycode (_("_Hide"));
	asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (hide_keycode)));
	if (asciicode != 0)
		hide_keycode = asciicode;
	undo_keycode = e2_utils_get_mnemonic_keycode (_("_Undo"));
	asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (undo_keycode)));
	if (asciicode != 0)
		undo_keycode = asciicode;
#ifdef E2_REDO_ENABLED
	redo_keycode = e2_utils_get_mnemonic_keycode (_("Re_do"));
	asciicode = GPOINTER_TO_UINT (g_hash_table_lookup (app.keysnative,
			GUINT_TO_POINTER (redo_keycode)));
	if (asciicode != 0)
		redo_keycode = asciicode;
#endif

	//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);
	//setup for cursor management
	GtkSettings *defs = gtk_widget_get_settings (rt->textview);
	if (defs != NULL)
	{
	  g_object_get (defs, "gtk-cursor-blink-time", &blinkmsec, NULL);
	}
	else
		blinkmsec = 1200;	//gtk default
	blinkmsec *= OVERWR_CURSOR_MULTIPLIER;
	rt->blink_init_id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
		(GSourceFunc)_e2_edit_dialog_blink_start, rt, NULL);

	//initialize undo arrangements
	_e2edtdlg_undo_initialize (rt->textbuffer, rt);

	//highest priority - arrange for key-translations from locale
	g_signal_connect (G_OBJECT (rt->dialog), "key-press-event",
		G_CALLBACK (e2_utils_key_translate_cb), NULL);
	g_signal_connect (G_OBJECT (rt->dialog), "key-release-event",
		G_CALLBACK (e2_utils_key_translate_cb), NULL);

#ifdef E2_TRANSIENTBINDINGS
	//add dialog-specific key bindings, before the key-press callback
	//see also dialog-general
	//group name (must be freeable)
	gchar *category = g_strconcat (_C(17),".",_C(11),".",_A(43),NULL);	//_(general.dialogs.edit
	e2_keybinding_enrol (GTK_WIDGET (rt->textview), category, _e2_edit_dialog_keybindings);
# ifdef E2_MOUSECUSTOM
	e2_mousebinding_enrol (rt->dialog, category, _e2_edit_dialog_mousebindings);
#  ifdef E2_PTRGESTURES
	e2_mousegesture_enrol (rt->dialog, category, _e2_edit_dialog_mousegestures);
#  endif
# endif
	g_free (category);
#endif

	g_signal_connect (G_OBJECT (rt->textbuffer), "modified-changed",
		G_CALLBACK (_e2_edit_dialog_dirty_change_cb), rt);
	g_signal_connect (G_OBJECT (rt->textview), "popup-menu",
		G_CALLBACK (_e2_edit_dialog_popup_menu_cb), rt);
	//this cb does generic stuff and is needed with or without E2_MOUSECUSTOM
	g_signal_connect (G_OBJECT (rt->textview), "button-press-event",
		G_CALLBACK (_e2_edit_dialog_button_press_cb), rt);
	g_signal_connect (G_OBJECT (rt->textview), "key-press-event",
		G_CALLBACK (e2_edit_dialog_key_press_cb), rt);

	gtk_widget_show_all (dialog_vbox);
	gtk_widget_hide (rt->panel);
	gtk_widget_hide (rt->info_label);
	gtk_widget_hide (rt->findbtn);
	gtk_widget_hide (rt->replacebtn);

	if (srt != NULL)
		*srt = rt;	//make rt available to re-edit func
}

  /******************/
 /***** action *****/
/******************/

/**
@brief re-open file for editing, 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_edit_dialog_reopen (gpointer from, E2_ActionRuntime *art)
{
	return (e2_task_do_task (E2_TASK_EDIT, art, from,
		_e2_edit_dialog_reeditQ, NULL));
}
static gboolean _e2_edit_dialog_reeditQ (E2_ActionTaskData *qed)
{
	gboolean retval;
	//process the 1st selected item in active pane
	//FIXME allow specification of a name
	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);
	if (e2_option_bool_get ("use-external-editor"))
	{
		gchar *editor = e2_option_str_get ("command-editor");
		if (*editor != '\0')
		{
			gchar *utf = F_FILENAME_FROM_LOCALE (localpath);
			gchar *command = e2_utils_replace_name_macros (editor, utf);
			if (command == editor)	//no replacement of macro in editor
			{
//tag E2_BADQUOTES
				//CHECKME may be bad if a file.view action is supplied
				gchar *qp = e2_utils_quote_string (utf);
				command = g_strconcat (editor, " ", qp, NULL);
				g_free (qp);
			}
			gdk_threads_enter ();
			gint res = e2_command_run (command, E2_COMMAND_RANGE_DEFAULT, curr_view->treeview
#ifdef E2_COMMANDQ
			, TRUE
#endif
			);
			gdk_threads_leave ();
			F_FREE (utf, localpath);
			g_free (command);
			retval = (res == 0);
		}
		else
			//FIXME warn user
			retval = FALSE;
	}
	else
	{
#ifdef E2_VFS
		VPATH ddata = { localpath, qed->currspace };
#endif
		E2_ViewDialogRuntime *vrt = NULL;
		gdk_threads_enter ();
#ifdef E2_VFS
		_e2_edit_dialog_create (&ddata, NULL, &vrt);
#else
		_e2_edit_dialog_create (localpath, NULL, &vrt);
#endif
		gdk_threads_leave ();
		if (vrt != NULL)
		{
			gdk_threads_enter ();
			e2_view_dialog_show_atlast
#ifdef E2_VFS
				(&ddata, vrt);
#else
				(localpath, vrt);
#endif
			gdk_threads_leave ();
			retval = TRUE;
		}
		else
			retval = FALSE;
	}
	g_free (localpath);
	return retval;
}

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

/**
@brief create and show edit dialog
@a filename 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
Expects BGL closed
@param localpath localised string which has at least the name of item to be processed, or NULL
@param buf pointer to a GtkTextBuffer to edit an output pane tab, or NULL
@return TRUE if the dialog was created
*/
gboolean e2_edit_dialog_create (VPATH *localpath, GtkTextBuffer *buf)
{
	//there is no async thread protection here, as this func can be called from
	//different contexts - any protection is done by the caller
	E2_ViewDialogRuntime *vrt;
	if (localpath != NULL || buf != NULL)
	{
		_e2_edit_dialog_create (localpath, buf, &vrt);
		if (vrt != NULL)
		{
			WAIT_FOR_EVENTS_SLOWLY
			e2_dialog_show (vrt->dialog, app.main_window, E2_DIALOG_DONT_SHOW_ALL, NULL);
			gtk_window_present (GTK_WINDOW (vrt->dialog));
			return TRUE;
		}
	}
	return FALSE;
}
/**
@brief register action related to edit dialog
@return
*/
void e2_edit_dialog_actions_register (void)
{
	E2_Action action =
	{g_strconcat(_A(6),".",_A(44),NULL),_e2_edit_dialog_reopen,FALSE,E2_ACTION_TYPE_ITEM,0,NULL,NULL};
	e2_action_register (&action);
#ifdef E2_SPELLCHECK
	//HACK while we're at it, init some session parameters
	e2_cache_list_register ("language-history", &lang_history);
#endif
}
/**
@brief register config option related to edit dialog
@return
*/
void e2_edit_dialog_options_register (void)
{
	gchar *group_name = g_strconcat(_C(11),":",_A(43),NULL); //_("dialogs:edit"
	//first some options that may, but probably won't, change during the session
	e2_option_bool_register ("edit-save-backup", group_name, _("backup when saving"),
		_("When saving an edited file, an existing file with the same name will be renamed"),
		NULL, FALSE,
		E2_OPTION_FLAG_BASIC | E2_OPTION_FLAG_FREEGROUP);
}
