/*
 *  $Id: maskedit.c 30029 2026-06-09 13:46:07Z yeti-dn $
 *  Copyright (C) 2003-2026 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 <glib/gi18n-lib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gwy.h>

enum {
    SENS_DATA = 1 << 0,
    SENS_MASK = 1 << 1,
};

enum {
    PARAM_STYLE,
    PARAM_DIST_TYPE,
    PARAM_MODE,
    PARAM_SHAPE,
    PARAM_TOOL,
    PARAM_RADIUS,
    PARAM_GSAMOUNT,
    PARAM_FILL_NONSIMPLE,
    PARAM_FROM_BORDER,
    PARAM_PREVENT_MERGE,
    PARAM_LINES_4_CONNECTED,

    WIDGET_ACTIONS,
    WIDGET_FILL_VOIDS,
    WIDGET_GROW_SHRINK,
};

typedef enum {
    ACTION_REMOVE,
    ACTION_INVERT,
    ACTION_FILL,
    ACTION_FILL_VOIDS,
    ACTION_GROW,
    ACTION_SHRINK,
} MaskEditAction;

typedef enum {
    MASK_EDIT_STYLE_SHAPES  = 0,
    MASK_EDIT_STYLE_DRAWING = 1,
} MaskEditStyle;

typedef enum {
    MASK_EDIT_OP_SET       = 0,
    MASK_EDIT_OP_ADD       = 1,
    MASK_EDIT_OP_REMOVE    = 2,
    MASK_EDIT_OP_INTERSECT = 3,
} MaskEditLogicalOp;

typedef enum {
    MASK_SHAPE_RECTANGLE  = 0,
    MASK_SHAPE_ELLIPSE    = 1,
    MASK_SHAPE_LINE       = 2,
    /* Not a real shape, but permits putting layer-related things into one array. */
    MASK_SHAPE_POINT      = 3,
    MASK_NLAYER_TYPES
} MaskEditShape;

typedef enum {
    MASK_TOOL_PAINT_DRAW  = 0,
    MASK_TOOL_PAINT_ERASE = 1,
    MASK_TOOL_FILL_DRAW   = 2,
    MASK_TOOL_FILL_ERASE  = 3,
} MaskEditTool;

typedef gsize (*FillFunc)    (GwyNield *mask, gint col, gint row, gint width, gint height, gint value);
typedef gsize (*ExtractFunc) (GwyNield *mask, gint col, gint row, gint width, gint height, gint *data);
typedef void  (*InsertFunc)  (GwyNield *mask, gint col, gint row, gint width, gint height, const gint *data);
typedef gsize (*SizeFunc)    (gint col, gint row, gint width, gint height);

#define GWY_TYPE_TOOL_MASK_EDITOR            (gwy_tool_mask_editor_get_type())
#define GWY_TOOL_MASK_EDITOR(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_MASK_EDITOR, GwyToolMaskEditor))
#define GWY_IS_TOOL_MASK_EDITOR(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_MASK_EDITOR))
#define GWY_TOOL_MASK_EDITOR_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_MASK_EDITOR, GwyToolMaskEditorClass))

typedef struct _GwyToolMaskEditor      GwyToolMaskEditor;
typedef struct _GwyToolMaskEditorClass GwyToolMaskEditorClass;

struct _GwyToolMaskEditor {
    GwyPlainTool parent_instance;

    GwyParams *params;

    GwyParamTable *table;
    GwySensitivityGroup *sensgroup;
    GtkSizeGroup *sizegroup;

    /* paintrbrush only */
    gboolean drawing_started;
    gint oldisel[2];
};

struct _GwyToolMaskEditorClass {
    GwyPlainToolClass parent_class;
};

static gboolean     module_register                   (void);
static GwyParamDef* define_module_params              (void);
static GType        gwy_tool_mask_editor_get_type     (void)                      G_GNUC_CONST;
static void         gwy_tool_mask_editor_finalize     (GObject *object);
static void         gwy_tool_mask_editor_init_dialog  (GwyToolMaskEditor *tool);
static void         gwy_tool_mask_editor_data_switched(GwyTool *gwytool,
                                                       GwyDataView *data_view);
static void         gwy_tool_mask_editor_mask_changed (GwyPlainTool *plain_tool);
static void         selection_changed                 (GwyPlainTool *plain_tool,
                                                       gint hint);
static void         selection_finished                (GwyPlainTool *plain_tool);
static void         terrible_hack                     (GwyToolMaskEditor *tool);
static GtkWidget*   create_actions                    (gpointer user_data);
static GtkWidget*   create_fill_voids                 (gpointer user_data);
static GtkWidget*   create_grow_shrink                (gpointer user_data);
static void         connect_selection                 (GwyToolMaskEditor *tool);
static void         setup_layer                       (GwyToolMaskEditor *tool);
static void         param_changed                     (GwyToolMaskEditor *tool,
                                                       gint id);
static void         fill_nonsimple_changed            (GwyToolMaskEditor *tool,
                                                       GtkToggleButton *toggle);
static void         run_action                        (GwyToolMaskEditor *tool,
                                                       GtkButton *button);
static void         bucket_fill                       (GwyToolMaskEditor *tool,
                                                       gint j,
                                                       gint i);

