/* $Id: e2_combobox.c 874 2008-05-07 14:47:27Z 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/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
This applies to all created comboboxes whether or not they have a text-entry
@param entry the combobox entry widget which received the keypress
@param event pointer to key event data struct
@param data pointerised gboolean, TRUE to cycle to other end of history

@return TRUE if pressed key was <Up> or <Down>
*/
static gboolean _e2_combobox_key_press_cb (GtkWidget *entry, GdkEventKey *event,
	gpointer data)
{
	if (!(event->keyval == GDK_Up || event->keyval == GDK_Down))
		return FALSE;

	GtkComboBox *combo = GTK_COMBO_BOX (entry->parent);
	gboolean cycle = (data != NULL);
	gint pos = gtk_combo_box_get_active (combo);
	if (event->keyval == GDK_Down)
	{
		if (pos < _e2_combobox_last_index (combo))
			pos++;	//assumes -1 can go to 0 always
		else if (cycle)
			pos = 0;
	}
	else //(event->keyval == GDK_Up)
	{
		if (pos > 0)
			pos--;
//		else if ((pos == 0 && cycle) || pos == -1)
		else if ((pos == 0 || pos == -1) && cycle) //disable goto end of history if no cycling
			pos = _e2_combobox_last_index (combo);
	}
	if (pos == -1)
			gtk_entry_set_text (GTK_ENTRY (entry), "");
	else
	{
		e2_combobox_set_active (GTK_WIDGET (combo), pos);
		gchar *text = e2_combobox_get_active (GTK_WIDGET (combo));
		if (text != NULL)
		{
			gtk_entry_set_text (GTK_ENTRY (entry), text);
			g_free (text);
		}
		gtk_editable_set_position (GTK_EDITABLE (entry), -1);
	}
	return TRUE;
}
/**
@brief key press signal callback to handle text shortening or history clearing
A <Alt>Delete keypress in the combobox 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)
{
	gint start;
	GtkComboBox *combo;
	GtkTreeModel *model;

	if (event->keyval == GDK_Delete)
	{
		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);
			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 ?
			{
				combo = GTK_COMBO_BOX (entry->parent);
				model = gtk_combo_box_get_model (combo);
				GtkTreeIter iter;

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

				if (history != NULL && *history != NULL)
				{
					GList *tmp;
					while ((tmp = g_list_find_custom (*history, this,
							(GCompareFunc) e2_list_strcmp)) != NULL)
					{
						g_free (tmp->data);
						*history = g_list_delete_link (*history, tmp);
					}
				}
			}
			gtk_entry_set_text (GTK_ENTRY (entry), "");
			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
			gtk_entry_set_text (GTK_ENTRY (entry), "");
			combo = GTK_COMBO_BOX (entry->parent);
			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 combobox "changed" signal callback
When a history item is selected, if @a data is non-NULL (TRUE) the combobox
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, 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 ((GTK_WIDGET_VISIBLE (combo)) &&
		(gtk_combo_box_get_active (GTK_COMBO_BOX (combo)) != -1))
	{
		gboolean focus = GPOINTER_TO_INT (data);
		GtkWidget *entry = GTK_BIN (combo)->child;
		if (focus)
		{
			gtk_widget_grab_focus (entry);
			gtk_editable_set_position (GTK_EDITABLE (entry), -1);
		}
		else
			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 of 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 (NOTICE, "e2_combobox_activated_cb");
	gchar *text = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
	g_strstrip (text);
	if (*text != '\0')
	{
		GtkComboBox *combo = GTK_COMBO_BOX (entry->parent);
		gtk_combo_box_prepend_text (combo, text);
		e2_combobox_set_active (GTK_WIDGET (combo), 0);
		if (data == NULL)	// !GPOINTER_TO_INT (data))
		{	//remove any existing item that's the same
			GtkTreeModel *model = gtk_combo_box_get_model (combo);
			GtkTreeIter iter;
			gtk_tree_model_get_iter_first (model, &iter);	//this is the one just prepended
			if (gtk_tree_model_iter_next (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;
				}
			}
		}
	}
	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 = GTK_BIN (combo)->child;
//	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 entry text combo box 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 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

This function gets the active GtkTreeIter from the GtkComboBox and
tries to get the history string from the model and returns it.

@param combo GtkComboBox to work on

@return string from history or NULL if nothing is selected
*/
gchar *e2_combobox_get_active (GtkWidget *combo)
{
/*	GtkTreeIter iter;
	if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (combo), &iter))
	{
		GtkTreeModel *model = gtk_combo_box_get_model (GTK_COMBO_BOX (combo));
		gchar *retval;
		gtk_tree_model_get (model, &iter, 0, &retval, -1);
		return retval;
	} else
		return NULL;
*/
	return (gtk_combo_box_get_active_text (GTK_COMBO_BOX (combo)));	//FIXME only for text combos
}
/* *
@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 GtkComboBox.
NOTE only for boxes created with gtk_combo_box_new_text ()
@param combo GtkComboBox 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, respecting the @a flags, and then
connects @a activate_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 (gpointer activate_cb, gpointer activate_data,
	GList **history, E2_ComboBoxFlags flags)
{
	GtkWidget *combo;
	if (flags & E2_COMBOBOX_HAS_ENTRY)
	{
		combo = gtk_combo_box_entry_new_text ();
		//enable deletions
		g_signal_connect (G_OBJECT (GTK_BIN (combo)->child), "key-press-event",
			G_CALLBACK (_e2_combobox_key_press_cb2), history);

		g_signal_connect (G_OBJECT (combo), "changed",
			G_CALLBACK (_e2_combobox_changed_cb),
			GINT_TO_POINTER (flags & E2_COMBOBOX_FOCUS_ON_CHANGE));

		if (!(flags & E2_COMBOBOX_NO_AUTO_HISTORY))
			g_signal_connect (G_OBJECT (GTK_BIN (combo)->child), "activate",
				G_CALLBACK (e2_combobox_activated_cb),
				GINT_TO_POINTER (flags & E2_COMBOBOX_ALLOW_DOUBLE));
	}
	else
		combo = gtk_combo_box_new_text ();

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

	//enable keyboard-initiated history-accessing
	g_signal_connect (G_OBJECT (GTK_BIN (combo)->child), "key-press-event",
		G_CALLBACK (_e2_combobox_key_press_cb),
		GINT_TO_POINTER (flags & E2_COMBOBOX_CYCLE_HISTORY));
	//connect caller-specified callback for "activate" signal to combobox's entry
	if (activate_cb != NULL)
		g_signal_connect (G_OBJECT (GTK_BIN (combo)->child), "activate",
			G_CALLBACK (activate_cb), activate_data);

	if (!(flags & E2_COMBOBOX_MENU_STYLE))
	{
/*		gchar *name = "list-style";
		//work around gtk silliness, the 'appears-as-list'
		//is a style property instead of a widget property
		gchar *str = g_strconcat ("style \"", name,
		"-style\" { GtkComboBox::appears-as-list = 1 } widget \"*.", name,
		"\" style \"", name, "-style\"", NULL);
		gtk_rc_parse_string (str);
		g_free (str);
		gtk_widget_set_name (combo, name);
*/
		gtk_rc_parse_string (
		"style \"list-style-style\" { GtkComboBox::appears-as-list = 1 } "
		"widget \"*.list-style\" style \"list-style-style\"");
		gtk_widget_set_name (combo, "list-style");
	}

	gtk_widget_show (combo);
	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,
	gpointer activate_cb, 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);
	gtk_widget_show (pad_box);
	return combo;
}
