/*
 *  $Id: selectionmanager.c 30151 2026-06-25 13:17:45Z yeti-dn $
 *  Copyright (C) 2009-2026 David Necas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program 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 2 of the License, or (at your option) any
 *  later version.
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
#include "config.h"
#include <glib/gi18n-lib.h>
#include <string.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>
#include <gwy.h>

/* The GtkTargetEntry for tree model drags.
 * FIXME: Is it Gtk+ private or what? */
#define GTK_TREE_MODEL_ROW { "GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_APP, 0 }

enum {
    COLUMN_KEY, COLUMN_OBJECT, NCOLUMNS
};

enum {
    PARAM_ALL_FILES,
};

typedef struct {
    GwySelection *selection;
    const gchar *name;
    GwyUnit *xyunit;
    gdouble origin[2];
} DistributeData;

#define GWY_TYPE_TOOL_SELECTION_MANAGER            (gwy_tool_selection_manager_get_type())
#define GWY_TOOL_SELECTION_MANAGER(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_SELECTION_MANAGER, GwyToolSelectionManager))
#define GWY_IS_TOOL_SELECTION_MANAGER(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_SELECTION_MANAGER))
#define GWY_TOOL_SELECTION_MANAGER_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_SELECTION_MANAGER, GwyToolSelectionManagerClass))

typedef struct _GwyToolSelectionManager      GwyToolSelectionManager;
typedef struct _GwyToolSelectionManagerClass GwyToolSelectionManagerClass;

struct _GwyToolSelectionManager {
    GwyPlainTool parent_instance;

    GwyParams *params;
    gboolean in_setup;

    /* Have our own file container pointer to use gwy_set_member_object(). */
    GwyFile *file;
    const gchar *prefix;
    gulong item_changed_id;
    gboolean doing_mass_change;

    GtkListStore *model;
    GtkWidget *treeview;
    GtkWidget *allfiles;
    GtkWidget *distribute;
    GtkWidget *copy;
    GtkWidget *export;
    GtkWidget *delete;
};

struct _GwyToolSelectionManagerClass {
    GwyPlainToolClass parent_class;
};

static GType gwy_tool_selection_manager_get_type(void) G_GNUC_CONST;

static gboolean     module_register            (void);
static GwyParamDef* define_module_params       (void);
static void         finalize                   (GObject *object);
static void         init_dialog                (GwyToolSelectionManager *tool);
static void         data_switched              (GwyTool *gwytool,
                                                GwyDataView *data_view);
static void         file_item_changed          (GwyToolSelectionManager *tool,
                                                GQuark key,
                                                GwyDict *data);
static void         update_selection_item      (GwyToolSelectionManager *tool,
                                                GQuark quark,
                                                gboolean addit);
static void         tool_response              (GwyTool *gwytool,
                                                gint response_id);
static void         clear_selection            (GwyToolSelectionManager *tool);
static void         list_selection_changed     (GwyToolSelectionManager *tool,
                                                GtkTreeSelection *selection);
static void         all_files_toggled          (GwyToolSelectionManager *tool,
                                                GtkToggleButton *button);
static gboolean     key_pressed                (GwyToolSelectionManager *tool,
                                                GdkEventKey *event,
                                                GtkTreeView *treeview);
static void         distribute                 (GwyToolSelectionManager *tool);
static void         distribute_one             (GwyFile *file,
                                                gpointer user_data);
static void         copy_selection_to_clipboard(GwyToolSelectionManager *tool);
static void         export_selection           (GwyToolSelectionManager *tool);
static void         delete_selection           (GwyToolSelectionManager *tool);
static gchar*       create_report              (GwyToolSelectionManager *tool);
static void         setup_layer                (GwyToolSelectionManager *tool,
                                                guint current,
                                                GQuark quark);

/* NB: This is not 1:1 mapping because LayerCross also uses SelectionPoint. We might also want to add some knowledge
 * of selection names in the file here. We need to use get-type functions because the types might not have been
 * registered yet and type macros cannot be used to initialise static data. */