static GType layer_types[MASK_NLAYER_TYPES];

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Mask editor tool, allows interactive modification of parts of the mask."),
    "Yeti <yeti@gwyddion.net>",
    "5.0",
    "David Nečas (Yeti) & Petr Klapetek",
    "2004",
};

static const GwyEnum mode_icons[] = {
    { GWY_ICON_MASK_SET,       MASK_EDIT_OP_SET,       },
    { GWY_ICON_MASK_ADD,       MASK_EDIT_OP_ADD,       },
    { GWY_ICON_MASK_SUBTRACT,  MASK_EDIT_OP_REMOVE,    },
    { GWY_ICON_MASK_INTERSECT, MASK_EDIT_OP_INTERSECT, },
};
static const GwyEnum shape_icons[] = {
    { GWY_ICON_MASK,        MASK_SHAPE_RECTANGLE, },
    { GWY_ICON_MASK_CIRCLE, MASK_SHAPE_ELLIPSE,   },
    { GWY_ICON_MASK_LINE,   MASK_SHAPE_LINE,      },
};
static const GwyEnum tool_icons[] = {
    { GWY_ICON_MASK_PAINT_DRAW,  MASK_TOOL_PAINT_DRAW,  },
    { GWY_ICON_MASK_PAINT_ERASE, MASK_TOOL_PAINT_ERASE, },
    { GWY_ICON_MASK_FILL_DRAW,   MASK_TOOL_FILL_DRAW,   },
    { GWY_ICON_MASK_FILL_ERASE,  MASK_TOOL_FILL_ERASE,  },
};

GWY_MODULE_QUERY2(module_info, maskedit)

G_DEFINE_TYPE(GwyToolMaskEditor, gwy_tool_mask_editor, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    layer_types[MASK_SHAPE_RECTANGLE] = GWY_TYPE_LAYER_RECTANGLE;
    layer_types[MASK_SHAPE_ELLIPSE] = GWY_TYPE_LAYER_ELLIPSE;
    layer_types[MASK_SHAPE_LINE] = GWY_TYPE_LAYER_LINE;
    /* Not a real shape, but permits putting layer-related things into one array. */
    layer_types[MASK_SHAPE_POINT] = GWY_TYPE_LAYER_POINT;

    gwy_tool_func_register(GWY_TYPE_TOOL_MASK_EDITOR);

    return TRUE;
}

static GwyParamDef*
define_module_params(void)
{
    static const GwyEnum styles[] = {
        { N_("_Shapes"),        MASK_EDIT_STYLE_SHAPES,  },
        { N_("_Drawing Tools"), MASK_EDIT_STYLE_DRAWING, },
    };
    static const GwyEnum modes[] = {
        { N_("Set mask to selection"),         MASK_EDIT_OP_SET,       },
        { N_("Add selection to mask"),         MASK_EDIT_OP_ADD,       },
        { N_("Subtract selection from mask"),  MASK_EDIT_OP_REMOVE,    },
        { N_("Intersect selection with mask"), MASK_EDIT_OP_INTERSECT, },
    };
    static const GwyEnum shapes[] = {
        { N_("Rectangular shapes"), MASK_SHAPE_RECTANGLE, },
        { N_("Elliptic shapes"),    MASK_SHAPE_ELLIPSE,   },
        { N_("Thin lines"),         MASK_SHAPE_LINE,      },
    };
    static const GwyEnum tools[] = {
        { N_("Freehand mask drawing"),                 MASK_TOOL_PAINT_DRAW,  },
        { N_("Freehand mask erasing"),                 MASK_TOOL_PAINT_ERASE, },
        { N_("Fill continuous empty areas with mask"), MASK_TOOL_FILL_DRAW,   },
        { N_("Erase continuous parts of mask"),        MASK_TOOL_FILL_ERASE,  },
    };
    static GwyParamDef *paramdef = NULL;

    if (paramdef)
        return paramdef;

    paramdef = gwy_param_def_new();
    gwy_param_def_set_function_name(paramdef, "maskedit");
    gwy_param_def_add_gwyenum(paramdef, PARAM_STYLE, "style", NULL,
                              styles, G_N_ELEMENTS(styles), MASK_EDIT_STYLE_SHAPES);
    gwy_param_def_add_enum(paramdef, PARAM_DIST_TYPE, "dist_type", _("_Distance type"),
                           GWY_TYPE_DISTANCE_TRANSFORM_TYPE, GWY_DISTANCE_TRANSFORM_EUCLIDEAN);
    gwy_param_def_add_gwyenum(paramdef, PARAM_MODE, "mode", _("Mode"), modes, G_N_ELEMENTS(modes), MASK_EDIT_OP_SET);
    gwy_param_def_add_gwyenum(paramdef, PARAM_SHAPE, "shape", _("Shape"),
                              shapes, G_N_ELEMENTS(shapes), MASK_SHAPE_RECTANGLE);
    gwy_param_def_add_gwyenum(paramdef, PARAM_TOOL, "tool", _("Tool"),
                              tools, G_N_ELEMENTS(tools), MASK_TOOL_PAINT_DRAW);
    gwy_param_def_add_int(paramdef, PARAM_RADIUS, "radius", _("_Radius"), 1, 15, 5);
    gwy_param_def_add_int(paramdef, PARAM_GSAMOUNT, "gsamount", _("_Amount"), 1, 256, 1);
    gwy_param_def_add_boolean(paramdef, PARAM_FILL_NONSIMPLE, "fill_nonsimple", _("Fill non-simple-connected"), FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_FROM_BORDER, "from_border", _("Shrink from _border"), TRUE);
    gwy_param_def_add_boolean(paramdef, PARAM_PREVENT_MERGE, "prevent_merge", _("_Prevent grain merging by growing"),
                              FALSE);
    gwy_param_def_add_boolean(paramdef, PARAM_LINES_4_CONNECTED, "lines-4-connected",
                              _("Thin lines are 4-connected"), FALSE);

    return paramdef;
}

