/*
 *  $Id: gwyplaintool.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2006-2022 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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 <string.h>
#include <stdlib.h>
#include <glib/gi18n-lib.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/dict.h"
#include "libgwyddion/shaped.h"
#include "libgwyui/utils.h"

#include "libgwyapp/menu.h"
#include "libgwyapp/log.h"
#include "libgwyapp/gwyplaintool.h"
#include "libgwyapp/internal.h"
#include "libgwyapp/param-internal.h"

#define ITEM_CHANGED "item-changed::"

enum {
    RLABEL_X, RLABEL_Y, RLABEL_W, RLABEL_H, NRLABELS
};

typedef void (*GwyRectSelectionLabelsFunc)(gpointer user_data);

struct _GwyRectSelectionLabels {
    GtkWidget *table;
    GtkLabel *real[NRLABELS];
    GtkWidget *px[NRLABELS];
    GtkSpinButton *pix[NRLABELS];
    gulong pix_id[NRLABELS];
    gboolean in_update;

    gboolean none_is_full;
    GCallback callback;
    gpointer cbdata;
};

struct _GwyToolHeldSelection {
    GwySelection *selection;
    gdouble xreal;
    gdouble yreal;
    gboolean is_switching_data;
};

static void gwy_plain_tool_finalize                (GObject *object);
static void gwy_plain_tool_show                    (GwyTool *tool);
static void gwy_plain_tool_hide                    (GwyTool *tool);
static void gwy_plain_tool_data_switched           (GwyTool *tool,
                                                    GwyDataView *data_view);
static void reconnect_container                    (GwyPlainTool *plain_tool,
                                                    GwyDataView *data_view);
static void data_item_changed                      (GwyDict *container,
                                                    GQuark quark,
                                                    GwyPlainTool *plain_tool);
static void mask_item_changed                      (GwyDict *container,
                                                    GQuark quark,
                                                    GwyPlainTool *plain_tool);
static void show_item_changed                      (GwyDict *container,
                                                    GQuark quark,
                                                    GwyPlainTool *plain_tool);
static void selection_item_changed                 (GwyDict *container,
                                                    GQuark quark,
                                                    GwyPlainTool *plain_tool);
static void data_changed                           (GwyPlainTool *plain_tool);
static void mask_changed                           (GwyPlainTool *plain_tool);
static void show_changed                           (GwyPlainTool *plain_tool);
static void selection_disconnect                   (GwyPlainTool *plain_tool);
static void selection_reconnect                    (GwyPlainTool *plain_tool);
static void selection_changed                      (GwySelection *selection,
                                                    gint hint,
                                                    GwyPlainTool *plain_tool);
static void selection_finished                     (GwySelection *selection,
                                                    GwyPlainTool *plain_tool);
static void update_units                           (GwyPlainTool *plain_tool);
static void gwy_plain_tool_response                (GwyTool *tool,
                                                    gint response_id);
static void remember_selection                     (GwyPlainTool *plain_tool);
static void gwy_rect_selection_labels_spinned      (GtkSpinButton *spin,
                                                    GwyRectSelectionLabels *rlabels);
static void gwy_rect_selection_labels_set_sensitive(GwyRectSelectionLabels *rlabels,
                                                    gboolean sensitive);

G_DEFINE_ABSTRACT_TYPE(GwyPlainTool, gwy_plain_tool, GWY_TYPE_TOOL)

static guint param_table_param_changed = 0;
static GType selection_type_rect = 0;

static void
gwy_plain_tool_class_init(GwyPlainToolClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);

    gobject_class->finalize = gwy_plain_tool_finalize;

    tool_class->hide = gwy_plain_tool_hide;
    tool_class->show = gwy_plain_tool_show;
    tool_class->data_switched = gwy_plain_tool_data_switched;
    tool_class->response = gwy_plain_tool_response;
}

static void
gwy_plain_tool_finalize(GObject *object)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(object);

    selection_disconnect(plain_tool);
    g_clear_object(&plain_tool->layer);
    if (plain_tool->held_selection) {
        g_clear_object(&plain_tool->held_selection->selection);
        g_free(plain_tool->held_selection);
    }
    reconnect_container(plain_tool, NULL);

    g_free(plain_tool->selection_bname);
    GWY_FREE_VALUE_FORMAT(plain_tool->coord_format);
    GWY_FREE_VALUE_FORMAT(plain_tool->value_format);

    G_OBJECT_CLASS(gwy_plain_tool_parent_class)->finalize(object);

    /* This is a bit weird, but in contrast to GwyDialog which *is* a dialog, we just *hold* a dialog. We want to run
     *
     * gtk_widget_destroy(tool->dialog);
     *
     * in GwyTool first, i.e. destroy the GwyParamTable's widget before destroing the table object itself. This is the
     * normal order of things. GwyParamTable may need to support also the opposite order, but then it needs to
     * carefully disconnect all widget signal handlers before disconnecting from "destroy" – and it is not currently
     * doing that. */
    gwy_debug("finalising param tables");
    g_ptr_array_free(plain_tool->param_tables, TRUE);
    g_string_free(plain_tool->str, TRUE);
}

static void
gwy_plain_tool_init(GwyPlainTool *plain_tool)
{
    plain_tool->param_tables = g_ptr_array_new_full(0, g_object_unref);
    plain_tool->str = g_string_new(NULL);
    plain_tool->id = -1;
}

static void
gwy_plain_tool_show(GwyTool *tool)
{
    GwyPlainTool *plain_tool;
    GwyPlainToolClass *klass;

    plain_tool = GWY_PLAIN_TOOL(tool);
    if (plain_tool->lazy_updates && plain_tool->pending_updates) {
        klass = GWY_PLAIN_TOOL_GET_CLASS(tool);

        /* Since we remember only what has changed, the order can be different. The following order should cause the
         * least odd effects.  If a tool has problems with it, it can simply look at @pending_updates in its show()
         * method, update self and clear @pending_updates before calling parent's show(). */
        if (plain_tool->pending_updates & GWY_PLAIN_TOOL_CHANGED_DATA) {
            update_units(plain_tool);
            if (klass->data_changed)
                klass->data_changed(plain_tool);
            plain_tool->pending_updates &= ~GWY_PLAIN_TOOL_CHANGED_DATA;
        }

        if (plain_tool->pending_updates & GWY_PLAIN_TOOL_CHANGED_MASK) {
            if (klass->mask_changed)
                klass->mask_changed(plain_tool);
            plain_tool->pending_updates &= ~GWY_PLAIN_TOOL_CHANGED_MASK;
        }

        if (plain_tool->pending_updates & GWY_PLAIN_TOOL_CHANGED_SHOW) {
            if (klass->show_changed)
                klass->show_changed(plain_tool);
            plain_tool->pending_updates &= ~GWY_PLAIN_TOOL_CHANGED_SHOW;
        }

        if (plain_tool->pending_updates & GWY_PLAIN_TOOL_CHANGED_SELECTION) {
            /* This is gross. */
            if (klass->selection_changed)
                klass->selection_changed(plain_tool, -1);
            plain_tool->pending_updates &= ~GWY_PLAIN_TOOL_CHANGED_SELECTION;
        }

        if (plain_tool->pending_updates & GWY_PLAIN_TOOL_FINISHED_SELECTION) {
            if (klass->selection_finished)
                klass->selection_finished(plain_tool);
            plain_tool->pending_updates &= ~GWY_PLAIN_TOOL_FINISHED_SELECTION;
        }

        if (plain_tool->pending_updates) {
            g_warning("Stray bits in pending_updates: %u", plain_tool->pending_updates);
            plain_tool->pending_updates = 0;
        }
    }
    GWY_TOOL_CLASS(gwy_plain_tool_parent_class)->show(tool);
}

