/* $Id: e2_option_tree.c 886 2008-05-19 11:13:57Z tpgww $

Copyright (C) 2003-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
*/

/**
@file src/config/e2_option_tree.c
@brief functions to handle tree options

Functions to handle tree options
*/

#include "emelfm2.h"
#include <string.h>
#include "e2_option_tree.h"
#include "e2_dialog.h"

//data struct for a deferred tree-option overwrite dialog
typedef struct _E2_OptionTreeCheck
{
	E2_OptionSet *set; //pointer to set data struct
	GtkTreeRowReference *ref;
	gint colnum;
	gboolean *lock;
	gchar *newvalue;
	gchar *prompt;
} E2_OptionTreeCheck;

#define E2_OVERRIDE_MASK (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)
//data struct for key-binding change
typedef struct _E2_KeyChangeData
{
	E2_OptionSet *set; //pointer to set data struct
	gchar *path_string; //string form of gtk tree path to the row to be amended
	GtkCellRenderer *renderer;
//	gchar *oldtext;
} E2_KeyChangeData;

static void _e2_option_tree_set (E2_OptionSet *set, GtkTreeIter *iter, gint col,
	void *data);

extern GtkWidget *config_dialog;
static void _e2_option_tree_add_default (E2_OptionSet *set, GtkTreeIter *iter,
	GtkTreeIter *parent, gboolean sibling);
static void _e2_option_tree_move (E2_OptionSet *set, GtkTreeIter *iter,
	GtkTreePath *path_dest, gboolean sibling, gboolean before);
static gboolean _e2_option_tree_add_below_cb (GtkWidget *widget, E2_OptionSet *set);
static gboolean _e2_option_tree_add_child_cb (GtkWidget *widget, E2_OptionSet *set);
static gboolean _e2_option_tree_move_up_cb (GtkWidget *widget, E2_OptionSet *set);
static gboolean _e2_option_tree_move_down_cb (GtkWidget *widget, E2_OptionSet *set);
static gboolean _e2_option_tree_button_press_cb (GtkWidget *treeview,
	GdkEventButton *event, E2_OptionSet *set);
static void *_e2_option_tree_get_void_simple (E2_OptionTreeColumn *col, gchar *option);

extern GtkTreeStore *actions_store;

  /************************/
 /***** context menu *****/
/************************/

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

/**
@brief create flag for the config dialog, to signal that the set's tree data has been modified

@param set pointer to data struct for the option that the dialog configures

@return
*/
void e2_option_tree_flag_change (E2_OptionSet *set)
{
	set->ex.tree.flags |= E2_OPTION_TREE_SET_EDITED;
}

  /**************************/
 /***** data functions *****/
/**************************/
/*These functions set some properties of a treeview cell instead of using
the straight mapping between the cell and the treemodel. They are called
whenever the cell is rendered, and at the end of (not during) cell-editing */
static void _e2_option_tree_bool_data_func (GtkTreeViewColumn *tree_column,
	GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter,
	E2_OptionTreeColumn *col)
{
	gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cell), "column"));

	gboolean b;
	gtk_tree_model_get (model, iter, column, &b, -1);

	gboolean visible = TRUE;
	if (col->visible_check_func != NULL)
	{
		gboolean (*fun) (GtkTreeModel *, GtkTreeIter *, GtkCellRenderer *,
			gpointer) = col->visible_check_func;
		visible = fun (model, iter, cell,  col->visible_check_data);
	}
	g_object_set (G_OBJECT (cell), "visible", visible, "active", b, NULL);
}

static void _e2_option_tree_str_data_func (GtkTreeViewColumn *tree_column,
	GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter,
	E2_OptionTreeColumn *col)
{
	gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cell), "column"));

	gchar *str;
	gtk_tree_model_get (model, iter, column, &str, -1);

	gboolean visible = TRUE;
	if (col->visible_check_func != NULL)
	{
		gboolean (*fun) (GtkTreeModel *, GtkTreeIter *, GtkCellRenderer *,
			gpointer) = col->visible_check_func;
		visible = fun (model, iter, cell,  col->visible_check_data);
	}
	g_object_set (G_OBJECT (cell), "visible", visible, "text", str, NULL);
	g_free (str);
}

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

/* the relevant one of funcs is called whenever an edited cell is re-rendered,
and when the edit is finished, and when Esc is pressed (in the latter case,
as part of the widget_destroy process for the config dialog
They set treestore data in accord with the renderer */

/**
@brief save edited text value in the underlying treestore

@param renderer the renderer for the cell
@param path_string string form of gtk tree path to the row to be amended
@param new_text replacement text string for the cell
@param set pointer to set data struct

@return
*/
static void _e2_option_tree_string_edited_cb (GtkCellRendererText *cell,
	gchar *path_string, gchar *new_text, E2_OptionSet *set)
{
	if (new_text == NULL)	//this probably can't happen
		return;
	printd (DEBUG, "tree-option edited cb, new text is %s", new_text);
	if (g_str_equal (set->name, "keybindings")
		&& g_str_equal (new_text, _("Press key")))
		return;

	GtkTreeIter iter;
	if (gtk_tree_model_get_iter_from_string (GTK_TREE_MODEL (set->ex.tree.model),
		&iter, path_string))
	{
		gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cell), "column"));
		_e2_option_tree_set (set, &iter, column, (gchar *)new_text);
	}
	//clean up
	//revert focus to edited row
//CHECKME when editing done, focus the cancel button ??
	gtk_widget_grab_focus (set->widget);
}
/**
@brief save toggled boolean value in the underlying treestore

@param renderer the renderer for the cell
@param path_string string form of gtk tree path to the row to be amended
@param set pointer to set data struct

@return
*/
static void _e2_option_tree_toggle_cb (GtkCellRendererToggle *cell,
	const gchar *path_str, E2_OptionSet *set)
{
	//find out where
	GtkTreePath *path = gtk_tree_path_new_from_string (path_str);
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter (set->ex.tree.model, &iter, path))
	{
		gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (cell), "column"));
		//invert current value
		gboolean toggle_item;
		gtk_tree_model_get (set->ex.tree.model, &iter, column, &toggle_item, -1);
		toggle_item ^= 1;
		//change model
		gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), &iter, column, toggle_item, -1);
		e2_option_tree_flag_change (set);
	}
	//clean up
	gtk_tree_path_free (path);
}

static void _e2_option_tree_cell_pixbuf_data_func (GtkTreeViewColumn *tree_column,
	GtkCellRenderer *cell, GtkTreeModel *model, GtkTreeIter *iter, gpointer data)
{
	gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tree_column), "column"));
	gchar *icon;
	gtk_tree_model_get (model, iter, column, &icon, -1);
#ifdef E2_IMAGECACHE
	GdkPixbuf *pb;
#endif
	if (icon != NULL && *icon != '\0')
	{
#ifdef E2_IMAGECACHE
// FIXME = adjust img->refcount ??
		GtkIconSize sz = GPOINTER_TO_INT (data);
		E2_Image *img = e2_cache_image_get (icon, sz);
		pb = img->pixbuf;
#else
		if (e2_utils_check_stock_icon (icon))
		{
			g_object_set (G_OBJECT (cell), "stock-id", icon, NULL);
			g_object_set (G_OBJECT (cell), "pixbuf", NULL, NULL);
		}
		else	//not a stock item
		{
			GdkPixbuf *pixbuf;
			GdkPixbuf *pixbuf2;
			if ((pixbuf = gdk_pixbuf_new_from_file (icon, NULL)) != NULL)
			{
				pixbuf2 = pixbuf;
				pixbuf = gdk_pixbuf_scale_simple (pixbuf2, 16, 16, GDK_INTERP_BILINEAR);
				g_object_unref (pixbuf2);
			}
			g_object_set (G_OBJECT (cell), "stock-id", NULL, NULL);
			g_object_set (G_OBJECT (cell), "pixbuf", pixbuf, NULL);
			if (pixbuf != NULL)
				g_object_unref (pixbuf);
		}
#endif
	}
	else
#ifdef E2_IMAGECACHE
		pb = NULL;
	g_object_set (G_OBJECT (cell), "pixbuf", pb, NULL);
#else
	{
		g_object_set (G_OBJECT (cell), "stock-id", NULL, NULL);
		g_object_set (G_OBJECT (cell), "pixbuf", NULL, NULL);
	}
