/* $Id: e2_combobox.c 2085 2010-05-20 00:15:38Z tpgww $

Copyright (C) 2004-2010 tooar <tooar@emelfm2.net>

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

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

You should have received a copy of the GNU General Public License
along with emelFM2; see the file GPL. If not, see http://www.gnu.org/licenses.
*/

/**
@file src/utils/e2_combobox.c
@brief GtkComboBox utilities

This file contains utility functions for the GtkComboBox widget
*/

#include "emelfm2.h"
#include "e2_combobox.h"
#include "e2_pane.h"
#include "e2_command_line.h"

static void _e2_combobox_block_changed (GtkWidget *combo)
{
	g_object_set_data (G_OBJECT (combo), "e2_changed_blocked",
		GINT_TO_POINTER (TRUE));
}

static void _e2_combobox_unblock_changed (GtkWidget *combo)
{
	g_object_set_data (G_OBJECT (combo), "e2_changed_blocked",
		GINT_TO_POINTER (FALSE));	//= NULL
}
/**
@brief determine the index of the last row in the model of @a combo

@param combo the entry text combo box to be interrogated

@return the index of the last item in the model, -1 if there's nothing
*/
static gint _e2_combobox_last_index (GtkComboBox *combo)
{
	GtkTreeModel *model = gtk_combo_box_get_model (combo);
	return ((gint)gtk_tree_model_iter_n_children (model, NULL) - 1);
}

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

