/*
 *  $Id: icolorange.c 30047 2026-06-11 08:30:45Z 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 <stdlib.h>
#include <gtk/gtk.h>
#include <gwy.h>

#define APP_RANGE_KEY "/app/default-range-type"

enum {
    RESPONSE_SET_TO_MASKED = GWY_RESPONSE_USER,
    RESPONSE_SET_TO_UNMASKED,
    RESPONSE_SET_TO_FULL,
    RESPONSE_INVERT,
    RESPONSE_SET_ZERO,
};

enum {
    PARAM_START,
    PARAM_END,
    PARAM_USE_SELECTION,

    BUTTON_SET_TO_MASKED,
    BUTTON_SET_TO_UNMASKED,
    BUTTON_SET_TO_FULL,
    BUTTON_INVERT,
    BUTTON_SET_ZERO,

    INFO_MINIMUM,
    INFO_MAXIMUM,
};

typedef enum {
    USE_SELECTION = 0,
    USE_HISTOGRAM = 1,
} ColorRangeSource;

#define GWY_TYPE_TOOL_COLOR_RANGE            (gwy_tool_color_range_get_type())
#define GWY_TOOL_COLOR_RANGE(obj)            (G_TYPE_CHECK_INSTANCE_CAST((obj), GWY_TYPE_TOOL_COLOR_RANGE, GwyToolColorRange))
#define GWY_IS_TOOL_COLOR_RANGE(obj)         (G_TYPE_CHECK_INSTANCE_TYPE((obj), GWY_TYPE_TOOL_COLOR_RANGE))
#define GWY_TOOL_COLOR_RANGE_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS((obj), GWY_TYPE_TOOL_COLOR_RANGE, GwyToolColorRangeClass))

typedef struct _GwyToolColorRange      GwyToolColorRange;
typedef struct _GwyToolColorRangeClass GwyToolColorRangeClass;

struct _GwyToolColorRange {
    GwyPlainTool parent_instance;

    GwyParams *params;

    GwyParamTable *table;
    GwyRectSelectionLabels *rlabels;

    GwyGraph *histogram;
    GwyGraphModel *histogram_model;
    GwyLine *heightdist;
    GwySelection *graph_selection;
    gint isel[4];
    gdouble rsel[4];

    ColorRangeSource range_source;
    gboolean programmatic_update;
    gboolean data_switch;
    GSList *modelist;
    GtkWidget *is_default;

    GQuark key_min_max[2];
};

struct _GwyToolColorRangeClass {
    GwyPlainToolClass parent_class;
};

static gboolean            module_register                       (void);
static GwyParamDef*        define_module_params                  (void);
static GType               gwy_tool_color_range_get_type         (void)                         G_GNUC_CONST;
static void                gwy_tool_color_range_finalize         (GObject *object);
static void                gwy_tool_color_range_init_dialog      (GwyToolColorRange *tool);
static void                gwy_tool_color_range_data_switched    (GwyTool *gwytool,
                                                                  GwyDataView *data_view);
static void                gwy_tool_color_range_data_changed     (GwyPlainTool *plain_tool);
static void                gwy_tool_color_range_mask_changed     (GwyPlainTool *plain_tool);
static void                gwy_tool_color_range_selection_changed(GwyPlainTool *plain_tool,
                                                                  gint hint);
static void                gwy_tool_color_range_response         (GwyTool *tool,
                                                                  gint response_id);
static void                make_keys                             (GwyToolColorRange *tool,
                                                                  GwyDataView *data_view);
static void                xsel_changed                          (GwySelection *selection,
                                                                  gint hint,
                                                                  GwyToolColorRange *tool);
static void                type_changed                          (GtkWidget *radio,
                                                                  GwyToolColorRange *tool);
static void                set_default_mode                      (GtkToggleButton *check,
                                                                  GwyToolColorRange *tool);
static GwyColorMappingType get_range_type                        (GwyToolColorRange *tool);
static void                set_range_type                        (GwyToolColorRange *tool,
                                                                  GwyColorMappingType mapping);
static void                get_min_max                           (GwyToolColorRange *tool,
                                                                  gdouble *selection);
static void                set_min_max                           (GwyToolColorRange *tool);
static void                update_fullrange                      (GwyToolColorRange *tool);
static void                update_histogram                      (GwyToolColorRange *tool);
static void                param_changed                         (GwyToolColorRange *tool,
                                                                  gint id);
static void                invert_mapping                        (GwyToolColorRange *tool);
static void                set_zero_to_minimum                   (GwyToolColorRange *tool);
static void                update_selected_rectangle             (GwyToolColorRange *tool);

static GwyModuleInfo module_info = {
    GWY_MODULE_ABI_VERSION,
    &module_register,
    N_("Interactive color range tool, allows selecting the data range false color scale should map to, "
       "either on data or on height distribution histogram."),
    "Yeti <yeti@gwyddion.net>",
    "4.4",
    "David Nečas (Yeti) & Petr Klapetek",
    "2004",
};

GWY_MODULE_QUERY2(module_info, icolorange)

G_DEFINE_TYPE(GwyToolColorRange, gwy_tool_color_range, GWY_TYPE_PLAIN_TOOL)

static gboolean
module_register(void)
{
    gwy_tool_func_register(GWY_TYPE_TOOL_COLOR_RANGE);

    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, "colorrange");
    gwy_param_def_add_double(paramdef, PARAM_START, NULL, _("_Start"), -1e6, 1e6, 0);
    gwy_param_def_add_double(paramdef, PARAM_END, NULL, _("_End"), -1e6, 1e6, 0);
    gwy_param_def_add_boolean(paramdef, PARAM_USE_SELECTION, "use-selection", _("_Adapt color range to selection"),
                              TRUE);

    return paramdef;
}

static void
gwy_tool_color_range_class_init(GwyToolColorRangeClass *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_color_range_finalize;

    tool_class->icon_name = GWY_ICON_COLOR_RANGE;
    tool_class->title = _("Color Range");
    tool_class->tooltip = _("Stretch color range to part of data");
    tool_class->prefix = "/module/colorrange";
    tool_class->data_switched = gwy_tool_color_range_data_switched;
    tool_class->response = gwy_tool_color_range_response;

    ptool_class->data_changed = gwy_tool_color_range_data_changed;
    ptool_class->mask_changed = gwy_tool_color_range_mask_changed;
    ptool_class->selection_changed = gwy_tool_color_range_selection_changed;
}

static void
gwy_tool_color_range_finalize(GObject *object)
{
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(object);

    gwy_params_save_to_settings(tool->params);
    g_clear_object(&tool->params);
    g_clear_object(&tool->heightdist);

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

static void
gwy_tool_color_range_init(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyDict *settings = gwy_app_settings_get();

    tool->params = gwy_params_new_from_settings(define_module_params());
    if (!gwy_dict_contains_by_name(settings, APP_RANGE_KEY))
        gwy_dict_set_enum_by_name(settings, APP_RANGE_KEY, GWY_COLOR_MAPPING_FULL);

    plain_tool->unit_style = GWY_UNIT_FORMAT_VFMARKUP;

    gwy_plain_tool_connect_selection(plain_tool, GWY_TYPE_LAYER_RECTANGLE, "rectangle");

    gwy_tool_color_range_init_dialog(tool);
}

static void
gwy_tool_crop_rect_updated(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    gwy_rect_selection_labels_select(tool->rlabels, plain_tool->selection, plain_tool->field);
}

static void
gwy_tool_color_range_init_dialog(GwyToolColorRange *tool)
{
    static struct {
        guint type;
        const gchar *stock_id;
        const gchar *text;
    }
    const range_types[] = {
        {
            GWY_COLOR_MAPPING_FULL, GWY_ICON_COLOR_RANGE_FULL,
            N_("Full color range from minimum to maximum"),
        },
        {
            GWY_COLOR_MAPPING_FIXED, GWY_ICON_COLOR_RANGE_FIXED,
            N_("Explicitly set fixed color range"),
        },
        {
            GWY_COLOR_MAPPING_AUTO, GWY_ICON_COLOR_RANGE_AUTO,
            N_("Automatic color range with tails cut off"),
        },
        {
            GWY_COLOR_MAPPING_ADAPT, GWY_ICON_COLOR_RANGE_ADAPTIVE,
            N_("Adaptive nonlinear color mapping"),
        },
    };

    GtkDialog *dialog = GTK_DIALOG(GWY_TOOL(tool)->dialog);
    GtkWidget *hbox, *button, *image;
    GwyColorMappingType mapping = GWY_COLOR_MAPPING_FULL;
    GtkRadioButton *group;
    GwyParamTable *table;
    GwyGraphCurveModel *cmodel;
    GwyGraphArea *garea;
    gint i;

    /* Mode switch */
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, FALSE, FALSE, 0);
    GtkWidget *label = gtk_label_new("Mapping type:");
    gtk_widget_set_margin_end(label, 6);
    gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);

    group = NULL;
    for (i = 0; i < G_N_ELEMENTS(range_types); i++) {
        button = gtk_radio_button_new_from_widget(group);
        g_object_set(button, "draw-indicator", FALSE, NULL);
        image = gtk_image_new_from_icon_name(range_types[i].stock_id, GTK_ICON_SIZE_LARGE_TOOLBAR);
        gtk_container_add(GTK_CONTAINER(button), image);
        gwy_radio_button_set_value(button, range_types[i].type);
        gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
        gtk_widget_set_tooltip_text(button, _(range_types[i].text));
        g_signal_connect(button, "clicked", G_CALLBACK(type_changed), tool);
        if (!group)
            group = GTK_RADIO_BUTTON(button);
    }
    tool->modelist = gtk_radio_button_get_group(group);

    /* Is default */
    tool->is_default = gtk_check_button_new_with_mnemonic(_("is _default"));
    gtk_box_pack_start(GTK_BOX(hbox), tool->is_default, FALSE, FALSE, 4);
    g_signal_connect(tool->is_default, "toggled", G_CALLBACK(set_default_mode), tool);

    /* Height distribution */
    tool->heightdist = gwy_line_new(1.0, 1.0, TRUE);
    cmodel = gwy_graph_curve_model_new();
    g_object_set(cmodel, "description", _("Height histogram"), "mode", GWY_GRAPH_CURVE_LINE, NULL);

    tool->histogram_model = gwy_graph_model_new();
    gwy_graph_model_add_curve(tool->histogram_model, cmodel);
    tool->histogram = GWY_GRAPH(gwy_graph_new(tool->histogram_model));
    gwy_graph_set_status(tool->histogram, GWY_GRAPH_STATUS_XSEL);
    garea = GWY_GRAPH_AREA(gwy_graph_get_area(tool->histogram));
    gtk_widget_set_size_request(GTK_WIDGET(garea), -1, 48);
    tool->graph_selection = gwy_graph_area_get_selection(garea, GWY_GRAPH_STATUS_XSEL);
    gwy_selection_set_max_objects(tool->graph_selection, 1);
    g_signal_connect(tool->graph_selection, "changed", G_CALLBACK(xsel_changed), tool);

    g_object_set(tool->histogram_model, "label-visible", FALSE, NULL);
    gwy_graph_set_axis_visible(tool->histogram, GTK_POS_TOP, FALSE);
    gwy_graph_set_axis_visible(tool->histogram, GTK_POS_BOTTOM, FALSE);
    gwy_graph_set_axis_visible(tool->histogram, GTK_POS_LEFT, FALSE);
    gwy_graph_set_axis_visible(tool->histogram, GTK_POS_RIGHT, FALSE);
    gwy_graph_enable_user_input(tool->histogram, FALSE);
    gtk_widget_set_sensitive(GTK_WIDGET(tool->histogram), FALSE);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), GTK_WIDGET(tool->histogram), TRUE, TRUE, 2);

    /* Data ranges */
    table = tool->table = gwy_param_table_new(tool->params);
    gwy_param_table_append_header(table, -1, _("Color Mapping"));
    gwy_param_table_append_entry(table, PARAM_START);
    gwy_param_table_set_sensitive(table, PARAM_START, FALSE);
    gwy_param_table_append_entry(table, PARAM_END);
    gwy_param_table_set_sensitive(table, PARAM_END, FALSE);
    gwy_param_table_append_button(table, BUTTON_SET_TO_MASKED, -1, RESPONSE_SET_TO_MASKED, _("Set to _Masked"));
    gwy_param_table_append_button(table, BUTTON_SET_TO_UNMASKED, BUTTON_SET_TO_MASKED,
                                  RESPONSE_SET_TO_UNMASKED, _("Set to _Unmasked"));
    gwy_param_table_append_button(table, BUTTON_SET_TO_FULL, -1, RESPONSE_SET_TO_FULL, _("Set to _Full Range"));
    gwy_param_table_append_button(table, BUTTON_INVERT, BUTTON_SET_TO_FULL, RESPONSE_INVERT, _("_Invert Mapping"));
    gwy_param_table_append_checkbox(table, PARAM_USE_SELECTION);

    gwy_param_table_append_header(table, -1, _("Data Range"));
    gwy_param_table_append_info(table, INFO_MINIMUM, _("Minimum"));
    gwy_param_table_append_info(table, INFO_MAXIMUM, _("Maximum"));
    gwy_param_table_append_button(table, BUTTON_SET_ZERO, -1, RESPONSE_SET_ZERO, _("Set Zero to Color Map Minimum"));

    gwy_plain_tool_add_param_table(GWY_PLAIN_TOOL(tool), table);
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
    gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(dialog)), hbox, FALSE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_param_table_widget(table), TRUE, TRUE, 0);

    /* Selection info */
    tool->rlabels = gwy_rect_selection_labels_new(TRUE, G_CALLBACK(gwy_tool_crop_rect_updated), tool);
    gtk_box_pack_start(GTK_BOX(hbox), gwy_rect_selection_labels_get_table(tool->rlabels), FALSE, FALSE, 0);

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

    /* Switch to the default */
    gwy_dict_gis_enum_by_name(gwy_app_settings_get(), APP_RANGE_KEY, &mapping);
    gwy_radio_buttons_set_current(tool->modelist, mapping);
    type_changed(NULL, tool);

    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
