/* $Id: e2_keybinding.c 870 2008-05-06 05:52:35Z tpgww $

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

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

emelfm2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

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

/**
@file src/e2_keybinding.c
@brief functions for handling key bindings
*/

/**
\page bindings key bindings

The primary data for key-bindings are stored in a treestore established
at session-start. Its hierarchy currently provides for 2 levels of "category"
(more levels can be defined at compile-time). One or more categories can be
bound to any widget. Any level can be bound to the widget. Any key can be
assigned to any level. Keys assigned to a category apply to all widgets in
that category, and to all widgets in any descendant category. Keys assiged
to a lowest-level category apply to widgets to which that category only are
bound.

After the last level of categories in any tree branch, the next level of the
hierarchy has key names and related data. Any such key may have child(ren),
grand-child(ren) etc. Descendant-keys are treated as chained, i.e key-sequences
can be created. Any key in a sequence, or more than one of them (i.e. not
just the last one) can have its own action.

Categories can be added to the keys treestore at any time. If E2_TREEINCREMENT
is defined, they can be added from strings like "1.2.3", without any keys.
A category must, and its keys may, be added to the treestore before the
category is bound to any widget. When a category is first bound to any widget,
the keys in the category (if any) are prepared for use - see below. Keys can
be added to a category later, provided they are specifically converted to
runtime format.

Treestore data are converted to a list (keys) for ready access when used.
Each entry in keys corresponds to a binding category. Among other things,
the entry has a list of bound widgets, and a list of key-data structs.
The latter each has specific key data and the bound command.  A category
can be "registered" (converted to runtime list) at any time, and any prior
rt data for the category will then be destroyed.

Category data for "core" widgets are destroyed and rebuilt as appropriate
during a config dialog. As of now, there are no "transient" bindings. They
probably don't need to be rebuilt, anyhow.

Category names must be set at compile time, as the relevant widgets need
to know the names to which to bind. In theory at least, category names could
be made configurable. If so, it would be reasonable to also allow creation
and deletion of categories in a config dialog.

Binding a category to a widget means that a key-press on the widget will
callback to scan the keys in the bound group, initiate or continue a
chaining process if appropriate, or otherwise act on the key if matched.

Look for code tagged with #ifdef E2_TRANSIENTKEYS, and in particular a
dummy set of bindings and their initialization and cancellation, in
e2_view_dialog.c
Keypresses may be 'aliased' by binding them to the action 'key.alias'. The
argument for that action must be a string containing one or more key-names
in a form gtk can parse, like <Ctrl><Alt>m, or just a letter. If more than
one key is in the replacement string, they must be separated by a " " char
(the name of a substituted space is <Space>)
*/
/**
\page bindings_ui key bindings interface

ToDo - describe how this works
*/

#include "e2_keybinding.h"
#include "e2_option_tree.h"
#include <string.h>
//enable data conversion at idle-time, instead of when a binding is registered
//don't do that, it interferes with config dialog edit-cancellation
#define E2_IDLE_KEYSYNC

//keybindings is a list of E2_KeybindingRuntime structs, each for a 'category' of bindings
GList *keybindings = NULL;
#ifdef E2_IDLE_KEYSYNC
//keys is a pointer for use with the keybindings GList
//GList *binding_cur = NULL;
#endif

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

