/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * gtk-combo-text.c - A combo box for selecting from a list.
 * Copyright 2000, 2001, Ximian, Inc.
 *
 * Authors:
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License, version 2, as published by the Free Software Foundation.
 *
 * This library 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#include <config.h>
#include <ctype.h>
#include <gal/util/e-util.h>
#include <gtk/gtksignal.h>
#include <gtk/gtkentry.h>
#include <gtk/gtklist.h>
#include <gtk/gtkscrolledwindow.h>
#include "gal-combo-text.h"

#define PARENT_TYPE GAL_COMBO_BOX_TYPE
static GtkObjectClass *gal_combo_text_parent_class;

static gboolean cb_pop_down (GtkWidget *w, GtkWidget *pop_down,
			     gpointer dummy);

static void list_unselect_cb (GtkWidget *list, GtkWidget *child,
			      gpointer data);

static void update_list_selection (GalComboText *ct, const gchar *text);

static void
gal_combo_text_destroy (GtkObject *object)
{
	GalComboText *ct = GAL_COMBO_TEXT (object);

	if (ct->elements != NULL) {
		g_hash_table_destroy (ct->elements);
		ct->elements = NULL;
	}
	if (ct->list != NULL) {
		g_signal_handlers_disconnect_matched (ct,
						      G_SIGNAL_MATCH_FUNC,
						      0, 0, NULL,
						      cb_pop_down, NULL);

		g_signal_handlers_disconnect_matched (ct->list,
						      G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA,
						      0, 0, NULL,
						      list_unselect_cb, ct);
		ct->list = NULL;
	}

	(*gal_combo_text_parent_class->destroy) (object);
}

static void
gal_combo_text_class_init (GtkObjectClass *object_class)
{
	object_class->destroy = &gal_combo_text_destroy;
	gal_combo_text_parent_class = g_type_class_ref (PARENT_TYPE);
}

static void
gal_combo_text_init (GalComboText *object)
{
}

E_MAKE_TYPE (gal_combo_text,
	     "GalComboText",
	     GalComboText,
	     gal_combo_text_class_init,
	     gal_combo_text_init,
	     PARENT_TYPE)

static gint
strcase_equal (gconstpointer v, gconstpointer v2)
{
	return g_strcasecmp ((const gchar*) v, (const gchar*)v2) == 0;
}


/*
 * a char* hash function from ASU
 *
 * This is cut/paste from gutils.c
 * We've got to do this, because this widget will soon move out of the
 * Gnumeric source and into a separate library.
 */
static guint
strcase_hash (gconstpointer v)
{
	const unsigned char *s = (const unsigned char *)v;
	const unsigned char *p;
	guint h = 0, g;

	for(p = s; *p != '\0'; p += 1) {
		h = ( h << 4 ) + tolower (*p);
		if ( ( g = h & 0xf0000000 ) ) {
			h = h ^ (g >> 24);
			h = h ^ g;
		}
	}

	return h /* % M */;
}

/**
 * gal_combo_text_set_case_sensitive
 * @combo_text:  ComboText widget
 * @val:         make case sensitive if TRUE
 *
 * Specifies whether the text entered into the GtkEntry field and the text
 * in the list items is case sensitive. Because the values are stored in a
 * hash, it is not legal to change case sensitivity when the list contains
 * elements.
 *
 * Returns: The function returns -1 if request could not be honored. On
 * success, it returns 0.
 */
gint
gal_combo_text_set_case_sensitive (GalComboText *combo, gboolean val)
{
	if (combo->elements
	    && g_hash_table_size (combo->elements) > 0
	    && val != combo->case_sensitive)
		return -1;
	else {
		combo->case_sensitive = val;
		if (val != combo->case_sensitive) {
			GHashFunc hashfunc;
			GCompareFunc comparefunc;

			g_hash_table_destroy (combo->elements);
			if (combo->case_sensitive) {
				hashfunc = g_str_hash;
				comparefunc = g_str_equal;
			} else {
				hashfunc = strcase_hash;
				comparefunc = strcase_equal;
			}
			combo->elements = g_hash_table_new (hashfunc,
							    comparefunc);
		}
		return 0;
	}
}

static void
entry_activate_cb (GtkWidget *entry, gpointer data)
{
	GalComboText *combo = GAL_COMBO_TEXT (data);

	update_list_selection (combo,
			       gtk_entry_get_text (GTK_ENTRY (combo->entry)));
}