#endif
	g_free (icon);
}
/**
@brief set key name determined from key press event @a event

This is a callback after a key cell edit begins

@param entry the entry widget associated with the cell renderer
@param event pointer to event data struct for the pressed key
@param data pointer to data needed to update the cell the renderer

@return TRUE unless the key value is invalid
*/
static gboolean _e2_option_tree_keychange_cb (GtkEntry *entry,
	GdkEventKey *event, E2_KeyChangeData *data)
{
//	printd (DEBUG, "key change cb");
	//one-time only
	g_signal_handlers_disconnect_by_func (
		entry, _e2_option_tree_keychange_cb, data->set);

	guint keyval = event->keyval;
	GdkModifierType mask = event->state;
	if (!gtk_accelerator_valid (keyval, mask))
		return TRUE;
	mask &= ~(GDK_MOD2_MASK);	//ignore Mod2 (numlock)
	//<Esc> always cancels (so unmodified, it can't be set as a binding here)
	if (keyval == GDK_Escape && !(mask & E2_OVERRIDE_MASK))
	{
		gtk_widget_grab_focus (data->set->widget);
		g_free (data->path_string);
		DEALLOCATE (KeyChangeData, data);
		return TRUE;
	}
	gchar *keyname;
	if (keyval < 0xF000 || keyval > 0xFFFF)
	{
		if ((mask & E2_OVERRIDE_MASK) == GDK_SHIFT_MASK)
		{	//control and alt not pressed
			if (mask & GDK_LOCK_MASK)
				//lcase already, we don't want <Shift>
				keyname = gdk_keyval_name (keyval);
			else
/*			{	//toggle case
				if (gdk_keyval_is_lower (keyval))
					keyname = gdk_keyval_name (gdk_keyval_to_upper (keyval));
				else
					keyname = gdk_keyval_name (gdk_keyval_to_lower (keyval));
			} */
			{
				if (gdk_keyval_is_lower (keyval))
					keyval = gdk_keyval_to_upper (keyval);
				keyname = gdk_keyval_name (keyval);
			}
			keyname = g_strdup (keyname);
		}
		else if ((mask & GDK_LOCK_MASK)
			 && !(mask & E2_OVERRIDE_MASK))
			//only lock is active
			keyname = g_strdup (gdk_keyval_name (gdk_keyval_to_upper (keyval)));
		else
			keyname = gtk_accelerator_name (keyval, mask);
	}
	else
		keyname = gtk_accelerator_name (keyval, mask);
/* THE DIALOG CAUSES CRASH
	//check for and warn about duplicate
	GtkTreeView *treeview = GTK_TREE_VIEW (data->set->widget);
	GtkTreeSelection *sel = gtk_tree_view_get_selection (treeview);
	GtkTreeIter iter;
	if (!gtk_tree_selection_get_selected (sel, NULL, &iter))
		return TRUE;
	DialogButtons result = OK;
	GtkTreeIter parent, srch_iter;
	GtkTreeModel *mdl = data->set->ex.tree.model;
	gtk_tree_model_iter_parent (mdl, &parent, &iter);
	gtk_tree_model_iter_children (mdl, &srch_iter, &parent);
	do
	{
		gchar *storedkey, *action, *arg;
		gtk_tree_model_get (mdl, &srch_iter, 1, &storedkey,
			3, &action, 4, &arg, -1);
		if (g_str_equal (storedkey, keyname)
			&& (srch_iter.stamp != iter.stamp
			 || srch_iter.user_data != iter.user_data
			 || srch_iter.user_data2 != iter.user_data2
			 || srch_iter.user_data3 != iter.user_data3
				))
		{
			gchar *s, *prompt, *fmt = _("%s is assigned to '%s'. Proceed ?");
			if (*action != '\0' && *arg != '\0')
			{
				s = g_strdup_printf (fmt, keyname, "%s %s");
				prompt = g_strdup_printf (s, action, arg);
				g_free (s);
			}
			else
				prompt = g_strdup_printf (fmt, keyname, action);
			result = e2_dialog_warning (prompt);
			g_free (prompt);
			g_free (storedkey);
			g_free (action);
			g_free (arg);
			break;
		}
		g_free (storedkey);
		g_free (action);
		g_free (arg);
	} while (gtk_tree_model_iter_next (mdl, &srch_iter));

	if (result == OK)
	{
*/
//		g_signal_handlers_block_by_func (data->renderer,
//			_e2_option_tree_key_edit_start_cb, data->set);
		//this causes _e2_option_tree_key_edit_start_cb () SMALL YUK
		g_signal_emit_by_name (data->renderer, "edited", data->path_string,
			keyname, data->set);
//		g_signal_handlers_unblock_by_func (data->renderer,
//			_e2_option_tree_key_edit_start_cb, data->set);
//	}

	g_free (data->path_string);
//	g_free (data->oldtext);
	DEALLOCATE (KeyChangeData, data);
	g_free (keyname);
	return TRUE;
}
/**
@brief setup to amend the key name in a treeview cell

@param renderer the renderer for the cell
@param editable the interface to @a cell
@param path_string string form of gtk tree path to the row to be amended
@param set pointer to set data struct

@return
*/
static void _e2_option_tree_key_edit_start_cb (GtkCellRenderer *renderer,
	GtkCellEditable *editable, gchar *path_string, E2_OptionSet *set)
{
//	printd (DEBUG, "START KEY EDIT CB");
	if (GTK_IS_ENTRY (editable))
    {
		E2_KeyChangeData *data = ALLOCATE (E2_KeyChangeData);
		CHECKALLOCATEDWARN (data, return;)
		data->set = set;
		data->path_string = g_strdup (path_string);
		data->renderer = renderer;
		GtkEntry *entry = GTK_ENTRY (editable);
//		data->oldtext = g_strdup (gtk_entry_get_text (entry));
		gtk_entry_set_text (entry, _("Press key"));
		g_signal_connect (G_OBJECT (entry), "key-press-event",
			G_CALLBACK (_e2_option_tree_keychange_cb), data);
    }
}
/**
@brief change <Esc> key handling when cell editing starts

@param renderer the renderer for the cell
@param editable the interface to @a renderer
@param path_string string form of gtk tree path to the row to be amended
@param user_data UNUSED pointer to data specified when callback was connected

@return
*/
static void _e2_option_tree_cell_edit_start_cb (GtkCellRenderer *renderer,
	GtkCellEditable *editable, gchar *path_string, gpointer user_data)
{
//	printd (DEBUG, "start cell edit cb");
	g_signal_handlers_block_by_func (G_OBJECT (config_dialog),
		e2_dialog_key_neg_cb, config_dialog);
/*	if (GTK_IS_ENTRY (editable))
	{
		GtkEntry *entry = GTK_ENTRY (editable);
		//do stuff
	} */
}
/**
@brief revert <Esc> key handling when cell editing is finished

@param renderer the renderer for the cell
@param user_data UNUSED pointer to data specified when callback was connected

@return
*/
static void _e2_option_tree_cell_edit_stop_cb (GtkCellRenderer *renderer,
	gpointer user_data)
{
//	printd (DEBUG, "stop cell edit cb");
	g_signal_handlers_unblock_by_func (G_OBJECT (config_dialog),
		e2_dialog_key_neg_cb, config_dialog);
}

static void _e2_option_tree_page_opened_cb (GtkWidget *widget, E2_OptionSet *set)
{
	//backup set data when the page is opened (if it's not done already)
	e2_option_tree_backup (set);
}

//this is the row_draggable function for the treeview's GtkTreeDragSourceIface
static gboolean _e2_option_tree_draggable_check_cb (GtkTreeDragSource *drag_source,
	GtkTreePath *path)
{
	if (!GTK_IS_TREE_MODEL (drag_source))
		return TRUE;
	GtkTreeModel *model = GTK_TREE_MODEL (drag_source);
	E2_OptionSet *set = g_object_get_data (G_OBJECT (model), "e2-option-set");
	if (set == NULL)
		return TRUE;
	if (set->ex.tree.draggable_check_func != NULL)
	{
		gboolean (*fun) (GtkTreeDragSource *, GtkTreePath *) =
			set->ex.tree.draggable_check_func;
		return fun (drag_source, path);
	}
	else
		return TRUE;
}
/**
@brief delete the row at @a path, because it was moved somewhere else via drag-and-drop.

@param drag_source drag source data struct
@param path	gtk tree path to the row to be deleted

@return FALSE if the deletion fails because path no longer exists, or for some model-specific reason
*/
static gboolean _e2_option_tree_drag_delete_cb (GtkTreeDragSource *drag_source,
	GtkTreePath *path)
{
	if (!GTK_IS_TREE_MODEL (drag_source))
		return FALSE;
	GtkTreeModel *model = GTK_TREE_MODEL (drag_source);
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter (model, &iter, path))
	{
		gtk_tree_store_remove (GTK_TREE_STORE (model), &iter);
		E2_OptionSet *set = g_object_get_data (G_OBJECT (model), "e2-option-set");
		e2_option_tree_flag_change (set);
		return TRUE;
	}
	else
		return FALSE;
}