static void
gwy_plain_tool_hide(GwyTool *tool)
{
    if (GWY_PLAIN_TOOL(tool)->pending_updates)
        g_warning("We have pending updates when tool is being hidden?");

    GWY_TOOL_CLASS(gwy_plain_tool_parent_class)->hide(tool);
}

void
gwy_plain_tool_data_switched(GwyTool *tool,
                             GwyDataView *data_view)
{
    GwyPlainTool *plain_tool;

    gwy_debug("%s %p", GWY_TOOL_GET_CLASS(tool)->title, data_view);
    if (GWY_TOOL_CLASS(gwy_plain_tool_parent_class)->data_switched)
        GWY_TOOL_CLASS(gwy_plain_tool_parent_class)->data_switched(tool, data_view);

    plain_tool = GWY_PLAIN_TOOL(tool);
    if (data_view == plain_tool->data_view)
        return;

    if (plain_tool->held_selection)
        plain_tool->held_selection->is_switching_data = TRUE;
    selection_disconnect(plain_tool);
    reconnect_container(plain_tool, data_view);
    update_units(plain_tool);
    if (data_view && plain_tool->layer_type) {
        gwy_plain_tool_ensure_layer(plain_tool, plain_tool->layer_type);
        selection_reconnect(plain_tool);
    }
    else {
        g_clear_object(&plain_tool->layer);
        selection_changed(NULL, -1, plain_tool);
    }
    if (plain_tool->held_selection)
        plain_tool->held_selection->is_switching_data = FALSE;
}

/**
 * reconnect_container:
 * @plain_tool: A plain tool.
 * @data_view: A new data view to switch the tool to.
 *
 * Performs signal diconnection and reconnection when data is swtiched.
 *
 * The @data_view and @container fields have to still point to the old
 * objects (or be %NULL).
 **/
static void
reconnect_container(GwyPlainTool *tool, GwyDataView *data_view)
{
    gwy_debug("%s %p", GWY_TOOL_GET_CLASS(tool)->title, data_view);
    g_clear_signal_handler(&tool->data_id, tool->field);
    g_clear_signal_handler(&tool->mask_id, tool->mask_field);
    g_clear_signal_handler(&tool->show_id, tool->show_field);

    g_clear_signal_handler(&tool->data_item_id, tool->container);
    g_clear_signal_handler(&tool->mask_item_id, tool->container);
    g_clear_signal_handler(&tool->show_item_id, tool->container);

    g_clear_object(&tool->field);
    g_clear_object(&tool->mask_field);
    g_clear_object(&tool->show_field);
    g_clear_object(&tool->container);

    tool->id = -1;

    if (!(tool->data_view = data_view))
        return;

    GwyDataKind kind;
    GwyFile *file;
    if (!gwy_data_browser_get_data_for_widget(GTK_WIDGET(data_view), &file, &kind, &tool->id)) {
        g_critical("Cannot find data for widget %p", data_view);
        return;
    }
    g_return_if_fail(kind == GWY_FILE_IMAGE);
    tool->container = g_object_ref(GWY_DICT(file));

    GString *str = tool->str;
    GQuark quark;

    quark = gwy_file_key_image(tool->id);
    g_string_assign(str, ITEM_CHANGED);
    g_string_append(str, g_quark_to_string(quark));
    tool->data_item_id = g_signal_connect(tool->container, str->str, G_CALLBACK(data_item_changed), tool);
    if (gwy_dict_gis_object(tool->container, quark, &tool->field)) {
        g_object_ref(tool->field);
        tool->data_id = g_signal_connect_swapped(tool->field, "data-changed", G_CALLBACK(data_changed), tool);
    }

    quark = gwy_file_key_image_mask(tool->id);
    g_string_assign(str, ITEM_CHANGED);
    g_string_append(str, g_quark_to_string(quark));
    tool->mask_item_id = g_signal_connect(tool->container, str->str, G_CALLBACK(mask_item_changed), tool);
    if (gwy_dict_gis_object(tool->container, quark, &tool->mask_field)) {
        g_object_ref(tool->mask_field);
        tool->mask_id = g_signal_connect_swapped(tool->mask_field, "data-changed", G_CALLBACK(mask_changed), tool);
    }

    quark = gwy_file_key_image_picture(tool->id);
    g_string_assign(str, ITEM_CHANGED);
    g_string_append(str, g_quark_to_string(quark));
    tool->show_item_id = g_signal_connect(tool->container, str->str, G_CALLBACK(show_item_changed), tool);
    if (gwy_dict_gis_object(tool->container, quark, &tool->show_field)) {
        g_object_ref(tool->show_field);
        tool->show_id = g_signal_connect_swapped(tool->show_field, "data-changed", G_CALLBACK(show_changed), tool);
    }
}

static void
data_item_changed(GwyDict *container,
                  GQuark quark,
                  GwyPlainTool *tool)
{
    g_clear_signal_handler(&tool->data_id, tool->field);
    g_clear_object(&tool->field);

    if (gwy_dict_gis_object(container, quark, &tool->field)) {
        g_object_ref(tool->field);
        tool->data_id = g_signal_connect_swapped(tool->field, "data-changed", G_CALLBACK(data_changed), tool);
        data_changed(tool);
    }
    else {
        /* No data field means we disconnect completely. */
        gwy_tool_data_switched(GWY_TOOL(tool), NULL);
    }
}

