/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2
 * as published by the Free Software Foundation.
 *
 * This program 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 this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

/*
 * This is an example of how to manage drag and drop with a sorted tree model
 *
 * Sergio Villar Senín, 2007. Igalia.
 *
 *  $ gcc -o tv-dnd `pkg-config --cflags --libs gtk+-2.0` -Wall treeview.c
 */

#include <gtk/gtk.h>
#include <string.h>

static guint timer_expander;

static GtkWidget *
create_window (GtkTreeView *treeview)
{
	GtkWidget *window;
	GtkWidget *scrolledwindow;

	window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
	gtk_window_set_title (GTK_WINDOW (window), "GtkTreeSortedModel dnd sample");
	gtk_window_set_default_size (GTK_WINDOW (window), 400, 250);

	scrolledwindow = gtk_scrolled_window_new (NULL, NULL);
	gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow),
                                      GTK_POLICY_AUTOMATIC,
                                      GTK_POLICY_AUTOMATIC);
	gtk_container_add (GTK_CONTAINER (window), scrolledwindow);

	gtk_container_add (GTK_CONTAINER (scrolledwindow), GTK_WIDGET (treeview));

	g_signal_connect ((gpointer) window, "delete_event",
			  G_CALLBACK (gtk_main_quit), NULL);

	return window;
}

static gint
cmp_rows (GtkTreeModel *tree_model, 
	  GtkTreeIter *iter1, 
	  GtkTreeIter *iter2,
	  gpointer user_data)
{
	gchar *name1, *name2;
	
	gtk_tree_model_get (tree_model, iter1, 0, &name1, -1);
	gtk_tree_model_get (tree_model, iter2, 0, &name2, -1);

	if (name1 == NULL && name2 == NULL)
		return 0;
	else if (name1 == NULL)
		return 1;
	else if (name2 == NULL)
		return -1;
	else
		return g_ascii_strcasecmp (name1, name2);
}

static GtkTreeModel *
create_model (void)
{
	GtkTreeStore *model;
	GtkTreeModel *model_sort;
	GtkTreeIter iter, par, sub;

	model = gtk_tree_store_new (1, G_TYPE_STRING);

	/* New model with two root nodes */
	gtk_tree_store_append (model, &par, NULL);
	gtk_tree_store_set (model, &par, 0, "root1", -1);

	gtk_tree_store_append (model, &par, NULL);
	gtk_tree_store_set (model, &par, 0, "root2", -1);

	gtk_tree_store_append (model, &iter, &par);
	gtk_tree_store_set (model, &iter, 0, "one", -1);

	gtk_tree_store_append (model, &iter, &par);
	gtk_tree_store_set (model, &iter, 0, "two", -1);

	gtk_tree_store_append (model, &sub, &iter);
	gtk_tree_store_set (model, &sub, 0, "a", -1);

	gtk_tree_store_append (model, &sub, &iter);
	gtk_tree_store_set (model, &sub, 0, "b", -1);

	gtk_tree_store_append (model, &iter, &par);
	gtk_tree_store_set (model, &iter, 0, "three", -1);

	model_sort = gtk_tree_model_sort_new_with_model (GTK_TREE_MODEL (model));

	gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (model_sort),
					      0, 
					      GTK_SORT_ASCENDING);

	gtk_tree_sortable_set_sort_func (GTK_TREE_SORTABLE (model_sort),
					 0,
					 cmp_rows, NULL, NULL);

	return model_sort;
}

static void
add_columns (GtkTreeView * treeview)
{
	GtkCellRenderer *renderer;
	GtkTreeViewColumn *column;

	column = gtk_tree_view_column_new ();
	gtk_tree_view_column_set_title (column, "Text");

	renderer = gtk_cell_renderer_text_new ();
	gtk_tree_view_column_pack_start (column, renderer, TRUE);
	gtk_tree_view_column_set_attributes (column, renderer,
                         "text", 0, NULL);

	gtk_tree_view_append_column (treeview, column);
}