/**
@brief cleanup bound-keys data in list @a keys
Recurses to clean chained keys, if any
@param keys pointer to a bound-keys list

@return
*/
static void _e2_keybinding_free_keyslist (GList *usedkeys)
{
	GList *list;
	for (list = usedkeys; list != NULL; list = list->next)
	{
		E2_KeyRuntime *k = list->data;
		if (k != NULL)
		{
			if (k->chained != NULL)
				_e2_keybinding_free_keyslist (k->chained);
			//strings can't be NULL
			g_free (k->action);
			g_free (k->action_data);
			DEALLOCATE (E2_KeyRuntime, k);
		}
	}
	g_list_free (usedkeys);
}
/**
@brief cleanup data for the bound keys for the category associated with @a rt
@param rt pointer to keybinding category data struct

@return
*/
static void _e2_keybinding_free_keys (E2_KeybindingRuntime *rt)
{
	if (rt->keys != NULL)
	{
		_e2_keybinding_free_keyslist (rt->keys);
		rt->keys = NULL;
	}
}
/**
@brief recursively translate a keybinding treestore branch into a listed struct
Some cute stuff is done to conform keys-cell-renderer behaviour with gtk's
@param model model for the bindings treestore
@param iter pointer to iter to be used to interrogage @a model
@param keys pointer to list to which the created struct(s) will be appended

@return
*/
static void _e2_keybinding_sync_one (GtkTreeModel *model, GtkTreeIter *iter, GList **keys)
{
	gchar *cat, *key, *action, *action_data;
	gboolean cont;
	gtk_tree_model_get (model, iter, 0, &cat, 1, &key, 2, &cont, 3 , &action,
		4, &action_data, -1);
	gboolean valid = (*cat == '\0'//for a key row, the category should be empty
					&& *key != '\0');
	if (valid)
	{
		E2_Action *act = e2_action_check (action);
		valid = (act == NULL || !(act->exclude & E2_ACTION_EXCLUDE_ACCEL));
	}
	if (!valid)
	{
		g_free (cat);
		g_free (key);
		g_free (action);
		g_free (action_data);
		return;
	}
	GdkModifierType mask;
	guint keyval;
	//this converts ucase to lcase, when there is no modifier
	gtk_accelerator_parse (key, &keyval, &mask);
	if (keyval != 0)
	{
		/*for letters, when a <Shift> is pressed, or after <Lock>,
		gtk actually returns the upper-case code, = lower-case - 0x20
		if <Shift> , there will also be GDK_SHIFT_MASK
		but e2's config widget always provides the <Shift>lower-case pair FIXED
		we need to work around this mismatch */
//		printd (DEBUG, "key - %s - %d - %d", key, keyval, mask);
//		if (keyval >= GDK_a && keyval <= GDK_z && mask == GDK_SHIFT_MASK)
//			keyval -= 0x20;
		if (mask == 0 && (keyval < 0xF000 || keyval > 0xFFFF))
		{
			gchar *realkey = gdk_keyval_name (keyval);
			if (!g_str_equal (key, realkey))
				keyval = gdk_keyval_to_upper (keyval);
		}
		E2_KeyRuntime *k = ALLOCATE (E2_KeyRuntime);
		CHECKALLOCATEDWARN (k, )
		if (k == NULL)
		{
			g_free (cat);
			g_free (key);
			return;
		}
		k->keyval = keyval;
		k->mask = mask;
		k->action = action;	//may be ""
		k->action_data = action_data;	//may be ""
		k->cont = cont;
		k->chained = NULL;
		GtkTreeIter child_iter;
		if (gtk_tree_model_iter_children (model, &child_iter, iter))
		{
			do
			{
				_e2_keybinding_sync_one (model, &child_iter, &k->chained);
			} while (gtk_tree_model_iter_next (model, &child_iter));
		}
		*keys = g_list_append (*keys, k);
	}
	else
	{
		g_free (action);
		g_free (action_data);
	}
	g_free (cat);
	g_free (key);
	return;
}
/**
@brief translate, from primary treestore to runtime glist, the data for a
key-binding category
This can be called at any time, including when there are no keys for the
category in the treestore. Pre-existing data for the category are cleared,
so this cannot be used to _add_ keys to the group
Keys can be nested to any level i.e key-chains can be as long as desired
@param rt pointer to data struct for the keybinding category to be processed

@return if E2_IDLE_KEYSYNC applies, TRUE if the process completed properly
*/
static
#ifdef E2_IDLE_KEYSYNC
gboolean
#else
void
#endif
 e2_keybinding_sync (E2_KeybindingRuntime *rt)
{
	if (!gtk_tree_row_reference_valid (rt->ref))
	{
		printd (WARN, "internal keybinding error, row reference not valid anymore");
#ifdef E2_IDLE_KEYSYNC
		return FALSE;
#else
		return;
#endif
	}
	GtkTreePath *path = gtk_tree_row_reference_get_path (rt->ref);
	if (path == NULL)
	{
		printd (WARN, "internal keybinding error, path is NULL");
#ifdef E2_IDLE_KEYSYNC
		return FALSE;
#else
		return;
#endif
	}

	printd (DEBUG, "%s e2_keybinding_sync", rt->name);
	_e2_keybinding_free_keys (rt);	//get rid of any old key data for this category

	E2_OptionSet *opt_keys = e2_option_get ("keybindings");
	GtkTreeIter iter;
	gtk_tree_path_down (path);	//down to the keys level
	while (gtk_tree_model_get_iter (opt_keys->ex.tree.model, &iter, path))
	{
		//convert current key and all descendants to runtime form
		_e2_keybinding_sync_one (opt_keys->ex.tree.model, &iter, &rt->keys);
		gtk_tree_path_next (path);
	}
	gtk_tree_path_free (path);
#ifdef E2_IDLE_KEYSYNC
//	rt->synced = TRUE;
	return TRUE;
#else
	return;
#endif
}
/**
@brief check if @a name is a registered keybinding category
@param name the name of the binding category to search for

@return list member that matches @a name, or NULL if no match
*/
static GList *_e2_keybinding_find_by_name (const gchar *name)
{
	GList *member;
	for (member = keybindings; member != NULL; member = member->next)
	{
		if (g_str_equal (name, ((E2_KeybindingRuntime *) member->data)->name))
			break;
	}
	return member;
}

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