static void
mask_item_changed(GwyDict *container,
                  GQuark quark,
                  GwyPlainTool *tool)
{
    g_clear_signal_handler(&tool->mask_id, tool->mask_field);
    g_clear_object(&tool->mask_field);

    if (gwy_dict_gis_object(container, quark, &tool->mask_field)) {
        g_object_ref(tool->mask_field);
        tool->mask_id = g_signal_connect_swapped(tool->mask_field, "data-changed", G_CALLBACK(mask_changed), tool);
    }

    mask_changed(tool);
}

static void
show_item_changed(GwyDict *container,
                  GQuark quark,
                  GwyPlainTool *tool)
{
    g_clear_signal_handler(&tool->show_id, tool->show_field);
    g_clear_object(&tool->show_field);

    if (gwy_dict_gis_object(container, quark, &tool->show_field)) {
        g_object_ref(tool->show_field);
        tool->show_id = g_signal_connect_swapped(tool->show_field, "data-changed", G_CALLBACK(show_changed), tool);
    }

    show_changed(tool);
}

static void
selection_item_changed(GwyDict *container,
                       GQuark quark,
                       GwyPlainTool *tool)
{
    g_clear_signal_handler(&tool->selection_cid, tool->selection);
    g_clear_signal_handler(&tool->selection_fid, tool->selection);
    g_clear_object(&tool->selection);

    if (gwy_dict_gis_object(container, quark, &tool->selection))
        g_object_ref(tool->selection);
    else {
        /* FIXME: This is not enough. The selection is created here with the default properties, which may not be
         * what the tool needs.
         *
         * In 2.x, data processing functions which change coordinates only clear selections. In 3.x they straight
         * remove them. Which is good to get rid of a bunch of useleess empty selections. However, for the active
         * selection, we need to set it up afresh. And there is no tool virtual method for that. Fix it when rewriting
         * the tool classes… */
        tool->selection = g_object_new(gwy_vector_layer_get_selection_type(tool->layer), NULL);
        gwy_dict_set_object(tool->container, quark, tool->selection);
    }
    tool->selection_cid = g_signal_connect(tool->selection, "changed", G_CALLBACK(selection_changed), tool);
    tool->selection_fid = g_signal_connect(tool->selection, "finished", G_CALLBACK(selection_finished), tool);
    gwy_vector_layer_set_selection(tool->layer, tool->selection);

    selection_changed(tool->selection, -1, tool);
}

static void
data_changed(GwyPlainTool *tool)
{
    GwyPlainToolClass *klass;

    klass = GWY_PLAIN_TOOL_GET_CLASS(tool);
    if (tool->lazy_updates && !gwy_tool_is_visible(GWY_TOOL(tool)))
        tool->pending_updates |= GWY_PLAIN_TOOL_CHANGED_DATA;
    else {
        update_units(tool);
        if (klass->data_changed)
            klass->data_changed(tool);
    }
}

static void
mask_changed(GwyPlainTool *tool)
{
    GwyPlainToolClass *klass;

    klass = GWY_PLAIN_TOOL_GET_CLASS(tool);
    if (tool->lazy_updates && !gwy_tool_is_visible(GWY_TOOL(tool)))
        tool->pending_updates |= GWY_PLAIN_TOOL_CHANGED_MASK;
    else if (klass->mask_changed)
        klass->mask_changed(tool);
}

static void
show_changed(GwyPlainTool *tool)
{
    GwyPlainToolClass *klass;

    klass = GWY_PLAIN_TOOL_GET_CLASS(tool);
    if (tool->lazy_updates && !gwy_tool_is_visible(GWY_TOOL(tool)))
        tool->pending_updates |= GWY_PLAIN_TOOL_CHANGED_SHOW;
    else if (klass->show_changed)
        klass->show_changed(tool);
}

static void
selection_changed(GwySelection *selection,
                  gint hint,
                  GwyPlainTool *plain_tool)
{
    GwyPlainToolClass *klass;

    if (plain_tool->clear) {
        GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(plain_tool)->dialog);
        gtk_dialog_set_response_sensitive(dialog, GWY_RESPONSE_CLEAR,
                                          selection && !!gwy_selection_get_data(selection, NULL));
    }

    klass = GWY_PLAIN_TOOL_GET_CLASS(plain_tool);
    if (plain_tool->lazy_updates && !gwy_tool_is_visible(GWY_TOOL(plain_tool)))
        plain_tool->pending_updates |= GWY_PLAIN_TOOL_CHANGED_SELECTION;
    else if (klass->selection_changed)
        klass->selection_changed(plain_tool, hint);
    remember_selection(plain_tool);
}

static void
selection_finished(G_GNUC_UNUSED GwySelection *selection,
                   GwyPlainTool *plain_tool)
{
    GwyPlainToolClass *klass;

    klass = GWY_PLAIN_TOOL_GET_CLASS(plain_tool);
    if (plain_tool->lazy_updates && !gwy_tool_is_visible(GWY_TOOL(plain_tool)))
        plain_tool->pending_updates |= GWY_PLAIN_TOOL_FINISHED_SELECTION;
    else if (klass->selection_finished)
        klass->selection_finished(plain_tool);
}

static void
remember_selection(GwyPlainTool *plain_tool)
{
    GwyToolHeldSelection *held_sel = plain_tool->held_selection;
    GwySelection *selection = plain_tool->selection;

    if (!held_sel || held_sel->is_switching_data)
        return;

    if (selection) {
        if (held_sel->selection)
            gwy_selection_assign(held_sel->selection, selection);
        else
            held_sel->selection = gwy_selection_copy(selection);

        g_return_if_fail(GWY_IS_FIELD(plain_tool->field));
        held_sel->xreal = gwy_field_get_xreal(plain_tool->field);
        held_sel->yreal = gwy_field_get_yreal(plain_tool->field);
    }
    else if (held_sel->selection)
        gwy_selection_clear(held_sel->selection);
}

/**
 * update_units:
 * @plain_tool: A plain tool.
 *
 * Updates plain tool's unit formats.
 *
 * More precisely, @coord_format and @value_format are updated according to the current data field and @unit_style.
 * If @unit_style is %GWY_UNIT_FORMAT_NONE existing formats are destroyed and set to %NULL.
 **/
static void
update_units(GwyPlainTool *plain_tool)
{
    if (plain_tool->field && plain_tool->unit_style) {
        plain_tool->coord_format = gwy_field_get_value_format_xy(plain_tool->field,
                                                                 plain_tool->unit_style, plain_tool->coord_format);
        plain_tool->value_format = gwy_field_get_value_format_z(plain_tool->field,
                                                                plain_tool->unit_style, plain_tool->value_format);
    }
    else {
        GWY_FREE_VALUE_FORMAT(plain_tool->coord_format);
        GWY_FREE_VALUE_FORMAT(plain_tool->value_format);
    }
}