gwy_tool_color_range_data_switched(GwyTool *gwytool,
                                   GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(gwytool);
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(gwytool);
    gboolean ignore = (data_view == plain_tool->data_view);
    GwyColorMappingType mapping;

    gwy_debug("A");
    tool->data_switch = TRUE;
    GWY_TOOL_CLASS(gwy_tool_color_range_parent_class)->data_switched(gwytool, data_view);
    tool->data_switch = FALSE;
    gwy_debug("B");

    make_keys(tool, data_view);

    if (plain_tool->init_failed)
        return;

    if (data_view) {
        gwy_object_set_or_reset(plain_tool->layer, GWY_TYPE_LAYER_RECTANGLE,
                                "editable", TRUE,
                                "focus", -1,
                                NULL);
        gwy_selection_set_max_objects(plain_tool->selection, 1);
        /* This is a potentially useful operation no matter the mode. In full-range mode it basically replicates
         * the standard ‘Set Zero’ function. */
        gwy_param_table_set_sensitive(tool->table, BUTTON_SET_ZERO, TRUE);
    }
    else {
        gtk_widget_set_sensitive(GTK_WIDGET(tool->histogram), FALSE);
        gwy_param_table_set_sensitive(tool->table, PARAM_START, FALSE);
        gwy_param_table_set_sensitive(tool->table, PARAM_END, FALSE);
        gwy_param_table_set_sensitive(tool->table, BUTTON_INVERT, FALSE);
        gwy_param_table_set_sensitive(tool->table, BUTTON_SET_TO_FULL, FALSE);
        gwy_param_table_set_sensitive(tool->table, BUTTON_SET_ZERO, FALSE);
        gwy_param_table_set_sensitive(tool->table, PARAM_USE_SELECTION, FALSE);
        gwy_selection_clear(tool->graph_selection);
    }

    update_histogram(tool);

    if (ignore)
        return;

    mapping = get_range_type(tool);
    if (data_view) {
        if (mapping == GWY_COLOR_MAPPING_FIXED) {
            gdouble sel[2];

            get_min_max(tool, sel);
            gwy_debug("[%g, %g]", sel[0], sel[1]);
            gwy_selection_set_data(tool->graph_selection, 1, sel);
        }
        else
            gwy_selection_clear(tool->graph_selection);

        tool->programmatic_update = TRUE;
        type_changed(NULL, tool);
        tool->programmatic_update = FALSE;
    }
    gwy_radio_buttons_set_current(tool->modelist, mapping);
    update_fullrange(tool);
    gwy_tool_color_range_mask_changed(plain_tool);
    gwy_debug("set min max after data switch");
    set_min_max(tool);
}

