/* eggtreemultidnd.c
 * Copyright (C) 2001  Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * 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., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#define EGG_TREE_MULTI_DND_STRING "EggTreeMultiDndString"

typedef struct
{
    guint pressed_button;
    gint x;
    gint y;
    guint motion_notify_handler;
    guint button_release_handler;
    guint drag_data_get_handler;
    GSList *event_list;
} EggTreeMultiDndData;

/* CUT-N-PASTE from gtktreeview.c */
typedef struct _TreeViewDragInfo TreeViewDragInfo;
struct _TreeViewDragInfo
{
    GdkModifierType start_button_mask;
    GtkTargetList *source_target_list;
    GdkDragAction source_actions;

    GtkTargetList *dest_target_list;

    guint source_set : 1;
    guint dest_set : 1;
};


GType
egg_tree_multi_drag_source_get_type (void)
{
    static GType our_type = 0;

    if (!our_type)
    {
        const GTypeInfo our_info =
        {
            sizeof (EggTreeMultiDragSourceIface), /* class_size */
            NULL,		/* base_init */
            NULL,		/* base_finalize */
            NULL,
            NULL,		/* class_finalize */
            NULL,		/* class_data */
            0,
            0,              /* n_preallocs */
            NULL
        };

        our_type = g_type_register_static (G_TYPE_INTERFACE, "EggTreeMultiDragSource", &our_info, 0);
    }

    return our_type;
}


/**
 * egg_tree_multi_drag_source_row_draggable:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row on which user is initiating a drag
 *
 * Asks the #EggTreeMultiDragSource whether a particular row can be used as
 * the source of a DND operation. If the source doesn't implement
 * this interface, the row is assumed draggable.
 *
 * Return value: %TRUE if the row can be dragged
 **/
gboolean
egg_tree_multi_drag_source_row_draggable (EggTreeMultiDragSource *drag_source,
        GList                  *path_list)
{
    EggTreeMultiDragSourceIface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

    g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
    g_return_val_if_fail (iface->row_draggable != NULL, FALSE);
    g_return_val_if_fail (path_list != NULL, FALSE);

    if (iface->row_draggable)
        return (* iface->row_draggable) (drag_source, path_list);
    else
        return TRUE;
}


/**
 * egg_tree_multi_drag_source_drag_data_delete:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row that was being dragged
 *
 * Asks the #EggTreeMultiDragSource to delete the row at @path, because
 * it was moved somewhere else via drag-and-drop. Returns %FALSE
 * if the deletion fails because @path no longer exists, or for
 * some model-specific reason. Should robustly handle a @path no
 * longer found in the model!
 *
 * Return value: %TRUE if the row was successfully deleted
 **/
gboolean
egg_tree_multi_drag_source_drag_data_delete (EggTreeMultiDragSource *drag_source,
        GList                  *path_list)
{
    EggTreeMultiDragSourceIface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

    g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
    g_return_val_if_fail (iface->drag_data_delete != NULL, FALSE);
    g_return_val_if_fail (path_list != NULL, FALSE);

    return (* iface->drag_data_delete) (drag_source, path_list);
}

/**
 * egg_tree_multi_drag_source_drag_data_get:
 * @drag_source: a #EggTreeMultiDragSource
 * @path: row that was dragged
 * @selection_data: a #EggSelectionData to fill with data from the dragged row
 *
 * Asks the #EggTreeMultiDragSource to fill in @selection_data with a
 * representation of the row at @path. @selection_data->target gives
 * the required type of the data.  Should robustly handle a @path no
 * longer found in the model!
 *
 * Return value: %TRUE if data of the required type was provided
 **/