/**
 * gwy_plain_tool_check_layer_type:
 * @plain_tool: A plain tool.
 * @name: Layer type name (e.g. <literal>"GwyLayerPoint"</literal>).
 *
 * Checks for a required layer type.
 *
 * If the layer exists, its #GType is returned.  If it does not exist, zero is returned and a warning message is added
 * to the tool dialog.  In addition, it sets @init_failed to %TRUE.
 *
 * Therefore, this function should be called early in tool instance initialization and it should not be called again
 * once it fails.
 *
 * Returns: The type of the layer, or 0 on failure.
 **/
GType
gwy_plain_tool_check_layer_type(GwyPlainTool *plain_tool,
                                const gchar *name)
{
    GtkWidget *label;
    GtkBox *vbox;
    GType type;
    gchar *s;

    g_return_val_if_fail(GWY_IS_PLAIN_TOOL(plain_tool), 0);
    g_return_val_if_fail(name, 0);

    if (plain_tool->init_failed) {
        g_warning("Tool layer check already failed.");
        return 0;
    }

    type = g_type_from_name(name);
    if (type)
        return type;

    plain_tool->init_failed = TRUE;

    vbox = GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(GWY_TOOL(plain_tool)->dialog)));

    label = gtk_label_new(NULL);
    gtk_label_set_markup(GTK_LABEL(label), _("<big><b>Missing layer module.</b></big>"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    //gtk_misc_set_padding(GTK_MISC(label), 12, 0);
    gtk_box_pack_start(vbox, label, FALSE, FALSE, 6);

    label = gtk_label_new(NULL);
    s = g_strdup_printf(_("This tool requires layer of type %s to work, which does not seem to be installed.  "
                          "Please check your installation."),
                        name);
    gtk_label_set_markup(GTK_LABEL(label), s);
    gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    //gtk_misc_set_padding(GTK_MISC(label), 12, 0);
    gtk_box_pack_start(vbox, label, FALSE, FALSE, 6);
    g_free(s);

    gwy_tool_add_hide_button(GWY_TOOL(plain_tool), TRUE);

    gtk_widget_show_all(GTK_WIDGET(vbox));

    return 0;
}

/**
 * gwy_plain_tool_connect_selection:
 * @plain_tool: A plain tool.
 * @layer_type: Layer type.  Use gwy_plain_tool_check_layer_type() in tool instance initialization to check for layer
 *              types.
 * @bname: Selection key base name, for example <literal>"line"</literal>.
 *
 * Sets up a plain tool to automatically manage layer selection.
 *
 * When @layer_type is 0 and @bname %NULL, plain tool stops automatically managing selection (hopefully).
 *
 * This method performs gwy_plain_tool_ensure_layer(), connecting to the
 * selection and making sure the @selection field always points to the correct selection object (or is %NULL).
 *
 * The @selection_changed method of #GwyPlainToolClass is only invoked for a tool instance once this method was called
 * to set up the selection tracking.
 **/
void
gwy_plain_tool_connect_selection(GwyPlainTool *plain_tool,
                                 GType layer_type,
                                 const gchar *bname)
{
    gwy_debug("%s %s", GWY_TOOL_GET_CLASS(plain_tool)->title, bname);
    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));
    if (layer_type || bname) {
        g_return_if_fail(g_type_is_a(layer_type, GWY_TYPE_VECTOR_LAYER));
        g_return_if_fail(bname);
    }

    selection_disconnect(plain_tool);
    gwy_assign_string(&plain_tool->selection_bname, bname);
    plain_tool->layer_type = layer_type;

    if (!bname || !layer_type || !plain_tool->data_view)
        return;

    gwy_plain_tool_ensure_layer(plain_tool, layer_type);
    selection_reconnect(plain_tool);
}

static void
selection_disconnect(GwyPlainTool *plain_tool)
{
    gwy_debug("");

    g_clear_signal_handler(&plain_tool->selection_item_id, plain_tool->container);
    g_clear_signal_handler(&plain_tool->selection_cid, plain_tool->selection);
    g_clear_signal_handler(&plain_tool->selection_fid, plain_tool->selection);
    g_clear_object(&plain_tool->selection);
}

static void
selection_reconnect(GwyPlainTool *plain_tool)
{
    gwy_debug("%s", plain_tool->selection_bname);

    if (!plain_tool->selection_bname)
        return;

    GQuark key = gwy_file_key_image_selection(plain_tool->id, plain_tool->selection_bname);
    gchar *sigdetail = g_strconcat(ITEM_CHANGED, g_quark_to_string(key), NULL);
    plain_tool->selection_item_id = g_signal_connect(plain_tool->container, sigdetail,
                                                     G_CALLBACK(selection_item_changed), plain_tool);
    g_free(sigdetail);

    if (gwy_dict_gis_object(plain_tool->container, key, &plain_tool->selection)) {
        g_object_ref(plain_tool->selection);
        plain_tool->selection_cid = g_signal_connect(plain_tool->selection, "changed",
                                                     G_CALLBACK(selection_changed), plain_tool);
        plain_tool->selection_fid = g_signal_connect(plain_tool->selection, "finished",
                                                     G_CALLBACK(selection_finished), plain_tool);
    }
    else {
        plain_tool->selection = g_object_new(gwy_vector_layer_get_selection_type(plain_tool->layer), NULL);
        gwy_dict_set_object(plain_tool->container, key, plain_tool->selection);
    }
    gwy_vector_layer_set_selection(plain_tool->layer, plain_tool->selection);

    selection_changed(plain_tool->selection, -1, plain_tool);
}

/**
 * gwy_plain_tool_ensure_layer:
 * @plain_tool: A plain tool.
 * @layer_type: Layer type.  Use gwy_plain_tool_check_layer_type() in tool instance initialization to check for layer
 *              types.
 *
 * Makes sure a plain tool's layer is of the correct type.
 *
 * This is a low-level function, normally you would use gwy_plain_tool_connect_selection().
 **/
void
gwy_plain_tool_ensure_layer(GwyPlainTool *plain_tool,
                            GType layer_type)
{
    gwy_debug("%s %s", GWY_TOOL_GET_CLASS(plain_tool)->title, g_type_name(layer_type));
    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));
    g_return_if_fail(g_type_is_a(layer_type, GWY_TYPE_VECTOR_LAYER));

    g_clear_object(&plain_tool->layer);
    plain_tool->layer = gwy_data_view_get_interactive_layer(plain_tool->data_view);
    if (!plain_tool->layer || G_TYPE_FROM_INSTANCE(plain_tool->layer) != layer_type) {
        plain_tool->layer = g_object_new(layer_type, NULL);
        gwy_data_view_set_interactive_layer(plain_tool->data_view, plain_tool->layer);
    }
    g_object_ref(plain_tool->layer);
}