static gboolean _e2_option_tree_move_up_cb (GtkWidget *widget,
	E2_OptionSet *set)
{
	GtkTreeModel *model;
	GtkTreeIter iter;

	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (set->widget)), &model, &iter))
	{
		GtkTreePath *path;
		path = gtk_tree_model_get_path (model, &iter);
		if (gtk_tree_path_prev (path))
		{
			_e2_option_tree_move (set, &iter, path, TRUE, TRUE);	//this sets the dirty-flag
		}
		if (path)
	    	gtk_tree_path_free (path);
	}
	return TRUE;
}

static gboolean _e2_option_tree_move_down_cb (GtkWidget *widgget,
	E2_OptionSet *set)
{
	GtkTreeModel *model;
	GtkTreeIter iter;

	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (set->widget)), &model, &iter))
	{
		GtkTreePath *path;
		path = gtk_tree_model_get_path (model, &iter);
		gtk_tree_path_next (path);
		_e2_option_tree_move (set, &iter, path, TRUE, FALSE);	//this sets the dirty-flag
		e2_option_tree_flag_change (set);
		if (path)
	    	gtk_tree_path_free (path);
	}
	return TRUE;
}

static void _e2_option_tree_icon_column_set_cb (GtkDialog *dialog,
	gint response, E2_OptionSet *set)
{
	if ((response == GTK_RESPONSE_APPLY) || (response == GTK_RESPONSE_OK) ||
		(response == E2_RESPONSE_REMOVE))
	{
		GtkTreeIter iter;
		if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection
			(GTK_TREE_VIEW (set->widget)), NULL, &iter))
		{
			gpointer icon = g_object_get_data (G_OBJECT (dialog), "image");
			if (icon != NULL) //user didn't cancel or do something silly
			{
				gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (dialog), "column"));
				_e2_option_tree_set (set, &iter, column, (gchar *)icon);	//this sets the dirty-flag
			}
		}
	}
	if ((response != GTK_RESPONSE_APPLY) && (response != E2_RESPONSE_REMOVE))
		gtk_widget_destroy (GTK_WIDGET (dialog));
}

static gboolean _e2_option_tree_popup_menu_cb (GtkWidget *treeview,
	GtkWidget *menu)
{
	//set appropriate item sensitivities
	e2_option_tree_menu_set_sensitive (menu, treeview);

	gint event_time = gtk_get_current_event_time ();
	gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
		(GtkMenuPositionFunc) _e2_option_tree_menu_set_position,
		treeview, 0, event_time);	//button code = 0 as this was a menu-button press
	return TRUE;
}
/**
@brief create context menu

Context menu is created when mouse button-3 is pressed

@param treeview widget where the button was pressed
@param event gdk event data
@param menu the context-menu widget

@return TRUE (to prevent further handlers) for a button=3 press, else FALSE
*/
static gboolean _e2_option_tree_button_press_cb2 (GtkWidget *treeview,
	GdkEventButton *event, GtkWidget *menu)
{
	if (event->button != 3)
		return FALSE;
	//set appropriate item sensitivities
	e2_option_tree_menu_set_sensitive (menu, treeview);

	gtk_menu_popup (GTK_MENU (menu), NULL, NULL, NULL, NULL, event->button, event->time);
	return TRUE;
}

static gboolean _e2_option_tree_button_press_cb (GtkWidget *treeview,
	GdkEventButton *event, E2_OptionSet *set)
{
	if (event->button != 1)
		return FALSE;
	GtkTreePath *path;
	GtkTreeViewColumn *tree_column;
	if (!gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (treeview),
		event->x, event->y, &path, &tree_column, NULL, NULL))
			return FALSE;
	GtkTreeSelection *selection = gtk_tree_view_get_selection
		(GTK_TREE_VIEW (treeview));
	if (!gtk_tree_selection_path_is_selected (selection, path))
		return FALSE;
	gint column = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (tree_column), "column"));
	E2_OptionTreeColumn *opt = g_list_nth_data (set->ex.tree.columns, column);
	if (opt->type == E2_OPTION_TREE_TYPE_ICON)
	{
		//get the current value
		gchar *icon;
		GtkTreeIter iter;
		gtk_tree_model_get_iter (set->ex.tree.model, &iter, path);
		gtk_tree_model_get (set->ex.tree.model, &iter, column, &icon, -1);
		gchar *dialog_name = g_strdup_printf (_("select icon for %s"), set->name);
		GtkWidget *dialog = e2_sid_create (config_dialog, dialog_name, icon,
			_e2_option_tree_icon_column_set_cb, set);
		g_free (dialog_name);
		g_object_set_data (G_OBJECT (dialog), "column", GINT_TO_POINTER (column));
		return TRUE;
	}
	return FALSE;
}

static gboolean _e2_option_tree_help_cb (GtkWidget *button, E2_OptionSet *set)
{
	//get the config dialog page label, from set->group
	// (after a parent separator '.' if any)
	gchar *page, *realpage;
	page = set->group;	//FIXME this is translated, but the help doc is not (yet?)
	if ((realpage = strchr (page, '.')) != NULL)	//if always ascii ',', don't need g_utf8_strchr()
	{
		NCHR(realpage); page = realpage;
	}
	//relevant section label is "[page]"
	gchar *command = g_strconcat(_A(5),".",_A(103)," ",
		e2_option_str_get ("config-help-doc")," [",page,"]",NULL);	//file.view_at;
#ifdef E2_COMMANDQ
	e2_command_run (command, E2_COMMAND_RANGE_DEFAULT, FALSE);
#else
	e2_command_run (command, E2_COMMAND_RANGE_DEFAULT);
#endif
	g_free (command);
	return TRUE;
}

static gboolean e2_option_tree_del_direct_cb (GtkWidget *button, E2_OptionSet *set)
{
	GtkTreeIter iter;
	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection
			(GTK_TREE_VIEW (set->widget)), NULL, &iter))
	{
		GtkTreePath *path = gtk_tree_model_get_path (set->ex.tree.model, &iter);

		gint response = GTK_RESPONSE_YES;
		gint children = gtk_tree_model_iter_n_children (set->ex.tree.model, &iter);
		if (children > 0)
		{
			gchar *question = g_strdup_printf (
				_("Are you sure that you want to delete this row and <b>%d</b> %s?"),
				children, children == 1 ? _("child") : _("children"));

			GtkWidget *dialog = e2_dialog_create (GTK_STOCK_DIALOG_QUESTION, question,
			_("confirm row delete"), NULL, NULL);
			e2_dialog_set_negative_response (dialog, GTK_RESPONSE_NO);
			response = e2_dialog_show (dialog, config_dialog,
				E2_DIALOG_RUNMODAL | E2_DIALOG_FREE, &E2_BUTTON_NO, &E2_BUTTON_YES, NULL);
			g_free (question);
		}

		if (response == GTK_RESPONSE_YES)
			e2_option_tree_del_direct (set, &iter);

		if (iter.user_data == NULL)
		{
			if (!gtk_tree_path_prev (path))
				if (!gtk_tree_path_up (path))
					path = NULL;
		} else
			path = gtk_tree_model_get_path
				(GTK_TREE_MODEL (set->ex.tree.model), &iter);
		if ((path != NULL) &&
			(gtk_tree_model_get_iter_first (set->ex.tree.model, &iter)))
		{
			gtk_tree_view_set_cursor (GTK_TREE_VIEW (set->widget), path,
				gtk_tree_view_get_column (GTK_TREE_VIEW (set->widget), 0),
				FALSE);
		}
	}
	return TRUE;
}

static gboolean _e2_option_tree_add_child_cb (GtkWidget *wid, E2_OptionSet *set)
{
	GtkTreeIter iter;
	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (set->widget)), NULL, &iter))
	{
		if (iter.user_data != NULL)
		{
			GtkTreeIter iter2;
			_e2_option_tree_add_default (set, &iter2, &iter, FALSE);
		}
	} else
	{
		_e2_option_tree_add_default (set, &iter, NULL, FALSE);
	}

	return TRUE;
}

static gboolean _e2_option_tree_add_below_cb (GtkWidget *wid, E2_OptionSet *set)
{
	GtkTreeIter iter;
	if (gtk_tree_selection_get_selected (gtk_tree_view_get_selection (GTK_TREE_VIEW (set->widget)), NULL, &iter))
	{
		if (iter.user_data != NULL)
		{
			GtkTreeIter iter2;
			_e2_option_tree_add_default (set, &iter2, &iter, TRUE);
		}
	} else
	{
		_e2_option_tree_add_default (set, &iter, NULL, TRUE);
	}

	return TRUE;
}

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