/**
@brief (de)sensitize option tree buttons for selected option tree row
Config dialog page buttons are de-sensitized if the row is a category.
@param selection pointer to selection
@param model
@param path
@param path_currently_selected
@param set data struct for the keybings option

@return TRUE always (the row is always selectable)
*/
static gboolean _e2_keybinding_tree_selection_check_cb
	(GtkTreeSelection *selection, GtkTreeModel *model, GtkTreePath *path,
	gboolean path_currently_selected, E2_OptionSet *set)
{
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter (set->ex.tree.model, &iter, path))
	{
		gchar *cat;
		gtk_tree_model_get (set->ex.tree.model, &iter, 0, &cat, -1);
		gboolean retval = (cat[0] == '\0');
		g_free (cat);
		GtkTreeView *view = gtk_tree_selection_get_tree_view (selection);
		e2_option_tree_adjust_buttons (view, retval);
	}
	return TRUE;
}
/**
@brief ensure that cells in the same row as any category name are hidden

Checks whether @a iter in @a model has an empty string in column 0.
If not (i.e. it is a category row), the cell content is hidden,

@param model treemodel for the keybindings option tree
@param iter pointer to iter with data for the current model row
@param cell cell renderer UNUSED
@param data pointer to data UNUSED

@return TRUE (cell is visible) if row is not a category name
*/
static gboolean _e2_keybinding_visible_check_cb (GtkTreeModel *model,
	GtkTreeIter *iter, GtkCellRenderer *cell, gpointer data)
{
	gchar *cat;
	gtk_tree_model_get (model, iter, 0, &cat, -1);
	gboolean retval = (cat[0] == '\0');
	g_free (cat);
	return retval;
}
/**
@brief decide whether tree row is draggable
Checks whether column 0 of the current row has a null string
ie the row is not a category. If so, the row is draggable
@param drag_source GtkTreeDragSource data struct
@param path tree path to a row on which user is initiating a drag

@return TRUE if the row can be dragged
*/
static gboolean _e2_keybinding_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);
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter (model, &iter, path))
	{
		gchar *cat;
		gtk_tree_model_get (model, &iter, 0, &cat, -1);
		gboolean retval = (cat[0] == '\0');
		g_free (cat);
		return retval;
	}
	return TRUE;
}
/**
@brief cancel the chained keys specified in category binding detailed by @a rt
This func is a mainloop idle callbacK
@param rt pointer to keybinding data struct

@return FALSE always to cancel the timer
*/
static gboolean _e2_keybinding_remove_chained_idle_cb (E2_KeybindingRuntime *rt)
{
	rt->curr_chain = NULL;
	rt->timeout_id = 0;
	return FALSE;
}
/**
@brief find and run the action that's bound to a key
@param widget the widget to which the binding was bound
@param event data struct for the event
@param rt pointer to keybinding data struct

@return TRUE (i.e. prevent other handlers) if any key has been acted upon
*/
static gboolean _e2_keybinding_key_press_cb (GtkWidget *widget,
	GdkEventKey *event, E2_KeybindingRuntime *rt)
{
#ifdef USE_GTK2_10
	if (event->is_modifier)
		return FALSE;
#endif
//	printd (DEBUG,"key binding cb begins");
	gboolean retval = FALSE;
	guint modifiers = gtk_accelerator_get_default_mod_mask ();
	modifiers = event->state & modifiers;
//	printd (DEBUG, "key 1: %d - modifiers: %x", event->keyval, modifiers);
	/*workaround for matching our storage of <Shift>-letter
	with gtk's approach to returning keycodes
	See comments in _e2_keybinding_sync_one () */
//	if (event->state & GDK_LOCK_MASK)
//		modifiers |= GDK_SHIFT_MASK;
	//get rid of shift flag if that's the only one
	if ((event->keyval < 0xF000 || event->keyval > 0xFFFF)
			&& !(modifiers & (GDK_CONTROL_MASK | GDK_MOD1_MASK)))
		modifiers &= ~(GDK_SHIFT_MASK);
	GList *list;
	//if there was a chain from the last key, check if this one is a valid
	//part of the chain. We need to update a pointer and adjust the timer
	for (list = rt->curr_chain; list != NULL; list = list->next)
	{
		E2_KeyRuntime *k = list->data;
		if ((k->keyval == event->keyval) && (k->mask == modifiers))
		{
			if (k->chained != NULL)
			{	//there is more chain left
				rt->curr_chain = k->chained;
				if (rt->timeout_id != 0)
					g_source_remove (rt->timeout_id);
				//FIXME make this timer cancellable at session end
				rt->timeout_id = g_timeout_add (e2_option_int_get ("keybindings-timeout"),
					(GSourceFunc) _e2_keybinding_remove_chained_idle_cb, rt);
			}
			else
			{	//we've finished the chain
				if (rt->timeout_id != 0)
					g_source_remove (rt->timeout_id);
				_e2_keybinding_remove_chained_idle_cb (rt);
			}
			//CHECKME should we run the action before updating the timer ?
			if (*k->action != '\0')
			{	//there is an action to run
				e2_action_run_simple_from (k->action, k->action_data, widget);
				if (!k->cont)
				{
//					printd (DEBUG,"key binding cb ends, returned value = TRUE");
					return TRUE;
				}
				else
					retval = TRUE;
			}
			//keep looking through the chain and then the rest
		}
	}
	//then we check whether the key is in the category bound to the widget
	for (list = rt->keys; list != NULL; list = g_list_next (list))
	{
		E2_KeyRuntime *k = list->data;
		if ((k->keyval == event->keyval) && (k->mask == modifiers))
		{
			if (k->chained != NULL)
			{	//setup for chaining
				rt->curr_chain = k->chained;
				if (rt->timeout_id != 0)
					g_source_remove (rt->timeout_id);
				//FIXME make this timer cancellable at session end
				rt->timeout_id = g_timeout_add (e2_option_int_get ("keybindings-timeout"),
					(GSourceFunc) _e2_keybinding_remove_chained_idle_cb, rt);
			}
			if (*k->action != '\0')
			{
				e2_action_run_simple_from (k->action, k->action_data, widget);
				retval = TRUE;
				if (!k->cont)
					break;
			}
		}
	}
//	printd (DEBUG,"key binding cb ends, returned value = %s", (retval) ? "TRUE":"FALSE");
	return retval;
}
#ifdef E2_IDLE_KEYSYNC
/**
@brief convert data for one or all binding category(ies) from treestore to runtime list
(if any category remains to be processed)
This func is mainloop idle callback CHECKME why sync that way?
@param data pointer to binding to be processed, or NULL to do whole list

@return FALSE (so the idle is cancelled)
*/
static gboolean _e2_keybinding_idle_sync_cb (E2_KeybindingRuntime *data)
{
	if (data != NULL)
		//sync a single KeybindingRuntime
		e2_keybinding_sync (data);
	else
	{
/*		if (binding_cur == NULL)	//after all bindings (or none) have been synced ...
			binding_cur = keybindings;	//start syncing from beginning of list
		if (binding_cur != NULL)
		{
			//sync a single KeybindingRuntime
			e2_keybinding_sync ((E2_KeybindingRuntime *) binding_cur->data);
			//setup pointer for next idle callback
			binding_cur = binding_cur->next;
		}
*/
		GList *member;
		for (member = keybindings; member != NULL; member = member->next)
		{
			e2_keybinding_sync ((E2_KeybindingRuntime *) member->data);
		}
	}
	return FALSE;	//remove this fn from event sources
}
/* *
@brief setup for converting keybinding data, after the bindindgs treestore is altered
@param model UNUSED
@param arg1  UNUSED
@param arg2  UNUSED
@param data  UNUSED
@return
*/
/*static void _e2_keybinding_model_changed_cb
		(GtkTreeModel *model,
		GtkTreePath *arg1,
		GtkTreeIter *arg2,
		gpointer data)
{
	keys_cur = NULL;
FIXME new approach timer does one category only
	g_idle_add ((GSourceFunc) _e2_keybinding_idle_sync_cb, NULL);
} */
#endif  //def E2_IDLE_KEYSYNC

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