/**
 * gwy_plain_tool_add_clear_button:
 * @plain_tool: A plain tool.
 *
 * Adds a `Clear' button to a plain tool.
 *
 * The button works with automatically managed selection (see gwy_plain_tool_connect_selection()). It clears the
 * selection and its sensitivity depends on whether the selection is non-empty.
 *
 * The button will have response id %GWY_RESPONSE_CLEAR.
 *
 * Returns: The button widget.
 **/
GtkWidget*
gwy_plain_tool_add_clear_button(GwyPlainTool *plain_tool)
{
    g_return_val_if_fail(GWY_IS_PLAIN_TOOL(plain_tool), NULL);
    g_return_val_if_fail(!plain_tool->clear, NULL);

    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(plain_tool)->dialog);
    plain_tool->clear = gwy_stock_add_button_to_dialog(dialog, GWY_STOCK_CLEAR);
    gtk_widget_set_tooltip_text(plain_tool->clear, _("Clear selected objects"));
    if (!plain_tool->selection || !gwy_selection_get_data(plain_tool->selection, NULL))
        gtk_dialog_set_response_sensitive(dialog, GWY_RESPONSE_CLEAR, FALSE);

    return plain_tool->clear;
}

/**
 * gwy_plain_tool_add_apply_button:
 * @plain_tool: A plain tool.
 *
 * Adds an `Apply' button to a plain tool.
 *
 * This is a convenience (and consistency) function. The button is not handled by the plain tool.
 *
 * It will have response id %GTK_RESPONSE_APPLY and the response will become the default response of the tool's dialog.
 *
 * Returns: The button widget.
 **/
GtkWidget*
gwy_plain_tool_add_apply_button(GwyPlainTool *plain_tool)
{
    g_return_val_if_fail(GWY_IS_PLAIN_TOOL(plain_tool), NULL);

    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(plain_tool)->dialog);
    GtkWidget *button = gwy_stock_add_button_to_dialog(dialog, GWY_STOCK_APPLY);
    gtk_widget_set_tooltip_text(plain_tool->clear, _("Apply the operation"));
    gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_APPLY);

    return button;
}

static void
gwy_plain_tool_response(GwyTool *tool,
                        gint response_id)
{
    GwyPlainTool *plain_tool;

    plain_tool = GWY_PLAIN_TOOL(tool);
    if (response_id == GWY_RESPONSE_CLEAR && plain_tool->selection) {
        gwy_selection_clear(plain_tool->selection);
    }
}

/**
 * gwy_plain_tool_get_z_average:
 * @field: A data field.
 * @point: Real X and Y-coordinate of area center in physical units.
 * @radius: Area radius in pixels, 1 means a signle pixel.  The actual radius passed to
 *          gwy_field_circular_area_extract() is @radius-0.5.
 *
 * Computes average value over a part of data field @dfield.
 *
 * It is not an error if part of it lies outside the data field borders (it is simply not counted in), however the
 * intersection have to be nonempty.
 *
 * Returns: The average value.
 **/
gdouble
gwy_plain_tool_get_z_average(GwyField *field,
                             const gdouble *point,
                             gint radius)
{
    gint col, row, n, i;
    gdouble *values;
    gdouble avg;

    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    g_return_val_if_fail(point, 0.0);
    g_return_val_if_fail(radius > 0, 0.0);

    col = gwy_field_rtoj(field, point[0]);
    row = gwy_field_rtoi(field, point[1]);

    if (radius == 1)
        return gwy_field_get_val(field, col, row);

    values = g_new(gdouble, (2*radius + 1)*(2*radius + 1));
    n = gwy_field_circular_area_extract(field, col, row, radius - 0.5, values, NULL, NULL);
    avg = 0.0;
    if (n) {
        for (i = 0; i < n; i++)
            avg += values[i];
        avg /= n;
    }
    else
        g_warning("Z average calculated from an empty area");

    g_free(values);

    return avg;
}

static gboolean
delete_selection_object(GtkTreeView *treeview,
                        GdkEventKey *event,
                        GwyPlainTool *plain_tool)
{
    GtkTreeSelection *selection;
    GtkTreeModel *model;
    GtkTreePath *path;
    GtkTreeIter iter;
    const gint *indices;

    if (event->keyval != GDK_KEY_Delete)
        return FALSE;

    selection = gtk_tree_view_get_selection(treeview);
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return FALSE;

    path = gtk_tree_model_get_path(model, &iter);
    indices = gtk_tree_path_get_indices(path);
    gwy_selection_delete_object(plain_tool->selection, indices[0]);
    gtk_tree_path_free(path);

    return TRUE;
}

/**
 * gwy_plain_tool_enable_object_deletion:
 * @plain_tool: A plain tool.
 * @treeview: A tree view that displays selection objects in order, each row corresponding to one selection object.
 *
 * Enables deletion of selection objects by presssing Delete in a tree view diplaying them.
 **/
void
gwy_plain_tool_enable_object_deletion(GwyPlainTool *plain_tool,
                                      GtkTreeView *treeview)
{
    g_return_if_fail(GTK_IS_TREE_VIEW(treeview));
    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));

    g_signal_connect(treeview, "key-press-event", G_CALLBACK(delete_selection_object), plain_tool);
}

/**
 * gwy_plain_tool_hold_selection:
 * @plain_tool: A plain tool.
 * @hold_flags: Flags controlling when the selection is held (transferred to another image).
 *
 * Possibly transfers the selection to a new image upon plain tool data switch.
 *
 * This function should be called from data_switched() class method in a tool after setting up the layer and
 * selection. If the selection should be transferred to the new image according to @hold_flags the previous (held)
 * selection will replace whatever selection might be there currently. Otherwise the new image's selection is
 * preserved (and in fact held itself).
 *
 * Selection holding must be enabled in tool instance init function using gwy_plain_tool_enable_selection_holding().
 **/