/**
@brief (de)sensitize config dialog buttons that are not relevant to 'category' lines
@param view UNUSED the treeview to which the buttons relate
@param value TRUE to senstitize the buttons, FALSE to desensitize

@return
*/
void e2_option_tree_adjust_buttons (GtkTreeView *view, gboolean value)
{
	E2_Sextet *q = g_object_get_data (G_OBJECT (view), "e2-config-buttons");
	if (q == NULL)
		return;
	if (q->b != NULL)
		gtk_widget_set_sensitive (q->b, value);
	if (q->c != NULL)
		gtk_widget_set_sensitive (q->c, value);
	if (q->d != NULL)
		gtk_widget_set_sensitive (q->d, value);
	if (q->e != NULL)
		gtk_widget_set_sensitive (q->e, value);
}
/**
@brief create, and add to @a box, a config-data treeview for tree-option @a set
This also backs up the current option data
@param parent UNUSED the parent config-dialog widget
@param box the widget into which the treeview and related things will be packed
@param set set data structure

@return
*/
void e2_option_tree_add_widget (GtkWidget *parent, GtkWidget *box,
	E2_OptionSet *set)
{
//	config_dialog = parent;	//keep a static reference
	GtkWidget *vbox = gtk_vbox_new (FALSE, 0);
	gtk_box_pack_start (GTK_BOX (box), vbox, TRUE, TRUE, 0);
	GtkWidget *sw = e2_widget_add_sw (vbox, GTK_POLICY_AUTOMATIC,
		GTK_POLICY_AUTOMATIC, TRUE, E2_PADDING);
	gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (sw), GTK_SHADOW_IN);

	//setup treeview
	GtkTreeView *treeview = GTK_TREE_VIEW (gtk_tree_view_new_with_model
		(set->ex.tree.model));
	gtk_container_add (GTK_CONTAINER (sw), GTK_WIDGET (treeview));
	gtk_tree_view_set_reorderable (treeview, TRUE);
//	gtk_tree_view_set_headers_clickable (treeview, FALSE);  =default
//	gtk_tree_view_set_rules_hint (treeview, FALSE);  =default
#ifdef USE_GTK2_10
	gtk_tree_view_set_enable_tree_lines (treeview, TRUE);
#endif

	set->widget = GTK_WIDGET (treeview);

#ifdef E2_IMAGECACHE
	//setup for appropriate icon size
	gint phigh;
	e2_widget_get_font_pixels (set->widget, NULL, &phigh);
	GtkIconSize iconsizes = e2_utils_get_best_icon_size (phigh + 2);
#endif

	GtkTreeSelection *selection = gtk_tree_view_get_selection (treeview);
	gtk_tree_selection_set_mode (selection, GTK_SELECTION_BROWSE);
//	gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
	g_object_set_data (G_OBJECT (set->ex.tree.model), "e2-option-set", set);
//	if (set->ex.tree.draggable_check_func != NULL)
//	{
//		printd (INFO, "drag func: %d", set->ex.tree.draggable_check_func);
		GtkTreeDragSourceIface *iface = GTK_TREE_DRAG_SOURCE_GET_IFACE (set->ex.tree.model);
//		iface->row_draggable = set->ex.tree.draggable_check_func;
		iface->row_draggable = _e2_option_tree_draggable_check_cb;
//	}
	//we need a way to set the 'dirty-flag' when any row is dragged
	iface->drag_data_delete = _e2_option_tree_drag_delete_cb;

	if (set->ex.tree.selection_check_func != NULL)
		gtk_tree_selection_set_select_function (selection,
			(GtkTreeSelectionFunc) set->ex.tree.selection_check_func,
			set, NULL);

	//add columns
	int j = 0;
	GList *member;
	for (member = set->ex.tree.columns; member != NULL; member = member->next)
	{
		GtkCellRenderer *renderer;
		GtkTreeViewColumn *column;
		E2_OptionTreeColumn *opt = member->data;
		gboolean editable = !(opt->flags & E2_OPTION_TREE_COL_NOT_EDITABLE);
		switch (opt->type)
		{
			case E2_OPTION_TREE_TYPE_BOOL:
				renderer = gtk_cell_renderer_toggle_new ();
				g_object_set (G_OBJECT (renderer), "activatable", TRUE, NULL);
				g_signal_connect (G_OBJECT (renderer), "toggled",
					G_CALLBACK (_e2_option_tree_toggle_cb), set);
				column = gtk_tree_view_column_new_with_attributes
					(opt->label, renderer, "active", j, NULL);
				g_object_set (G_OBJECT (column), "alignment", 0.5, NULL);
				gtk_tree_view_column_set_resizable (GTK_TREE_VIEW_COLUMN (column), TRUE);
				gtk_tree_view_column_set_cell_data_func (column, renderer,
					(GtkTreeCellDataFunc) _e2_option_tree_bool_data_func, opt, NULL);
				gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
				break;
			case E2_OPTION_TREE_TYPE_STR:
			case E2_OPTION_TREE_TYPE_KEY:
				renderer = gtk_cell_renderer_text_new ();
				g_object_set (G_OBJECT (renderer), "editable", editable, NULL);

//				g_signal_connect (G_OBJECT (?), "key-press-event",
//					G_CALLBACK (_e2_option_tree_cell_keypress_cb), renderer);
				g_signal_connect (G_OBJECT (renderer), "editing-started",
					G_CALLBACK (_e2_option_tree_cell_edit_start_cb), NULL);
				if (opt->type == E2_OPTION_TREE_TYPE_KEY)
					g_signal_connect (G_OBJECT (renderer), "editing-started",
						G_CALLBACK (_e2_option_tree_key_edit_start_cb), set);
				g_signal_connect (G_OBJECT (renderer), "editing-canceled",
					G_CALLBACK (_e2_option_tree_cell_edit_stop_cb), NULL);

				g_signal_connect (G_OBJECT (renderer), "edited",
					G_CALLBACK (_e2_option_tree_string_edited_cb), set);

				column = gtk_tree_view_column_new_with_attributes
					(opt->label, renderer, "text", j, NULL);
				gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN (column),
					GTK_TREE_VIEW_COLUMN_GROW_ONLY);
				gtk_tree_view_column_set_resizable (GTK_TREE_VIEW_COLUMN (column), TRUE);
				gtk_tree_view_column_set_cell_data_func (column, renderer,
					(GtkTreeCellDataFunc) _e2_option_tree_str_data_func, opt, NULL);
				gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
				break;
			case E2_OPTION_TREE_TYPE_ICON:
				renderer = gtk_cell_renderer_pixbuf_new ();
				g_object_set (G_OBJECT (renderer), "stock-size", GTK_ICON_SIZE_MENU, NULL);
				column = gtk_tree_view_column_new_with_attributes
					(opt->label, renderer, NULL);
				gtk_tree_view_column_set_cell_data_func (column, renderer,
#ifdef E2_IMAGECACHE
					_e2_option_tree_cell_pixbuf_data_func, GINT_TO_POINTER (iconsizes), NULL);
#else
					_e2_option_tree_cell_pixbuf_data_func, NULL, NULL);
#endif
				gtk_tree_view_column_set_resizable (GTK_TREE_VIEW_COLUMN (column), TRUE);
				gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN (column),
					GTK_TREE_VIEW_COLUMN_GROW_ONLY);
				gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
				break;
			case E2_OPTION_TREE_TYPE_SEL:
				renderer = gtk_cell_renderer_combo_new ();
				GtkTreeModel *mdl = (opt->visible_check_data == NULL) ?
					GTK_TREE_MODEL (actions_store):
					e2_action_filter_store (opt->visible_check_data);
				g_object_set (G_OBJECT (renderer), "model", mdl,
					"text-column", 0, "editable", editable, NULL);

//				g_signal_connect (G_OBJECT (?), "key-press-event",
//					G_CALLBACK (_e2_option_tree_cell_keypress_cb), renderer);
				g_signal_connect (G_OBJECT (renderer), "editing-started",
					G_CALLBACK (_e2_option_tree_cell_edit_start_cb), NULL);
				g_signal_connect (G_OBJECT (renderer), "editing-canceled",
					G_CALLBACK (_e2_option_tree_cell_edit_stop_cb), NULL);

				g_signal_connect (G_OBJECT (renderer), "edited",
					G_CALLBACK (_e2_option_tree_string_edited_cb), set);

//CHECKME is it possible to open the combo at the current spot?
				column = gtk_tree_view_column_new_with_attributes
					(opt->label, renderer, NULL);
				gtk_tree_view_column_set_cell_data_func (column, renderer,
					(GtkTreeCellDataFunc)  _e2_option_tree_str_data_func, opt, NULL);