static void
gwy_tool_mask_editor_class_init(GwyToolMaskEditorClass *klass)
{
    GwyPlainToolClass *ptool_class = GWY_PLAIN_TOOL_CLASS(klass);
    GwyToolClass *tool_class = GWY_TOOL_CLASS(klass);
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);

    gobject_class->finalize = gwy_tool_mask_editor_finalize;

    tool_class->icon_name = GWY_ICON_MASK_EDITOR;
    tool_class->title = _("Mask Editor");
    tool_class->tooltip = _("Edit mask");
    tool_class->prefix = "/module/maskeditor";
    tool_class->data_switched = gwy_tool_mask_editor_data_switched;

    ptool_class->mask_changed = gwy_tool_mask_editor_mask_changed;
    ptool_class->selection_changed = selection_changed;
    ptool_class->selection_finished = selection_finished;
}

static void
gwy_tool_mask_editor_finalize(GObject *object)
{
    GwyToolMaskEditor *tool = GWY_TOOL_MASK_EDITOR(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    g_clear_object(&tool->sizegroup);
    g_clear_object(&tool->sensgroup);

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

static void
gwy_tool_mask_editor_init(GwyToolMaskEditor *tool)
{
    tool->params = gwy_params_new_from_settings(define_module_params());
    connect_selection(tool);
    gwy_tool_mask_editor_init_dialog(tool);
}

static void
gwy_tool_mask_editor_init_dialog(GwyToolMaskEditor *tool)
{
    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GwyParamTable *table;

    table = tool->table = gwy_param_table_new(tool->params);
    tool->sensgroup = gwy_sensitivity_group_new();
    tool->sizegroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL);

    gwy_param_table_append_header(table, -1, _("Editor"));
    gwy_param_table_append_radio_item(table, PARAM_STYLE, MASK_EDIT_STYLE_SHAPES);
    gwy_param_table_append_radio_buttons(table, PARAM_MODE, mode_icons);
    gwy_param_table_append_radio_buttons(table, PARAM_SHAPE, shape_icons);
    gwy_param_table_append_checkbox(table, PARAM_LINES_4_CONNECTED);

    gwy_param_table_append_separator(table);
    gwy_param_table_append_radio_item(table, PARAM_STYLE, MASK_EDIT_STYLE_DRAWING);
    gwy_param_table_append_radio_buttons(table, PARAM_TOOL, tool_icons);
    gwy_param_table_append_slider(table, PARAM_RADIUS);
    gwy_param_table_set_unitstr(table, PARAM_RADIUS, _("px"));

    gwy_param_table_append_header(table, -1, _("Actions"));
    gwy_param_table_append_foreign(table, WIDGET_ACTIONS, create_actions, tool, NULL);
    gwy_param_table_append_foreign(table, WIDGET_FILL_VOIDS, create_fill_voids, tool, NULL);

    gwy_param_table_append_header(table, -1, _("Grow/Shrink"));
    gwy_param_table_append_foreign(table, WIDGET_GROW_SHRINK, create_grow_shrink, tool, NULL);
    gwy_param_table_append_slider(table, PARAM_GSAMOUNT);
    gwy_param_table_set_unitstr(table, PARAM_GSAMOUNT, _("px"));
    gwy_param_table_append_combo(table, PARAM_DIST_TYPE);
    gwy_param_table_append_checkbox(table, PARAM_FROM_BORDER);
    gwy_param_table_append_checkbox(table, PARAM_PREVENT_MERGE);

    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), gwy_param_table_widget(table), FALSE, FALSE, 0);
    terrible_hack(tool);

    gwy_tool_add_hide_button(GWY_TOOL(tool), TRUE);
    gwy_help_add_to_tool_dialog(dialog, GWY_TOOL(tool), GWY_HELP_DEFAULT);

    g_signal_connect_swapped(tool->table, "param-changed", G_CALLBACK(param_changed), tool);

    gtk_widget_show_all(gtk_dialog_get_content_area(dialog));
}

static void
ensure_style(GwyToolMaskEditor *tool, GObject *button)
{
    MaskEditStyle style = GPOINTER_TO_UINT(g_object_get_data(button, "mask-editor-style"));

    gwy_debug("switch to style %d", style);
    gwy_param_table_set_enum(tool->table, PARAM_STYLE, style);
}

/* XXX: Never do this. The documentation clearly says so. But it restores the convenient behaviour when clicking on
 * a mode/shape/tool button switches to the corresponding drawing style ***even when you click on an already active
 * button***. GwyParamTable is too smart for that and does not emit any signal when nothing changes. */