void
gwy_plain_tool_hold_selection(GwyPlainTool *plain_tool,
                              GwyHoldSelectionFlags hold_flags)
{
    GwyField *field;
    GwyToolHeldSelection *held_sel;
    GwySelection *selection;
    gboolean is_compatible, sel_is_empty, held_is_empty;
    gboolean hold = (hold_flags & GWY_HOLD_SELECTION_HOLD);
    gboolean overwrite = (hold_flags & GWY_HOLD_SELECTION_OVERWRITE);
    gboolean clear = (hold_flags & GWY_HOLD_SELECTION_CLEAR);

    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));
    if (!(field = plain_tool->field))
        return;

    g_return_if_fail(plain_tool->held_selection);
    held_sel = plain_tool->held_selection;
    selection = plain_tool->selection;
    is_compatible = (gwy_field_get_xreal(field) == held_sel->xreal
                     && gwy_field_get_yreal(field) == held_sel->yreal);

    if (selection) {
        if (!held_sel->selection)
            held_sel->selection = gwy_selection_copy(plain_tool->selection);
        else {
            sel_is_empty = !gwy_selection_get_data(selection, NULL);
            held_is_empty = !gwy_selection_get_data(held_sel->selection, NULL);
            if (hold && is_compatible && (sel_is_empty || (overwrite && (clear || !held_is_empty))))
                gwy_selection_assign(selection, held_sel->selection);
            else
                remember_selection(plain_tool);
        }
    }
    else if (held_sel->selection) {
        g_warning("The tool is holding a selection but the image cannot have any?");
    }
}

/**
 * gwy_plain_tool_enable_selection_holding:
 * @plain_tool: A plain tool.
 *
 * Enables selection holding for a plain tool.
 *
 * Selection holding means that, if certain conditions are satisfied, upon data switch the tool transfers the current
 * selection to the new image, replacing whatever selection might be there.
 *
 * This function should be called from the instance init function.
 *
 * Holding is currently unsupported for tools which use multiple selection types.
 **/
void
gwy_plain_tool_enable_selection_holding(GwyPlainTool *plain_tool)
{
    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));

    /* This should be a sufficient initialisation. */
    plain_tool->held_selection = g_new0(GwyToolHeldSelection, 1);
}

static GtkLabel*
gwy_rect_selection_labels_make_rlabel(GtkGrid *grid,
                                      gint col, gint row)
{
    GtkLabel *label;

    label = GTK_LABEL(gtk_label_new(NULL));
    gtk_label_set_single_line_mode(label, TRUE);
    gtk_label_set_width_chars(label, 10);
    gtk_label_set_xalign(label, 1.0);
    gtk_grid_attach(grid, GTK_WIDGET(label), col, row, 1, 1);

    return label;
}

static GtkSpinButton*
gwy_rect_selection_labels_make_pspin(GtkGrid *grid,
                                     gint col, gint row,
                                     gboolean from_1,
                                     GtkWidget **px)
{
    GtkWidget *spin, *label, *hbox;
    GtkAdjustment *adj;
    gdouble minval = from_1 ? 1.0 : 0.0;

    adj = gtk_adjustment_new(minval, minval, 100.0, 1.0, 10.0, 0.0);
    spin = gtk_spin_button_new(GTK_ADJUSTMENT(adj), 0.0, 0);
    gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(spin), TRUE);
    gtk_entry_set_width_chars(GTK_ENTRY(spin), 4);

    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);

    label = gtk_label_new(_("px"));
    gtk_box_pack_end(GTK_BOX(hbox), label, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(hbox), spin, FALSE, FALSE, 0);

    gtk_grid_attach(grid, hbox, col, row, 1, 1);

    if (px)
        *px = label;

    return GTK_SPIN_BUTTON(spin);
}

/**
 * gwy_rect_selection_labels_new:
 * @none_is_full: %TRUE to treat unselected state as full data selected.
 * @callback: Callback to call when the selection is edited.
 * @cbdata: Data to pass to @callback (as the first argument).
 *
 * Creates a table displaying rectangular selection information.
 *
 * The returned object will destroy itself when the table is destroyed.
 *
 * Returns: The newly created rectangular selection information, as an opaque pointer.  The table widget can be
 *          obtained with gwy_rect_selection_labels_get_table().
 **/