/* does not work
				gint colwidth;
				gtk_tree_view_column_cell_get_size (column, NULL, NULL, NULL,
					&colwidth, NULL);
				g_object_set (G_OBJECT (column), "min-width", colwidth+20, NULL);
*/
				gtk_tree_view_column_set_resizable (GTK_TREE_VIEW_COLUMN (column), TRUE);
				gtk_tree_view_column_set_sizing (GTK_TREE_VIEW_COLUMN (column),
					GTK_TREE_VIEW_COLUMN_AUTOSIZE);	//GROW_ONLY);
				gtk_tree_view_append_column (GTK_TREE_VIEW (treeview), column);
				break;
			default:
				renderer = NULL;
				column = NULL;  //warning prevention
				printd (WARN, "don't know how to render column %d of %s", j, set->name);
				break;
		}
		if (renderer != NULL)
		{
			g_object_set_data (G_OBJECT (renderer), "column", GINT_TO_POINTER (j));
			g_object_set_data (G_OBJECT (column), "column", GINT_TO_POINTER (j));
		}
		j++;
	}

	//spacer column
	if ((int) set->ex.tree.flags & E2_OPTION_TREE_LAST_COL_EMPTY)
	{
		GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
		GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes
			("", renderer, NULL);
		gtk_tree_view_append_column (treeview, column);
	}

	//now expand everything
	gtk_tree_view_expand_all (treeview);
//	gtk_tree_view_set_hover_expand (treeview, TRUE); //annoying ?

	//add control buttons
	sw = e2_widget_get_sw_plain (GTK_POLICY_AUTOMATIC, GTK_POLICY_NEVER);
	gtk_box_pack_start (GTK_BOX (vbox), sw, FALSE, FALSE, 0);
	GtkWidget *bbox = gtk_hbox_new (FALSE, E2_PADDING);
	e2_widget_sw_add_with_viewport (sw, bbox);
	E2_Sextet *q = (E2_Sextet *) e2_utils_sextet_new ();
	g_object_set_data_full (G_OBJECT (treeview), "e2-config-buttons", q,
		(GDestroyNotify) e2_utils_sextet_destroy);
	if (q != NULL)
	{
		q->a = e2_button_add (bbox, FALSE, 0, _("_Help"), GTK_STOCK_HELP,
			_("Get help on this option"),  _e2_option_tree_help_cb, set);
	//insert extra button on specific pages
		if (e2_option_get_simple ("plugins") == set)
			e2_button_add (bbox, FALSE, 0, _("_Select"), GTK_STOCK_OPEN,
			_("Show plugin-selection dialog"), e2_confdlg_choose_plugin_cb, set);
#ifdef E2_RAINBOW
		else if (e2_option_get_simple ("filetypes") == set)
			e2_button_add (bbox, FALSE, 0, _("Co_lor"), GTK_STOCK_SELECT_COLOR,
			_("Show color-selection dialog"), e2_confdlg_extcolorpick_cb, set);
#endif
		if (set->ex.tree.flags & E2_OPTION_TREE_UP_DOWN)
		{
			q->b = e2_button_add (bbox, FALSE, 0, _("_Up"), GTK_STOCK_GO_UP,
				_("Move the selected row one place up"),
				_e2_option_tree_move_up_cb, set);
			q->c = e2_button_add (bbox, FALSE, 0, _("_Down"), GTK_STOCK_GO_DOWN,
				_("Move the selected row one place down"),
				_e2_option_tree_move_down_cb, set);
		}
		if (set->ex.tree.flags & E2_OPTION_TREE_ADD_DEL)
		{
			q->d = e2_button_add (bbox, FALSE, 0, _("_Remove"), GTK_STOCK_REMOVE,
				_("Remove the selected row"),  e2_option_tree_del_direct_cb, set);
			q->e = e2_button_add (bbox, FALSE, 0, _("Add a_fter"), GTK_STOCK_ADD,
				_("Add a row after the currently selected row in the tree"),
				_e2_option_tree_add_below_cb, set);
			if (! (set->ex.tree.flags & E2_OPTION_TREE_LIST))
				q->f = e2_button_add (bbox, FALSE, 0, _("Add ch_ild"), GTK_STOCK_INDENT,
					_("Add a child row to the currently selected one"),
					_e2_option_tree_add_child_cb, set);
		}
	}
	//add context-menu
	E2_TreeContextMenuFlags context_flags = E2_TREE_CONTEXT_DEFAULT;
	if (!((gboolean) set->ex.tree.flags & E2_OPTION_TREE_LIST))
		context_flags |= E2_TREE_CONTEXT_EXP_COL;
	GtkWidget *context_menu = gtk_menu_new ();
	_e2_option_tree_menu_items_add (treeview, context_menu, context_flags, set);

		//connect callbacks
	//no longer needed it seems
//	g_signal_connect (treeview, "drag-drop", G_CALLBACK (drop_cb), set);
//	g_signal_connect (treeview, "drag-end", G_CALLBACK (drag_end_cb), set);

	//arrange for data backup if/when the page is opened
	g_signal_connect (treeview, "realize",
		G_CALLBACK (_e2_option_tree_page_opened_cb), set);
	g_signal_connect (treeview, "popup_menu",
		G_CALLBACK (_e2_option_tree_popup_menu_cb), context_menu);