static struct {
    const gchar *humanname;
    GType layer_type;
    GType selection_type;
    GType (*get_type)(void);
}
selection_info[] = {
    { N_("Axis lines"),     0, 0, gwy_layer_axis_get_type,      },
    { N_("Cross lines"),    0, 0, gwy_layer_cross_get_type,     },
    { N_("Ellipses"),       0, 0, gwy_layer_ellipse_get_type,   },
    { N_("Lattice"),        0, 0, gwy_layer_lattice_get_type,   },
    { N_("Lines"),          0, 0, gwy_layer_line_get_type,      },
    { N_("Points"),         0, 0, gwy_layer_point_get_type,     },
    { N_("Quadrilaterals"), 0, 0, gwy_layer_quad_get_type,      },
    { N_("Rectangles"),     0, 0, gwy_layer_rectangle_get_type, },
    { N_("Spline path"),    0, 0, gwy_layer_path_get_type,      },
};

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Selection manager tool which lists, clears and copies selections."),
    "Yeti <yeti@gwyddion.net>",
    "4.0",
    "David Nečas (Yeti)",
    "2009",
};

GWY_MODULE_QUERY2(module_info, selectionmanager)

G_DEFINE_TYPE(GwyToolSelectionManager, gwy_tool_selection_manager, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    for (guint i = 0; i < G_N_ELEMENTS(selection_info); i++) {
        GType type = selection_info[i].get_type();
        g_assert(g_type_is_a(type, GWY_TYPE_VECTOR_LAYER));

        selection_info[i].layer_type = type;
        GTypeClass *klass = g_type_class_ref(type);
        selection_info[i].selection_type = gwy_vector_layer_class_get_selection_type(GWY_VECTOR_LAYER_CLASS(klass));
        g_type_class_unref(klass);
    }

    gwy_tool_func_register(GWY_TYPE_TOOL_SELECTION_MANAGER);

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "selectionmanager");
    gwy_param_def_add_boolean(paramdef, PARAM_ALL_FILES, "allfiles", _("to _all files"), FALSE);

    return paramdef;
}

static void
gwy_tool_selection_manager_class_init(GwyToolSelectionManagerClass *klass)
{
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = finalize;

    tool_class->icon_name = GWY_ICON_SELECTIONS;
    tool_class->title = _("Selection Manager");
    tool_class->tooltip = _("Display, copy and export selections");
    tool_class->prefix = "/module/selectionmanager";
    tool_class->default_height = 280;
    tool_class->data_switched = data_switched;
    tool_class->response = tool_response;
}

static void
finalize(GObject *object)
{
    GwyToolSelectionManager *tool = GWY_TOOL_SELECTION_MANAGER(object);
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    g_clear_signal_handler(&tool->item_changed_id, plain_tool->container);
    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    g_clear_object(&tool->model);

    G_OBJECT_CLASS(gwy_tool_selection_manager_parent_class)->finalize(object);
}

static void
gwy_tool_selection_manager_init(GwyToolSelectionManager *tool)
{
    GtkTreeSelection *selection;

    tool->in_setup = TRUE;
    tool->params = gwy_params_new_from_settings(define_module_params());

    tool->model = gtk_list_store_new(NCOLUMNS, G_TYPE_INT, G_TYPE_OBJECT);
    gwy_app_set_tree_model_kind(GTK_TREE_MODEL(tool->model), "image-selections");

    init_dialog(tool);
    tool->in_setup = FALSE;

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    list_selection_changed(tool, selection);
}

static void
render_name(G_GNUC_UNUSED GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            G_GNUC_UNUSED gpointer user_data)
{
    GQuark quark;
    const gchar *s;

    gtk_tree_model_get(model, iter, COLUMN_KEY, &quark, -1);
    s = g_quark_to_string(quark);
    g_return_if_fail(s);
    s = strrchr(s, GWY_DICT_PATHSEP);
    g_return_if_fail(s);
    g_object_set(renderer, "text", s+1, NULL);
}