GwyRectSelectionLabels*
gwy_rect_selection_labels_new(gboolean none_is_full,
                              GCallback callback,
                              gpointer cbdata)
{
    GwyRectSelectionLabels *rlabels;
    GtkGrid *grid;
    GtkWidget *label, *px;
    guint i;

    if (!selection_type_rect) {
        selection_type_rect = g_type_from_name("GwySelectionRectangle");
        g_return_val_if_fail(selection_type_rect, FALSE);
    }

    if (!callback)
        g_warning("Without a callback, reverse updates will hardly work");

    rlabels = g_new(GwyRectSelectionLabels, 1);
    rlabels->none_is_full = none_is_full;
    rlabels->callback = callback;
    rlabels->cbdata = cbdata;
    rlabels->in_update = TRUE;

    rlabels->table = gtk_grid_new();
    grid = GTK_GRID(rlabels->table);
    gtk_grid_set_column_spacing(grid, 8);
    gtk_grid_set_row_spacing(grid, 2);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    g_signal_connect_swapped(grid, "destroy", G_CALLBACK(g_free), rlabels);

    /* TRANSLATORS: Origin of the selected rectangle. */
    gtk_grid_attach(grid, gwy_label_new_header(_("Origin")), 0, 0, 1, 1);

    label = gtk_label_new("X");
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 1, 1, 1);

    label = gtk_label_new("Y");
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 2, 1, 1);

    gtk_grid_attach(grid, gwy_label_new_header(_("Size")), 0, 3, 1, 1);

    label = gtk_label_new(_("Width"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 4, 1, 1);

    label = gtk_label_new(_("Height"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(grid, label, 0, 5, 1, 1);

    for (i = 0; i < NRLABELS; i++) {
        rlabels->real[i] = gwy_rect_selection_labels_make_rlabel(grid, 1, i+1 + i/2);
        rlabels->pix[i] = gwy_rect_selection_labels_make_pspin(grid, 2, i+1 + i/2, i/2, &px);
        rlabels->px[i] = px;
        g_signal_connect(rlabels->pix[i], "value-changed", G_CALLBACK(gwy_rect_selection_labels_spinned), rlabels);
    }
    gwy_rect_selection_labels_set_sensitive(rlabels, FALSE);

    rlabels->in_update = FALSE;

    return rlabels;
}

/**
 * gwy_rect_selection_labels_get_table:
 * @rlabels: Rectangular selection information table.
 *
 * Gets the table widget of a rectangular selection information.
 *
 * Returns: The table as a #GtkWidget.
 **/
GtkWidget*
gwy_rect_selection_labels_get_table(GwyRectSelectionLabels *rlabels)
{
    return rlabels->table;
}

static void
gwy_rect_selection_labels_spinned(GtkSpinButton *spin,
                                  GwyRectSelectionLabels *rlabels)
{
    if (rlabels->in_update)
        return;

    rlabels->in_update = TRUE;

    GtkAdjustment *adj = gtk_spin_button_get_adjustment(spin);
    gint val = gwy_adjustment_get_int(adj);
    gdouble upper = gtk_adjustment_get_upper(adj);

    if (spin == rlabels->pix[RLABEL_X])
        gtk_spin_button_set_range(rlabels->pix[RLABEL_W], 2.0, upper + 1.0 - val);
    else if (spin == rlabels->pix[RLABEL_Y])
        gtk_spin_button_set_range(rlabels->pix[RLABEL_H], 2.0, upper + 1.0 - val);
    else if (spin == rlabels->pix[RLABEL_W]) {
    }
    else if (spin == rlabels->pix[RLABEL_H]) {
    }
    else
        g_critical("Stray spin button signal");

    if (rlabels->callback) {
        GwyRectSelectionLabelsFunc func;

        func = (GwyRectSelectionLabelsFunc)rlabels->callback;
        func(rlabels->cbdata);
    }

    rlabels->in_update = FALSE;
}

/**
 * gwy_rect_selection_labels_select:
 * @rlabels: Rectangular selection information table.
 * @selection: A rectangular selection to fill information into.
 * @dfield: A data field to use for real/pixel coordinate transforms.
 *
 * Updates selection data using rectangular selection information table.
 **/
void
gwy_rect_selection_labels_select(GwyRectSelectionLabels *rlabels,
                                 GwySelection *selection,
                                 GwyField *dfield)
{
    GtkAdjustment *adj;
    gint isel[4];
    gdouble sel[4];
    guint i;

    g_return_if_fail(!dfield || GWY_IS_FIELD(dfield));
    g_return_if_fail(!selection || g_type_is_a(G_TYPE_FROM_INSTANCE(selection), selection_type_rect));

    if (!gtk_widget_get_sensitive(GTK_WIDGET(rlabels->pix[0]))) {
        g_warning("Nothing is selected");
        return;
    }

    rlabels->in_update = TRUE;

    for (i = 0; i < NRLABELS; i++) {
        adj = gtk_spin_button_get_adjustment(rlabels->pix[i]);
        isel[i] = gwy_adjustment_get_int(adj);
    }

    if (isel[2] && isel[3]) {
        isel[2] += isel[0] - 1;
        isel[3] += isel[1] - 1;

        sel[0] = gwy_field_jtor(dfield, isel[0] + 0.5);
        sel[1] = gwy_field_itor(dfield, isel[1] + 0.5);
        sel[2] = gwy_field_jtor(dfield, isel[2] + 0.5);
        sel[3] = gwy_field_itor(dfield, isel[3] + 0.5);

        gwy_selection_set_data(selection, 1, sel);
    }
    else
        gwy_selection_clear(selection);

    rlabels->in_update = FALSE;
}

/**
 * gwy_rect_selection_labels_fill:
 * @rlabels: Rectangular selection info table.
 * @selection: A rectangular selection to fill information from.  It can be %NULL to clear the labels.
 * @dfield: A data field to use for real/pixel coordinate transforms. It can be %NULL to clear the labels.
 * @selreal: If not %NULL, must be an array of size at least 4 and will be filled with selection data xmin, ymin,
 *           xmax, ymax in physical units.
 * @selpix: If not %NULL, must be an array of size at least 4 and will be filled with selection data xmin, ymin, xmax,
 *          ymax in pixels.
 *
 * Updates rectangular selection info display.
 *
 * It is possible to pass %NULL @dfield but non-%NULL @selection.  This can lead to %TRUE return value (if the
 * selection is non-empty), but the labels will be still cleared as there is no way to convert between real and pixel
 * coordinates.
 *
 * Returns: %TRUE if a selection is present, %FALSE otherwise.
 **/
gboolean
gwy_rect_selection_labels_fill(GwyRectSelectionLabels *rlabels,
                               GwySelection *selection,
                               GwyField *dfield,
                               gdouble *selreal,
                               gint *selpix)
{
    GwyValueFormat *vf;
    gdouble sel[4];
    gint isel[4];
    static gchar buffer[64];
    gdouble xoff, yoff;
    gboolean is_selected;
    GtkAdjustment *adj;
    gint xres, yres;
    guint i;

    g_return_val_if_fail(!dfield || GWY_IS_FIELD(dfield), FALSE);
    g_return_val_if_fail(!selection || g_type_is_a(G_TYPE_FROM_INSTANCE(selection), selection_type_rect), FALSE);

    rlabels->in_update = TRUE;

    is_selected = selection && gwy_selection_get_object(selection, 0, sel);
    if (!dfield || (!is_selected && !rlabels->none_is_full)) {
        gwy_rect_selection_labels_set_sensitive(rlabels, FALSE);
        for (i = 0; i < NRLABELS; i++) {
            gtk_label_set_text(rlabels->real[i], "");
            gtk_spin_button_set_value(rlabels->pix[i], 0.0);
        }

        rlabels->in_update = FALSE;
        return is_selected;
    }

    xres = gwy_field_get_xres(dfield);
    yres = gwy_field_get_yres(dfield);
    xoff = gwy_field_get_xoffset(dfield);
    yoff = gwy_field_get_yoffset(dfield);

    if (is_selected) {
        GWY_ORDER(gdouble, sel[0], sel[2]);
        GWY_ORDER(gdouble, sel[1], sel[3]);

        isel[0] = gwy_field_rtoj(dfield, sel[0]);
        isel[1] = gwy_field_rtoi(dfield, sel[1]);
        isel[2] = gwy_field_rtoj(dfield, sel[2]);
        isel[3] = gwy_field_rtoi(dfield, sel[3]);
    }
    else {
        sel[0] = sel[1] = 0.0;
        sel[2] = gwy_field_get_xreal(dfield);
        sel[3] = gwy_field_get_yreal(dfield);
        isel[0] = isel[1] = 0;
        isel[2] = xres-1;
        isel[3] = yres-1;
    }

    sel[0] = gwy_field_jtor(dfield, isel[0]);
    sel[1] = gwy_field_itor(dfield, isel[1]);
    sel[2] = gwy_field_jtor(dfield, isel[2]+1);
    sel[3] = gwy_field_itor(dfield, isel[3]+1);

    /* Provide sorted real and pixel selection to the caller; but before we add offsets and convert to position
     * & size.  The documentation has always said min and max pixel coordinates, so we must have res-1 there for
     * full-size selections. */
    if (selreal)
        gwy_assign(selreal, sel, 4);
    if (selpix)
        gwy_assign(selpix, isel, 4);

    isel[2] -= isel[0] - 1;
    isel[3] -= isel[1] - 1;

    sel[2] -= sel[0];
    sel[3] -= sel[1];
    sel[0] += xoff;
    sel[1] += yoff;

    vf = gwy_field_get_value_format_xy(dfield, GWY_UNIT_FORMAT_VFMARKUP, NULL);
    for (i = 0; i < NRLABELS; i++) {
        g_snprintf(buffer, sizeof(buffer), "%.*f%s%s",
                   vf->precision, sel[i]/vf->magnitude,
                   *vf->units ? " " : "", vf->units);
        gtk_label_set_markup(rlabels->real[i], buffer);
    }
    gwy_value_format_free(vf);

    gtk_spin_button_set_range(rlabels->pix[RLABEL_X], 0.0, xres - 1.0);
    gtk_spin_button_set_range(rlabels->pix[RLABEL_W], 2.0, xres - isel[0]);
    gtk_spin_button_set_range(rlabels->pix[RLABEL_Y], 0.0, yres - 1.0);
    gtk_spin_button_set_range(rlabels->pix[RLABEL_H], 2.0, yres - isel[1]);

    for (i = 0; i < NRLABELS; i++) {
        gwy_rect_selection_labels_set_sensitive(rlabels, TRUE);
        adj = gtk_spin_button_get_adjustment(rlabels->pix[i]);
        gtk_adjustment_set_value(adj, isel[i]);
    }

    rlabels->in_update = FALSE;

    return is_selected;
}

static void
gwy_rect_selection_labels_set_sensitive(GwyRectSelectionLabels *rlabels,
                                        gboolean sensitive)
{
    guint i;

    for (i = 0; i < NRLABELS; i++) {
        gtk_widget_set_sensitive(GTK_WIDGET(rlabels->pix[i]), sensitive);
        gtk_widget_set_sensitive(rlabels->px[i], sensitive);
    }
}

/**
 * gwy_plain_tool_log_add:
 * @plain_tool: A plain tool.
 *
 * Logs a data modification operation of a plain tool.
 *
 * This is a convenience wrapper for gwy_app_channel_log_add(), setting automatically the container, data id and
 * function name from the tool.  It can only be used for simple data modification operations in which the source and
 * target ids are the same.  In more complex cases you have to use gwy_app_channel_log_add() directly.
 **/
void
gwy_plain_tool_log_add(GwyPlainTool *plain_tool)
{
    gchar *qualname;

    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));
    g_return_if_fail(plain_tool->container);

    qualname = g_strconcat("tool::", G_OBJECT_TYPE_NAME(plain_tool), NULL);
    gwy_log_add_full(GWY_FILE(plain_tool->container), GWY_FILE_IMAGE,
                     plain_tool->id, plain_tool->id,
                     qualname,
                     NULL);
    g_free(qualname);
}