static void
make_keys(GwyToolColorRange *tool,
          GwyDataView *data_view)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    if (!data_view || plain_tool->id < 0) {
        gwy_clear(tool->key_min_max, 2);
        return;
    }

    tool->key_min_max[0] = gwy_file_key_image_range_min(plain_tool->id);
    tool->key_min_max[1] = gwy_file_key_image_range_max(plain_tool->id);
}

static void
gwy_tool_color_range_data_changed(GwyPlainTool *plain_tool)
{
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(plain_tool);

    update_selected_rectangle(tool);
    update_histogram(tool);
    update_fullrange(tool);
    /* When the range is defined by selection, follow data updates. When it is given on the histogram or numerically,
     * don't. */
    if (plain_tool->field
        && tool->range_source == USE_SELECTION
        && gwy_params_get_boolean(tool->params, PARAM_USE_SELECTION))
        gwy_tool_color_range_selection_changed(plain_tool, -1);
}

static void
gwy_tool_color_range_mask_changed(GwyPlainTool *plain_tool)
{
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(plain_tool);
    gboolean have_mask = !!plain_tool->mask_field;
    gboolean mapping = get_range_type(tool);
    gboolean msens = (have_mask && mapping == GWY_COLOR_MAPPING_FIXED);

    gwy_param_table_set_sensitive(tool->table, BUTTON_SET_TO_MASKED, msens);
    gwy_param_table_set_sensitive(tool->table, BUTTON_SET_TO_UNMASKED, msens);
}