static void
list_select_cb (GtkWidget *list, GtkWidget *child, gpointer data)
{
	GalComboText *combo = GAL_COMBO_TEXT (data);
	GtkEntry *entry = GTK_ENTRY (combo->entry);
	gchar *value = (gchar*) g_object_get_data (G_OBJECT (child), "value");

	g_return_if_fail (entry && value);

	if (combo->cached_entry == child)
		combo->cached_entry = NULL;

	gtk_entry_set_text (entry, value);
	gtk_signal_handler_block_by_func (GTK_OBJECT (entry), 
					  GTK_SIGNAL_FUNC (entry_activate_cb),
					  (gpointer) combo);
	g_signal_emit_by_name (entry, "activate");
	gtk_signal_handler_unblock_by_func (GTK_OBJECT (entry), 
					  GTK_SIGNAL_FUNC (entry_activate_cb),
					  (gpointer) combo);

	gal_combo_box_popup_hide (GAL_COMBO_BOX (data));
}

static void
list_unselect_cb (GtkWidget *list, GtkWidget *child, gpointer data)
{
	if (GTK_WIDGET_VISIBLE (list)) /* Undo interactive unselect */
		gtk_list_select_child (GTK_LIST (list), child);
}

static void
cb_toggle (GtkWidget *child, gpointer data)
{
	GalComboText *ct = GAL_COMBO_TEXT (data);

	gtk_list_select_child (GTK_LIST (ct->list), child);
}

void
gal_combo_text_select_item (GalComboText *ct, int elem)
{
	gtk_list_select_item (GTK_LIST(ct->list), elem);
}

static void
update_list_selection (GalComboText *ct, const gchar *text)
{
	gpointer candidate;
	GtkWidget *child;

	gtk_signal_handler_block_by_func (GTK_OBJECT (ct->list), 
					  GTK_SIGNAL_FUNC (list_select_cb),
					  (gpointer) ct);
	gtk_signal_handler_block_by_func (GTK_OBJECT (ct->list), 
					  GTK_SIGNAL_FUNC (list_unselect_cb),
					  (gpointer) ct);
	
	gtk_list_unselect_all (GTK_LIST (ct->list));
	candidate = g_hash_table_lookup (ct->elements, (gconstpointer) text);
	if (candidate && GTK_IS_WIDGET (candidate)) {
		child = GTK_WIDGET (candidate);
		gtk_list_select_child (GTK_LIST (ct->list), child);
		gtk_widget_grab_focus (child);
	}
	gtk_signal_handler_unblock_by_func (GTK_OBJECT (ct->list), 
					    GTK_SIGNAL_FUNC (list_select_cb),
					    (gpointer) ct);
	gtk_signal_handler_unblock_by_func (GTK_OBJECT (ct->list), 
					    GTK_SIGNAL_FUNC (list_unselect_cb),
					    (gpointer) ct);
}

void
gal_combo_text_set_text (GalComboText *ct, const gchar *text)
{
	gtk_entry_set_text (GTK_ENTRY (ct->entry), text);
	update_list_selection (ct, text);
}

/*
 * We can't just cache the old widget state on entry: If the pointer is
 * dragged, we receive two enter-notify-events, and the original cached
 * value would be overwritten with the GTK_STATE_ACTIVE we just set.
 *
 * However, we know that the gtklist only uses GTK_STATE_SELECTED and
 * GTK_STATE_NORMAL. We're OK if we only cache those two.
 */
static gboolean
cb_enter (GtkWidget *w, GdkEventCrossing *event,
	  gpointer user)
{
	GalComboText *ct = user;
	GtkStateType state = GTK_WIDGET_STATE (w);

	if (state == GTK_STATE_NORMAL || state == GTK_STATE_SELECTED) {
		ct->cached_entry = w;
		ct->cache_mouse_state = state;
	}
	if (state != GTK_STATE_SELECTED)
		gtk_widget_set_state (w, GTK_STATE_ACTIVE);

	return TRUE;
}
static gboolean
cb_exit (GtkWidget *w, GdkEventCrossing *event,
	  gpointer user)
{
	GalComboText *ct = user;

	if (ct->cached_entry == w)
		gtk_widget_set_state (w, ct->cache_mouse_state);

	return TRUE;
}

static gboolean
cb_pop_down (GtkWidget *w, GtkWidget *pop_down, gpointer dummy)
{
	GalComboText *ct = GAL_COMBO_TEXT (w);

	if (ct->cached_entry)
		gtk_widget_set_state (ct->cached_entry, ct->cache_mouse_state);
	ct->cached_entry = NULL;

	return FALSE;
}