static void
render_type(G_GNUC_UNUSED GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            G_GNUC_UNUSED gpointer user_data)
{
    GwySelection *sel;

    gtk_tree_model_get(model, iter, COLUMN_OBJECT, &sel, -1);
    g_return_if_fail(GWY_IS_SELECTION(sel));
    GType type = G_OBJECT_TYPE(sel);
    const gchar *name = g_type_name(type);
    for (guint i = 0; i < G_N_ELEMENTS(selection_info); i++) {
        if (type == selection_info[i].selection_type) {
            name = _(selection_info[i].humanname);
            break;
        }
    }
    g_object_set(renderer, "text", name, NULL);
    g_object_unref(sel);
}

static void
render_objects(G_GNUC_UNUSED GtkTreeViewColumn *column,
               GtkCellRenderer *renderer,
               GtkTreeModel *model,
               GtkTreeIter *iter,
               G_GNUC_UNUSED gpointer user_data)
{
    gchar buffer[16];
    GwySelection *sel;

    gtk_tree_model_get(model, iter, COLUMN_OBJECT, &sel, -1);
    g_return_if_fail(GWY_IS_SELECTION(sel));
    g_snprintf(buffer, sizeof(buffer), "%d", gwy_selection_get_data(sel, NULL));
    g_object_set(renderer, "text", buffer, NULL);
    g_object_unref(sel);
}