static void
terrible_hack(GwyToolMaskEditor *tool)
{
    GList *l, *children = gtk_container_get_children(GTK_CONTAINER(gwy_param_table_widget(tool->table)));
    gint hack_count = 0;

    for (l = children; l; l = g_list_next(l)) {
        GtkWidget *widget = GTK_WIDGET(l->data);

        if (gwy_widget_is_hbox(widget)) {
            GList *l2, *children2 = gtk_container_get_children(GTK_CONTAINER(widget));

            for (l2 = children2; l2; l2 = g_list_next(l2)) {
                GtkWidget *img, *widget2 = GTK_WIDGET(l2->data);

                if (GTK_IS_RADIO_BUTTON(widget2) && (img = gtk_bin_get_child(GTK_BIN(widget2))) && GTK_IS_IMAGE(img)) {
                    const gchar *icon_name = NULL;

                    gtk_image_get_icon_name(GTK_IMAGE(img), &icon_name, NULL);
                    if (icon_name) {
                        MaskEditStyle style;
                        gboolean known_style = FALSE;
                        guint i;

                        for (i = 0; i < G_N_ELEMENTS(mode_icons) && !known_style; i++) {
                            if (gwy_strequal(mode_icons[i].name, icon_name)) {
                                style = MASK_EDIT_STYLE_SHAPES;
                                known_style = TRUE;
                            }
                        }
                        for (i = 0; i < G_N_ELEMENTS(shape_icons) && !known_style; i++) {
                            if (gwy_strequal(shape_icons[i].name, icon_name)) {
                                style = MASK_EDIT_STYLE_SHAPES;
                                known_style = TRUE;
                            }
                        }
                        for (i = 0; i < G_N_ELEMENTS(tool_icons) && !known_style; i++) {
                            if (gwy_strequal(tool_icons[i].name, icon_name)) {
                                style = MASK_EDIT_STYLE_DRAWING;
                                known_style = TRUE;
                            }
                        }

                        if (known_style) {
                            g_object_set_data(G_OBJECT(widget2), "mask-editor-style", GUINT_TO_POINTER(style));
                            g_signal_connect_swapped(widget2, "clicked", G_CALLBACK(ensure_style), tool);
                            hack_count++;
                        }
                    }
                }
            }
            g_list_free(children2);
        }
    }
    g_list_free(children);

    if (hack_count != 4+3+4)
        g_warning("Hacked only %d mode/shape/tool icons of %d. Something went wrong.", hack_count, 4+3+4);
}

static void
create_button(GwyToolMaskEditor *tool, GtkBox *hbox,
              const gchar *label, const gchar *stock_id, MaskEditAction action, guint sens_flags)
{
    GtkWidget *button;

    if (stock_id)
        button = gwy_create_stock_like_button(label, stock_id);
    else
        button = gtk_button_new_with_mnemonic(label);

    g_object_set_data(G_OBJECT(button), "action", GUINT_TO_POINTER(action));
    gwy_sensitivity_group_add_widget(tool->sensgroup, button, sens_flags);
    gtk_size_group_add_widget(tool->sizegroup, button);
    gtk_box_pack_start(hbox, button, TRUE, TRUE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(run_action), tool);
}

static GtkWidget*
create_actions(gpointer user_data)
{
    GwyToolMaskEditor *tool = (GwyToolMaskEditor*)user_data;
    GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));

    gtk_box_set_homogeneous(hbox, TRUE);
    create_button(tool, hbox, _("_Invert"), GWY_ICON_MASK_INVERT, ACTION_INVERT, SENS_DATA | SENS_MASK);
    create_button(tool, hbox, _("_Remove"), GWY_ICON_MASK_REMOVE, ACTION_REMOVE, SENS_DATA | SENS_MASK);
    create_button(tool, hbox, _("_Fill"), GWY_ICON_MASK, ACTION_FILL, SENS_DATA);

    return GTK_WIDGET(hbox);
}

static GtkWidget*
create_fill_voids(gpointer user_data)
{
    GwyToolMaskEditor *tool = (GwyToolMaskEditor*)user_data;
    GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4));
    GtkWidget *checkbox;

    create_button(tool, hbox, _("Fill _Voids"), NULL, ACTION_FILL_VOIDS, SENS_DATA | SENS_MASK);

    checkbox = gtk_check_button_new_with_mnemonic(_("Fill non-simple-connected"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox),
                                 gwy_params_get_boolean(tool->params, PARAM_FILL_NONSIMPLE));
    gtk_box_pack_start(hbox, checkbox, TRUE, TRUE, 0);
    g_signal_connect_swapped(checkbox, "toggled", G_CALLBACK(fill_nonsimple_changed), tool);

    return GTK_WIDGET(hbox);
}

static GtkWidget*
create_grow_shrink(gpointer user_data)
{
    GwyToolMaskEditor *tool = (GwyToolMaskEditor*)user_data;
    GtkBox *hbox = GTK_BOX(gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0));

    gtk_box_set_homogeneous(hbox, TRUE);
    create_button(tool, hbox, _("_Grow"), GWY_ICON_MASK_GROW, ACTION_GROW, SENS_DATA | SENS_MASK);
    create_button(tool, hbox, _("Shrin_k"), GWY_ICON_MASK_SHRINK, ACTION_SHRINK, SENS_DATA | SENS_MASK);

    return GTK_WIDGET(hbox);
}