/* the dialog's "negative response" handler takes care of this
	g_signal_connect_after (treeview, "key-press-event",
		G_CALLBACK (e2_confdlg_key_press_cb), NULL); */
	//this one handles left-button clicks
	g_signal_connect (treeview, "button-press-event",
		G_CALLBACK (_e2_option_tree_button_press_cb), set);
	//this one handles right-button clicks
	g_signal_connect (treeview, "button-press-event",
		G_CALLBACK (_e2_option_tree_button_press_cb2), context_menu);

	//goto first row & column
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (set->ex.tree.model), &iter))
	{
		GtkTreePath *path = gtk_tree_model_get_path (GTK_TREE_MODEL
			(set->ex.tree.model), &iter);
		gtk_tree_view_set_cursor (treeview, path,
			gtk_tree_view_get_column (treeview, 0), FALSE);
		gtk_tree_path_free (path);
	}
}
/**
@brief begin registration of a tree option
Complete registraion involves this func, then e2_option_tree_add_column()
for each column in the treestore, tnen e2_option_tree_create_store() to
finish things
@param name name of the option, generally a constant string but sometimes runtime-created
@param group group the option belongs to, used in config dialog, can be a r-t string FREEME or a _()
@param desc textual description of the option used in config dialog, a r-t _() string
@param depends name of another option this one depends on
@param selection_check_func function to check whether row is selectable
@param draggable_check_func function to check whether selected row is draggable
@param flags flags set when registering a tree option
@param flags2 bitflags determining how the option data is to be handled

@return E2_OptionSet data struct for the option
*/
E2_OptionSet *e2_option_tree_register (gchar *name, gchar *group, gchar *desc,
	gchar *depends, gpointer selection_check_func, gpointer draggable_check_func,
	E2_OptionTreeTypeFlags flags, E2_OptionFlags flags2)
{
	E2_OptionSet *set = e2_option_register (E2_OPTION_TYPE_TREE, name, group,
		desc, NULL, depends, flags2);
	set->ival = -1;
	set->sval = NULL;
	set->ex.tree.synced = FALSE;
	set->ex.tree.def = NULL;
//	set->ex.tree.columns_num = 0;	//set when all cols processed
	set->ex.tree.columns = NULL;
	set->ex.tree.flags = flags;
//	set->ex.tree.unknown = NULL;
	set->ex.tree.selection_check_func = selection_check_func;
	set->ex.tree.draggable_check_func = draggable_check_func;
	return set;
}
/**
@brief setup data for a column of a tree-set
This establishes some parameters for regular use, and others for
config-dialog use.
@param set pointer to data struct for the tree-set
@param name column label string
@param type indicator of the type of data in the column
@param idef default value if the column has a bool value
@param sdef default value if the column has a non-bool value
@param flags flags relating to data editability, cleanup
@param visible_check_func function to check whether a cell in the column is displayed in a config dialog
@param visible_check_data data to provide to @a visible_check_func

@return
*/
void e2_option_tree_add_column (E2_OptionSet *set, const gchar *name,
	E2_OptionTreeType type, gint idef, const gchar *sdef,
	E2_OptionTreeColFlags flags,
	gpointer visible_check_func, gpointer visible_check_data)
{
	E2_OptionTreeColumn *col = ALLOCATE (E2_OptionTreeColumn); //FIXME no deallocation
	CHECKALLOCATEDFATAL (col);
	set->ex.tree.columns = g_list_append (set->ex.tree.columns, col);
	col->label = name;
	col->type = type;
	col->idef = idef;
	col->sdef = sdef;
	col->flags = flags;
	col->visible_check_func = visible_check_func;
	col->visible_check_data = visible_check_data;
}
/**
@brief create a treestore for the data of a tree-set
This works from listed columns' data at set->ex.tree.columns
@param set pointer to data struct for the tree-set

@return
*/
void e2_option_tree_create_store (E2_OptionSet *set)
{
	set->ex.tree.columns_num = g_list_length (set->ex.tree.columns);
	GType t[set->ex.tree.columns_num + 1];	//space for registered columns + 1
	//the extra trailing column is for visibility flag
	t[set->ex.tree.columns_num] = G_TYPE_BOOLEAN;

	GList *member;
	gint i;
	for (member = set->ex.tree.columns, i = 0; member != NULL; member = member->next, i++)
	{
		E2_OptionTreeColumn *tcol = member->data;
		switch (tcol->type)
		{
			case E2_OPTION_TREE_TYPE_BOOL:
				t[i] = G_TYPE_INT;
				break;
			case E2_OPTION_TREE_TYPE_STR:
			case E2_OPTION_TREE_TYPE_ICON:
			case E2_OPTION_TREE_TYPE_SEL:
			case E2_OPTION_TREE_TYPE_KEY:
				t[i] = G_TYPE_STRING;
				break;
			default:
				printd (ERROR,
					"internal error, tree options may only have bool or string values");
				break;
		}
	}

	set->ex.tree.model = gtk_tree_store_new (1, t[0]);
	gtk_tree_store_set_column_types (set->ex.tree.model, set->ex.tree.columns_num + 1, t);
}
/**
@brief create a string representing a tree row

This is a helper fn for backup and config file writing
Depth is represented by leading tabs
Columns are separated by '|'
Any other embedded '|', or an initial '>', is escaped with '\'
c.f. e2_tree_row_to_string() which does not store bools as "true" or "false"

@param set set data structure
@param iter treeiter used for scanning the treemodel
@param level tree path-depth of the processed line

@return  the constructed string
*/
gchar *e2_option_tree_row_write_to_string (E2_OptionSet *set,
	GtkTreeIter *iter, gint level)
{
	GString *treerow = g_string_sized_new (128);
	gint j;
	for (j = 0; j < level; j++)
		treerow = g_string_append_c (treerow, '\t');
	GList *column;
	j = 0;
	for (column = set->ex.tree.columns; column != NULL; column = g_list_next (column))
	{
		E2_OptionTreeColumn *opt = column->data;
		switch (opt->type)
		{
			case E2_OPTION_TREE_TYPE_BOOL:
			{
				gboolean int_data;
				gtk_tree_model_get (set->ex.tree.model, iter, j, &int_data, -1);
				if (int_data)
					treerow = g_string_append (treerow, "true");
				else
					treerow = g_string_append (treerow, "false");
			}
			break;
			case E2_OPTION_TREE_TYPE_STR:
			case E2_OPTION_TREE_TYPE_ICON:
			case E2_OPTION_TREE_TYPE_SEL:
			case E2_OPTION_TREE_TYPE_KEY:
			{
				gchar *str_data;
				gtk_tree_model_get (set->ex.tree.model, iter,  j, &str_data, -1);

				if (str_data != NULL)
				{
					if (j == 0 && str_data[0] == '>')
						treerow = g_string_append (treerow, "\\");
					if ((str_data[0] != '\0') && ((strchr (str_data, '|')) != NULL))	//if always ascii |, don't need g_utf8_strchr()
					{
						gchar **split = g_strsplit (str_data, "|", -1);
						g_free (str_data);
						str_data = g_strjoinv ("\\|", split);
						g_strfreev (split);
					}
					treerow = g_string_append (treerow, str_data);
					g_free (str_data);
				}
			}
			break;
			default:
			break;
		}
		treerow = g_string_append_c (treerow, '|');
		j++;
	}
	//get rid of superfluous trailing '|'
	treerow = g_string_truncate (treerow, treerow->len -1);

	gchar *result = g_string_free (treerow, FALSE);
	return result;
}
/**
@brief write a string-representation of tree set @a set to @a file

This is a helper fn for wrting config file data
There is no error checking
It is recursive, to deal with descendants

@param f pointer to file descriptor
@param set the set data structure
@param iter treeiter used for scanning the treemodel
@param level the current path depth in the tree

@return
*/
void e2_option_tree_write_to_file (E2_FILE *f, E2_OptionSet *set,
	GtkTreeIter *iter, gint level)
{
	do
	{
		gchar *rowstring = e2_option_tree_row_write_to_string (set, iter, level);
		gchar *writestring = g_strconcat (rowstring, "\n", NULL);
//		if (
		e2_fs_put_stream (f, writestring, "config" E2_ERR_NONE());
//		== EOF)
//		{ FIXME handle error }
		g_free (rowstring);
		g_free (writestring);

		GtkTreeIter iter2;
		if (gtk_tree_model_iter_children (set->ex.tree.model, &iter2, iter))
			e2_option_tree_write_to_file (f, set, &iter2, level + 1);

	} while (gtk_tree_model_iter_next (set->ex.tree.model, iter));
}
/**
@brief create a string array of option-tree row data for a specified optionset

This is a helper fn for backup
It is recursive, to deal with descendants

@param lines ptr-array where the strings are recorded
@param set the set data structure
@param iter treeiter used for scanning the treemodel
@param level the current path depth in the tree

@return
*/
static void e2_option_tree_write_to_storage (GPtrArray *lines, E2_OptionSet *set,
	GtkTreeIter *iter, gint level)
{
	do
	{
		gchar *rowstring = e2_option_tree_row_write_to_string (set, iter, level);
		g_ptr_array_add (lines, (gpointer) rowstring);

		GtkTreeIter iter2;
		if (gtk_tree_model_iter_children (set->ex.tree.model, &iter2, iter))
			e2_option_tree_write_to_storage (lines, set, &iter2, level + 1);

	} while (gtk_tree_model_iter_next (set->ex.tree.model, iter));
}
/**
@brief create backup data for a specified treeoption set

Data are stored as strings, one for each tree row, in the same format
as used for the config file
Each row is referred to in a NULL-terminated pointer array
This fn does nothing if there is already backup data for the set

@param set the set data structure

@return
*/
void e2_option_tree_backup (E2_OptionSet *set)
{
	if (set->ex.tree.def == NULL)
	{	//backup data does not exist now, so ok to create it
		GPtrArray *lines = g_ptr_array_sized_new (21);
		g_ptr_array_add (lines, (gpointer) g_strdup_printf ("%s=<", set->name));
		GtkTreeIter iter;
		if (gtk_tree_model_get_iter_first (set->ex.tree.model, &iter))
			e2_option_tree_write_to_storage (lines, set, &iter, 0);
		g_ptr_array_add (lines, (gpointer) g_strdup (">"));
//		set->ex.tree.def_num = (gint) lines->len;
		g_ptr_array_add (lines, NULL);
		set->ex.tree.def = lines->pdata;
		g_ptr_array_free (lines, FALSE);
	}
}
/**
@brief revert tree option backup data

@param set pointer to set data structure
@param empty_check TRUE to check for backup data only if loaded config data is missing

@return
*/
static void _e2_option_tree_revert (E2_OptionSet *set, gboolean empty_check)
{
	if ((!set->ex.tree.synced || !empty_check) && set->ex.tree.def != NULL) //CHECKME this test
	{
		g_object_unref (set->ex.tree.model);
		e2_option_tree_create_store (set);
		//option needs default configuration which is stored in
		//array at ex.tree.def of the OptionSet
		e2_option_tree_set_from_array (set->name,
			(gchar **) set->ex.tree.def, NULL, NULL);
		//make sure the set is recognised as clean
		set->ex.tree.flags &= ~E2_OPTION_TREE_SET_EDITED;
	}
}
/**
@brief revert tree option backup data

@param option_name the name of the set

@return
*/
void e2_option_tree_revert (gchar *option_name)
{
	E2_OptionSet *set = e2_option_tree_get (option_name);
	_e2_option_tree_revert (set, FALSE);
}
/**
@brief optionally revert, then clean, tree option backup data

@param set the set data structure
@param revert TRUE to restore the backup data to the tree, FALSE to cleanup only

@return
*/
void e2_option_tree_unbackup (E2_OptionSet *set, gboolean revert)
{
	if (set->ex.tree.def != NULL)
	{	//backup data exists
		if (revert)
			_e2_option_tree_revert (set, FALSE);
		//cleanup
		g_strfreev ((gchar **)set->ex.tree.def);
		set->ex.tree.def = NULL;
	}
}
/**
@brief iterate over all option trees, reverting any with backup data

@return
*/
void e2_option_tree_restore_all (void)
{
	guint i;
	gpointer *walker;
	E2_OptionSet *set;
	for (i = 0, walker = options_array->pdata; i < options_array->len; i++, walker++)
	{
		set = *walker;
		if (set->type == E2_OPTION_TYPE_TREE)
			e2_option_tree_unbackup (set, TRUE);
	}
}
/**
@brief log a function to be called to setup tree-option defaults if needed

@a func needs to be of the form void (*func) (E2_Optionset *) and to call
e2_option_tree_setup_defaults ()

@param set the set data structure
@param func pointer to function which will install default tree data

@return
*/
void e2_option_tree_prepare_defaults (E2_OptionSet *set, gpointer func)
{
	set->ex.tree.def = func;
}
/**
@brief setup default config tree contents, ready for installation

This creates a NULL-terminated vector of tree-row data strings, which
in aggregate (are expected to) have the same format as the config file
data for the set being processed.

@param set the set data structure
@param first start of a null-terminated series of arguments,
 each a newly-allocated string which is to be converted to a tree row

@return
*/
void e2_option_tree_setup_defaults (E2_OptionSet *set, gchar *first, ...)
{
	if (first == NULL) return;
//	printd (DEBUG, "preparing default option tree for %s>", first);
	va_list args;
	va_start (args, first);
	gchar *cur = first;
	GPtrArray *lines = g_ptr_array_sized_new (32);
	while (cur)
	{
//		cur = g_strdup (cur);
		//store ptr to constant string (do not free, later)
		g_ptr_array_add (lines, (gpointer) cur);
		cur = va_arg (args, gchar *);
	}
	va_end (args);
//	set->ex.tree.def_num = (gint) lines->len; UNUSED
	g_ptr_array_add (lines, NULL);  //null signals end, to parser
	set->ex.tree.def = lines->pdata;
	g_ptr_array_free (lines, FALSE);
}
/**
@brief install default config tree data where needed

after loading the config file (if any) at session start, check if any
tree option needs a default config, (stored in arrays)
and install it if so
then free the default tree 'branches'

@return
*/
void e2_option_tree_install_defaults (void)
{
	guint i;
	gpointer *walker;
	E2_OptionSet *set;
	for (i = 0, walker = options_array->pdata; i < options_array->len; i++, walker++)
	{
		set = *walker;
		if (set->type == E2_OPTION_TYPE_TREE)
		{
			if (!set->ex.tree.synced //option needs default data
				&& set->ex.tree.def != NULL)	//setup func has been logged
			{
//				printd (DEBUG, "installing default option tree %s>",
//				VPSTR(set->ex.tree.def));
				void (*install_func) (E2_OptionSet *) = set->ex.tree.def;
				//call the defaults-install function, which in turn calls
				// e2_option_tree_setup_defaults () which sets up a
				// pointer array at set->ex.tree.def again
				(*install_func) (set);
				//parse the data
				e2_option_tree_set_from_array (set->name,
					(gchar **) set->ex.tree.def, NULL, NULL);
				//cleanup
				g_strfreev ((gchar **) set->ex.tree.def);
			}
			set->ex.tree.def = NULL;	//always zap the function/vector pointer
		}
	}
}
/* *
@brief Append top-level tree iter for @a name, and populate if from @a options

@param name name of option
@param iter pointer to iter which stores the result
@param n_options number of treestore columns
@param options array of values to store in the new iter

@return
*/
/* UNUSED
void e2_option_tree_add_simple (gchar *name, GtkTreeIter *iter, gint n_options,
	void *options[])
{
	E2_OptionSet *set = e2_option_tree_get (name);
	if (set != NULL)
		e2_option_tree_add (set, iter, NULL, FALSE, TRUE, n_options, options);
} */
/**
@brief Add tree iter to @a set, and populate if from @a options
Any NULL value in @a options is ignored, its stored value is undefined
@param set data struct for the set
@param iter pointer to iter which stores the result
@param parent pointer the iter which is the "base" for relative insertion
@param sibling TRUE to add new iter at same level as @a parent, FALSE to add a child
@param before TRUE to add iter before @a parent
@param n_options number of treestore columns
@param options array of values to store in the new iter

@return
*/
void e2_option_tree_add (E2_OptionSet *set, GtkTreeIter *iter, GtkTreeIter *parent,
	gboolean sibling, gboolean before, gint n_options, void *options[])
{
	if (sibling)
	{	//new iter at same level
		if (before)
			//before parent, or appended to toplevel if parent == NULL
			gtk_tree_store_insert_before (set->ex.tree.model, iter, NULL, parent);
		else
			//after parent, or prepended to toplevel if parent == NULL
			gtk_tree_store_insert_after (set->ex.tree.model, iter, NULL, parent);
	}
	else
	{	//new child iter
		if (before)
			//append to parent's children
			gtk_tree_store_insert_before (set->ex.tree.model, iter, parent, NULL);
		else
			//prepend to parent's children
			gtk_tree_store_insert_after (set->ex.tree.model, iter, parent, NULL);
	}

	gint i;
	for (i = 0; i < n_options; i++)
	{
		if (options[i] != NULL)
			gtk_tree_store_set (set->ex.tree.model, iter, i, options[i], -1);
	}
	gtk_tree_store_set (set->ex.tree.model, iter, set->ex.tree.columns_num, TRUE, -1);

	e2_option_tree_flag_change (set);
}