static void
init_dialog(GwyToolSelectionManager *tool)
{
    static const GtkTargetEntry dnd_target_table[] = { GTK_TREE_MODEL_ROW };

    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GtkWidget *scwin, *hbox, *label;
    GtkCellRenderer *renderer;
    GtkTreeViewColumn *column;
    GtkTreeSelection *selection;

    scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), scwin, TRUE, TRUE, 0);

    tool->treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(tool->model));
    gtk_container_add(GTK_CONTAINER(scwin), tool->treeview);
    gtk_tree_view_enable_model_drag_source(GTK_TREE_VIEW(tool->treeview), GDK_BUTTON1_MASK,
                                           dnd_target_table, G_N_ELEMENTS(dnd_target_table),
                                           GDK_ACTION_COPY);

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);

    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Name"));
    gtk_tree_view_append_column(GTK_TREE_VIEW(tool->treeview), column);
    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_column_pack_start(column, renderer, TRUE);
    gtk_tree_view_column_set_cell_data_func(column, renderer, render_name, tool, NULL);

    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, "Type");
    gtk_tree_view_append_column(GTK_TREE_VIEW(tool->treeview), column);
    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_column_pack_start(column, renderer, TRUE);
    gtk_tree_view_column_set_cell_data_func(column, renderer, render_type, tool, NULL);

    column = gtk_tree_view_column_new();
    gtk_tree_view_column_set_title(column, _("Objects"));
    gtk_tree_view_append_column(GTK_TREE_VIEW(tool->treeview), column);
    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_column_pack_start(column, renderer, TRUE);
    gtk_tree_view_column_set_cell_data_func(column, renderer, render_objects, tool, NULL);

    label = gtk_label_new(_("Manage chosen selection"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gwy_set_widget_margins(label, 0, 0, 2, 2);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), label, FALSE, FALSE, 0);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, FALSE, FALSE, 2);

    tool->distribute = gtk_button_new_with_mnemonic(_("_Distribute"));
    gtk_box_pack_start(GTK_BOX(hbox), tool->distribute, FALSE, FALSE, 0);

    tool->allfiles = gtk_check_button_new_with_mnemonic(_("to _all files"));
    gtk_box_pack_start(GTK_BOX(hbox), tool->allfiles, FALSE, FALSE, 0);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tool->allfiles),
                                 gwy_params_get_boolean(tool->params, PARAM_ALL_FILES));

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, FALSE, FALSE, 2);

    tool->copy = gwy_create_stock_button(GWY_STOCK_COPY);
    gtk_box_pack_start(GTK_BOX(hbox), tool->copy, FALSE, FALSE, 0);

    tool->export = gwy_create_stock_button(GWY_STOCK_EXPORT);
    gtk_box_pack_start(GTK_BOX(hbox), tool->export, FALSE, FALSE, 0);

    tool->delete = gwy_create_stock_button(GWY_STOCK_DELETE);
    gtk_box_pack_start(GTK_BOX(hbox), tool->delete, FALSE, FALSE, 0);

    gwy_tool_add_hide_button(GWY_TOOL(tool), TRUE);
    gwy_stock_add_button_to_dialog(GTK_DIALOG(GWY_TOOL(tool)->dialog), GWY_STOCK_CLEAR);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_DEFAULT);

    g_signal_connect_swapped(tool->distribute, "clicked", G_CALLBACK(distribute), tool);
    g_signal_connect_swapped(tool->delete, "clicked", G_CALLBACK(delete_selection), tool);
    g_signal_connect_swapped(tool->copy, "clicked", G_CALLBACK(copy_selection_to_clipboard), tool);
    g_signal_connect_swapped(tool->allfiles, "toggled", G_CALLBACK(all_files_toggled), tool);
    g_signal_connect_swapped(tool->export, "clicked", G_CALLBACK(export_selection), tool);
    g_signal_connect_swapped(tool->treeview, "key-press-event", G_CALLBACK(key_pressed), tool);
    g_signal_connect(tool->treeview, "drag-begin", G_CALLBACK(gwy_data_browser_block_switching), NULL);
    g_signal_connect(tool->treeview, "drag-end", G_CALLBACK(gwy_data_browser_unblock_switching), NULL);
    g_signal_connect_swapped(selection, "changed", G_CALLBACK(list_selection_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
add_selection(GQuark quark, G_GNUC_UNUSED GValue *value, gpointer user_data)
{
    GwyToolSelectionManager *tool = (GwyToolSelectionManager*)user_data;
    update_selection_item(tool, quark, TRUE);
}

static void
data_switched(GwyTool *gwytool, GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolSelectionManager *tool = GWY_TOOL_SELECTION_MANAGER(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);
    GtkTreeSelection *selection;

    GWY_TOOL_CLASS(gwy_tool_selection_manager_parent_class)->data_switched(gwytool, data_view);

    tool->prefix = NULL;
    if (ignore || plain_tool->init_failed)
        return;

    gwy_set_member_object(tool, plain_tool->container, GWY_TYPE_FILE, &tool->file,
                          "item-changed", file_item_changed, &tool->item_changed_id, G_CONNECT_SWAPPED,
                          NULL);

    gtk_list_store_clear(tool->model);
    if (data_view) {
        tool->prefix = g_quark_to_string(gwy_file_key_image_selection(plain_tool->id, NULL));
        gwy_dict_foreach(plain_tool->container, tool->prefix, add_selection, tool);
    }

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    list_selection_changed(tool, selection);
}

static void
file_item_changed(GwyToolSelectionManager *tool,
                  GQuark key,
                  G_GNUC_UNUSED GwyDict *data)
{
    if (tool->doing_mass_change)
        return;

    g_return_if_fail(tool->prefix);

    const gchar *strkey = g_quark_to_string(key);
    if (!g_str_has_prefix(strkey, tool->prefix)
        || strkey[strlen(tool->prefix)] != '/')
        return;

    gwy_debug("candidate item %s changed", strkey);
    update_selection_item(tool, key, FALSE);
}

/* If addit=TRUE, the caller knows it is a new selection item. Otherwise we need to look for it in the tree model to
 * see if we already have it. */
static void
update_selection_item(GwyToolSelectionManager *tool,
                      GQuark quark,
                      gboolean addit)
{
    const gchar *strkey = g_quark_to_string(quark);
    GwyFileKeyParsed parsed;
    gwy_parse_file_key(strkey, &parsed);
    /* Ignore selections with names starting with an underscore. */
    if (parsed.piece != GWY_FILE_PIECE_SELECTION || (parsed.suffix && parsed.suffix[0] == '_'))
        return;

    GwySelection *sel = NULL;
    gboolean object_exists = gwy_dict_gis_object(GWY_PLAIN_TOOL(tool)->container, quark, &sel);
    if (object_exists && !GWY_IS_SELECTION(sel)) {
        gwy_debug("item which is not a Selection under /selection?");
        return;
    }
    if (addit && !object_exists) {
        g_warning("The caller thinks we should add %s, but it does not exist.", strkey);
    }

    GtkTreeModel *model = GTK_TREE_MODEL(tool->model);
    GtkTreeIter iter;

    /* If addit=FALSE, we need try to find the item. */
    gboolean foundit = FALSE;
    if (!addit && gtk_tree_model_get_iter_first(model, &iter)) {
        do {
            GQuark modelquark;
            gtk_tree_model_get(model, &iter, COLUMN_KEY, &modelquark, -1);
            if (modelquark == quark) {
                foundit = TRUE;
                break;
            }
        } while (gtk_tree_model_iter_next(model, &iter));
    }

    g_return_if_fail(object_exists || foundit);

    /* New selection. Add it. */
    if (addit || (object_exists && !foundit)) {
        /* FIXME: Any ordering? Currently the order is “whatever”. */
        gtk_list_store_insert_with_values(tool->model, &iter, G_MAXINT,
                                          COLUMN_KEY, quark,
                                          COLUMN_OBJECT, sel,
                                          -1);
        return;
    }

    /* Removed selection. Delete it. */
    if (foundit && !object_exists) {
        gtk_list_store_remove(tool->model, &iter);
        return;
    }

    g_assert(object_exists && foundit);
    gtk_list_store_set(tool->model, &iter, COLUMN_OBJECT, sel, -1);
}

static void
tool_response(GwyTool *gwytool, gint response_id)
{
    GwyToolSelectionManager *tool = GWY_TOOL_SELECTION_MANAGER(gwytool);

    GWY_TOOL_CLASS(gwy_tool_selection_manager_parent_class)->response(gwytool, response_id);

    if (response_id == GWY_RESPONSE_CLEAR)
        clear_selection(tool);
}

static void
clear_selection(GwyToolSelectionManager *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    /* FIXME: Handle data kind generically with AnyTool. */
    if (plain_tool->container) {
        tool->doing_mass_change = TRUE;
        gwy_file_remove_selections(GWY_FILE(plain_tool->container), GWY_FILE_IMAGE, plain_tool->id);
        /* Just clear it explicitly instead of one by one. */
        gtk_list_store_clear(tool->model);
        tool->doing_mass_change = FALSE;
    }
}

static void
list_selection_changed(GwyToolSelectionManager *tool, GtkTreeSelection *selection)
{
    GtkTreeIter iter;
    gboolean is_selected = gtk_tree_selection_get_selected(selection, NULL, &iter);
    guint current = G_MAXUINT;
    GQuark quark = 0;

    gtk_widget_set_sensitive(tool->distribute, is_selected);
    gtk_widget_set_sensitive(tool->copy, is_selected);
    gtk_widget_set_sensitive(tool->export, is_selected);
    gtk_widget_set_sensitive(tool->delete, is_selected);

    if (tool->in_setup)
        return;

    if (is_selected) {
        GwySelection *sel;

        gtk_tree_model_get(GTK_TREE_MODEL(tool->model), &iter,
                           COLUMN_OBJECT, &sel,
                           COLUMN_KEY, &quark,
                           -1);
        gint n = gwy_selection_get_data(sel, NULL);
        if (!n) {
            gtk_widget_set_sensitive(tool->distribute, FALSE);
            gtk_widget_set_sensitive(tool->copy, FALSE);
            gtk_widget_set_sensitive(tool->export, FALSE);
        }

        GType type = G_OBJECT_TYPE(sel);
        for (guint i = 0; i < G_N_ELEMENTS(selection_info); i++) {
            if (type == selection_info[i].selection_type) {
                current = i;
                break;
            }
        }
        g_object_unref(sel);
    }

    setup_layer(tool, current, quark);
}

static void
all_files_toggled(GwyToolSelectionManager *tool, GtkToggleButton *button)
{
    gwy_params_set_boolean(tool->params, PARAM_ALL_FILES, gtk_toggle_button_get_active(button));
}

static gboolean
key_pressed(GwyToolSelectionManager *tool,
            GdkEventKey *event,
            G_GNUC_UNUSED GtkTreeView *treeview)
{
    if (event->keyval != GDK_KEY_Delete)
        return FALSE;

    delete_selection(tool);
    return TRUE;
}

static void
distribute(GwyToolSelectionManager *tool)
{
    GwyPlainTool *plain_tool;
    GtkTreeSelection *treesel;
    GwyField *field;
    DistributeData distdata;
    GtkTreeIter iter;
    GQuark quark;

    treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    if (!gtk_tree_selection_get_selected(treesel, NULL, &iter))
        return;

    gtk_tree_model_get(GTK_TREE_MODEL(tool->model), &iter,
                       COLUMN_KEY, &quark,
                       COLUMN_OBJECT, &distdata.selection,
                       -1);

    GwyFileKeyParsed parsed;
    gwy_parse_file_key(g_quark_to_string(quark), &parsed);
    distdata.name = parsed.suffix;
    g_return_if_fail(distdata.name);

    plain_tool = GWY_PLAIN_TOOL(tool);
    field = plain_tool->field;
    distdata.xyunit = gwy_field_get_unit_xy(field);
    distdata.origin[0] = gwy_field_get_xoffset(field);
    distdata.origin[1] = gwy_field_get_yoffset(field);
    gwy_debug("source: %p %s", field, g_quark_to_string(quark));

    if (gwy_params_get_boolean(tool->params, PARAM_ALL_FILES))
        gwy_data_browser_foreach(distribute_one, &distdata, NULL);
    else
        distribute_one(GWY_FILE(plain_tool->container), &distdata);
}

static void
distribute_one(GwyFile *file, gpointer user_data)
{
    DistributeData *distdata = (DistributeData*)user_data;
    GwyDict *container = GWY_DICT(file);
    gint *ids;
    gint i;

    gwy_debug("dest: %p", file);
    ids = gwy_file_get_ids(file, GWY_FILE_IMAGE);
    for (i = 0; ids[i] >= 0; i++) {
        GQuark quark = gwy_file_key_image_selection(ids[i], distdata->name);

        /* Avoid copying to self */
        GwySelection *selection = gwy_selection_copy(distdata->selection);
        if (gwy_dict_gis_object(container, quark, &selection) && selection == distdata->selection) {
            gwy_debug("avoiding copy-to-self");
            continue;
        }

        /* Check units */
        GwyField *field;
        if (!(field = gwy_file_get_image(file, ids[i]))) {
            gwy_debug("data field not found?!");
            continue;
        }
        if (!gwy_unit_equal(gwy_field_get_unit_xy(field), distdata->xyunit)) {
            gwy_debug("units differ");
            continue;
        }

        gdouble xoff = gwy_field_get_xoffset(field);
        gdouble yoff = gwy_field_get_yoffset(field);
        gdouble xreal = gwy_field_get_xreal(field);
        gdouble yreal = gwy_field_get_yreal(field);

        /* Crop the selection, taking into account that the coordinates do not include field offset, and move it
         * relative to the new origin. But for Lattice, which is origin-free, just limit it so that it fits inside. */
        selection = gwy_selection_copy(distdata->selection);
        if (GWY_IS_SELECTION_LATTICE(selection))
            gwy_selection_crop(selection, -0.5*xreal, -0.5*yreal, 0.5*xreal, 0.5*yreal);
        else {
            gwy_selection_move(selection, distdata->origin[0], distdata->origin[1]);
            gwy_selection_crop(selection, xoff, yoff, xoff + xreal, yoff + yreal);
            gwy_selection_move(selection, -xoff, -yoff);
        }

        if (gwy_selection_get_data(selection, NULL))
            gwy_dict_pass_object(container, quark, selection);
        else {
            gwy_debug("selection empty after cropping");
            g_object_unref(selection);
        }
    }
    g_free(ids);
}

static void
copy_selection_to_clipboard(GwyToolSelectionManager *tool)
{
    GtkClipboard *clipboard;
    GdkDisplay *display;
    gchar *text;

    text = create_report(tool);
    if (!text)
        return;

    display = gtk_widget_get_display(GTK_WIDGET(GWY_TOOL(tool)->dialog));
    clipboard = gtk_clipboard_get_for_display(display, GDK_SELECTION_CLIPBOARD);
    gtk_clipboard_set_text(clipboard, text, -1);
    g_free(text);
}

static void
export_selection(GwyToolSelectionManager *tool)
{
    gchar *text = create_report(tool);
    if (!text)
        return;

    gwy_save_auxiliary_data(_("Save Table"), GTK_WINDOW(GWY_TOOL(tool)->dialog), text, -1, FALSE);
    g_free(text);
}

static void
add_coordinate_header(GString *text, GwySelection *selection, GwyUnit *unit)
{
    gchar *unitstr = gwy_unit_get_string(unit, GWY_UNIT_FORMAT_PLAIN);
    gchar *s = (strlen(unitstr) ? g_strconcat(" [", unitstr, "]", NULL) : g_strdup(""));
    guint objsize = gwy_selection_get_object_size(selection);

    for (guint i = 0; i < objsize; i++) {
        g_string_append(text, gwy_selection_get_coord_symbol(selection, i));
        g_string_append(text, s);
        g_string_append_c(text, (i == objsize-1) ? '\n' : '\t');
    }

    g_free(s);
    g_free(unitstr);
}

static gchar*
create_report(GwyToolSelectionManager *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *field = plain_tool->field;
    GtkTreeSelection *treesel;
    GwySelection *selection, *movedselection = NULL;
    GwyUnit *xyunit;
    gdouble xoff, yoff;
    GtkTreeIter iter;
    GString *text;
    guint i, j, n, objsize;
    gdouble *coords;
    gchar buf[64];

    treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    if (!gtk_tree_selection_get_selected(treesel, NULL, &iter))
        return NULL;

    gtk_tree_model_get(GTK_TREE_MODEL(tool->model), &iter, COLUMN_OBJECT, &selection, -1);
    n = gwy_selection_get_data(selection, NULL);
    if (!n)
        return NULL;

    xyunit = gwy_field_get_unit_xy(field);
    xoff = gwy_field_get_xoffset(field);
    yoff = gwy_field_get_yoffset(field);
    /* Apply the data field offset in a generic manner. */
    if (xoff != 0.0 || yoff != 0.0) {
        movedselection = gwy_selection_copy(selection);
        gwy_selection_move(movedselection, xoff, yoff);
        selection = movedselection;
    }

    text = g_string_new(NULL);
    add_coordinate_header(text, selection, xyunit);

    objsize = gwy_selection_get_object_size(selection);
    coords = g_new(gdouble, objsize);
    for (i = 0; i < n; i++) {
        gwy_selection_get_object(selection, i, coords);
        for (j = 0; j < objsize; j++) {
            g_ascii_formatd(buf, sizeof(buf), "%g", coords[j]);
            g_string_append(text, buf);
            g_string_append_c(text, (j == objsize-1) ? '\n' : '\t');
        }
    }

    g_free(coords);
    g_clear_object(&movedselection);

    return g_string_free(text, FALSE);
}

static void
delete_selection(GwyToolSelectionManager *tool)
{
    GtkTreeModel *model;
    GtkTreeIter iter;

    GtkTreeSelection *treesel = gtk_tree_view_get_selection(GTK_TREE_VIEW(tool->treeview));
    if (gtk_tree_selection_get_selected(treesel, &model, &iter)) {
        GQuark quark;
        gtk_tree_model_get(model, &iter, COLUMN_KEY, &quark, -1);
        gwy_dict_remove(GWY_PLAIN_TOOL(tool)->container, quark);
    }
}

static void
setup_layer(GwyToolSelectionManager *tool,
            guint current,
            GQuark quark)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    const gchar *s;

    if (!plain_tool->data_view || current >= G_N_ELEMENTS(selection_info) || !quark)
        return;

    s = g_quark_to_string(quark);
    g_return_if_fail(s);
    s = strrchr(s, GWY_DICT_PATHSEP);
    g_return_if_fail(s);

    gwy_plain_tool_connect_selection(plain_tool, selection_info[current].layer_type, s+1);
    gwy_object_set_or_reset(plain_tool->layer, selection_info[current].layer_type,
                            "editable", TRUE,
                            "focus", -1,
                            NULL);
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
