/*
 * 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
 *
 *  $ gcc -o treeview-dnd `pkg-config --cflags --libs gtk+-2.0` -Wall treeview.c
 */

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

static guint timer_expander;

enum {
	TARGET_TREE_ROW,
};

static const GtkTargetEntry drag_types[] =
{
	{ "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, TARGET_TREE_ROW },
};

#define ROW_REF_DATA_NAME "row-ref"

/*************************************************************************/
/*                        U I    F U N C T I O N S                       */
/*************************************************************************/
static GtkWidget *
create_window (GtkWidget *top_treeview, GtkWidget *down_treeview)
{
	GtkWidget *window;
	GtkWidget *scrolledwindow;
	GtkWidget *hpaned;

	window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
	gtk_window_set_title (GTK_WINDOW (window), "GtkTreeView sample");
	gtk_window_set_default_size (GTK_WINDOW (window), 300, 300);

	hpaned = gtk_hpaned_new ();

	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_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolledwindow), 
					       hpaned);

	gtk_paned_add1 (GTK_PANED (hpaned), top_treeview);
	gtk_paned_add2 (GTK_PANED (hpaned), down_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 GtkTreeModel *
create_list_model (void)
{
	GtkListStore *model;
	GtkTreeIter iter;

	model = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_INT);

	gtk_list_store_append (model, &iter);
	gtk_list_store_set (model, &iter, 0, "line1", 1, 5, -1);

	gtk_list_store_append (model, &iter);
	gtk_list_store_set (model, &iter, 0, "line2", 1, 6, -1);

	gtk_list_store_append (model, &iter);
	gtk_list_store_set (model, &iter, 0, "line3", 1, 24, -1);

	gtk_list_store_append (model, &iter);
	gtk_list_store_set (model, &iter, 0, "line4", 1, 15, -1);

	return GTK_TREE_MODEL (model);
}


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
add_list_columns (GtkTreeView * treeview)
{
	GtkCellRenderer *renderer;
	GtkTreeViewColumn *column;

	column = gtk_tree_view_column_new ();
	gtk_tree_view_column_set_title (column, "Name");
	
	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);
	
	column = gtk_tree_view_column_new ();
	gtk_tree_view_column_set_title (column, "Number 1");
	
	renderer = gtk_cell_renderer_text_new ();
	gtk_tree_view_column_pack_start (column, renderer, TRUE);
	gtk_tree_view_column_set_attributes (column, renderer,
					     "text", 1, NULL);
	
	gtk_tree_view_append_column (treeview, column);
}

/*************************************************************************/
/*                     D&D GtkTreeStore Handlers                         */
/*************************************************************************/
static void
drag_data_get_cb (GtkWidget *widget, 
		  GdkDragContext *context, 
		  GtkSelectionData *selection_data, 
		  guint target_type, 
		  guint time, 
		  gpointer data)
{
	GtkTreeSelection *selection;
	GtkTreeModel *model_sort, *model;
	GtkTreeIter iter;
	GtkTreePath *source_row_sort;
	GtkTreePath *source_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);

	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);

	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 target_type, 
		       guint time, 
		       gpointer data)
{
	GtkWidget *source_widget;
	GtkTreeModel *model_sort, *dest_model, *source_model;
 	GtkTreePath *source_row, *dest_row, *child_dest_row;
	GtkTreeViewDropPosition pos;
	gboolean success = FALSE;
	gboolean delete_source = FALSE;

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

	/* Check if the get_data failed */
	if (selection_data == NULL || selection_data->length < 0)
		return;

	/* Get the action */
	if (context->action == GDK_ACTION_MOVE)
		delete_source = TRUE;

	/* Get the models */
	source_widget = gtk_drag_get_source_widget (context);
	model_sort = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));

	gtk_tree_get_row_drag_data (selection_data,
				    &source_model,
				    &source_row);

	/* Select the destination model */
	if (source_widget == widget)
		dest_model = source_model;
	else
		dest_model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (model_sort));


	/* 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 finish;

	/* 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);

	/* Drags from the list */
	if ((target_type == TARGET_TREE_ROW) && (source_widget != widget)) {
		GtkTreeIter iter;
		gint num;
		gchar *name;

		gtk_tree_model_get_iter (source_model, &iter, source_row);
		gtk_tree_model_get (source_model, &iter, 1, &num, -1);

		gtk_tree_model_get_iter (dest_model, &iter, child_dest_row);
		gtk_tree_model_get (dest_model, &iter, 0, &name, -1);

		g_print ("Row with number %d %s to %s\n", num, 
			 (delete_source) ? "moved" : "copied", name);

		success = TRUE;
	} else {
		GtkTreeRowReference *source_row_reference;

		/* Check if the drag is possible */
		if (!gtk_tree_path_compare (source_row, child_dest_row))
			goto out;

		if (!gtk_tree_drag_dest_row_drop_possible (GTK_TREE_DRAG_DEST (dest_model),
							   child_dest_row,
							   selection_data))
			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 (source_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 (dest_model),
							   child_dest_row,
							   selection_data)) {

			source_row = gtk_tree_row_reference_get_path (source_row_reference);

			success = TRUE;
		}
		gtk_tree_row_reference_free (source_row_reference);
	}
 out:
	gtk_tree_path_free (child_dest_row);

	/* Save the new path, will be used by the
	   drag-data-delete handler */
	if (success)
		g_object_set_data (G_OBJECT (source_widget),
				   ROW_REF_DATA_NAME,
				   gtk_tree_path_copy (source_row));

 finish:
	gtk_drag_finish (context, success, (success && delete_source), time);
}