static void
gwy_tool_color_range_selection_changed(GwyPlainTool *plain_tool,
                                       gint hint)
{
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(plain_tool);
    GwyColorMappingType mapping;
    gboolean is_selected = FALSE;
    gdouble range[4];

    g_return_if_fail(hint <= 0);

    update_selected_rectangle(tool);
    if (plain_tool->selection)
        is_selected = gwy_selection_get_data(plain_tool->selection, NULL);

    mapping = get_range_type(tool);
    if (mapping != GWY_COLOR_MAPPING_FIXED || !gwy_params_get_boolean(tool->params, PARAM_USE_SELECTION))
        return;

    if (!tool->programmatic_update)
        tool->range_source = USE_SELECTION;

    gwy_debug("set min max after area selection");
    set_min_max(tool);
    if (!tool->programmatic_update) {
        tool->programmatic_update = TRUE;
        if (is_selected) {
            get_min_max(tool, range);
            gwy_selection_set_object(tool->graph_selection, 0, range);
        }
        else
            gwy_selection_clear(tool->graph_selection);
        tool->programmatic_update = FALSE;
    }
}

static void
xsel_changed(GwySelection *selection,
             gint hint,
             GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);

    g_return_if_fail(hint <= 0);

    if (tool->programmatic_update)
        return;

    if (gwy_selection_get_data(selection, NULL)) {
        tool->range_source = USE_HISTOGRAM;
        gwy_debug("set min max after histogram selection");
        set_min_max(tool);

        /* when user begins a selection on the histogram, the selection on the image is now invalid, and should be
         * removed. */
        tool->programmatic_update = TRUE;
        gwy_selection_clear(plain_tool->selection);
        tool->programmatic_update = FALSE;
    }
    else {
        if (gwy_params_get_boolean(tool->params, PARAM_USE_SELECTION))
            tool->range_source = USE_SELECTION;
        tool->programmatic_update = TRUE;
        gwy_tool_color_range_selection_changed(GWY_PLAIN_TOOL(tool), -1);
        tool->programmatic_update = FALSE;
    }
}