/**
@brief "key-press-event" signal callback to handle accessing combobox history items and translate relevant keycodes
This applies to _all_ created comboboxes whether or not they have a text-entry
@param combo a combobox widget where the keypress occurred
@param event pointer to key event data struct
@param data pointerised gboolean, TRUE to cycle to other end of history when relevant

@return TRUE if pressed key was <Up> or <Down> and cycling not-necessary or allowed
*/
static gboolean _e2_combobox_key_press_cb (GtkWidget *combo, GdkEventKey *event,
	gpointer data)
{
	if (!(event->keyval == GDK_Up || event->keyval == GDK_Down))
	{
		e2_utils_translate_key_event (event);
		return FALSE;
	}

	if (event->keyval == GDK_Down &&
		(event->state & gtk_accelerator_get_default_mod_mask ()) == GDK_CONTROL_MASK)
	{
		gtk_combo_box_popup (GTK_COMBO_BOX (combo));
		return TRUE;
	}

	if (data == NULL)
		return FALSE;	//no cycling allowed

	gint pos = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
	if (event->keyval == GDK_Down)
	{
		if (pos < _e2_combobox_last_index (GTK_COMBO_BOX (combo)))
			pos++;	//assumes -1 can go to 0 always
		else
			pos = 0;
	}
	else //(event->keyval == GDK_Up)
	{
		if (pos > 0)
			pos--;
		else if (pos == 0 || pos == -1)
			pos = _e2_combobox_last_index (GTK_COMBO_BOX (combo));
	}
	e2_combobox_set_active (combo, pos);
	if (GTK_IS_COMBO_BOX_ENTRY (combo))
	{
		GtkWidget *entry =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (combo));
#else
			GTK_BIN (combo)->child;
#endif
		if (pos == -1)
			gtk_entry_set_text (GTK_ENTRY (entry), "");
		else
			gtk_editable_set_position (GTK_EDITABLE (entry), -1);
	}

	return TRUE;
}
/**
@brief "key-press-event" signal callback to handle GtkComboBoxEntry text shortening or history clearing
A <Alt>Delete keypress in the combobox's entry triggers deletion of current
string and any matching item(s) in @a history.
A <Shift><Alt>Delete keypress in the combobox entry triggers deletion of
current string and the whole history list @a history (hopefully that's not
static if also used by other widgets)
Otherwise, <Shift>Delete clears text from cursor to end
@param entry the entry widget for the combo box
@param event pointer to event data struct
@param history pointer to history list for the combo box

@return TRUE if the keypress was <Alt>Delete
*/
static gboolean _e2_combobox_key_press_cb2 (GtkWidget *entry, GdkEventKey *event,
	GList **history)
{
	//prevent sporadic bad latency in UI updates
	gtk_widget_queue_draw (entry);

	if (event->keyval == GDK_Delete)
	{
		gint start;
		guint modifiers = gtk_accelerator_get_default_mod_mask ();
		if ((event->state & modifiers) == GDK_SHIFT_MASK)
		{	//there is no action for clearing "the rest" of an entry
			start = gtk_editable_get_position (GTK_EDITABLE (entry));
			gtk_editable_delete_text (GTK_EDITABLE (entry), start, -1);
			if (g_object_get_data (G_OBJECT (entry), "e2-dir-line") != NULL)
				e2_command_line_highlight (entry, FALSE);
			return TRUE;
		}
		else if ((event->state & modifiers) == GDK_MOD1_MASK)
		{
			const gchar *this = gtk_entry_get_text (GTK_ENTRY (entry));
			if (this != NULL)	//cannot happen ?
			{
				e2_combobox_clear_value (
#ifdef USE_GTK2_14
					gtk_widget_get_parent (entry),
#else
					entry->parent,
#endif
					this, TRUE);
			}
			return TRUE;
		}
		else if ((event->state & modifiers) == (GDK_SHIFT_MASK | GDK_MOD1_MASK))
		{	//hardcode history clearance for most comboboxes
			//a keybinding probably applies to dirlines and commandline but
			//in that case they don't use this function
			GtkComboBox *combo;
			GtkTreeModel *model;

			gtk_entry_set_text (GTK_ENTRY (entry), "");
			combo = GTK_COMBO_BOX (
#ifdef USE_GTK2_14
				gtk_widget_get_parent (entry)
#else
				entry->parent
#endif
			);
			model = gtk_combo_box_get_model (combo);
			gtk_list_store_clear (GTK_LIST_STORE (model));
			if (history != NULL && *history != NULL)
				//too bad for any other widget using the history, if that's static
				e2_list_free_with_data (history);
			return TRUE;
		}
	}
	return FALSE;
}
/**
@brief "scroll-event" signal callback for the entry belonging to a GtkComboBoxEntry

@param entry the entry widget for the combo box
@param event pointer to event data struct
@param combo the parent widget

@return
*/
static gboolean _e2_combobox_scroll_cb (GtkWidget *entry, GdkEventScroll *event,
	GtkWidget *combo)
{
	gint pos;
	guint modifiers;
//	printd (DEBUG, "_e2_combobox_scroll_cb");
	if (event->direction == GDK_SCROLL_UP)
	{
		pos = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
		if (pos > 0)
		{
			modifiers = gtk_accelerator_get_default_mod_mask ();
			if ((event->state & modifiers) == GDK_CONTROL_MASK)
				e2_combobox_set_active (combo, 0);
			else
				e2_combobox_set_active (combo, pos-1);
		}
		return TRUE;
	}
	else if (event->direction == GDK_SCROLL_DOWN)
	{
		pos = gtk_combo_box_get_active (GTK_COMBO_BOX (combo));
		guint lastpos = _e2_combobox_last_index (GTK_COMBO_BOX (combo));
		if (pos == -1 || pos < lastpos)
		{
			modifiers = gtk_accelerator_get_default_mod_mask ();
			if ((event->state & modifiers) == GDK_CONTROL_MASK)
				e2_combobox_set_active (combo, lastpos);
			else
				e2_combobox_set_active (combo, pos+1);
		}
		return TRUE;
	}
	return FALSE;
}
/**
@brief "changed" signal callback for a GtkComboBoxEntry
When a history item is selected, if @a data is non-NULL (TRUE) the combobox's
entry is focused. If @a data is NULL (FALSE) then an "activated" signal is
emitted on the entry.
When the combobox entry is simply edited, nothing is done.
@param combo the combobox widget whose entry was changed
@param data pointerised gboolean, non-NULL (TRUE) if @a combo was created with
 the E2_COMBOBOX_FOCUS_ON_CHANGE flag set
@return
*/
static void _e2_combobox_changed_cb (GtkWidget *combo, gpointer data)
{
//	printd (DEBUG, "_e2_combobox_changed_cb: combo:_ data: %s", (data == NULL) ? "FALSE":"TRUE");
	if (g_object_get_data (G_OBJECT (combo), "e2_changed_blocked") != NULL)
	{
//		printd (DEBUG, "_e2_combobox_changed_cb BLOCKED");
		return;
	}
	//active item will be -1 whenever an item is edited
	if (
#ifdef USE_GTK2_18
		gtk_widget_get_visible (combo)
#else
		GTK_WIDGET_VISIBLE (combo)
#endif
		&& gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) != -1)
	{
		gboolean focus = GPOINTER_TO_INT (data);
		GtkWidget *entry =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (combo));