static void
connect_selection(GwyToolMaskEditor *tool)
{
    static const gchar *const shape_selection_names[MASK_NLAYER_TYPES] = {
        "rectangle", "ellipse", "line", "pointer",
    };

    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    MaskEditStyle style = gwy_params_get_enum(tool->params, PARAM_STYLE);
    MaskEditShape shape = (style == MASK_EDIT_STYLE_DRAWING
                           ? MASK_SHAPE_POINT
                           : gwy_params_get_enum(tool->params, PARAM_SHAPE));

    gwy_plain_tool_connect_selection(plain_tool, layer_types[shape], shape_selection_names[shape]);
}

static void
gwy_tool_mask_editor_data_switched(GwyTool *gwytool,
                                   GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolMaskEditor *tool = GWY_TOOL_MASK_EDITOR(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);

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

    if (ignore || plain_tool->init_failed)
        return;

    gwy_sensitivity_group_set_state(tool->sensgroup, SENS_DATA, data_view ? SENS_DATA : 0);
    gwy_tool_mask_editor_mask_changed(plain_tool);
    gwy_debug("explicitly calling param_changed()");
    if (tool->table)
        gwy_param_table_param_changed(tool->table, -1);
    gwy_debug("explicit call ended");
}

static void
gwy_tool_mask_editor_mask_changed(GwyPlainTool *plain_tool)
{
    GwyToolMaskEditor *tool = GWY_TOOL_MASK_EDITOR(plain_tool);
    GwyNield *mask = plain_tool->mask_field;

    gwy_sensitivity_group_set_state(tool->sensgroup, SENS_MASK, mask && gwy_nield_max(mask) > 0.0 ? SENS_MASK : 0);
}

static void
param_changed(GwyToolMaskEditor *tool, gint id)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    /* First ensure that clicking on a shape/mode/tool switches to the corresponding style. */
    if (id == PARAM_RADIUS || id == PARAM_TOOL) {
        if (gwy_params_get_enum(tool->params, PARAM_STYLE) != MASK_EDIT_STYLE_DRAWING) {
            gwy_param_table_set_enum(tool->table, PARAM_STYLE, MASK_EDIT_STYLE_DRAWING);
            id = PARAM_STYLE;
        }
    }
    if (id == PARAM_SHAPE || id == PARAM_MODE) {
        if (gwy_params_get_enum(tool->params, PARAM_STYLE) != MASK_EDIT_STYLE_SHAPES) {
            gwy_param_table_set_enum(tool->table, PARAM_STYLE, MASK_EDIT_STYLE_SHAPES);
            id = PARAM_STYLE;
        }
    }

    /* Then we can handle the changes specifically. */
    if (id < 0 || id == PARAM_STYLE || id == PARAM_SHAPE || id == PARAM_TOOL) {
        connect_selection(tool);
        setup_layer(tool);
        if (plain_tool->selection)
            gwy_selection_clear(plain_tool->selection);
    }
}

static void
fill_nonsimple_changed(GwyToolMaskEditor *tool, GtkToggleButton *toggle)
{
    gwy_params_set_boolean(tool->params, PARAM_FILL_NONSIMPLE, gtk_toggle_button_get_active(toggle));
}

static void
setup_layer(GwyToolMaskEditor *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    MaskEditStyle style = gwy_params_get_enum(tool->params, PARAM_STYLE);
    MaskEditShape shape = (style == MASK_EDIT_STYLE_DRAWING
                           ? MASK_SHAPE_POINT
                           : gwy_params_get_enum(tool->params, PARAM_SHAPE));
    MaskEditTool drawtool = gwy_params_get_enum(tool->params, PARAM_TOOL);

    if (!plain_tool->data_view)
        return;

    gwy_debug("setting up layer %s", g_type_name(layer_types[shape]));
    gwy_object_set_or_reset(plain_tool->layer, layer_types[shape],
                            "editable", TRUE,
                            "focus", -1,
                            NULL);
    if (style == MASK_EDIT_STYLE_SHAPES) {
        if (shape == MASK_SHAPE_LINE)
            g_object_set(plain_tool->layer, "numbered", FALSE, "thickness", 1.0, NULL);
    }
    else {
        if (drawtool == MASK_TOOL_PAINT_DRAW || drawtool == MASK_TOOL_PAINT_ERASE) {
            g_object_set(plain_tool->layer,
                         "marker-radius", (gdouble)gwy_params_get_int(tool->params, PARAM_RADIUS),
                         NULL);
        }
        else
            g_object_set(plain_tool->layer, "draw-marker", FALSE, NULL);
    }

    gwy_selection_set_max_objects(plain_tool->selection, 1);
}

static GwyNield*
ensure_mask(GwyPlainTool *plain_tool)
{
    if (!plain_tool->mask_field) {
        GwyNield *mask = gwy_field_new_nield_alike(plain_tool->field);
        gwy_file_pass_image_mask(GWY_FILE(plain_tool->container), plain_tool->id, mask);
    }
    g_assert(plain_tool->mask_field);
    return plain_tool->mask_field;
}