/* TODO: this is not enough, we need to restore range from container; add USE_CONTAINER source type? */
static void
type_changed(GtkWidget *radio,
             GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyColorMappingType mapping, old_mode;
    gboolean fixed_sens = FALSE;

    old_mode = get_range_type(tool);
    if (radio) {
        mapping = gwy_radio_button_get_value(radio);
        if (old_mode == mapping)
            return;
    }
    else
        mapping = old_mode;  /* Initialization */

    if (plain_tool->container) {
        fixed_sens = (mapping == GWY_COLOR_MAPPING_FIXED);
        set_range_type(tool, mapping);
        if (fixed_sens && !tool->data_switch) {
            gwy_debug("set min max after range type change");
            set_min_max(tool);
        }
    }
    gtk_widget_set_sensitive(GTK_WIDGET(tool->histogram), fixed_sens);
    gwy_param_table_set_sensitive(tool->table, PARAM_START, fixed_sens);
    gwy_param_table_set_sensitive(tool->table, PARAM_END, fixed_sens);
    gwy_param_table_set_sensitive(tool->table, BUTTON_INVERT, fixed_sens);
    gwy_param_table_set_sensitive(tool->table, BUTTON_SET_TO_FULL, fixed_sens);
    gwy_param_table_set_sensitive(tool->table, PARAM_USE_SELECTION, fixed_sens);

    old_mode = -1;
    gwy_dict_gis_enum_by_name(gwy_app_settings_get(), APP_RANGE_KEY, &old_mode);
    gtk_widget_set_sensitive(tool->is_default, old_mode != mapping);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(tool->is_default), old_mode == mapping);
    gwy_tool_color_range_mask_changed(plain_tool);
}