void e2_option_tree_add_default (gchar *option, GtkTreeIter *iter, GtkTreeIter *parent, gboolean sibling)
{
	E2_OptionSet *set = e2_option_tree_get (option);
	if (set != NULL)
	_e2_option_tree_add_default (set, iter, parent, sibling);
}

static void _e2_option_tree_add_default (E2_OptionSet *set, GtkTreeIter *iter, GtkTreeIter *parent, gboolean sibling)
{
	void *arg[set->ex.tree.columns_num];
	GList *column;
	gint i = 0;
	for (column = set->ex.tree.columns; column != NULL; column = g_list_next (column))
	{
		E2_OptionTreeColumn *opt = column->data;
		switch (opt->type)
		{
			case E2_OPTION_TREE_TYPE_BOOL:
				arg[i] = GINT_TO_POINTER (opt->idef);
				break;
			case E2_OPTION_TREE_TYPE_SEL:
			case E2_OPTION_TREE_TYPE_STR:
			case E2_OPTION_TREE_TYPE_ICON:
			case E2_OPTION_TREE_TYPE_KEY:
				arg[i] = (void *)opt->sdef;
				break;
			default:
				break;
		}
		i++;
	}

	e2_option_tree_add (set, iter, parent, sibling, FALSE, set->ex.tree.columns_num, arg);

	if ((parent != NULL) && (sibling == FALSE))
		gtk_tree_view_expand_all (GTK_TREE_VIEW (set->widget));
	gtk_tree_view_set_cursor  (GTK_TREE_VIEW (set->widget),
		gtk_tree_model_get_path (GTK_TREE_MODEL (set->ex.tree.model), iter),
		gtk_tree_view_get_column (GTK_TREE_VIEW (set->widget), 0), FALSE);

	if (set->widget)
	{
		gtk_widget_grab_focus (set->widget);
	}
}

void e2_option_tree_del (gchar *option, GtkTreeIter *iter)
{
	E2_OptionSet *set = e2_option_tree_get (option);
	if (set != NULL)
		e2_option_tree_del_direct (set, iter);
}

void e2_option_tree_del_direct (E2_OptionSet *set, GtkTreeIter *iter)
{
	gtk_tree_store_remove (set->ex.tree.model, iter);
	e2_option_tree_flag_change (set);
}

static void _e2_option_tree_move (E2_OptionSet *set, GtkTreeIter *iter, GtkTreePath *path_dest, gboolean sibling, gboolean before)
{
	//find out real number of columns, we also want to move the meta data
	gint n = gtk_tree_model_get_n_columns (set->ex.tree.model);
	gpointer values[n];
	gint i;
	//save old row contents
	for (i = 0; i < n; i++)
	{
		gtk_tree_model_get (set->ex.tree.model, iter, i, values + i, -1);
	}
	GtkTreePath *path_old = gtk_tree_model_get_path (set->ex.tree.model, iter);
	GtkTreeRowReference *ref_old = gtk_tree_row_reference_new (set->ex.tree.model, path_old);
	GtkTreeIter iter2;
	if (gtk_tree_model_get_iter (set->ex.tree.model, &iter2, path_dest))
	{
		GtkTreeIter iter_new;
		e2_option_tree_add (set, &iter_new, &iter2, sibling, before, n, values);

		gtk_tree_path_free (path_old);
		path_old = gtk_tree_row_reference_get_path (ref_old);
		if (gtk_tree_model_get_iter (set->ex.tree.model, &iter2, path_old))
		{
			if (gtk_tree_model_iter_has_child (set->ex.tree.model, &iter2))
			{
				gboolean expand = FALSE;
				if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (set->widget), path_old))
				{
					expand = TRUE;
					gtk_tree_view_collapse_row (GTK_TREE_VIEW (set->widget), path_old);
				}

				GtkTreePath *path_new = gtk_tree_model_get_path (set->ex.tree.model, &iter_new);
				GtkTreeIter iter_child;
				while (gtk_tree_model_iter_children (set->ex.tree.model, &iter_child, &iter2))
					_e2_option_tree_move (set, &iter_child, path_new, FALSE, TRUE);

				if (expand)
					gtk_tree_view_expand_row (GTK_TREE_VIEW (set->widget), path_new, FALSE);
				gtk_tree_path_free (path_new);
			}
			e2_option_tree_del_direct (set, &iter2);
		}

		gtk_tree_view_set_cursor  (GTK_TREE_VIEW (set->widget),
			path_dest, gtk_tree_view_get_column (GTK_TREE_VIEW (set->widget), 0), FALSE);
	}
	gtk_tree_path_free (path_old);
	gtk_tree_row_reference_free (ref_old);
}