static void
drag_data_get_cb (GtkWidget *widget, 
		  GdkDragContext *context, 
		  GtkSelectionData *selection_data, 
		  guint info, 
		  guint time, 
		  gpointer data)
{
	GtkTreeSelection *selection;
	GtkTreeModel *model_sort, *model;
	GtkTreeIter iter;
	GtkTreePath *source_row_sort;
	GtkTreePath *source_row;

	/* Get the path to the selected row */
	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
	gtk_tree_selection_get_selected (selection, &model_sort, &iter);
	source_row_sort = gtk_tree_model_get_path (model_sort, &iter);

	/* Get the unsorted model and the path to the selected row in
	   the unsorted model */
	model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (model_sort));
	source_row = gtk_tree_model_sort_convert_path_to_child_path (GTK_TREE_MODEL_SORT (model_sort),
								     source_row_sort);

	/* Set the row as the drag data */
	gtk_tree_set_row_drag_data (selection_data,
				    model,
				    source_row);

	gtk_tree_path_free (source_row_sort);
	gtk_tree_path_free (source_row);
}

static void 
drag_data_received_cb (GtkWidget *widget, 
		       GdkDragContext *context, 
		       gint x, 
		       gint y, 
		       GtkSelectionData *selection_data, 
		       guint info, 
		       guint time, 
		       gpointer data)
{
	GtkTreeModel *model_sort, *model;
	GtkTreeRowReference *source_row_reference;
 	GtkTreePath *source_row, *dest_row, *child_dest_row;
	GtkTreeViewDropPosition pos;
	gboolean success = FALSE;

	/* Do not allow further process */
	g_signal_stop_emission_by_name (widget, "drag-data-received");

	if (selection_data->length == 0)
		goto out;

	/* Get the unsorted model, and the path to the source row */
	model_sort = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
	gtk_tree_get_row_drag_data (selection_data,
				    &model,
				    &source_row);

	/* Get the path to the destination row. Can not call
	   gtk_tree_view_get_drag_dest_row() because the source row
	   it's not selected anymore */
	gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget),
					   x, y,
					   &dest_row,
					   &pos);

	/* Only allow drops IN other rows */
	if (!dest_row || 
	    pos == GTK_TREE_VIEW_DROP_BEFORE ||
	    pos == GTK_TREE_VIEW_DROP_AFTER)
		goto out;

	/* Get the destination row in the usorted model */
	child_dest_row = 
		gtk_tree_model_sort_convert_path_to_child_path (GTK_TREE_MODEL_SORT (model_sort),
								dest_row);
	gtk_tree_path_free (dest_row);

	/* Check if the drag is possible */
	if (!gtk_tree_drag_dest_row_drop_possible (GTK_TREE_DRAG_DEST (model),
						   child_dest_row,
						   selection_data)) {
		gtk_tree_path_free (child_dest_row);
		goto out;
	}

	/* Get a row reference to the source path because the path
	   could change after the insertion. The gtk_drag_finish() is
	   not able to delete the source because that, so we have to
	   do it manually */
	source_row_reference = gtk_tree_row_reference_new (model, source_row);
	gtk_tree_path_free (source_row);

	/* Insert the dragged row as a child of the dest row */
	gtk_tree_path_down (child_dest_row);
	if (gtk_tree_drag_dest_drag_data_received (GTK_TREE_DRAG_DEST (model),
						   child_dest_row,
						   selection_data)) {

		/* Clean dest row */
		gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget),
						 NULL,
						 GTK_TREE_VIEW_DROP_BEFORE);

		/* Get the new path of the source row */
		source_row = gtk_tree_row_reference_get_path (source_row_reference);

		/* Delete the source row */
		gtk_tree_drag_source_drag_data_delete (GTK_TREE_DRAG_SOURCE (model),
						       source_row);

		success = TRUE;

		gtk_tree_path_free (source_row);
	}

	gtk_tree_row_reference_free (source_row_reference);
	gtk_tree_path_free (child_dest_row);
 out:
	/* Never delete the source, we do it manually */
	gtk_drag_finish (context, success, FALSE, time);
}

/* This function expands a tree node candidate to be the destination
   row of a drag if it's not already expanded. Comes from the
   gtktreeview.c source */