static void
set_default_mode(GtkToggleButton *check,
                 GwyToolColorRange *tool)
{
    if (!gtk_toggle_button_get_active(check))
        return;

    gwy_dict_set_enum_by_name(gwy_app_settings_get(), APP_RANGE_KEY, get_range_type(tool));
    /* This is a bit silly.  However unchecking the check box has not defined meaning, so just don't allow it. */
    gtk_widget_set_sensitive(tool->is_default, FALSE);
}

static GwyColorMappingType
get_range_type(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyColorMappingType mapping = GWY_COLOR_MAPPING_FULL;

    plain_tool = GWY_PLAIN_TOOL(tool);
    if (plain_tool->data_view)
        mapping = gwy_data_view_get_color_mapping(plain_tool->data_view);
    else
        gwy_dict_gis_enum_by_name(gwy_app_settings_get(), APP_RANGE_KEY, &mapping);

    return mapping;
}

static void
set_range_type(GwyToolColorRange *tool,
               GwyColorMappingType mapping)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    g_return_if_fail(plain_tool->data_view);
    gwy_dict_set_enum(plain_tool->container, gwy_file_key_image_color_mapping(plain_tool->id), mapping);
}

static void
get_min_max(GwyToolColorRange *tool,
            gdouble *selection)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    gint i;

    g_return_if_fail(plain_tool->data_view && plain_tool->field);

    for (i = 0; i < 2; i++) {
        selection[i] = gwy_field_min(plain_tool->field);
        gwy_dict_gis_double(plain_tool->container, tool->key_min_max[i], &selection[i]);
    }
}