typedef struct {
	GalComboText *ct;
	gchar *value;
} WeakRefClosure;

static void
cb_remove_from_hash (gpointer data, GObject *where_object_was)
{
	WeakRefClosure *closure = data;
	
	if (closure->ct->elements)
		g_hash_table_remove (closure->ct->elements, closure->value);

	g_free (closure->value);
	g_free (closure);
}

void
gal_combo_text_add_item (GalComboText *ct,
			 const gchar *item,
			 const gchar *value)
{
	WeakRefClosure *weak_ref_closure;
	GtkWidget *listitem;
	gchar *value_copy;

	g_return_if_fail (item);

	if (!value)
		value = item;

	value_copy = g_strdup (value);

	listitem = gtk_list_item_new_with_label (item);
	gtk_widget_show (listitem);

	g_object_set_data_full (G_OBJECT (listitem), "value",
				value_copy, g_free);
	g_signal_connect (listitem, "enter-notify-event",
			  G_CALLBACK (cb_enter),
			  (gpointer) ct);
	g_signal_connect (listitem, "leave-notify-event",
			  G_CALLBACK (cb_exit),
			  (gpointer) ct);
	g_signal_connect (listitem, "toggle",
			  G_CALLBACK (cb_toggle),
			  (gpointer) ct);

	gtk_container_add (GTK_CONTAINER (ct->list),
			   listitem);

	g_hash_table_insert (ct->elements, (gpointer)value_copy,
			     (gpointer) listitem);

	weak_ref_closure = g_new (WeakRefClosure, 1);
	weak_ref_closure->ct = ct;
	weak_ref_closure->value = g_strdup (value_copy);
	
	g_object_weak_ref (G_OBJECT (listitem),
			   cb_remove_from_hash,
			   weak_ref_closure);
}

static void
cb_list_mapped (GtkWidget *widget, gpointer user_data)
{
	GtkList *list = GTK_LIST (widget);

	if (g_list_length (list->selection) > 0)
		gtk_widget_grab_focus (GTK_WIDGET ((list->selection->data)));
}

void
gal_combo_text_construct (GalComboText *ct, gboolean const is_scrolled)
{
	GtkWidget *entry, *list, *scroll, *display_widget;

	ct->case_sensitive = FALSE;
	ct->elements = g_hash_table_new (&strcase_hash,
					 &strcase_equal);

	/* Probably irrelevant, but lets be careful */
	ct->cache_mouse_state = GTK_STATE_NORMAL;
	ct->cached_entry = NULL;

	entry = ct->entry = gtk_entry_new ();
	list = ct->list = gtk_list_new ();
	if (is_scrolled) {
		display_widget = scroll = gtk_scrolled_window_new (NULL, NULL);
		gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW(scroll),
						GTK_POLICY_NEVER,
						GTK_POLICY_AUTOMATIC);

		gtk_scrolled_window_add_with_viewport (
			GTK_SCROLLED_WINDOW(scroll), list);
		gtk_container_set_focus_hadjustment (
			GTK_CONTAINER (list),
			gtk_scrolled_window_get_hadjustment (
				GTK_SCROLLED_WINDOW (scroll)));
		gtk_container_set_focus_vadjustment (
			GTK_CONTAINER (list),
			gtk_scrolled_window_get_vadjustment (
				GTK_SCROLLED_WINDOW (scroll)));
		gtk_widget_set_usize (scroll, 0, 200); /* MAGIC NUMBER */
	} else
		display_widget = list;

	g_signal_connect (entry, "activate",
			  G_CALLBACK (entry_activate_cb),
			  ct);
	g_signal_connect (list, "select-child",
			  G_CALLBACK (list_select_cb),
			  ct);
	g_signal_connect (list, "unselect-child",
			  G_CALLBACK (list_unselect_cb),
			  ct);
	g_signal_connect (list, "map",
			  G_CALLBACK (cb_list_mapped), NULL);
	
	gtk_widget_show (display_widget);
	gtk_widget_show (entry);
	gal_combo_box_construct (GAL_COMBO_BOX (ct), entry, display_widget);
	g_signal_connect (ct, "pop_down_done",
			  G_CALLBACK (cb_pop_down), NULL);
}

GtkWidget*
gal_combo_text_new (gboolean const is_scrolled)
{
	GalComboText *ct;

	ct = g_object_new (GAL_COMBO_TEXT_TYPE, NULL);
	gal_combo_text_construct (ct, is_scrolled);
	return GTK_WIDGET (ct);
}