/**
 * gwy_plain_tool_add_param_table:
 * @plain_tool: A plain tool.
 * @partable: Parameter table to add.
 *
 * Registers a parameter table with a data processing module dialog window.
 *
 * See gwy_dialog_add_param_table() for details.
 **/
void
gwy_plain_tool_add_param_table(GwyPlainTool *plain_tool,
                               GwyParamTable *partable)
{
    GPtrArray *tables;
    guint i, n;

    g_return_if_fail(GWY_IS_PLAIN_TOOL(plain_tool));
    g_return_if_fail(GWY_IS_PARAM_TABLE(partable));

    if (!param_table_param_changed)
        param_table_param_changed = g_signal_lookup("param-changed", GWY_TYPE_PARAM_TABLE);

    tables = plain_tool->param_tables;
    n = tables->len;
    for (i = 0; i < n; i++) {
        GwyParamTable *othertable = g_ptr_array_index(tables, i);
        if (othertable == partable) {
            g_warning("Parameter table is already present in dialog.");
            return;
        }
    }
    g_object_ref_sink(partable);
    g_ptr_array_add(tables, partable);
    /* XXX: This function should not be called inside param change handlers. If it ever does we need to introduce
     * the complicated in_update counting as in GwyDialog. */
    _gwy_param_table_set_parent_plain_tool(partable, plain_tool);
}

void
_gwy_plain_tool_param_table_update_started(GwyPlainTool *plain_tool)
{
    GPtrArray *tables = plain_tool->param_tables;
    guint i, n = tables->len;

    for (i = 0; i < n; i++)
        _gwy_param_table_in_update((GwyParamTable*)g_ptr_array_index(tables, i), TRUE);
}

void
_gwy_plain_tool_param_table_update_finished(GwyPlainTool *plain_tool)
{
    GPtrArray *tables = plain_tool->param_tables;
    guint i, n = tables->len;

    for (i = 0; i < n; i++)
        _gwy_param_table_in_update((GwyParamTable*)g_ptr_array_index(tables, i), FALSE);
}

/************************** Documentation ****************************/

/**
 * SECTION:gwyplaintool
 * @title: GwyPlainTool
 * @short_description: Base class for simple tools
 **/

/**
 * GwyPlainToolChanged:
 * @GWY_PLAIN_TOOL_CHANGED_DATA: Channel data field was changed.
 * @GWY_PLAIN_TOOL_CHANGED_MASK: Mask data field was changed.
 * @GWY_PLAIN_TOOL_CHANGED_SHOW: Presentation data field was changed.
 * @GWY_PLAIN_TOOL_CHANGED_SELECTION: Selection was changed.
 * @GWY_PLAIN_TOOL_FINISHED_SELECTION: Selection was finished (that is emitted #GwySelection::finished).
 *
 * The type of pending changes that accumulated during tool inactivity.
 **/

/**
 * GwyHoldSelectionFlags:
 * @GWY_HOLD_SELECTION_HOLD: Selections are held (transferred to the new image).
 * @GWY_HOLD_SELECTION_OVERWRITE: Existing selections on the new image is replaced.
 * @GWY_HOLD_SELECTION_CLEAR: If the current image has no selection the selection on the new image is cleared.
 *
 * Flags controlling the selection holding behaviour of tools.
 *
 * Although any combination of flags is valid, this is mainly so that user's choice is not reset. For any holding to
 * be done at all the %GWY_HOLD_SELECTION_HOLD flag must be set. If it is unset then %GWY_HOLD_SELECTION_OVERWRITE
 * and %GWY_HOLD_SELECTION_CLEAR do not matter. Similarly, for %GWY_HOLD_SELECTION_CLEAR has any effect only if
 * overwriting is enabled by setting %GWY_HOLD_SELECTION_OVERWRITE.
 **/

/* 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 : */