gboolean
egg_tree_multi_drag_source_drag_data_get    (EggTreeMultiDragSource *drag_source,
        GList                  *path_list,
        GtkSelectionData  *selection_data)
{
    EggTreeMultiDragSourceIface *iface = EGG_TREE_MULTI_DRAG_SOURCE_GET_IFACE (drag_source);

    g_return_val_if_fail (EGG_IS_TREE_MULTI_DRAG_SOURCE (drag_source), FALSE);
    g_return_val_if_fail (iface->drag_data_get != NULL, FALSE);
    g_return_val_if_fail (path_list != NULL, FALSE);
    g_return_val_if_fail (selection_data != NULL, FALSE);

    return (* iface->drag_data_get) (drag_source, path_list, selection_data);
}

static void
stop_drag_check (GtkWidget *widget)
{
    EggTreeMultiDndData *priv_data;
    GSList *l;

    priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);

    for (l = priv_data->event_list; l != NULL; l = l->next)
        gdk_event_free (l->data);

    g_slist_free (priv_data->event_list);
    priv_data->event_list = NULL;
    g_signal_handler_disconnect (widget, priv_data->motion_notify_handler);
    g_signal_handler_disconnect (widget, priv_data->button_release_handler);
}

static gboolean
egg_tree_multi_drag_button_release_event (GtkWidget      *widget,
        GdkEventButton *event,
        gpointer        data)
{
    EggTreeMultiDndData *priv_data;
    GSList *l;

    priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);

    for (l = priv_data->event_list; l != NULL; l = l->next)
        gtk_propagate_event (widget, l->data);

    stop_drag_check (widget);

    return FALSE;
}

static void
selection_foreach (GtkTreeModel *model,
                   GtkTreePath  *path,
                   GtkTreeIter  *iter,
                   gpointer      data)
{
    GList **list_ptr;

    list_ptr = (GList **) data;

    *list_ptr = g_list_prepend (*list_ptr, gtk_tree_row_reference_new (model, path));
}

static void
path_list_free (GList *path_list)
{
    g_list_foreach (path_list, (GFunc) gtk_tree_row_reference_free, NULL);
    g_list_free (path_list);
}

static void
set_context_data (GdkDragContext *context,
                  GList          *path_list)
{
    g_object_set_data_full (G_OBJECT (context),
                            "egg-tree-view-multi-source-row",
                            path_list,
                            (GDestroyNotify) path_list_free);
}

static GList *
get_context_data (GdkDragContext *context)
{
    return g_object_get_data (G_OBJECT (context),
                              "egg-tree-view-multi-source-row");
}

/* CUT-N-PASTE from gtktreeview.c */
static TreeViewDragInfo*
get_info (GtkTreeView *tree_view)
{
    return g_object_get_data (G_OBJECT (tree_view), "gtk-tree-view-drag-info");
}


static void
egg_tree_multi_drag_drag_data_get (GtkWidget        *widget,
                                   GdkDragContext   *context,
                                   GtkSelectionData *selection_data,
                                   guint             info,
                                   guint             time)
{
    GtkTreeView *tree_view;
    GtkTreeModel *model;
    TreeViewDragInfo *di;
    GList *path_list;

    tree_view = GTK_TREE_VIEW (widget);

    model = gtk_tree_view_get_model (tree_view);

    if (model == NULL)
        return;

    di = get_info (GTK_TREE_VIEW (widget));

    if (di == NULL)
        return;

    path_list = get_context_data (context);

    if (path_list == NULL)
        return;

    /* We can implement the GTK_TREE_MODEL_ROW target generically for
     * any model; for DragSource models there are some other targets
     * we also support.
     */

    if (EGG_IS_TREE_MULTI_DRAG_SOURCE (model))
    {
        egg_tree_multi_drag_source_drag_data_get (EGG_TREE_MULTI_DRAG_SOURCE (model),
                path_list,
                selection_data);
    }
}