static void
set_min_max(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    const GwyValueFormat *vf = plain_tool->value_format;
    gboolean clear = FALSE;
    gdouble sel[2];
    gint i, col, row, w, h;

    if (tool->data_switch)
        return;

    if (!plain_tool->container) {
        update_fullrange(tool);
        return;
    }

    if (tool->range_source == USE_SELECTION) {
        gwy_debug("source: area selection");
        col = tool->isel[0];
        row = tool->isel[1];
        w = tool->isel[2]+1 - tool->isel[0];
        h = tool->isel[3]+1 - tool->isel[1];
        if (!plain_tool->selection || !gwy_selection_get_data(plain_tool->selection, NULL) || w <= 1 || h <= 1)
            clear = TRUE;
        else {
            gwy_field_area_min_max(plain_tool->field, NULL, GWY_MASK_IGNORE, col, row, w, h, &sel[0], &sel[1]);
        }
    }
    else if (tool->range_source == USE_HISTOGRAM) {
        gwy_debug("source: histogram");
        if (!gwy_selection_get_object(tool->graph_selection, 0, sel) || sel[0] == sel[1])
            clear = TRUE;
    }
    else {
        g_return_if_reached();
    }
    gwy_debug("[%g, %g]", sel[0], sel[1]);

    if (clear) {
        for (i = 0; i < 2; i++)
            gwy_dict_remove(plain_tool->container, tool->key_min_max[i]);
        gwy_field_min_max(plain_tool->field, &sel[0], &sel[1]);
    }
    else {
        for (i = 0; i < 2; i++)
            gwy_dict_set_double(plain_tool->container, tool->key_min_max[i], sel[i]);
    }

    if (!tool->programmatic_update) {
        tool->programmatic_update = TRUE;
        gwy_param_table_set_unitstr(tool->table, PARAM_START, vf->units);
        gwy_param_table_set_unitstr(tool->table, PARAM_END, vf->units);
        gwy_param_table_set_double(tool->table, PARAM_START, sel[0]/vf->magnitude);
        gwy_param_table_set_double(tool->table, PARAM_END, sel[1]/vf->magnitude);
        tool->programmatic_update = FALSE;
    }
}

static void
update_fullrange(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    const GwyValueFormat *vf = plain_tool->value_format;
    gdouble min, max;
    gchar buf[64];

    if (plain_tool->container) {
        gwy_field_min_max(plain_tool->field, &min, &max);
        g_snprintf(buf, sizeof(buf), "%.*f%s%s", vf->precision, min/vf->magnitude, *vf->units ? " " : "", vf->units);
        gwy_param_table_info_set_valuestr(tool->table, INFO_MINIMUM, buf);
        g_snprintf(buf, sizeof(buf), "%.*f%s%s", vf->precision, max/vf->magnitude, *vf->units ? " " : "", vf->units);
        gwy_param_table_info_set_valuestr(tool->table, INFO_MAXIMUM, buf);
    }
    else {
        gwy_param_table_info_set_valuestr(tool->table, INFO_MINIMUM, NULL);
        gwy_param_table_info_set_valuestr(tool->table, INFO_MAXIMUM, NULL);
    }
}

static void
update_histogram(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyGraphCurveModel *cmodel;
    gdouble data[2] = { 0.0, 0.0 };

    cmodel = gwy_graph_model_get_curve(tool->histogram_model, 0);
    if (!plain_tool->field) {
        gwy_graph_curve_model_set_data(cmodel, data, data, G_N_ELEMENTS(data));
        return;
    }

    gwy_field_height_dist(plain_tool->field, tool->heightdist, 0);
    /* rescale to sqrt to make more readable  */
    gwy_line_sqrt(tool->heightdist);

    gwy_graph_curve_model_set_data_from_line(cmodel, tool->heightdist, 0, 0);
}

static void
param_changed(GwyToolColorRange *tool, gint id)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    const GwyValueFormat *vf = plain_tool->value_format;
    gdouble sel[2];
    gint i;

    if (tool->programmatic_update)
        return;

    /* The logic what should happen is unclear. Switching immediately to range defined by the rectangle seems to be
     * intuitive only when the user manipulated the rectangle more recently than other things. */
    if (id == PARAM_USE_SELECTION
        && gwy_params_get_boolean(tool->params, PARAM_USE_SELECTION)
        && tool->range_source == USE_SELECTION)
        gwy_tool_color_range_selection_changed(plain_tool, -1);

    for (i = 0; i < 2; i++) {
        sel[i] = gwy_params_get_double(tool->params, PARAM_START + i)*vf->magnitude;
        gwy_dict_set_double(plain_tool->container, tool->key_min_max[i], sel[i]);
    }

    tool->programmatic_update = TRUE;
    gwy_selection_set_data(tool->graph_selection, 1, sel);
    tool->programmatic_update = FALSE;
}