#else
			GTK_BIN (combo)->child;
#endif
		if (focus)
		{
			gtk_widget_grab_focus (entry);
			gtk_editable_set_position (GTK_EDITABLE (entry), -1);
		}
		else
		{
			printd (DEBUG, "issue activate signal for combobox entry");
			g_signal_emit_by_name (G_OBJECT (entry), "activate");
		}
	}
}

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

/**
@brief "activate" signal callback for created GtkComboBoxEntry comboboxes

This function is a callback for the "activate" signal on the GtkEntry that
belongs to a combobox with a text entry, unless that combobox is created with
flag E2_COMBOBOX_NO_AUTO_HISTORY.
The current contents of @a entry, if not empty, are prepended to the box's
history list.

@param entry entry of a GtkComboBoxEntry
@param data pointerised gboolean, TRUE if double history entries are allowed

@return
*/
void e2_combobox_activated_cb (GtkWidget *entry, gpointer data)
{
	printd (DEBUG, "e2_combobox_activated_cb");
	gchar *text = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
	//a directory path may properly have trailing whitespace, so no strip
	g_strchug (text);
	if (*text != '\0')
	{
		GtkComboBox *combo = GTK_COMBO_BOX (
#ifdef USE_GTK2_14
			gtk_widget_get_parent (entry)
#else
			entry->parent
#endif
		);

		if (data == NULL)	// !GPOINTER_TO_INT (data))
		{
			GtkTreeIter iter;
			GtkTreeModel *model = gtk_combo_box_get_model (combo);
			if (gtk_tree_model_get_iter_first (model, &iter))
			{
				//actually, there should only ever be 1 match at most
				while (e2_tree_find_iter_from_str_simple (model, 0, text, &iter, FALSE))
				{
					GtkTreeIter test;
					test = iter;
					gboolean more = gtk_tree_model_iter_next (model, &test);
					gtk_list_store_remove (GTK_LIST_STORE (model), &iter); //after this, iter has data for next item, if any
					if (!more)
						break;
				}
			}
		}

		gtk_combo_box_prepend_text (combo, text);
		e2_combobox_set_active (GTK_WIDGET (combo), 0);
	}
	g_free (text);
}
/**
@brief determine whether the history model of @a combo has any content

@param combo the combo box to be interrogated

@return TRUE if the model has something
*/
gboolean e2_combobox_has_history (GtkComboBox *combo)
{
	GtkTreeModel *model = gtk_combo_box_get_model (combo);
	return (gtk_tree_model_iter_n_children (model, NULL) > 0);
}
/* *
@brief activate the last history item of combo box @a combo
This also puts the contents of the history line into the combo
entry, and moves the cursor to the end
@param combo  GtkComboBox to work on
@return
*/
/*void e2_combobox_select_last (GtkWidget *combo)
{
	GtkWidget *entry =
#ifdef USE_GTK2_14
		gtk_bin_get_child (GTK_BIN (combo));
#else
		GTK_BIN (combo)->child;
#endif
//	GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo));
	gint count = _e2_combobox_last_index ((GTK_COMBO_BOX (combo));
	gchar *text = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
	gint pos = gtk_editable_get_position (GTK_EDITABLE (entry));

//	_e2_combobox_block_changed (combo);	//CHECKME
	e2_combobox_set_active (combo, count);
//	_e2_combobox_unblock_changed (combo);	//CHECKME

	gtk_entry_set_text (GTK_ENTRY (entry), text);
	gtk_editable_set_position (GTK_EDITABLE (entry), pos);
	g_free (text);
} */
/* *
@brief get the string from the last row in the model of @a combo

@param combo the entry text combo box to be interrogated

@return newly-allocated copy of the string, or NULL if there's nothing
*/
/*gchar *e2_combobox_last_text (GtkComboBox *combo)
{
	gchar *text = NULL;
	GtkTreeModel *mdl = gtk_combo_box_get_model (combo);
	GtkTreeIter iter;
	guint count = gtk_tree_model_iter_n_children (mdl, NULL);
	if (count > 0
		&& gtk_tree_model_iter_nth_child (mdl, &iter, NULL, count-1))
		gtk_tree_model_get (mdl, &iter, 0, &text, -1);
	return text;
} */
/**
@brief get the string from the first row in the model of @a combo

@param combo the GtkComboBox or GtkComboBoxEntry to be interrogated

@return newly-allocated copy of the string, or NULL if there's nothing
*/
gchar *e2_combobox_first_text (GtkComboBox *combo)
{
	gchar *text = NULL;
	GtkTreeModel *mdl = gtk_combo_box_get_model (combo);
	GtkTreeIter iter;
	if (gtk_tree_model_get_iter_first (mdl, &iter))
		gtk_tree_model_get (mdl, &iter, 0, &text, -1);
	return text;
}
/**
@brief activate history item @a num of combo box @a combo

@param combo GtkComboBox or GtkComboBoxEntry to work on
@param num the integer index of the item to activate
@return
*/
void e2_combobox_set_active (GtkWidget *combo, gint num)
{
	printd (DEBUG, "e2_combobox_set_active: %d", num);
	_e2_combobox_block_changed (combo);
	gtk_combo_box_set_active (GTK_COMBO_BOX (combo), num);
	_e2_combobox_unblock_changed (combo);
}
/**
@brief return currently-selected string from history
At this time, this func is only used for GtkComboBoxEntry's
@param combo GtkComboBox or GtkComboBoxEntry to be interrogated

@return newly-allocated string from history or NULL if nothing is selected
*/
gchar *e2_combobox_get_active_text (GtkWidget *combo)
{
	if (gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) != -1)
		return (gtk_combo_box_get_active_text (GTK_COMBO_BOX (combo)));
	return NULL;
}
/**
@brief clear any history item of @a combo corresponding to @a value, and if relevant, the displayed value
@param combo a combo widget with a text entry
@param value the string to be cleared
@param with_entry TRUE to also clear the displayed value
@return
*/
void e2_combobox_clear_value (GtkWidget *combo, const gchar *value, gboolean with_entry)
{
	GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo));
	GtkTreeIter iter;

	if (gtk_tree_model_get_iter_first (model, &iter))
	{
		while (e2_tree_find_iter_from_str (model, 0, value, &iter, FALSE))
			gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
	}

	GList **history = (GList**) g_object_get_data (G_OBJECT(combo), "e2-combo-history");
	if (history != NULL && *history != NULL)
	{
		GList *tmp;
		while ((tmp = g_list_find_custom (*history, value,
				(GCompareFunc) e2_list_strcmp)) != NULL)
		{
			g_free (tmp->data);
			*history = g_list_delete_link (*history, tmp);
		}
	}

	if (with_entry)
	{
		//now it's safe to change entry content (and therefore, possibly value)
		GtkWidget *entry =
#ifdef USE_GTK2_14
			gtk_bin_get_child (GTK_BIN (combo));
#else
			GTK_BIN(combo)->child;
#endif
		const gchar *now = gtk_entry_get_text (GTK_ENTRY(entry));
		if (!strcmp (now, value))
		{
			gtk_entry_set_text (GTK_ENTRY(entry), "");
			if (g_object_get_data (G_OBJECT (entry), "e2-dir-line") != NULL)
				e2_command_line_highlight (entry, FALSE);
		}
	}
}
/* *
@brief backup all of the history data of @a combo into @a history

@param combo the combo box to be interrogated
@parm list list of history-items

@return
*/
/*void e2_combobox_save_history (GtkWidget *combo, GList **list)
{
	if (*list != NULL)
		e2_list_free_with_data (list);
	GtkTreeIter iter;
	GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo));
	if (gtk_tree_model_get_iter_first (model, &iter))
	{
		gchar *text;
		do
		{
			gtk_tree_model_get (model, &iter, 0, &text, -1);
			*list = g_list_append (*list, text);
		} while (gtk_tree_model_iter_next (model, &iter));
	}
}
*/
/**
@brief append a GList to a GtkComboBox's history

This function appends a GList of strings (@a list) to the history
of a GtkComboBoxEntry
NOTE only for boxes created with gtk_combo_box_new_text ()
@param combo GtkComboBoxEntry to work on
@param list GList of utf8 strings

@return
*/
void e2_combobox_append_history (GtkWidget *combo, GList *list)
{
	GList *tmp;
	for (tmp = list; tmp != NULL; tmp = g_list_next (tmp))
		gtk_combo_box_append_text (GTK_COMBO_BOX (combo), tmp->data);
}
/**
@brief append string array to a GtkComboBox's history

NOTE only for boxes created with gtk_combo_box_new_text ()
@param combo combobox
@param num number of strings in @a array
@param array array of utf8 strings

@return
*/
void e2_combobox_append_history_counted (GtkWidget *combo, guint num, gchar **array)
{
	guint i;
	for (i = 0; i < num; i++)
		gtk_combo_box_append_text (GTK_COMBO_BOX (combo), array[i]);
}
/**
@brief append NULL-terminated string array to a GtkComboBox's history

This function appends array of strings @a strv to the history
of a GtkComboBox.
NOTE only for boxes created with gtk_combo_box_new_text ()
@param combo combobox
@param strv a null-terminated array of utf8 strings

@return
*/
void e2_combobox_append_history_strv (GtkWidget *combo, gchar **strv)
{
	guint i;
	for (i = 0; strv[i] != NULL; i++)
		gtk_combo_box_append_text (GTK_COMBO_BOX (combo), strv[i]);
}
/**
@brief create a GtkComboBox

This function creates a GtkComboBox or GtkComboBoxEntry, in either case for text-
only, respecting the @a flags, and then for a GtkComboBoxEntry, connects
@a activated_cb to the "activate" signal of the GtkComboBox entry.
@a activate_data will be a pointer to E2_CommandLineRuntime struct for the
command line, or generally NULL for an ad-hoc combobox
@a history size figures prominently in any (downstream) clearing of the combobox
model, so it should not be static if there's a prospect of multiple widgets
clearing the history
@param activate_cb "activate" callback for the combobox entry, or NULL
@param activate_data data to provide to the activate callback
@param history history-list pointer, or NULL
@param flags flags to influence the usage of the GtkComboBox

@return the GtkComboBox that was created and packed
*/
GtkWidget *e2_combobox_get (void (*activate_cb)(GtkEntry*,gpointer),
	gpointer activate_data, GList **history, E2_ComboBoxFlags flags)
{
	GtkWidget *combo = (flags & E2_COMBOBOX_HAS_ENTRY) ?
		gtk_combo_box_entry_new_text () : gtk_combo_box_new_text ();

	if (!(flags & E2_COMBOBOX_MENU_STYLE))
	{
		static gboolean style_parsed = FALSE;
		if (!style_parsed)
		{
			gtk_rc_parse_string (
			"style \"list-style-style\" { GtkComboBox::appears-as-list = 1 } "
			"widget \"*.list-style\" style \"list-style-style\"");
			style_parsed = TRUE;
		}
		gtk_widget_set_name (combo, "list-style");
	}

	if (history != NULL)
		e2_combobox_append_history (combo, *history);

	if (flags & E2_COMBOBOX_HAS_ENTRY)
	{
#ifdef USE_GTK2_14
		GtkWidget *child = gtk_bin_get_child (GTK_BIN (combo));
#endif
		if (history != NULL)
		{
			g_object_set_data (G_OBJECT(combo), "e2-combo-history", history);
			//enable prompt UI updates and deletions
			g_signal_connect (G_OBJECT
#ifdef USE_GTK2_14
				(child),
#else
				(GTK_BIN (combo)->child),
#endif
				"key-press-event",
				G_CALLBACK (_e2_combobox_key_press_cb2), history); //for <Delete>, no key translation needed
			g_signal_connect (G_OBJECT
#ifdef USE_GTK2_14
				(child),
#else
				(GTK_BIN (combo)->child),
#endif
				"scroll-event",
				G_CALLBACK (_e2_combobox_scroll_cb), combo);
			if (!(flags & E2_COMBOBOX_NO_AUTO_HISTORY))
				g_signal_connect (G_OBJECT
#ifdef USE_GTK2_14
					(child),
#else
					(GTK_BIN (combo)->child),
#endif
					"activate",
					G_CALLBACK (e2_combobox_activated_cb),
					GINT_TO_POINTER (flags & E2_COMBOBOX_ALLOW_DOUBLE));
		}
		//connect caller-specified callback for "activate" signal to combobox's entry
		if (activate_cb != NULL)
			g_signal_connect (G_OBJECT
#ifdef USE_GTK2_14
				(child),
#else
				(GTK_BIN (combo)->child),
#endif
				"activate",
				G_CALLBACK (activate_cb), activate_data);
		//enable focusing etc
		g_signal_connect (G_OBJECT (combo), "changed",
			G_CALLBACK (_e2_combobox_changed_cb),
			GINT_TO_POINTER (flags & E2_COMBOBOX_FOCUS_ON_CHANGE));
	}

	g_signal_connect (G_OBJECT ((combo)), "key-press-event",
		G_CALLBACK (_e2_combobox_key_press_cb), //includes translation
		GUINT_TO_POINTER (flags & E2_COMBOBOX_CYCLE_HISTORY));
	g_signal_connect (G_OBJECT (combo), "key-release-event",
		G_CALLBACK (e2_utils_key_translate_cb), NULL);

	return combo;
}
/**
@brief pack a new GtkComboBox into @a box

This function creates a GtkComboBox using e2_combobox_get(), and then packs
it into @a box and connects @a func to the "activate" signal.

@param box parent box to add the GtkComboBox to
@param expand packing property
@param padding packing property
@param activate_cb entry activate-signal callback, or NULL
@param activate_data callback data
@param history combo history list, or NULL
@param flags flags to influence the usage of the GtkComboBox

@return the GtkComboBox that was created and packed
*/
GtkWidget *e2_combobox_add (GtkWidget *box, gboolean expand, guint padding,
	void (*activate_cb)(GtkEntry*,gpointer), gpointer activate_data,
	GList **history, E2_ComboBoxFlags flags)
{
	GtkWidget *combo = e2_combobox_get (activate_cb, activate_data, history, flags);

	//add padding to the left & right side of the combobox
	GtkWidget *pad_box;
	if (GTK_IS_HBOX (box))
		pad_box = gtk_vbox_new (FALSE, padding);
	else
		pad_box = gtk_hbox_new (FALSE, padding);

	//now add the combo to the padding container
	gtk_box_pack_start (GTK_BOX (pad_box), combo, TRUE, TRUE, padding);
	//add padding container to parent container
	gtk_box_pack_start (GTK_BOX (box), pad_box, expand, expand, padding);
	return combo;
}