static void
run_action(GwyToolMaskEditor *tool, GtkButton *button)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    MaskEditAction action = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(button), "action"));
    GwyField *field = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field;
    gint gsamount = gwy_params_get_int(tool->params, PARAM_GSAMOUNT);
    GwyDistanceTransformType dist_type = gwy_params_get_enum(tool->params, PARAM_DIST_TYPE);
    gboolean prevent_merge = gwy_params_get_boolean(tool->params, PARAM_PREVENT_MERGE);
    gboolean from_border = gwy_params_get_boolean(tool->params, PARAM_FROM_BORDER);
    gboolean fill_nonsimple = gwy_params_get_boolean(tool->params, PARAM_FILL_NONSIMPLE);
    GQuark quark;

    g_return_if_fail(action == ACTION_FILL ? !!field : !!mask);

    quark = gwy_file_key_image_mask(plain_tool->id);
    gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);

    if (action == ACTION_FILL) {
        mask = ensure_mask(plain_tool);
        gwy_nield_fill(mask, 1);
    }
    else if (action == ACTION_REMOVE)
        gwy_dict_remove(plain_tool->container, quark);
    else if (action == ACTION_INVERT)
        gwy_nield_invert(mask);
    else if (action == ACTION_GROW)
        gwy_nield_grow(mask, gsamount, dist_type, prevent_merge);
    else if (action == ACTION_SHRINK)
        gwy_nield_shrink(mask, gsamount, dist_type, TRUE, from_border);
    else if (action == ACTION_FILL_VOIDS)
        gwy_nield_fill_voids(mask, fill_nonsimple);
    else {
        g_assert_not_reached();
    }

    if (action != ACTION_REMOVE)
        gwy_nield_data_changed(plain_tool->mask_field);

    gwy_params_save_to_settings(tool->params);
    gwy_plain_tool_log_add(plain_tool);
}

/* Adaptors. */
static gsize
rectangular_area_fill(GwyNield *nield,
                      gint col, gint row, gint width, gint height,
                      gint value)
{
    gwy_nield_area_fill(nield, NULL, GWY_MASK_IGNORE, col, row, width, height, value);
    return width*height;
}

static gsize
rectangular_area_extract(GwyNield *nield,
                         gint col, gint row, gint width, gint height,
                         gint *data)
{
    const gint *d = gwy_nield_get_data(nield);
    gint xres = gwy_nield_get_xres(nield);
    for (gint i = 0; i < height; i++)
        gwy_assign(data + i*width, d + (row + i)*xres + col, width);
    return width*height;
}

static void
rectangular_area_insert(GwyNield *nield,
                        gint col, gint row, gint width, gint height,
                        const gint *data)
{
    gint *d = gwy_nield_get_data(nield);
    gint xres = gwy_nield_get_xres(nield);
    for (gint i = 0; i < height; i++)
        gwy_assign(d + (row + i)*xres + col, data + i*width, width);
}

static gsize
rectangular_area_size(G_GNUC_UNUSED gint col, G_GNUC_UNUSED gint row, gint width, gint height)
{
    return width*height;
}

static gsize
elliptic_area_extract(GwyNield *nield,
                      gint col, gint row, gint width, gint height,
                      gint *data)
{
    return gwy_nield_elliptic_area_extract(nield, col, row, width, height, data, NULL, NULL);
}

static gsize
elliptic_area_size(G_GNUC_UNUSED gint col, G_GNUC_UNUSED gint row, gint width, gint height)
{
    return width*height;
}

/* 8-connected lines. */
static gsize
linear8_area_size(gint col1, gint row1, gint col2, gint row2)
{
    return gwy_linear_area_size(col1, row1, col2, row2, FALSE);
}

static gsize
linear8_area_fill(GwyNield *nield,
                  gint col1, gint row1, gint col2, gint row2,
                  gint value)
{
    return gwy_nield_linear_area_fill(nield, col1, row1, col2, row2, FALSE, value);
}

static gsize
linear8_area_extract(GwyNield *nield,
                     gint col1, gint row1, gint col2, gint row2,
                     gint *values)
{
    return gwy_nield_linear_area_extract(nield, col1, row1, col2, row2, FALSE, values, NULL, NULL);
}

static void
linear8_area_insert(GwyNield *nield,
                    gint col1, gint row1, gint col2, gint row2,
                    const gint *values)
{
    gwy_nield_linear_area_insert(nield, col1, row1, col2, row2, FALSE, values);
}

/* 4-connected lines. */
static gsize
linear4_area_size(gint col1, gint row1, gint col2, gint row2)
{
    return gwy_linear_area_size(col1, row1, col2, row2, TRUE);
}

static gsize
linear4_area_fill(GwyNield *nield,
                  gint col1, gint row1, gint col2, gint row2,
                  gint value)
{
    return gwy_nield_linear_area_fill(nield, col1, row1, col2, row2, TRUE, value);
}

static gsize
linear4_area_extract(GwyNield *nield,
                     gint col1, gint row1, gint col2, gint row2,
                     gint *values)
{
    return gwy_nield_linear_area_extract(nield, col1, row1, col2, row2, TRUE, values, NULL, NULL);
}

static void
linear4_area_insert(GwyNield *nield,
                    gint col1, gint row1, gint col2, gint row2,
                    const gint *values)
{
    gwy_nield_linear_area_insert(nield, col1, row1, col2, row2, TRUE, values);
}