static void
set_range_using_mask(GwyToolColorRange *tool, GwyMaskingType masking)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *dfield = plain_tool->field;
    GwyNield *mask = plain_tool->mask_field;
    const GwyValueFormat *vf = plain_tool->value_format;
    gdouble sel[2];
    gint i;

    if (!dfield)
        return;

    gwy_field_area_min_max(dfield, mask, masking, 0, 0, dfield->xres, dfield->yres, &sel[0], &sel[1]);
    /* No valid pixels? */
    if (sel[1] < sel[0])
        gwy_field_min_max(dfield, &sel[0], &sel[1]);

    for (i = 0; i < 2; i++)
        gwy_dict_set_double(plain_tool->container, tool->key_min_max[i], sel[i]);

    tool->programmatic_update = TRUE;
    gwy_selection_set_data(tool->graph_selection, 1, sel);
    gwy_param_table_set_double(tool->table, PARAM_START, sel[0]/vf->magnitude);
    gwy_param_table_set_double(tool->table, PARAM_END, sel[1]/vf->magnitude);
    tool->programmatic_update = FALSE;
}

static void
gwy_tool_color_range_response(GwyTool *gwytool, gint response_id)
{
    GwyToolColorRange *tool = GWY_TOOL_COLOR_RANGE(gwytool);

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

    if (response_id == RESPONSE_SET_TO_MASKED)
        set_range_using_mask(tool, GWY_MASK_INCLUDE);
    else if (response_id == RESPONSE_SET_TO_UNMASKED)
        set_range_using_mask(tool, GWY_MASK_EXCLUDE);
    else if (response_id == RESPONSE_SET_TO_FULL)
        set_range_using_mask(tool, GWY_MASK_IGNORE);
    else if (response_id == RESPONSE_INVERT)
        invert_mapping(tool);
    else if (response_id == RESPONSE_SET_ZERO)
        set_zero_to_minimum(tool);
}

static void
invert_mapping(GwyToolColorRange *tool)
{
    gdouble min, max;

    if (!GWY_PLAIN_TOOL(tool)->field)
        return;

    min = gwy_params_get_double(tool->params, PARAM_START);
    max = gwy_params_get_double(tool->params, PARAM_END);

    tool->programmatic_update = TRUE;
    gwy_param_table_set_double(tool->table, PARAM_START, max);
    gwy_param_table_set_double(tool->table, PARAM_END, min);
    tool->programmatic_update = FALSE;

    gwy_param_table_param_changed(tool->table, -1);
}

static void
set_zero_to_minimum(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwyField *dfield = plain_tool->field;
    const GwyValueFormat *vf = plain_tool->value_format;
    gdouble min, max;

    if (!dfield)
        return;

    min = gwy_params_get_double(tool->params, PARAM_START) * vf->magnitude;
    max = gwy_params_get_double(tool->params, PARAM_END) * vf->magnitude;

    tool->programmatic_update = TRUE;
    gwy_field_add(dfield, -min);
    gwy_field_data_changed(dfield);
    /* XXX: Is this always correct when vf changes? */
    gwy_param_table_set_double(tool->table, PARAM_START, 0.0);
    gwy_param_table_set_double(tool->table, PARAM_END, (max - min)/vf->magnitude);
    tool->programmatic_update = FALSE;

    gwy_param_table_param_changed(tool->table, -1);
}

static void
update_selected_rectangle(GwyToolColorRange *tool)
{
    GwyPlainTool *plain_tool = GWY_PLAIN_TOOL(tool);
    GwySelection *selection = plain_tool->selection;
    GwyField *field = plain_tool->field;
    gint n;

    n = selection ? gwy_selection_get_data(selection, NULL) : 0;
    gwy_rect_selection_labels_fill(tool->rlabels, n == 1 ? selection : NULL, field, NULL, tool->isel);
}

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