static gboolean
egg_tree_multi_drag_motion_event (GtkWidget      *widget,
                                  GdkEventMotion *event,
                                  gpointer        data)
{
    EggTreeMultiDndData *priv_data;

    priv_data = g_object_get_data (G_OBJECT (widget), EGG_TREE_MULTI_DND_STRING);

    if (gtk_drag_check_threshold (widget,
                                  priv_data->x,
                                  priv_data->y,
                                  event->x,
                                  event->y))
    {
        GList *path_list = NULL;
        GtkTreeSelection *selection;
        GtkTreeModel *model;
        GdkDragContext *context;
        TreeViewDragInfo *di;

        di = get_info (GTK_TREE_VIEW (widget));

        if (di == NULL)
            return FALSE;

        selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (widget));
        stop_drag_check (widget);
        gtk_tree_selection_selected_foreach (selection, selection_foreach, &path_list);
        path_list = g_list_reverse (path_list);
        model = gtk_tree_view_get_model (GTK_TREE_VIEW (widget));
        if (egg_tree_multi_drag_source_row_draggable (EGG_TREE_MULTI_DRAG_SOURCE (model), path_list))
        {

            context = gtk_drag_begin (widget,
                                      gtk_drag_source_get_target_list (widget),
                                      di->source_actions,
                                      priv_data->pressed_button,
                                      (GdkEvent*)event);
            set_context_data (context, path_list);
            gtk_drag_set_icon_default (context);

        }
        else
        {
            path_list_free (path_list);
        }
    }

    return TRUE;
}

static gboolean
egg_tree_multi_drag_button_press_event (GtkWidget      *widget,
                                        GdkEventButton *event,
                                        gpointer        data)
{
    GtkTreeView *tree_view;
    GtkTreePath *path = NULL;
    GtkTreeViewColumn *column = NULL;
    gint cell_x, cell_y;
    GtkTreeSelection *selection;
    EggTreeMultiDndData *priv_data;

    tree_view = GTK_TREE_VIEW (widget);
    priv_data = g_object_get_data (G_OBJECT (tree_view), EGG_TREE_MULTI_DND_STRING);
    if (priv_data == NULL)
    {
        priv_data = g_new0 (EggTreeMultiDndData, 1);
        g_object_set_data (G_OBJECT (tree_view), EGG_TREE_MULTI_DND_STRING, priv_data);
    }

    if (g_slist_find (priv_data->event_list, event))
        return FALSE;

    if (priv_data->event_list)
    {
        /* save the event to be propagated in order */
        priv_data->event_list = g_slist_append (priv_data->event_list, gdk_event_copy ((GdkEvent*)event));
        return TRUE;
    }

    if (event->type == GDK_2BUTTON_PRESS)
        return FALSE;

    gtk_tree_view_get_path_at_pos (tree_view,
                                   event->x, event->y,
                                   &path, &column,
                                   &cell_x, &cell_y);

    selection = gtk_tree_view_get_selection (tree_view);

    if (path && gtk_tree_selection_path_is_selected (selection, path))
    {
        priv_data->pressed_button = event->button;
        priv_data->x = event->x;
        priv_data->y = event->y;
        priv_data->event_list = g_slist_append (priv_data->event_list, gdk_event_copy ((GdkEvent*)event));
        priv_data->motion_notify_handler =
            g_signal_connect (G_OBJECT (tree_view), "motion_notify_event", G_CALLBACK (egg_tree_multi_drag_motion_event), NULL);
        priv_data->button_release_handler =
            g_signal_connect (G_OBJECT (tree_view), "button_release_event", G_CALLBACK (egg_tree_multi_drag_button_release_event), NULL);

        if (priv_data->drag_data_get_handler == 0)
        {
            priv_data->drag_data_get_handler =
                g_signal_connect (G_OBJECT (tree_view), "drag_data_get", G_CALLBACK (egg_tree_multi_drag_drag_data_get), NULL);
        }

        gtk_tree_path_free (path);

        return TRUE;
    }

    if (path)
    {
        gtk_tree_path_free (path);
    }

    return FALSE;
}

void
egg_tree_multi_drag_add_drag_support (GtkTreeView *tree_view)
{
    g_return_if_fail (GTK_IS_TREE_VIEW (tree_view));
    g_signal_connect (G_OBJECT (tree_view), "button_press_event", G_CALLBACK (egg_tree_multi_drag_button_press_event), NULL);
}