static gboolean
drag_drop_cb (GtkWidget      *widget,
	      GdkDragContext *context,
	      gint            x,
	      gint            y,
	      guint           time,
	      gpointer        user_data) 
{
	if (!context->targets)
		return FALSE;

	/* Request the data from the source. */
	gtk_drag_get_data(widget, 
			  context, 
			  GDK_POINTER_TO_ATOM (context->targets->data), 
			  time);

    return TRUE;
}

static void 
drag_data_delete_cb (GtkWidget      *widget,
		     GdkDragContext *context,
		     gpointer        user_data)
{
	GtkTreePath *source_row;
	GtkTreeModel *model_sort, *model;

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

	model_sort = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
	model = gtk_tree_model_sort_get_model (GTK_TREE_MODEL_SORT (model_sort));
	source_row = g_object_steal_data (G_OBJECT (widget), ROW_REF_DATA_NAME);

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

	gtk_tree_path_free (source_row);
}
     
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;
	GdkDragAction suggested_action;

	if (timer_expander != 0) {
		g_source_remove (timer_expander);
		timer_expander = 0;
	}

	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);

	/* Select the desired action. By default we pick MOVE */
	suggested_action = GDK_ACTION_MOVE;

        if (context->actions == GDK_ACTION_COPY)
            gdk_drag_status(context, GDK_ACTION_COPY, time);
	else if (context->actions == GDK_ACTION_MOVE)
            gdk_drag_status(context, GDK_ACTION_MOVE, time);
	else if (context->actions & suggested_action)
            gdk_drag_status(context, suggested_action, time);
	else
            gdk_drag_status(context, GDK_ACTION_DEFAULT, time);

	return TRUE;
}

/*************************************************************************/
/*                     D&D GtkListStore Handlers                         */
/*************************************************************************/
static void
list_drag_data_get_cb (GtkWidget *widget, 
		       GdkDragContext *context, 
		       GtkSelectionData *selection_data, 
		       guint target_type, 
		       guint time, 
		       gpointer data)
{
	GtkTreeSelection *selection;
	GtkTreeModel *model;
	GtkTreeIter iter;
	GtkTreePath *source_row;

	selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
	gtk_tree_selection_get_selected (selection, &model, &iter);
	source_row = gtk_tree_model_get_path (model, &iter);

	gtk_tree_set_row_drag_data (selection_data,
				    model,
				    source_row);

	gtk_tree_path_free (source_row);
}

static void 
list_drag_data_delete_cb (GtkWidget      *widget,
			  GdkDragContext *context,
			  gpointer        user_data)
{
	GtkTreePath *source_row;
	GtkTreeModel *model;

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

	model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
	source_row = g_object_steal_data (G_OBJECT (widget), ROW_REF_DATA_NAME);

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

	gtk_tree_path_free (source_row);
}



/*************************************************************************/
/*                     I N I T   D R A G  &  D R O P                     */
/*************************************************************************/
static void
setup_drag_and_drop (GtkWidget *treeview,
		     GtkWidget *list)
{
	/* Treeview settings */
	gtk_drag_dest_set (treeview,
			   GTK_DEST_DEFAULT_HIGHLIGHT,
			   drag_types,
			   G_N_ELEMENTS (drag_types),
			   GDK_ACTION_MOVE | GDK_ACTION_COPY);

	gtk_signal_connect(GTK_OBJECT (treeview),
			   "drag_data_received",
			   GTK_SIGNAL_FUNC(drag_data_received_cb),
			   NULL);

	gtk_drag_source_set (treeview,
			     GDK_BUTTON1_MASK,
			     drag_types,
			     G_N_ELEMENTS (drag_types),
			     GDK_ACTION_COPY | GDK_ACTION_MOVE);

	gtk_signal_connect(GTK_OBJECT (treeview),
			   "drag_data_delete",
			   GTK_SIGNAL_FUNC(drag_data_delete_cb),
			   NULL);

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

	gtk_signal_connect(GTK_OBJECT (treeview),
			   "drag_data_get",
			   GTK_SIGNAL_FUNC(drag_data_get_cb),
			   NULL);

	gtk_signal_connect(GTK_OBJECT (treeview),
			   "drag_drop",
			   GTK_SIGNAL_FUNC(drag_drop_cb),
			   NULL);

	/* List settings */
	gtk_drag_source_set (list,
			     GDK_BUTTON1_MASK,
			     drag_types,
			     G_N_ELEMENTS (drag_types),
			     GDK_ACTION_COPY | GDK_ACTION_MOVE);
	
	gtk_signal_connect(GTK_OBJECT (list),
			   "drag_data_get",
			   GTK_SIGNAL_FUNC(list_drag_data_get_cb),
			   NULL);

	gtk_signal_connect(GTK_OBJECT (list),
			   "drag_data_delete",
			   GTK_SIGNAL_FUNC(list_drag_data_delete_cb),
			   NULL);

}

/*************************************************************************/
/*                                M A I N                                */
/*************************************************************************/

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

	gtk_init (&argc, &argv);

	/* TreeView */
	treeview = gtk_tree_view_new ();
	add_columns (GTK_TREE_VIEW (treeview));

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

	/* List model */
	list = gtk_tree_view_new ();
	add_list_columns (GTK_TREE_VIEW (list));

	list_model = create_list_model ();
	gtk_tree_view_set_model (GTK_TREE_VIEW (list), list_model);
	g_object_unref (G_OBJECT (list_model));

	/* Window */
	setup_drag_and_drop (treeview, list);

	window = create_window (treeview, list);

	gtk_widget_show_all (window);

	gtk_main ();
	return 0;
}