/**
@brief re-register all "core" widgets' key bindings

@return
*/
void e2_keybinding_register_all (void)
{
	e2_keybinding_register (_C(17), app.main_window);  //_("general"

	gchar *category = g_strconcat(_C(17),".",_C(32),NULL);  //_("general.panes
	e2_keybinding_register (category, app.pane1_view.treeview);
	e2_keybinding_register (category, app.pane2_view.treeview);
	g_free (category);

	gchar *category2 = g_strconcat (_C(17),".",_C(5),NULL);  //_(general.command-line"
	gchar *category3 = g_strconcat (_C(17),".",_C(12),NULL);  //_(general.dir-line"
	E2_CommandLineRuntime *rt;
	GList *line;
	for (line = app.command_lines; line != NULL ; line = g_list_next (line))
	{
		rt = (E2_CommandLineRuntime *) line->data;
		category = (rt->original) ? category2 : category3 ;
		e2_keybinding_register (category, GTK_BIN (rt->cl)->child);
	}
	g_free (category2);
	g_free (category3);
}
/**
@brief apply key-binding category @a category to @a widget
If @a category doesn't exist in the keybindings treestore, it (any any
missing ancestor(s) are added to the store
If @a category hasn't been used before, its data are setup for runtime usage
@param category name of the keybinding, structured like level1[.level2.level3 ...]
@param widget the widget to which the keybinding will be attached

@return
*/
void e2_keybinding_register (gchar *category, GtkWidget *widget)
{
	GList *member = _e2_keybinding_find_by_name (category);
	E2_KeybindingRuntime *rt;
	if (member != NULL)
	{	//the category is already used
//		printd (DEBUG, "register another widget for keybinding category '%s'", category);
		rt = member->data;
		if (rt->instances != NULL)
		{
			g_signal_connect (G_OBJECT (widget), "key-press-event",
				G_CALLBACK (_e2_keybinding_key_press_cb), rt);
			//remember that it's connected to the widget
			rt->instances = g_slist_append (rt->instances, widget);
			return;
		}
	}
	//first-time usage of category
	E2_OptionSet *opt_keys = e2_option_get ("keybindings");
	GtkTreeIter iter;
	if (!e2_tree_find_lowest_iter_from_str (opt_keys->ex.tree.model, 0, category, &iter))
	{	//unknown category
#ifdef E2_TREEINCREMENT
		//add it to the keybindings store
		e2_tree_create_lowest_iter_from_str (opt_keys->ex.tree.model, 0, category, &iter);
		printd (DEBUG, "added unknown keybinding category '%s'", category);
#else
		printd (DEBUG, "ignored unknown keybinding category '%s'", category);
		return;
#endif
	}
	printd (DEBUG, "first widget registered for keybinding category '%s'", category);
	//init runtime object
	if (member == NULL)
	{
		rt = ALLOCATE0 (E2_KeybindingRuntime);	//FIXME never deallocated
		CHECKALLOCATEDWARN (rt, return;)
		keybindings = g_list_append (keybindings, rt);
		rt->name = g_strdup (category);
	}
	else
		rt = member->data;
	GtkTreePath *path = gtk_tree_model_get_path (opt_keys->ex.tree.model, &iter);
	rt->ref = gtk_tree_row_reference_new (opt_keys->ex.tree.model, path);
	gtk_tree_path_free (path);
	if (member == NULL || g_signal_handler_find
			(widget, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA,
			0, 0, NULL, _e2_keybinding_key_press_cb, rt) == 0)
		g_signal_connect (G_OBJECT (widget), "key-press-event",
			G_CALLBACK (_e2_keybinding_key_press_cb), rt);
	else
		printd (DEBUG, "key-press signal was already connected");
#ifdef E2_IDLE_KEYSYNC
	//FIXME one timer for all categories
	gboolean restart = TRUE;
	for (member = keybindings; member != NULL; member = member->next)
	{
		E2_KeybindingRuntime *thisrt = (E2_KeybindingRuntime *)member->data;
		if (g_str_equal (thisrt->name, category) && thisrt->instances != NULL)
		{
			restart = FALSE;
			break;
		}
	}
#endif
	//remember what it's connected to
	rt->instances = g_slist_append (rt->instances, widget);
#ifdef E2_IDLE_KEYSYNC
	if (   keybindings->next == NULL //first-ever registration of any category
		|| restart //first re-registration
//		|| binding_cur == g_list_last (keybindings) //nearly finished syncing
	   )
	{
		g_idle_add ((GSourceFunc) _e2_keybinding_idle_sync_cb, rt);
		printd (DEBUG, "arranged idle-sync for %s", category);
	}
	else
		printd (DEBUG, "NO idle-sync for %s", category);

#else
	//translate the option tree data to rt form, now
	//CHECKME handle missing keys in a newly-created category
	e2_keybinding_sync (rt);
#endif
}
#ifdef E2_TRANSIENTKEYS
/**
@brief update keybindings data for category @a category
This does nothing if @a category is already in the keybindings treestore
(from config file or a prior instance of a transient item that uses the
category)
@a defaults_func must have the same form as _e2_keybinding_tree_defaults ()
@param category name of binding to be processed
@parmm widget the widget to which the category is to be bound
@param defaults_func pointer to function which creates a tree-option array

@return
*/
void e2_keybinding_register_transient (gchar *category, GtkWidget *widget,
	gpointer defaults_func)
{
	printd (DEBUG, "register transient key-binding %s", category);
	E2_OptionSet *opt_keys = e2_option_get ("keybindings");
	GtkTreeIter iter;
	if (!e2_tree_find_lowest_iter_from_str (opt_keys->ex.tree.model, 0,
		category, &iter))
	{	//the category is not already in the store
		//run the defaults-install function, which in turn calls
		// e2_option_tree_setup_defaults() which sets up a
		// pointer array at dummy_set.ex.tree.def
		void (*install_func) (E2_OptionSet *) = defaults_func;
		E2_OptionSet dummyset;
		(*install_func) (&dummyset);
		gchar **split = (gchar **) dummyset.ex.tree.def;
		//find a shared ancestor, if any
		gchar *work = g_strdup (category);
		gchar *p;
		while ((p = strrchr (work, '.')) != NULL)
		{
			*p = '\0';
			if (e2_tree_find_lowest_iter_from_str (opt_keys->ex.tree.model, 0,
					work, &iter))
				break;
		}
		GtkTreeIter *parent = (p == NULL) ? NULL : &iter;
		//adjust start-index of parsed array accordingly
		gint i = 0;
		if (p != NULL)
		{	//find the array line which has the lowest segment of the parent
			p = strrchr (work, '.');
			if (p == NULL)
				p = work;
			else
				p++;

			while (split[i] != NULL && strstr (split[i], p) == NULL)
				i++;
			if (split[i] == NULL)
				i = 0;
		}
		g_free (work);
		//parse the data & add it to the bindings store
		e2_option_tree_set_from_array (opt_keys->name,
			&split[i], NULL, parent);
		//cleanup
		g_strfreev (split);
	}
	//apply the category to the widget
	e2_keybinding_register (category, widget);
}
/**
@brief cancel binding of @a category to @a widget
@param category name of binding to be processed, or NULL for all categories
@param widget the widget which is to be unbound, or NULL for all widgets

@return
*/
void e2_keybinding_unregister (gchar *category, GtkWidget *widget)
{
	printd (DEBUG, "e2_keybinding_unregister category: %s, widget: _", category);
	GList *categories;
	GSList *widgets;
	for (categories = keybindings; categories != NULL; categories = categories->next)
	{
		E2_KeybindingRuntime *rt = categories->data;
		if (category == NULL || g_str_equal (rt->name, category))
		{
			for (widgets = rt->instances; widgets != NULL; widgets = widgets->next)
			{
				if (widgets->data == widget || widget == NULL)
				{
	printd (DEBUG, "UNregister transient key-binding %s", category);
					/* do this if widget is not to be destroyed
					gulong handler = g_signal_handler_find (widgets->data,
						G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _e2_keybinding_key_press_cb, NULL);
					if (handler > 0)
						g_signal_handler_disconnect (widgets->data, handler); */
					widgets->data = NULL;	//don't remove, list pointer cannot change
				}
			}
		}
	}
}
#endif //def E2_TRANSIENTKEYS
/**
@brief cleanup some keybindings runtime data
The bindings data list is not affected
Key lists are not cleared - that happens when the first widget is re-registered
Callbacks are checked before re-connection

@return
*/
void e2_keybinding_clean (void)
{
	GList *member;
	for (member = keybindings; member != NULL; member = member->next)
	{
		E2_KeybindingRuntime *rt = member->data;
		if (rt->timeout_id != 0)
		{
			g_source_remove (rt->timeout_id);
			rt->timeout_id = 0;
		}
		gtk_tree_row_reference_free (rt->ref);
//		_e2_keybinding_free_keys (rt);
		//if bound widgets are not being destroyed,
		//must disconnect signals that have data = rt, before freeing rt
//		GSList *widgets;
/*		for (widgets = rt->instances; widgets != NULL; widgets = widgets->next)
		{
			if (GTK_IS_WIDGET (widgets->data))	//the bound widget may have been destroyed
			{
				gulong handler = g_signal_handler_find (widgets->data,
					G_SIGNAL_MATCH_FUNC, 0, 0, NULL, _e2_keybinding_key_press_cb, NULL);
				if (handler > 0)
					g_signal_handler_disconnect (widgets->data, handler);
			}
		} */
		g_slist_free (rt->instances);
		rt->instances = NULL;
//		g_free (rt->name);
//		g_free (rt);
	}
//	g_list_free (keybindings);
#ifdef E2_IDLE_KEYSYNC
//	binding_cur = NULL;	//force restart of bindings sync
#endif
//#endif
}
/**
@brief display current key bindings, all or a nominated section
Expects BGL on/closed
@param parameters argument(s) to help command, starting with _(keys)

@return
*/
void e2_keybinding_output_help (gchar *parameters)
{
	//decide which section of the bindings to report
	//NULL reports all (general)
	gchar *section = e2_utils_find_whitespace (parameters);
	if (section != NULL)
	{
		section =  g_strstrip (section);
		if (*section == '\0')
			section = NULL;
	}
	gchar *msg;
	// get treestore iter for start of bindings to report
	GtkTreeIter iter;
	E2_OptionSet *opt_keys = e2_option_get ("keybindings");
	GtkTreeModel *mdl = opt_keys->ex.tree.model;
	if (section == NULL)
		gtk_tree_model_get_iter_first (mdl, &iter);
	else if (!e2_tree_find_iter_from_str (mdl, 0, section, &iter, FALSE))
	{
		msg = g_strdup_printf (_("Cannot find a key binding named %s"), section);
		e2_output_print_error (msg, TRUE);
		return;
		//revert to showing all
//		gtk_tree_model_get_iter_first (mdl, &iter);
	}

	msg = g_strdup_printf (
		"\t  "PROGNAME" %s\n"
		"\t+------------------------+\n",
		_("key bindings"));
	e2_output_print (&app.tab, msg, NULL, FALSE, NULL);
	g_free (msg);

	gchar *cat, *key, *act, *arg;
	GtkTreeIter iter2, iter3;
	// get data from treestore
	//col 0 = category
	//col 1 = key
	//col 3 = action
	//col 4 = argument
	GString *str = g_string_new ("");
	do
	{
		if (gtk_tree_model_iter_children (mdl, &iter2, &iter))
		{
			do
			{
				gtk_tree_model_get (mdl, &iter2, 0, &cat, 1, &key, 3, &act, 4, &arg, -1);
				if (*cat != '\0')
				{
					g_string_append_printf (str, "\t%s\n", cat);
					g_free (cat);
					if (gtk_tree_model_iter_children (mdl, &iter3, &iter2))
					{
						do
						{
							gtk_tree_model_get (mdl, &iter3, 1, &key, 3, &act, 4, &arg, -1);
							g_string_append_printf (str, "\t\t%s\t%s\t%s\n", key, act, arg);
							g_free (key);
							g_free (act);
							g_free (arg);
						} while (gtk_tree_model_iter_next (mdl, &iter3));
					}
				}
				else
				{
					g_string_append_printf (str, "\t%s\t%s\t%s\n", key, act, arg);
					g_free (cat);
					g_free (key);
					g_free (act);
					g_free (arg);
				}
			} while (gtk_tree_model_iter_next (mdl, &iter2));
		}
	} while (gtk_tree_model_iter_next (mdl, &iter) && section == NULL);

	e2_output_print (&app.tab, str->str, NULL, FALSE, NULL);

	g_string_free (str, TRUE);
}
#ifdef E2_KEYALIAS
/**
@brief get a keypress alias and issue fake gdk event(s) with the substituted key(s)
This expects as action-data a string indicating which key(s) are to be issued
Each key name in the string must be in a form parsable by gtk e.g. <Ctrl><Alt>m.
Sequential keys must be separated by a space (actual spaces are <Space>)
@param from the widget where the action was initiated
@param art action runtime data

@return TRUE if timer is set up to issue fake event(s)
*/
static gboolean _e2_keybinding_substitute (gpointer from, E2_ActionRuntime *art)
{
	printd (DEBUG, "_e2_keybinding_substitute from:_ art->data: %s", art->data);
//	printd (DEBUG, "from pointer at %x", (GtkWidget *)from);
//	GtkWidget *debug = gtk_get_event_widget (event);	//what is this ?? (!= from, which is correct)
	gchar *s = (gchar *) art->data;
	if (s == NULL || *s == '\0')
		return FALSE;	//user goofed the config data
	GdkEvent *event = gtk_get_current_event ();
	if (event == NULL)
		return FALSE;	//should never happen
	if (event->type != GDK_KEY_PRESS)
	{
		gdk_event_free (event);
		return FALSE;	//should never happen
	}
//	gdk_event_free (event);
//	g_signal_stop_emission_by_name (from, "key-press-event");

	GdkEvent *event2 = gdk_event_new (GDK_KEY_PRESS);
//	printd (DEBUG, "fake event data at %x", event2);
	event2->any.window = event->any.window;	//must be correct
//	event2->any.send_event = '\1';	//TRUE;
	//multi-key separator is space char (comma etc might be part of fake sequence)
	gchar **split = g_strsplit (s, " ", -1);
	guint fakekey, i, j = g_strv_length (split);
	GdkModifierType fakestate;
	for (i = 0; i < j; i++)
	{
		if (split[i] == NULL || *split[i] == '\0')
			continue;
/*		//handle instances of "\," which are not a separator
		s = split[i] + strlen (split[i]) - 1;
		if (*s == '\\' && s > split[i] && *(s-1) != '\\')
			*s = ',';
*/
		printd (DEBUG, "constructed key name %s", split[i]);
		gtk_accelerator_parse (split[i], &fakekey, &fakestate);
		if (fakekey == 0 && fakestate == 0)
			continue;
		event2->key.keyval = fakekey;
//		event2->key.state = event2->key.state & ~GDK_MODIFIER_MASK;
//		event2->key.state |= fakestate;
		event2->key.state = fakestate;
/*		switch (fakestate)
		{
			case 0:
				event2->key.length = strlen (split[i]);
				event2->key.string = g_strdup (split[i]);
				break;
			case GDK_CONTROL_MASK:
				event2->key.length = 1;
				gchar fake[2] = { fakekey - 96, 0 };
				event2->key.string = g_strdup (fake);
				break;
			default:
				event2->key.length = 0;
				event2->key.string = g_strdup ("");
				break;
		}
*/
		//hardware_keycode must be correct for fake keycode
		gint n_keys;
		GdkKeymapKey *keys;
		if (!gdk_keymap_get_entries_for_keyval (NULL, fakekey, &keys, &n_keys)
			|| n_keys == 0)
			continue;
		event2->key.hardware_keycode = (guint16) keys->keycode;
		g_free (keys);
//		event2->key.time = event->key.time + i;

		gtk_propagate_event ((GtkWidget *)from, event2);
//		gboolean success = FALSE;
//		g_signal_emit_by_name (G_OBJECT (from), "key-press-event", event2, &success);
//		printd (DEBUG, "signal handler cb returned %s", (success) ? "TRUE":"FALSE");

//if this is set above - g_free (event2->key.string);
		//CHECKME corresponding release-event(s)
	}
	gdk_event_free (event);
//if this is set above - event2->key.string = NULL;	//prevent crash when clearing event
//	gdk_event_free (event2); CHECKME cleaning this causes error(s) when dialog is closed
	g_strfreev (split);

	printd (DEBUG, "_e2_keybinding_substitute action ends");
	return TRUE;
}
/**
@brief register actions related to keybindings

@return
*/
void e2_keybinding_actions_register (void)
{
	gchar *action_name = g_strconcat(_A(119),".",_A(120),NULL);	//_("key.alias"
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_keybinding_substitute, NULL, TRUE);
}
#endif	//def E2_KEYALIAS
/**
@brief install default tree options for keybindings
This function is called only if the default is missing from the config file
@param set pointer to set data

@return
*/
static void _e2_keybinding_tree_defaults (E2_OptionSet *set)
{
	//the key name strings are parsed by gtk, and except for simple letters
	//(which might reasonably change for different languages) no translation
	//is possible
	e2_option_tree_setup_defaults (set,
	g_strdup("keybindings=<"),  //internal name
	g_strconcat(_C(17),"||||",NULL),  //_("general"
	g_strconcat("\t|F1||",_A(13),".",_A(70),"|",NULL), //"refresh"
	g_strconcat("\t|<Shift>F1||",_A(7),".",_A(51),"|",NULL), //"list.history
	g_strconcat("\t|<Control>F1||",_A(7),".",_A(66),"|",NULL), //"list.pending
	g_strconcat("\t|<Alt>F1||",_A(7),".",_A(28),"|",NULL), //"list.children
	g_strconcat("\t|F2||",_A(5),".",_A(73),"|",NULL), //"rename"
	g_strconcat("\t|F3||",_A(5),".",_A(101),"|",NULL), //"view"
	g_strconcat("\t|<Shift>F3||",_A(5),".",_A(102),"|",NULL), //"view_again"
	g_strconcat("\t|<Control>F3||",_A(5),".",_A(6),"|",NULL), //"find" (was F4)
	g_strconcat("\t|F4||",_A(5),".",_A(39),"|",NULL), //"edit"
	g_strconcat("\t|<Shift>F4||",_A(5),".",_A(40),"|",NULL), //"edit_again"
	g_strconcat("\t|F5||",_A(5),".",_A(33),"|",NULL),  //"copy"
	g_strconcat("\t|<Shift>F5||",_A(5),".",_A(34),"|",NULL),  //"copy_as"
	g_strconcat("\t|<Control>F5||",_A(5),".",_A(36),"|",NULL),  //"copy_with_time"
	g_strconcat("\t|<Alt>F5||",_A(5),".",_A(35),"|",NULL),  //"copy_merge"
	g_strconcat("\t|F6||",_A(5),".",_A(58),"|",NULL), //"move"
	g_strconcat("\t|<Shift>F6||",_A(5),".",_A(59),"|",NULL), //"move_as"
	g_strconcat("\t|F7||",_A(1),".",_A(56),"|",NULL), //"mkdir"
	g_strconcat("\t|F8||",_A(5),".",_A(97),"|",NULL), //"trash"
	g_strconcat("\t|<Control>F8||",_A(6),".",_A(97),"|",NULL), //"goto trash"  modifiers not translated, gtk needs english to parse
	g_strconcat("\t|<Shift><Control>F8||",_A(5),".",_A(38),"|",NULL), //"delete"
	g_strconcat("\t|F9||",_A(5),".",_A(52),"|",NULL), //"file.info"
#ifdef E2_TREEDIALOG
	g_strconcat("\t|<Shift>F9||",_A(10),".",_A(99),"|",NULL), //"pane_active.tree" popup directories tree dialog
#endif
	g_strconcat("\t|F10||",_A(1),".",_A(69),"|",NULL), //"quit"
	g_strconcat("\t|F11||",_A(15),".",_A(44),"|",NULL), //"toggle.fullscreen"
	g_strconcat("\t|<Shift>F11||",_A(13),".",_A(94),"|",NULL), //"panes.toggle direction"
	g_strconcat("\t|<Control>F11||",_A(9),".",_A(26),"|",NULL), //"output.add
	g_strconcat("\t|<Shift><Control>F11||",_A(9),".",_A(38),"|",NULL), //"output.delete
	g_strconcat("\t|F12||",_A(2),".",_A(32),"|",NULL),  //"configure"
#ifdef E2_VFS
	g_strconcat("\t|<Shift>F12||",_A(10),".",_A(115),"|",NULL), //"pane_active.<spacemenu>"
	g_strconcat("\t|<Control>F12||",_A(3),".",_A(117),"|",NULL), //"dialog.namespace"
#endif
//	_I( single letters in the following not worth translating ?
	//conform this help string to the following 'jump-keys'
	g_strconcat("\t|<Control>","g","||",_A(9),".",_A(68),"|",_("Now press one of h,m,d\\n"),NULL), //"print"
	//these 3 children are the so-called "directory jump keys"
	g_strconcat("\t\t|","h","||cd|$HOME",NULL), //_A(17)
	g_strconcat("\t\t|","m","||cd|/mnt",NULL),  //_A(17)
	g_strconcat("\t\t|","d","||cd|$HOME/downloads",NULL),  //_A(17)
	g_strconcat("\t|<Control>w","||",_A(9),".",_A(27),"|*,1",NULL),  //"adjust ratio" no arg translation
	g_strconcat("\t|<Control>e","||",_A(9),".",_A(27),"|*,0",NULL),    //"adjust ratio" no arg translation
	g_strconcat("\t|<Control>z","||",_A(1),".",_A(43),"|",NULL), //"focus toggle"
	g_strconcat("\t|<Control>r","||",_A(13),".",_A(70),"|",NULL), //"refresh"
	g_strconcat("\t|<Control>1","||",_A(4),".",_A(43),"|1",NULL), //"focus toggle" no arg translation
	g_strconcat("\t|<Control>2","||",_A(4),".",_A(43),"|2",NULL), //"focus toggle"no arg translation
	g_strconcat("\t",_C(32),"||||",NULL),  //_("panes"
	//note "Space" not recognised, must be "space"
//	g_strconcat("\t\t|space||",_A(10),".",_A(90),"|",NULL), //"switch"  something else makes this 'open' the selected item
	g_strconcat("\t\t|space||",_A(10),".",_A(96),"|",NULL), //"toggle selected"
	g_strconcat("\t\t|Tab||",_A(10),".",_A(90),"|",NULL), //"switch"
	g_strconcat("\t\t|Return||",_A(5),".",_A(60),"|",NULL), //"open"
	g_strconcat("\t\t|<Shift>Return||",_A(5),".",_A(61),"|",NULL), //"open in other"
	g_strconcat("\t\t|Left||",_A(10),".",_A(45),"|",NULL), //"go back"
	g_strconcat("\t\t|Right||",_A(10),".",_A(46),"|",NULL), //"go forward"
	g_strconcat("\t\t|<Alt>Up||cd|..",NULL),  //_A(17)
	g_strconcat("\t\t|BackSpace||cd|..",NULL),  //_A(17)
	g_strconcat("\t\t|Delete||",_A(5),".",_A(38),"|",NULL), //"delete"
	g_strconcat("\t\t|<Shift>Delete||",_A(1),".",_A(98),"|",NULL), //"trashempty"
//	g_strconcat("\t\t|Home||cd|$HOME",NULL),   //_A(17)
	g_strconcat("\t\t|<Control>a","||",_A(10),".",_A(95),"|",NULL), //"toggle select all"
	g_strconcat("\t\t|<Control>i","||",_A(10),".",_A(54),"|",NULL), //"invert selection"
	g_strconcat("\t\t|<Control>f","||",_A(5),".",_A(6),"|",NULL), //"find"
	g_strconcat("\t\t|<Control>x","||",_A(13),".",_A(93),"|",NULL), //"sync"
	g_strconcat("\t\t|<Control>h","||",_A(10),".",_A(80),"|",NULL),  //"toggle hidden"
	g_strconcat("\t\t|<Control>m","||",_A(10),".",_A(21),"|",NULL), //"pane_active<bookmarks>"
	g_strconcat("\t\t|<Control>t","||",_A(10),".",_A(22),"|",NULL), //"pane_active<filters>"
//	g_strconcat("\t\t|/||",_A(5),".",_A(6),"|",NULL),  //"find"
	g_strconcat("\t\t|<Control>Left||",_A(13),".",_A(27),"|","*,1",NULL), //"adjust ratio" no arg translation
	g_strconcat("\t\t|<Control>Right||",_A(13),".",_A(27),"|","*,0",NULL), //"adjust ratio" no arg translation
	g_strconcat("\t\t|<Control>p","||",_A(10),".",_A(81),"|",NULL),  //"show menu"
	g_strconcat("\t\t|<Shift><Control>p","||",_A(10),".",_A(81),"|",_A(113),NULL), //"show menu, shift"
	g_strconcat("\t\t|<Control><Alt>p","||",_A(10),".",_A(81),"|",_A(105),NULL),  //"show menu, ctrl"
	g_strconcat("\t\t|<Control><Alt>n","||",_A(10),".",_A(86),"|",NULL),  //"pane_active.sortname"
	g_strconcat("\t\t|<Control><Alt>m","||",_A(10),".",_A(85),"|",NULL),  //"pane_active.sortmodified"
	g_strconcat("\t\t|<Control><Alt>s","||",_A(10),".",_A(88),"|",NULL),  //"pane_active.sortsize"
	g_strconcat("\t",_C(12),"||||",NULL),  //_"directory line"
	g_strconcat("\t\t|Tab||",_A(1),".",_A(31),"|",_A(106),NULL), //"complete dirs"
	g_strconcat("\t\t|<Control>Tab||",_A(13),".",_A(60),"|",NULL), //"open"
	g_strconcat("\t\t|<Control>Delete||",_A(1),".",_A(29),"|",NULL),  //"clear"
	g_strconcat("\t\t|<Alt>Delete||",_A(1),".",_A(30),"|",NULL),  //"clear_history"
	g_strconcat("\t\t|<Control>Return||",_A(1),".",_A(53),"|",_A(107),NULL), //"insert selection, escape"
	g_strconcat("\t\t|<Alt>Return||",_A(1),".",_A(53),"|",NULL), //"insert selection"
	g_strconcat("\t",_C(5),"||||",NULL),  //_("command line"
	g_strconcat("\t\t|<Shift>Return||",_A(1),".",_A(77),"|",NULL), //"command.send"
	g_strconcat("\t\t|<Control>Return||",_A(1),".",_A(53),"|",_A(107),NULL), //"insert selection, escape"
	g_strconcat("\t\t|<Alt>Return||",_A(1),".",_A(53),"|",_A(112),NULL), //"insert selection, quote"
	g_strconcat("\t\t|<Control>Delete||",_A(1),".",_A(29),"|",NULL), //"clear"
	g_strconcat("\t\t|<Alt>Delete||",_A(1),".",_A(30),"|",NULL),  //"clear_history"
	g_strconcat("\t\t|Tab||",_A(1),".",_A(31),"|",NULL), //"complete"
	g_strconcat("\t\t|<Alt>Insert||",_A(28),".",_A(24),"|",NULL), //"children<menu>"
//	g_strconcat("\t",_C(27),"||||",NULL),  //_("output"
//CHECKME these stay as command-line bindings ??
	g_strconcat("\t\t|Page_Up||",_A(9),".",_A(65),"|",NULL), //"page up"
	g_strconcat("\t\t|Page_Down||",_A(9),".",_A(64),"|",NULL), //"page down"
	g_strconcat("\t\t|<Shift>Page_Up||",_A(9),".",_A(49),"|",NULL), //"goto top"
	g_strconcat("\t\t|<Shift>Page_Down||",_A(9),".",_A(48),"|",NULL), //"goto bottom"
	g_strconcat("\t\t|<Shift>Up||",_A(9),".",_A(75),"|",NULL),  //"scroll up"
	g_strconcat("\t\t|<Shift>Down||",_A(9),".",_A(74),"|",NULL),  //"scroll down"
	g_strdup(">"),
	NULL);
}
/**
@brief initialize key-related options, and init keys pointer

@return
*/
void e2_keybinding_options_register (void)
{
	//no screen rebuilds needed after any change to these options
	gchar *group_name = g_strconcat(_C(20),".",_C(22),NULL);
	E2_OptionSet *set = e2_option_tree_register ("keybindings", group_name, _C(22),  //_("keybindings"
		NULL, _e2_keybinding_tree_selection_check_cb, _e2_keybinding_tree_draggable_check_cb,
		E2_OPTION_TREE_UP_DOWN | E2_OPTION_TREE_ADD_DEL,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDKEYS);
	e2_option_tree_add_column (set, _("Category"), E2_OPTION_TREE_TYPE_STR, 0, "",
		E2_OPTION_TREE_COL_NOT_EDITABLE, NULL, NULL);
	e2_option_tree_add_column (set, _("Key"), E2_OPTION_TREE_TYPE_KEY, 0, "",
		0, _e2_keybinding_visible_check_cb, NULL);
	e2_option_tree_add_column (set, _("Continue"), E2_OPTION_TREE_TYPE_BOOL, FALSE, "false", //no translation
		0, _e2_keybinding_visible_check_cb, NULL);
	e2_option_tree_add_column (set, _("Action"), E2_OPTION_TREE_TYPE_SEL, 0, "",
		0, NULL, GINT_TO_POINTER (E2_ACTION_EXCLUDE_GENERAL | E2_ACTION_EXCLUDE_ACCEL
		| E2_ACTION_EXCLUDE_LAYOUT));
	e2_option_tree_add_column (set, _("Argument"), E2_OPTION_TREE_TYPE_SEL , 0, "",
		0, NULL, GINT_TO_POINTER
		(E2_ACTION_EXCLUDE_GENERAL | E2_ACTION_EXCLUDE_ACCEL
		| E2_ACTION_EXCLUDE_LAYOUT | E2_ACTION_EXCLUDE_TOGGLE));
	e2_option_tree_create_store (set);

	e2_option_tree_prepare_defaults (set, _e2_keybinding_tree_defaults);

	group_name = g_strconcat(_C(20),":",_C(25),NULL); //_("interface:miscellaneous"
	e2_option_int_register ("keybindings-timeout", group_name, _("chained keybindings timeout (ms)"),
		_("This sets the time limit (in milliseconds) for accepting 'chained' keybindings"),
		NULL, 2000, 1, 1000000,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP);
}