static void
selection_finished(GwyPlainTool *plain_tool)
{
    GwyToolMaskEditor *tool = GWY_TOOL_MASK_EDITOR(plain_tool);
    MaskEditStyle style = gwy_params_get_enum(tool->params, PARAM_STYLE);
    MaskEditTool drawtool = gwy_params_get_enum(tool->params, PARAM_TOOL);
    MaskEditShape shape = gwy_params_get_enum(tool->params, PARAM_SHAPE);
    MaskEditLogicalOp mode = gwy_params_get_enum(tool->params, PARAM_MODE);
    gboolean lines_4_connected = gwy_params_get_boolean(tool->params, PARAM_LINES_4_CONNECTED);
    GwySelection *selection = plain_tool->selection;
    GwyNield *mask = plain_tool->mask_field;
    GwyField *field = plain_tool->field;
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    FillFunc fill_func = NULL;
    ExtractFunc extract_func = NULL;
    InsertFunc insert_func = NULL;
    SizeFunc size_func = NULL;
    gboolean log_it_anyway = FALSE;
    GQuark quark;
    gdouble sel[4];

    g_return_if_fail(field);

    tool->drawing_started = FALSE;
    if (!gwy_selection_get_object(selection, 0, sel))
        return;

    gint isel[4] = {
        (gint)floor(gwy_field_rtoj(field, sel[0])),
        (gint)floor(gwy_field_rtoi(field, sel[1])),
        (gint)floor(gwy_field_rtoj(field, sel[2])),
        (gint)floor(gwy_field_rtoi(field, sel[3])),
    };

    if (style == MASK_EDIT_STYLE_DRAWING) {
        if (drawtool == MASK_TOOL_PAINT_DRAW || drawtool == MASK_TOOL_PAINT_ERASE) {
            gwy_plain_tool_log_add(plain_tool);
            /* The mask has been already modified. */
            gwy_selection_clear(selection);
            return;
        }
        bucket_fill(tool, isel[0], isel[1]);
        if (plain_tool->mask_field)
            gwy_nield_data_changed(plain_tool->mask_field);
        return;
    }

    if (shape == MASK_SHAPE_RECTANGLE) {
        fill_func = rectangular_area_fill;
        extract_func = rectangular_area_extract;
        insert_func = rectangular_area_insert;
        size_func = rectangular_area_size;
    }
    else if (shape == MASK_SHAPE_ELLIPSE) {
        fill_func = gwy_nield_elliptic_area_fill;
        extract_func = elliptic_area_extract;
        insert_func = gwy_nield_elliptic_area_insert;
        size_func = elliptic_area_size;
    }
    else if (shape == MASK_SHAPE_LINE) {
        if (lines_4_connected) {
            fill_func = linear4_area_fill;
            extract_func = linear4_area_extract;
            insert_func = linear4_area_insert;
            size_func = linear4_area_size;
        }
        else {
            fill_func = linear8_area_fill;
            extract_func = linear8_area_extract;
            insert_func = linear8_area_insert;
            size_func = linear8_area_size;
        }
    }
    else {
        g_return_if_reached();
    }

    /* Lines are given using enpoints, so keep isel[] as-is for lines. For the rest, transform to width and height. */
    if (shape != MASK_SHAPE_LINE) {
        GWY_ORDER(gint, isel[0], isel[2]);
        GWY_ORDER(gint, isel[1], isel[3]);
        isel[2] = isel[2]+1 - isel[0];
        isel[3] = isel[3]+1 - isel[1];
    }
    gwy_debug("(%d,%d) (%d,%d)", isel[0], isel[1], isel[2], isel[3]);

    quark = gwy_file_key_image_mask(plain_tool->id);
    if (mode == MASK_EDIT_OP_SET) {
        gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);
        mask = ensure_mask(plain_tool);
        gwy_nield_clear(mask);
        fill_func(mask, isel[0], isel[1], isel[2], isel[3], 1);
    }
    else if (mode == MASK_EDIT_OP_ADD && (!mask || gwy_nield_count(mask) < xres*yres)) {
        gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);
        mask = ensure_mask(plain_tool);
        fill_func(mask, isel[0], isel[1], isel[2], isel[3], 1);
    }
    else if (mode == MASK_EDIT_OP_REMOVE && mask && gwy_nield_count(mask)) {
        gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);
        fill_func(mask, isel[0], isel[1], isel[2], isel[3], 0);
        if (!gwy_nield_count(mask)) {
            gwy_dict_remove(plain_tool->container, quark);
            mask = NULL;
        }
    }
    else if (mode == MASK_EDIT_OP_INTERSECT && mask && gwy_nield_count(mask)) {
        gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);

        gsize size = size_func(isel[0], isel[1], isel[2], isel[3]);
        gint *data = g_new(gint, size);
        extract_func(mask, isel[0], isel[1], isel[2], isel[3], data);
        gwy_nield_clear(mask);
        insert_func(mask, isel[0], isel[1], isel[2], isel[3], data);
        if (!gwy_nield_count(mask)) {
            gwy_dict_remove(plain_tool->container, quark);
            log_it_anyway = TRUE;
            mask = NULL;
        }
    }

    gwy_selection_clear(selection);
    if (mask)
        gwy_nield_data_changed(mask);

    if (mask || log_it_anyway) {
        gwy_params_save_to_settings(tool->params);
        gwy_plain_tool_log_add(plain_tool);
    }
}