static void _e2_option_tree_set (E2_OptionSet *set, GtkTreeIter *iter, gint col, void *data)
{
	gtk_tree_store_set (GTK_TREE_STORE (set->ex.tree.model), iter, col, data, -1);
	e2_option_tree_flag_change (set);
}
/**
@brief Read tree config data from @a f and setup treestore and model
If @a index is non-NULL, the value initially stored there index is the index
of the start of the set data, or of the data corresponding to @a root_iter
if that is not at the start of the set. The fist line is skipped (unless
we need to log the lot as an unknown set)
Critical characters in @a f are assumed to be ascii
Unknown options are logged as appropriate
If @a root_iter is NULL, the root node is used, of course.
This does not cope with badly-formed trees e.g. with missing level

@param name set name string
@param f NULL-terminated array of strings in config file format
@param index pointer to store for index of @a f, or NULL
@param root_iter pointer to treestore iter under which the data are to be added

@return TRUE if the option was installed properly
*/
gboolean e2_option_tree_set_from_array (gchar *name, gchar *f[], gint *index,
	GtkTreeIter *root_iter)
{
	gchar *line; //pointer to the current line
	gint idx = (index == NULL) ? 0 : *index;	//array index

	E2_OptionSet *set = e2_option_get_simple (name);
	if (set == NULL)
	{	//we've found an unknown tree option
//		printd (DEBUG, "found config tree for unknown option %s", name);
		//accumulate the lines
		GPtrArray *unknown_lines = g_ptr_array_sized_new (11);
		if (strstr (f[idx], "=<") == NULL)
		{	//incomplete tree, from transient store-updates
			line = g_strconcat (name, "-part=<", NULL);
			g_ptr_array_add (unknown_lines, line);
		}
		while ((line = f[idx]) != NULL)
		{
			idx++;	//now we know array's not finished
			g_strchomp (line);
			g_ptr_array_add (unknown_lines, line);
			if (line[0] == '>')
			{
				g_ptr_array_add (unknown_lines, NULL);
				gchar *value = g_strjoinv ("\n", (gchar **)unknown_lines->pdata);
				e2_option_unknown_record (g_strdup (name), value);
				break;
			}
		}
		g_ptr_array_free (unknown_lines, TRUE);
		if (index != NULL)
			*index = idx;
		return FALSE;
	}

	//process known tree option
	idx++;	//pass the "name=<" line
	//flag for whether the tree data is still valid
	E2_TreeStatus tree_mode = E2_TREE_STARTED;
	//list of iters for managing parent-child dependencies
	GList *parents = g_list_append (NULL, root_iter);
	GList *member;
	gboolean firstline = TRUE;
	gint firstdepth = 0;

	while ((line = f[idx]) != NULL)
	{
		idx++;	//now we know array's not finished
		g_strchomp (line);
		if (line[0] == '>') break;
		//ignore empty lines and comments
		if (*line == '\0') continue;
		if (line[0] == '#') continue;
		if (tree_mode == E2_TREE_ABORTED) continue;
		//path-depth of the current tree option row
		//determined by tabs at the start of the line
		gint level = 0;
		while (line[level] == '\t')
			level++;
		line += level;
		if (firstline)
		{
			//1st valid line corresponds to 1st child of root_iter,
			//not necessarily at depth 0 of the tree
			firstdepth = level;
			firstline = FALSE;
		}
		//"unescape" the first character if need be
		if (line[0] == '\\' && line[1] == '>')
			line++;

		//strsplit() isn't a great approach because it makes
		//unescaping complicated, but ...
		gchar **split = g_strsplit (line, "|", -1);
		void *tmp[set->ex.tree.columns_num];
		//split counter, increased when stepping through **split
		gint i = 0;
		//tree column counter
		gint j = 0;
		//tree column pointer
		GList *columns;
		//unescape helper for concatting, later on
		GList *freecats = NULL;
		//transform splitted values to void pointers and
		//add default values in case of a missing value/field
		for (columns = set->ex.tree.columns; columns != NULL; columns = columns->next)
		{
			//not enough separators in the line = bad config data
			if (split[i] == NULL)
			{
				tree_mode = E2_TREE_ABORTED;
				break;
			}
			//current tree column-data pointer
			E2_OptionTreeColumn *coldata = columns->data;
			if ( *(split[i]) != '\0')
			{
				gchar *value = split[i];
				//unescape | and concat values
				while (split[i + 1] != NULL)
				{
					gint len = strlen (value);
					if ((len > 0) && (value[len - 1] == '\\'))
					{
						value[len - 1] = '\0';
						value = g_strconcat (value, "|", split[i + 1], NULL);
						//save pointer to free it after
						//knowing how long this really gets
						//(there could be several escaped | in one line)
						freecats = g_list_append (freecats, value);
						i++;
					} else
						break;
				}
				//get void pointer to add it to the model
				tmp[j] = _e2_option_tree_get_void_simple (coldata, value);
			}
			else
				//empty string found found, use default value
				tmp[j] = _e2_option_tree_get_void_simple (coldata, (void *)coldata->sdef);
			j++;
			i++;
		}

		if (tree_mode != E2_TREE_ABORTED)
		{
			//append row to children of parent, and populate it
			GtkTreeIter iter;
			i = level - firstdepth;
			e2_option_tree_add (set, &iter, g_list_nth_data (parents, i),
				FALSE, TRUE, set->ex.tree.columns_num, tmp);
			//add iter pointer to be able to add a child to this row later on
			GList *member = g_list_nth (parents, i+1);
			if (member == NULL)
				parents = g_list_append (parents, gtk_tree_iter_copy (&iter));
			else
			{
				gtk_tree_iter_free (member->data);
				member->data = gtk_tree_iter_copy (&iter);
			}
		}
		else
		{
			//clear any data recorded already
			gtk_tree_store_clear (set->ex.tree.model);
//			set->ex.tree.synced = FALSE; redundant
			printd (WARN, "bad config data for %s, using default", set->name);
		}

		//cleanup
		g_strfreev (split);
		for (member = freecats; member != NULL; member = member->next)
			g_free (member->data);
		g_list_free (freecats);
	}

	if (line != NULL && tree_mode != E2_TREE_ABORTED)
	{
		//found config for this tree option, so don't init it
		//with the default config later on
		set->ex.tree.synced = TRUE;
//		set->ex.tree.def = NULL;
		//make sure the set is recognised as clean
		set->ex.tree.flags &= ~E2_OPTION_TREE_SET_EDITED;
	}
	//cleanup
//	parents = g_list_remove (parents, NULL);
//	g_list_foreach (parents, (GFunc) gtk_tree_iter_free, NULL);
	for (member = parents->next; member != NULL; member = member->next)
		gtk_tree_iter_free (member->data);
	g_list_free (parents);

	if (index != NULL)
		*index = idx;
	return (tree_mode != E2_TREE_ABORTED);
}

E2_OptionSet *e2_option_tree_get (gchar *option)
{
	E2_OptionSet *set;
	set = (E2_OptionSet *) g_hash_table_lookup (options_hash, option);
	if (set == NULL)
	{
		printd (WARN, "trying to access option '%s' which doesn't exist", option);
		return NULL;
	}
	if (set->type != E2_OPTION_TYPE_TREE)
	{
		printd (WARN, "trying to access tree option '%s' which isn't a tree", set->name);
		return NULL;
	}
	return set;
}

void *_e2_option_tree_get_void_simple (E2_OptionTreeColumn *col, gchar *option)
{
	switch (col->type)
	{
		case E2_OPTION_TREE_TYPE_BOOL:
			if (g_str_equal (option, "true"))
				return (void *) TRUE;
			else
				return (void *) FALSE;
			break;
		case E2_OPTION_TREE_TYPE_STR:
		case E2_OPTION_TREE_TYPE_ICON:
		case E2_OPTION_TREE_TYPE_SEL:
		case E2_OPTION_TREE_TYPE_KEY:
			return (void *) option;
		default:
			return NULL;
			break;
	}
	return NULL;
}