static gint
expand_row_timeout (gpointer data)
{
  GtkTreeView *tree_view = data;
  GtkTreePath *dest_path = NULL;
  GtkTreeViewDropPosition pos;
  gboolean result = FALSE;

  GDK_THREADS_ENTER ();

  gtk_tree_view_get_drag_dest_row (tree_view,
                                   &dest_path,
                                   &pos);

  if (dest_path &&
      (pos == GTK_TREE_VIEW_DROP_INTO_OR_AFTER ||
       pos == GTK_TREE_VIEW_DROP_INTO_OR_BEFORE)) {
	  gtk_tree_view_expand_row (tree_view, dest_path, FALSE);
	  gtk_tree_path_free (dest_path);
  }
  else {
	  if (dest_path)
		  gtk_tree_path_free (dest_path);
	  
	  result = TRUE;
  }
  
  GDK_THREADS_LEAVE ();

  return result;
}


static gboolean
drag_motion_cb (GtkWidget      *widget,
		GdkDragContext *context,
		gint            x,
		gint            y,
		guint           time,
		gpointer        user_data)  
{
	GtkTreeViewDropPosition pos;
	GtkTreePath *dest_row;

	/* Remove all pending expander timeouts */
	if (timer_expander != 0) {
		g_source_remove (timer_expander);
		timer_expander = 0;
	}

	/* Get the destination row */
	gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW (widget),
					   x, y,
					   &dest_row,
					   &pos);

	if (!dest_row)
		return FALSE;

	/* Expand the selected row after 1/2 second */
	if (!gtk_tree_view_row_expanded (GTK_TREE_VIEW (widget), dest_row)) {
		gtk_tree_view_set_drag_dest_row (GTK_TREE_VIEW (widget), dest_row, pos);
		timer_expander = g_timeout_add (500, expand_row_timeout, widget);
	}
	gtk_tree_path_free (dest_row);

	return TRUE;
}

/* The types of the data that could be dragged */
static const GtkTargetEntry drag_types[] =
{
	{ "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, 0 }
};

static void
setup_drag_and_drop (GtkTreeView *self)
{
	/* We could not use the specific treeview dnd API because that
	   does not give us enough control over the process. Most of
	   these functions do not work with sorted model so we have to
	   redefine several behaviours */

	/* Config the treeview as destination for dnd */
	gtk_drag_dest_set (GTK_WIDGET (self),
			   GTK_DEST_DEFAULT_ALL,
			   drag_types,
			   G_N_ELEMENTS (drag_types),
			   GDK_ACTION_MOVE);
	
	gtk_signal_connect(GTK_OBJECT (self),
			   "drag_data_received",
			   GTK_SIGNAL_FUNC(drag_data_received_cb),
			   NULL);
	

	/* Config the treeview as source for dnd */
	gtk_drag_source_set (GTK_WIDGET (self),
			     GDK_BUTTON1_MASK | GDK_BUTTON3_MASK,
			     drag_types,
			     G_N_ELEMENTS (drag_types),
			     GDK_ACTION_MOVE);


	gtk_signal_connect(GTK_OBJECT (self),
			   "drag_motion",
			   GTK_SIGNAL_FUNC(drag_motion_cb),
			   NULL);


	gtk_signal_connect(GTK_OBJECT (self),
			   "drag_data_get",
			   GTK_SIGNAL_FUNC(drag_data_get_cb),
			   NULL);
}


int
main (int argc, char *argv[])
{
	GtkWidget *window, *treeview;
	GtkTreeModel *model;

	gtk_init (&argc, &argv);

	/* Create treeview and model */
	treeview = gtk_tree_view_new ();
	add_columns (GTK_TREE_VIEW (treeview));
	setup_drag_and_drop (GTK_TREE_VIEW (treeview));

	model = create_model ();
	gtk_tree_view_set_model (GTK_TREE_VIEW (treeview), model);
	g_object_unref (G_OBJECT (model));

	/* Create window */
	window = create_window (GTK_TREE_VIEW (treeview));
	gtk_widget_show_all (window);

	gtk_main ();
	return 0;
}