static void
bucket_fill(GwyToolMaskEditor *tool, gint j, gint i)
{
    MaskEditTool drawtool = gwy_params_get_enum(tool->params, PARAM_TOOL);
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyNield *mask = plain_tool->mask_field;
    gboolean draw;

    if (!mask) {
        if (drawtool != MASK_TOOL_FILL_DRAW)
            return;

        GQuark quark = gwy_file_key_image_mask(plain_tool->id);
        gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);
        mask = ensure_mask(plain_tool);
        gwy_nield_fill(mask, 1);
        goto end;
    }

    gint xres = gwy_nield_get_xres(mask), yres = gwy_nield_get_yres(mask);
    if (i < 0 || i >= yres || j < 0 || j >= xres)
        return;

    if (drawtool == MASK_TOOL_FILL_DRAW)
        draw = TRUE;
    else if (drawtool == MASK_TOOL_FILL_ERASE)
        draw = FALSE;
    else {
        g_return_if_reached();
    }

    gint gno = gwy_nield_get_val(mask, j, i);
    if ((gno > 0 && draw) || (gno <= 0 && !draw))
        return;

    GQuark quark = gwy_file_key_image_mask(plain_tool->id);
    gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);

    if (draw)
        gwy_nield_invert(mask);
    gwy_nield_number_contiguous(mask);
    gwy_nield_clear_by_number(mask, gwy_nield_get_val(mask, j, i), FALSE);
    if (draw)
        gwy_nield_invert(mask);

end:
    gwy_params_save_to_settings(tool->params);
    gwy_plain_tool_log_add(plain_tool);
}

/* FIXME: This is woefully inefficient. */
static void
paint_wide_line(GwyNield *nield,
                gint col, gint row,
                gint width, gint height,
                gdouble radius, gint value)
{
    if (!width && !height) {
        gwy_nield_circular_area_fill(nield, col, row, radius, value);
        return;
    }

    if (ABS(height) >= width) {
        gint q = width/2;
        if (height > 0) {
            for (gint i = 0; i <= height; i++) {
                gwy_nield_circular_area_fill(nield, col + q/height, row + i, radius, value);
                q += width;
            }
        }
        else {
            height = ABS(height);
            for (gint i = 0; i <= height; i++) {
                gwy_nield_circular_area_fill(nield, col + q/height, row - i, radius, value);
                q += width;
            }
        }
    }
    else {
        gint q = height/2;
        for (gint i = 0; i <= width; i++) {
            gwy_nield_circular_area_fill(nield, col+i, row + q/width, radius, value);
            q += height;
        }
    }
}

static void
selection_changed(GwyPlainTool *plain_tool, G_GNUC_UNUSED gint hint)
{
    GwyToolMaskEditor *tool = GWY_TOOL_MASK_EDITOR(plain_tool);
    GwyField *field = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field;
    MaskEditStyle style = gwy_params_get_enum(tool->params, PARAM_STYLE);
    MaskEditTool drawtool = gwy_params_get_enum(tool->params, PARAM_TOOL);
    gint radius = gwy_params_get_int(tool->params, PARAM_RADIUS);
    GwySelection *selection = plain_tool->selection;
    gdouble sel[2];
    gint isel[2];

    gwy_debug("selection %s", selection ? g_type_name(G_TYPE_FROM_INSTANCE(selection)) : "(null)");

    if (style != MASK_EDIT_STYLE_DRAWING || !field || !selection || !gwy_selection_get_object(selection, 0, sel)) {
        tool->drawing_started = FALSE;
        return;
    }

    gint fillvalue;
    if (drawtool == MASK_TOOL_PAINT_DRAW)
        fillvalue = 1;
    else if (drawtool == MASK_TOOL_PAINT_ERASE)
        fillvalue = 0;
    else
        return;

    isel[0] = floor(gwy_field_rtoj(field, sel[0]));
    isel[1] = floor(gwy_field_rtoi(field, sel[1]));

    GQuark quark = gwy_file_key_image_mask(plain_tool->id);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);

    gdouble r = radius - 0.5;
    if (isel[0] >= 0 && isel[0] < xres && isel[1] >= 0 && isel[1] < yres) {
        if (!tool->drawing_started) {
            gwy_app_undo_qcheckpointv(plain_tool->container, 1, &quark);
            mask = ensure_mask(plain_tool);
            gwy_nield_circular_area_fill(mask, isel[0], isel[1], r, fillvalue);
        }
        else {
            gint xy[4];

            xy[0] = tool->oldisel[0];
            xy[1] = tool->oldisel[1];
            xy[2] = isel[0];
            xy[3] = isel[1];
            if (xy[2] < xy[0]) {
                GWY_SWAP(gdouble, xy[0], xy[2]);
                GWY_SWAP(gdouble, xy[1], xy[3]);
            }
            xy[2] -= xy[0];
            xy[3] -= xy[1];
            paint_wide_line(mask, xy[0], xy[1], xy[2], xy[3], r, fillvalue);
        }
        gwy_nield_data_changed(mask);
        tool->oldisel[0] = isel[0];
        tool->oldisel[1] = isel[1];
        tool->drawing_started = TRUE;
    }
}

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