/*
 *  $Id: data-view.c 30130 2026-06-25 07:46:27Z yeti-dn $
 *  Copyright (C) 2003-2025 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-object.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/field.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/serializable-boxed.h"

#include "libgwyui/types.h"
#include "libgwyui/data-view.h"
#include "libgwyui/pixbuf-render.h"
#include "libgwyui/widget-impl-utils.h"

/* Rounding errors */
#define EPS 1e-6

enum {
    SGNL_REDRAWN,
    SGNL_RESIZED,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_FIELD,
    PROP_MASK,
    PROP_GRADIENT,
    PROP_MASK_COLOR,
    PROP_REAL_SQUARE,
    PROP_COLOR_MAPPING,
    PROP_ZOOM,
    PROP_RESIZEABLE,
    NUM_PROPERTIES
};

struct _GwyDataViewPrivate {
    GdkWindow *event_window;
    gchar *cursor_name;

    GwyField *field;
    GwyNield *mask;
    GwyGradient *gradient;
    GwyVectorLayer *interactive_layer;
    GwyColorMappingType color_mapping;
    gboolean real_square;
    gboolean fixed_range_set;
    gdouble fixed_min;
    gdouble fixed_max;
    gdouble range_min;
    gdouble range_max;
    GwyRGBA mask_color;

    gulong interactive_update_id;
    gulong field_changed_id;
    gulong mask_changed_id;
    gulong gradient_changed_id;
    gulong default_gradient_changed_id;

    gboolean size_requested;
    gboolean field_needs_repaint;
    gboolean mask_needs_repaint;

    gboolean resizable;
    gdouble zoom;    /* real zoom (larger number means larger pixmaps) */
    gdouble newzoom;    /* requested (ideal) zoom value */
    gdouble xmeasure;    /* physical units per pixel */
    gdouble ymeasure;    /* physical units per pixel */
    gint xoff;    /* x offset of the pixbuf from widget->allocation.x */
    gint yoff;    /* y offset of the pixbuf from widget->allocation.y */

    /* These are cached data field properties. We use them a lot… */
    gint xres;
    gint yres;
    gdouble xreal;
    gdouble yreal;

    gint scwidth;
    gint scheight;

    GdkPixbuf *field_pixbuf;
    GdkPixbuf *mask_pixbuf;
};

static void     dispose                 (GObject *object);
static void     set_property            (GObject *object,
                                         guint prop_id,
                                         const GValue *value,
                                         GParamSpec *pspec);
static void     get_property            (GObject *object,
                                         guint prop_id,
                                         GValue *value,
                                         GParamSpec *pspec);
static void     realize                 (GtkWidget *widget);
static void     unrealize               (GtkWidget *widget);
static void     map                     (GtkWidget *widget);
static void     unmap                   (GtkWidget *widget);
static void     get_preferred_width     (GtkWidget *widget,
                                         gint *minimum,
                                         gint *natural);
static void     get_preferred_height    (GtkWidget *widget,
                                         gint *minimum,
                                         gint *natural);
static void     size_allocate           (GtkWidget *widget,
                                         GdkRectangle *allocation);
static void     calculate_scaling       (GwyDataView *view);
static void     make_pixbufs            (GwyDataView *view);
static gboolean draw                    (GtkWidget *widget,
                                         cairo_t *cr);
static void     set_layer               (GwyDataView *view,
                                         gpointer *which_layer,
                                         gulong *hid,
                                         GwyDataViewLayer *layer);
static void     field_changed           (GwyDataView *view);
static void     mask_changed            (GwyDataView *view);
static void     gradient_changed        (GwyDataView *view);
static void     follow_default_gradient (GwyDataView *view,
                                         gboolean follow);
static void     default_gradient_changed(GwyDataView *view);
static void     paint_field             (GwyDataView *view);
static void     paint_mask              (GwyDataView *view);
static void     set_cursor              (GwyDataView *view);

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWidgetClass *parent_class = NULL;

static const GwyRGBA default_mask_color = { 1.0, 0.0, 0.0, 0.5 };

G_DEFINE_TYPE_WITH_CODE(GwyDataView, gwy_data_view, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyDataView))

static void
gwy_data_view_class_init(GwyDataViewClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_data_view_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->draw = draw;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->size_allocate = size_allocate;

    /**
     * GwyDataView::redrawn:
     * @gwydataview: The #GwyDataView which received the signal.
     *
     * The ::redrawn signal is emitted when #GwyDataView redraws pixbufs after an update.  That is, when it's the
     * right time to get a new pixbuf from gwy_data_view_get_pixbuf().
     **/
    signals[SGNL_REDRAWN] = g_signal_new("redrawn", type,
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET(GwyDataViewClass, redrawn),
                                         NULL, NULL,
                                         g_cclosure_marshal_VOID__VOID,
                                         G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_REDRAWN], type, g_cclosure_marshal_VOID__VOIDv);

    /**
     * GwyDataView::resized:
     * @gwydataview: The #GwyDataView which received the signal.
     *
     * The ::resized signal is emitted when #GwyDataView wants to be resized, that is when the dimensions of base data
     * field changes or square mode is changed. Its purpose is to subvert the normal resizing logic of #GwyDataWindow
     * (due to geometry hints, its size requests are generally ignored, so an explicit resize is needed).  You should
     * usually ignore it.
     **/
    signals[SGNL_RESIZED] = g_signal_new("resized", type,
                                         G_SIGNAL_RUN_FIRST,
                                         G_STRUCT_OFFSET(GwyDataViewClass, resized),
                                         NULL, NULL,
                                         g_cclosure_marshal_VOID__VOID,
                                         G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_RESIZED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_FIELD] = g_param_spec_object("field", NULL,
                                                 "Data field to display",
                                                 GWY_TYPE_FIELD, GWY_GPARAM_RWE);
    properties[PROP_MASK] = g_param_spec_object("mask", NULL,
                                                "Mask field to display",
                                                GWY_TYPE_FIELD, GWY_GPARAM_RWE);
    properties[PROP_GRADIENT] = g_param_spec_object("gradient", NULL,
                                                    "Color gradient used for data visualization",
                                                    GWY_TYPE_GRADIENT, GWY_GPARAM_RWE);
    properties[PROP_MASK_COLOR] = g_param_spec_boxed("mask-color", NULL,
                                                     "Color use for mask visualization",
                                                     GWY_TYPE_RGBA, GWY_GPARAM_RWE);
    properties[PROP_REAL_SQUARE] = g_param_spec_boolean("real-square", NULL,
                                                        "Whether to display data with physical aspect ratio "
                                                        "(as opposed to square pixels)",
                                                        FALSE, GWY_GPARAM_RWE);
    properties[PROP_COLOR_MAPPING] = g_param_spec_enum("color-mapping", NULL,
                                                       "Type of mapping from data values to colors",
                                                       GWY_TYPE_COLOR_MAPPING_TYPE, GWY_COLOR_MAPPING_FULL,
                                                       GWY_GPARAM_RWE);
    properties[PROP_ZOOM] = g_param_spec_double("zoom", NULL,
                                                "The size of a pixel on screen, in horizontal direction",
                                                1/64.0, 64.0, 1.0, GWY_GPARAM_RWE);
    properties[PROP_RESIZEABLE] = g_param_spec_boolean("resizable", NULL,
                                                       "Whether resizing by the user is enabled",
                                                       TRUE, GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_data_view_init(GwyDataView *view)
{
    GwyDataViewPrivate *priv;

    view->priv = priv = gwy_data_view_get_instance_private(view);
    priv->zoom = 1.0;
    priv->newzoom = 1.0;
    priv->resizable = TRUE;
    priv->mask_color = default_mask_color;
    priv->fixed_min = priv->range_min = 0.0;
    priv->fixed_max = priv->range_max = 1.0;
    priv->color_mapping = GWY_COLOR_MAPPING_FULL;

    gtk_widget_set_can_focus(GTK_WIDGET(view), TRUE);
    gtk_widget_set_has_window(GTK_WIDGET(view), FALSE);
    /* Connect to the default gradient. This will the right thing because initially priv->gradient is NULL. */
    gwy_data_view_set_gradient(view, NULL);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyDataView *view = GWY_DATA_VIEW(object);

    switch (prop_id) {
        case PROP_FIELD:
        gwy_data_view_set_field(view, g_value_get_object(value));
        break;

        case PROP_MASK:
        gwy_data_view_set_mask(view, g_value_get_object(value));
        break;

        case PROP_GRADIENT:
        gwy_data_view_set_gradient(view, g_value_get_object(value));
        break;

        case PROP_MASK_COLOR:
        gwy_data_view_set_mask_color(view, g_value_get_boxed(value));
        break;

        case PROP_REAL_SQUARE:
        gwy_data_view_set_real_square(view, g_value_get_boolean(value));
        break;

        case PROP_COLOR_MAPPING:
        gwy_data_view_set_color_mapping(view, g_value_get_enum(value));
        break;

        case PROP_ZOOM:
        gwy_data_view_set_zoom(view, g_value_get_double(value));
        break;

        case PROP_RESIZEABLE:
        gwy_data_view_set_resizable(view, g_value_get_boolean(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyDataView *dataview = GWY_DATA_VIEW(object);
    GwyDataViewPrivate *priv = dataview->priv;

    switch (prop_id) {
        case PROP_FIELD:
        g_value_set_object(value, priv->field);
        break;

        case PROP_MASK:
        g_value_set_object(value, priv->mask);
        break;

        case PROP_GRADIENT:
        g_value_set_object(value, gwy_data_view_get_gradient(dataview));
        break;

        case PROP_MASK_COLOR:
        g_value_set_boxed(value, &priv->mask_color);
        break;

        case PROP_REAL_SQUARE:
        g_value_set_boolean(value, priv->real_square);
        break;

        case PROP_COLOR_MAPPING:
        g_value_set_enum(value, priv->color_mapping);
        break;

        case PROP_ZOOM:
        /* FIXME GTK3: This is what we have been traditionally doing. */
        g_value_set_double(value, priv->newzoom);
        break;

        case PROP_RESIZEABLE:
        g_value_set_boolean(value, priv->resizable);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
dispose(GObject *object)
{
    GwyDataView *view = GWY_DATA_VIEW(object);
    GwyDataViewPrivate *priv = view->priv;

    set_layer(view, (gpointer*)&priv->interactive_layer, NULL, NULL);

    follow_default_gradient(view, FALSE);
    g_clear_signal_handler(&priv->gradient_changed_id, priv->gradient);

    g_clear_signal_handler(&priv->field_changed_id, priv->field);
    g_clear_signal_handler(&priv->mask_changed_id, priv->mask);

    g_clear_object(&priv->interactive_layer);
    g_clear_object(&priv->gradient);
    g_clear_object(&priv->mask);
    g_clear_object(&priv->field);

    G_OBJECT_CLASS(parent_class)->dispose(object);
}

/**
 * gwy_data_view_new: (constructor)
 *
 * Creates a new data field-displaying widget.
 *
 * Use gwy_data_view_set_field() to set the data field to display.
 *
 * Returns: (transfer full):
 *          A newly created data view as a #GtkWidget.
 **/
GtkWidget*
gwy_data_view_new(void)
{
    return gtk_widget_new(GWY_TYPE_DATA_VIEW, NULL);
}

static void
realize(GtkWidget *widget)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;

    /* FIXME GTK3 widgets generally do not call parent's realize. Not sure why because it does a couple of things for
     * us like setting the widget realized. */
    parent_class->realize(widget);
    /* TODO GTK3 scroll and touch events */
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
                                                        | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
                                                        | GDK_POINTER_MOTION_MASK);

    make_pixbufs(view);
    set_cursor(view);
}

static void
unrealize(GtkWidget *widget)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;

    gwy_destroy_widget_input_window(widget, &priv->event_window);

    g_clear_object(&priv->field_pixbuf);
    g_clear_object(&priv->mask_pixbuf);

    parent_class->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyDataViewPrivate *priv = GWY_DATA_VIEW(widget)->priv;
    parent_class->map(widget);
    // FIXME: Interactive layers used to have realize() and unrealize(). This seems misguided, but they may need
    // map() and unmap(). Not much can rally change for them, but maybe fonts. Cursors are sort of handled for them.
    gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyDataViewPrivate *priv = GWY_DATA_VIEW(widget)->priv;
    gdk_window_hide(priv->event_window);
    parent_class->unmap(widget);
}

static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;

    *minimum = *natural = 2;
    if (!priv->xres || !priv->yres)
        return;

    gwy_debug("xres = %d, yres = %d", priv->xres, priv->yres);
    if (priv->real_square) {
        gdouble scale = fmax(priv->xres/priv->xreal, priv->yres/priv->yreal);
        scale *= priv->newzoom;
        *natural = GWY_ROUND(scale * priv->xreal);
    }
    else
        *natural = GWY_ROUND(priv->newzoom * priv->xres);

    if (!priv->resizable)
        *minimum = *natural;

    priv->size_requested = TRUE;
    gwy_debug("requesting width min: %d, nat: %d", *minimum, *natural);
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;

    *minimum = *natural = 2;
    if (!priv->xres || !priv->yres)
        return;

    if (priv->real_square) {
        gdouble scale = fmax(priv->xres/priv->xreal, priv->yres/priv->yreal);
        scale *= priv->newzoom;
        *natural = GWY_ROUND(scale * priv->yreal);
    }
    else {
        *natural = GWY_ROUND(priv->newzoom * priv->yres);
    }

    if (!priv->resizable)
        *minimum = *natural;

    priv->size_requested = TRUE;
    gwy_debug("requesting height min: %d, nat: %d", *minimum, *natural);
}

static void
size_allocate(GtkWidget *widget,
              GdkRectangle *allocation)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;

    gwy_debug("allocating %d x %d", allocation->width, allocation->height);

    parent_class->size_allocate(widget, allocation);
    if (priv->event_window)
        gdk_window_move_resize(priv->event_window, allocation->x, allocation->y, allocation->width, allocation->height);

    /* FIXME: Is it necessary to do it here? We should only update the pixbuf when we need to paint. Or someone
     * asks for the pixbuf programatically. */
    //make_pixbufs(view);
    /* Update ideal zoom after a `spontanoues' size-allocate when someone simply changed the size w/o asking us.  But
     * if we were queried first, be persistent and request the same zoom also next time */
    if (!priv->size_requested) {
        priv->newzoom = priv->zoom;
        g_object_notify_by_pspec(G_OBJECT(view), properties[PROP_ZOOM]);
    }
    priv->size_requested = FALSE;
    calculate_scaling(view);
}

static void
make_pixbufs(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;

    if (!priv->xres || !priv->yres) {
        g_clear_object(&priv->field_pixbuf);
        g_clear_object(&priv->mask_pixbuf);
        return;
    }

    paint_field(view);
    if (priv->mask)
        paint_mask(view);
}

static void
calculate_scaling(GwyDataView *view)
{
    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(view), &allocation);
    gdouble allocwidth = allocation.width, allocheight = allocation.height;

    GwyDataViewPrivate *priv = view->priv;
    if (priv->real_square) {
        gdouble scale = fmax(priv->xres/priv->xreal, priv->yres/priv->yreal);
        priv->zoom = fmin(allocwidth/(scale*priv->xreal), allocheight/(scale*priv->yreal));
        scale *= priv->zoom;
        priv->scwidth = GWY_ROUND(scale * priv->xreal);
        priv->scheight = GWY_ROUND(scale * priv->yreal);
    }
    else {
        priv->zoom = fmin(allocwidth/priv->xres, allocheight/priv->yres);
        priv->scwidth = GWY_ROUND(priv->xres * priv->zoom);
        priv->scheight = GWY_ROUND(priv->yres * priv->zoom);
    }
    priv->scwidth = MAX(priv->scwidth, 1);
    priv->scheight = MAX(priv->scheight, 1);
    priv->xmeasure = priv->xreal/priv->scwidth;
    priv->ymeasure = priv->yreal/priv->scheight;
    priv->xoff = (allocwidth - priv->scwidth)/2;
    priv->yoff = (allocheight - priv->scheight)/2;
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyDataView *view = GWY_DATA_VIEW(widget);
    GwyDataViewPrivate *priv = view->priv;
    gboolean emit_redrawn = FALSE;

    if (!priv->xres || !priv->yres) {
        cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
        cairo_paint(cr);
        return FALSE;
    }

    /* XXX GTK3 we are doing a bunch of stupid redraws every window switch event, even though nothing changes in the
     * data view. It is because there are some animations of stupid box shadow decorations which queue resize (sic)
     * and of course redraw. The entire thing is stupid, stupid, stupid. */
    {
        GdkRectangle rect;
        gdk_cairo_get_clip_rectangle(cr, &rect);
    }

    /* This means we requested new size, but received no allocation -- because the new size was identical to the old
     * one.  BUT we had a reason for that.  Typically this happens after a rotation of real_square-displayed data: the
     * new widget size is the same, but the the stretched dimension is now the other one and thus pixmap sizes have to
     * be recalculated. */
    if (priv->size_requested) {
        /* TODO: If we want to cache a Cairo pattern rendered at the correct size, we would do something here. */
        priv->size_requested = FALSE;
    }

    if (priv->field_needs_repaint || priv->mask_needs_repaint)
        emit_redrawn = TRUE;

    /* This is a no-op when we are not repainting anything. Run it uncoditionally. */
    make_pixbufs(view);
    calculate_scaling(view);

    cairo_save(cr);
    cairo_translate(cr, priv->xoff, priv->yoff);
    cairo_scale(cr, (gdouble)priv->scwidth/priv->xres, (gdouble)priv->scheight/priv->yres);
    gdk_cairo_set_source_pixbuf(cr, priv->field_pixbuf, 0, 0);
    cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
    cairo_paint(cr);
    if (priv->mask_pixbuf) {
        gdk_cairo_set_source_pixbuf(cr, priv->mask_pixbuf, 0, 0);
        cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_NEAREST);
        cairo_paint(cr);
    }
    cairo_restore(cr);

    if (priv->interactive_layer)
        gwy_vector_layer_draw(GWY_VECTOR_LAYER(priv->interactive_layer), cr);

    if (emit_redrawn)
        g_signal_emit(view, signals[SGNL_REDRAWN], 0);

    return FALSE;
}

static void
update(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    GwyField *field = priv->field;
    gboolean need_resize = FALSE;

    if (!field)
        return;

    priv->xres = gwy_field_get_xres(field);
    priv->yres = gwy_field_get_yres(field);
    priv->xreal = gwy_field_get_xreal(field);
    priv->yreal = gwy_field_get_yreal(field);
    gwy_debug("updated xres=%d, yres=%d", priv->xres, priv->yres);

    GtkWidget *widget = GTK_WIDGET(view);
    if (!gtk_widget_get_realized(widget))
        return;

    if (priv->field_pixbuf) {
        gint pxres = gdk_pixbuf_get_width(priv->field_pixbuf);
        gint pyres = gdk_pixbuf_get_height(priv->field_pixbuf);
        gwy_debug("field: %dx%d, pixbuf: %dx%d", priv->xres, priv->yres, pxres, pyres);
        if (pxres != priv->xres || pyres != priv->yres)
            need_resize = TRUE;
    }

    calculate_scaling(view);
    if (need_resize) {
        gwy_debug("needs resize");
        gtk_widget_queue_resize(widget);
        g_signal_emit(widget, signals[SGNL_RESIZED], 0);
    }
    else
        gtk_widget_queue_draw(widget);
}

/**
 * gwy_data_view_get_interactive_layer:
 * @view: A data view.
 *
 * Returns the top layer this data view currently uses, or %NULL if none is present.
 *
 * Returns: The currently used top layer.
 **/
GwyVectorLayer*
gwy_data_view_get_interactive_layer(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), NULL);
    return view->priv->interactive_layer;
}

/* FIXME: There is currently only one settable layer.
 *        Can se just use gwy_set_member_object() and do the reparenting afterwards? */
static void
set_layer(GwyDataView *view,
          gpointer *which_layer,
          gulong *hid,
          GwyDataViewLayer *layer)
{
    //GwyDataViewPrivate *priv = view->priv;
    GwyDataViewLayer *old_layer = (GwyDataViewLayer*)*which_layer;

    if (layer == old_layer)
        return;
    if (old_layer) {
        if (hid) {
            g_signal_handler_disconnect(old_layer, *hid);
            *hid = 0;
        }
#if 0
        if (gtk_widget_get_realized(GTK_WIDGET(view)))
            gwy_data_view_layer_unrealize(GWY_DATA_VIEW_LAYER(*which_layer));
#endif
        /* XXX: Some things used to expect we send the signal first, actually unplug later. But we do not do that
         * any more. */
        gwy_data_view_layer_set_parent(old_layer, NULL);
        g_object_unref(old_layer);
    }
    if (layer) {
        g_assert(!gwy_data_view_layer_get_parent(layer));
        g_object_ref_sink(layer);
        if (hid)
            *hid = g_signal_connect_swapped(layer, "updated", G_CALLBACK(update), view);
        gwy_data_view_layer_set_parent(layer, GTK_WIDGET(view));
    }
    *which_layer = layer;
    update(view);
}

/**
 * gwy_data_view_set_interactive_layer:
 * @view: A data view.
 * @layer: A layer to be used as the top layer for @view.
 *
 * Plugs @layer to @view as the top layer.
 *
 * If another top layer is present, it's unplugged.
 *
 * The layer can be %NULL, meaning no top layer is to be used.
 **/
void
gwy_data_view_set_interactive_layer(GwyDataView *view,
                                    GwyVectorLayer *layer)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(!layer || GWY_IS_VECTOR_LAYER(layer));

    GwyDataViewPrivate *priv = view->priv;
    set_layer(view, (gpointer)&priv->interactive_layer, &priv->interactive_update_id, GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_data_view_get_field:
 * @view: A data view.
 *
 * Gets the data field currently shown by the data view.
 *
 * Returns: (nullable) (transfer none):
 *          The currently displayed data field, possibly %NULL.
 **/
GwyField*
gwy_data_view_get_field(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), NULL);
    return view->priv->field;
}

/**
 * gwy_data_view_set_field:
 * @view: A data view.
 * @field: (transfer none) (nullable):
 *         Data field to display (or %NULL).
 *
 * Sets the data field to display in a data view.
 *
 * Setting the same field object repeatedly is generally a no-op. Emit GwyField::data-changed on @field to notify the
 * view to redraw.
 **/
void
gwy_data_view_set_field(GwyDataView *view,
                        GwyField *field)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (!gwy_set_member_object(view, field, GWY_TYPE_FIELD, &priv->field,
                               "data-changed", &field_changed, &priv->field_changed_id, G_CONNECT_SWAPPED,
                               NULL)) {
        // We do not promise to do anything when the caller sets the same field again instead of emitting
        // "data-changed". But it can confuse size calculations. Catch at least dimension changes.
        if (field
            && gwy_field_get_xres(field) == priv->xres
            && gwy_field_get_yres(field) == priv->yres
            && (!priv->real_square || (gwy_field_get_yreal(field) == priv->xreal
                                       && gwy_field_get_yreal(field) == priv->xreal)))
            return;
    }
    g_clear_object(&priv->field_pixbuf);
    field_changed(view);
}

/**
 * gwy_data_view_get_mask:
 * @view: A data view.
 *
 * Gets the mask field currently shown by the data view.
 *
 * Returns: (nullable) (transfer none):
 *          The currently displayed mask field, possibly %NULL.
 **/
GwyNield*
gwy_data_view_get_mask(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), NULL);
    return view->priv->mask;
}

/**
 * gwy_data_view_set_mask:
 * @view: A data view.
 * @mask: (transfer none) (nullable): Number field to draw as the mask.
 *
 * Sets the mask field currently shown by the data view.
 *
 * Setting the same nield object repeatedly is generally a no-op. Emit GwyNield::data-changed on @mask to notify the
 * view to redraw.
 **/
void
gwy_data_view_set_mask(GwyDataView *view,
                       GwyNield *mask)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (!gwy_set_member_object(view, mask, GWY_TYPE_NIELD, &priv->mask,
                               "data-changed", &mask_changed, &priv->mask_changed_id, G_CONNECT_SWAPPED,
                               NULL)) {
        // We do not promise to do anything when the caller sets the same field again instead of emitting
        // "data-changed". But it can confuse size calculations. Catch at least dimension changes.
        if (mask
            && gwy_nield_get_xres(mask) == priv->xres
            && gwy_nield_get_yres(mask) == priv->yres)
            return;
    }
    g_clear_object(&priv->mask_pixbuf);
    mask_changed(view);
}

/**
 * gwy_data_view_get_real_square:
 * @view: A data view.
 *
 * Gets the whether the aspect ratio of a data view should be based on physical or pixel coordinates.
 *
 * Returns: %TRUE for square aspect ratio in physical coordinates; %FALSE for square pixels.
 **/
gboolean
gwy_data_view_get_real_square(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), FALSE);
    return view->priv->real_square;
}

/**
 * gwy_data_view_set_real_square:
 * @view: A data view.
 * @real_square: %TRUE for square aspect ratio in physical coordinates; %FALSE for square pixels.
 *
 * Sets the whether the aspect ratio of a data view should be based on physical or pixel coordinates.
 **/
void
gwy_data_view_set_real_square(GwyDataView *view,
                              gboolean real_square)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (!real_square == !priv->real_square)
        return;

    priv->real_square = real_square;
    GtkWidget *widget = GTK_WIDGET(view);
    if (gtk_widget_get_realized(GTK_WIDGET(widget))) {
        gtk_widget_queue_resize(widget);
        g_signal_emit(widget, signals[SGNL_RESIZED], 0);
    }
}

/**
 * gwy_data_view_get_color_range:
 * @view: A data view.
 * @min: (out) (nullable):
 *       Location to store the data value corresponding to the beginning of colour mapping.
 * @max: (out) (nullable):
 *       Location to store the data value corresponding to the end of colour mapping.
 *
 * Gets the actual colour mapping range of a data view.
 *
 * The actual range is updated only when the widet is actually drawn. See gwy_data_view_get_fixed_color_range() for
 * the fixed colour mapping range, which applied in the %GWY_COLOR_MAPPING_FIXED mode.
 **/
void
gwy_data_view_get_color_range(GwyDataView *view,
                              gdouble *min,
                              gdouble *max)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (min)
        *min = priv->range_min;
    if (max)
        *max = priv->range_max;
}

/**
 * gwy_data_view_get_fixed_color_range:
 * @view: A data view.
 * @min: (out) (nullable):
 *       Location to store the data value corresponding to the beginning of fixed colour mapping.
 * @max: (out) (nullable):
 *       Location to store the data value corresponding to the end of fixed colour mapping.
 *
 * Gets the fixed colour mapping range of a data view.
 *
 * See gwy_data_view_get_color_range() for the actual range, even when it is automatic.
 *
 * The function always fills the values. However, when it returns %FALSE the values are not actually used.
 *
 * Returns: %TRUE if the fixed range is set.
 **/
gboolean
gwy_data_view_get_fixed_color_range(GwyDataView *view,
                                    gdouble *min,
                                    gdouble *max)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), FALSE);
    GwyDataViewPrivate *priv = view->priv;
    if (min)
        *min = priv->fixed_min;
    if (max)
        *max = priv->fixed_max;
    return priv->fixed_range_set;
}

/**
 * gwy_data_view_set_fixed_color_range:
 * @view: A data view.
 * @min: Data value corresponding to the beginning of fixed colour mapping.
 * @max: Data value corresponding to the end of fixed colour mapping.
 *
 * Sets the fixed colour mapping range of a data view.
 *
 * The fixed minimum and maximum apply if the mapping type %GWY_COLOR_MAPPING_FIXED, as set by
 * gwy_data_view_set_color_mapping().
 **/
void
gwy_data_view_set_fixed_color_range(GwyDataView *view,
                                    gdouble min,
                                    gdouble max)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (min == priv->fixed_min && max == priv->fixed_max)
        return;

    priv->fixed_min = min;
    priv->fixed_max = max;
    priv->fixed_range_set = TRUE;
    if (priv->color_mapping == GWY_COLOR_MAPPING_FIXED && priv->field) {
        priv->field_needs_repaint = TRUE;
        update(view);
    }
}

/**
 * gwy_data_view_unset_fixed_color_range:
 * @view: A data view.
 *
 * Unsets the fixed colour mapping range of a data view.
 *
 * When the values are unset, the colour mapping remains %GWY_COLOR_MAPPING_FIXED but the values are not used. So the
 * data view behaves as if the mapping was %GWY_COLOR_MAPPING_FULL.
 **/
void
gwy_data_view_unset_fixed_color_range(GwyDataView *view)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (!priv->fixed_range_set)
        return;

    priv->fixed_range_set = FALSE;
    if (priv->color_mapping == GWY_COLOR_MAPPING_FIXED && priv->field) {
        priv->field_needs_repaint = TRUE;
        update(view);
    }
}

/**
 * gwy_data_view_get_color_mapping:
 * @view: A data view.
 *
 * Gets the colour mapping type of a data view.
 *
 * Returns: The mapping type used for visualising data values as colours.
 **/
GwyColorMappingType
gwy_data_view_get_color_mapping(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), GWY_COLOR_MAPPING_FULL);
    return view->priv->color_mapping;
}

/**
 * gwy_data_view_set_color_mapping:
 * @view: A data view.
 * @mapping: Colour mapping type to use.
 *
 * Sets the colour mapping type of a data view.
 *
 * The mapping type determines how data values are visualised as colours.
 **/
void
gwy_data_view_set_color_mapping(GwyDataView *view,
                                GwyColorMappingType mapping)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(mapping <= GWY_COLOR_MAPPING_ADAPT);
    GwyDataViewPrivate *priv = view->priv;
    if (mapping == priv->color_mapping)
        return;
    priv->color_mapping = mapping;
    if (priv->field) {
        priv->field_needs_repaint = TRUE;
        update(view);
    }
}

/**
 * gwy_data_view_get_gradient:
 * @view: A data view.
 *
 * Gets the colour gradient use for data visualisation in a data view.
 *
 * If no specific gradient has been set and the default one is used, the function returns %NULL. This is consistent
 * with getting the corresponding property. If you still need the #GwyGradient object in such case, use
 * gwy_gradients_get_gradient() with %NULL argument.
 *
 * Returns: (transfer none) (nullable):
 *          The current colour gradient, possibly %NULL.
 **/
GwyGradient*
gwy_data_view_get_gradient(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), NULL);
    GwyDataViewPrivate *priv = view->priv;
    return priv->default_gradient_changed_id ? NULL : priv->gradient;
}

/**
 * gwy_data_view_set_gradient:
 * @view: A data view.
 * @gradient: (transfer none) (nullable):
 *            Colour gradient to use for data visualisation.
 *
 * Sets the colour gradient to use for data visualisation in a data view.
 *
 * If %NULL is passed as @gradient, the data view will use the default gradient. Within libgwyui alone, the default
 * gradient is greyscale. However, when Gwyddion user settings are loaded, the default gradient is set according to
 * user preferences.
 **/
void
gwy_data_view_set_gradient(GwyDataView *view,
                           GwyGradient *gradient)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(!gradient || GWY_IS_GRADIENT(gradient));
    GwyDataViewPrivate *priv = view->priv;
    GwyGradient *real_gradient = gradient ? gradient : gwy_gradients_get_gradient(NULL);
    gboolean prop_changed = (priv->default_gradient_changed_id ? !!gradient : gradient != priv->gradient);
    gboolean visually_changed = gwy_set_member_object(view, real_gradient, GWY_TYPE_GRADIENT, &priv->gradient,
                                                      "data-changed", gradient_changed,
                                                      &priv->gradient_changed_id, G_CONNECT_SWAPPED,
                                                      NULL);
    if (visually_changed)
        gradient_changed(view);
    if (prop_changed) {
        follow_default_gradient(view, !gradient);
        g_object_notify_by_pspec(G_OBJECT(view), properties[PROP_GRADIENT]);
    }
}

/**
 * gwy_data_view_get_mask_color:
 * @view: A data view.
 * @color: (out):
 *         Colour used for mask visualisation.
 *
 * Gets the colour used for mask visualisation in a data view.
 **/
void
gwy_data_view_get_mask_color(GwyDataView *view,
                             GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(color);
    *color = view->priv->mask_color;
}

/**
 * gwy_data_view_set_mask_color:
 * @view: A data view.
 * @color: Colour to use for mask visualisation.
 *
 * Sets the colour used for mask visualisation in a data view.
 **/
void
gwy_data_view_set_mask_color(GwyDataView *view,
                             const GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(color);
    GwyDataViewPrivate *priv = view->priv;
    if (gwy_serializable_boxed_equal(GWY_TYPE_RGBA, color, &priv->mask_color))
        return;
    priv->mask_color = *color;
    if (priv->mask)
        update(view);
}

/**
 * gwy_data_view_get_hexcess:
 * @view: A data view.
 *
 * Return the horizontal excess of widget size to data size.
 *
 * Do not use.  Only useful for #GwyDataWindow implementation.
 *
 * Returns: The execess.
 **/
gdouble
gwy_data_view_get_hexcess(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 0);
    return (gdouble)gtk_widget_get_allocated_width(GTK_WIDGET(view))/view->priv->scwidth - 1.0;
}

/**
 * gwy_data_view_get_vexcess:
 * @view: A data view.
 *
 * Return the vertical excess of widget size to data size.
 *
 * Do not use.  Only useful for #GwyDataWindow implementation.
 *
 * Returns: The execess.
 **/
gdouble
gwy_data_view_get_vexcess(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 0);
    return (gdouble)gtk_widget_get_allocated_height(GTK_WIDGET(view))/view->priv->scheight - 1.0;
}

/**
 * gwy_data_view_set_zoom:
 * @view: A data view.
 * @zoom: A new zoom value.
 *
 * Sets zoom of @view to @zoom.
 *
 * Zoom greater than 1 means larger image on screen and vice versa.
 *
 * Note window manager can prevent the window from resize and thus the zoom from change.
 **/
void
gwy_data_view_set_zoom(GwyDataView *view,
                       gdouble zoom)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    gwy_debug("zoom = %g, new = %g", priv->newzoom, zoom);
    if (fabs(log(priv->newzoom/zoom)) < 0.001)
        return;

    priv->newzoom = zoom;
    g_object_notify_by_pspec(G_OBJECT(view), properties[PROP_ZOOM]);
    gtk_widget_queue_resize(GTK_WIDGET(view));
}

/**
 * gwy_data_view_get_zoom:
 * @view: A data view.
 *
 * Returns current ideal zoom of a data view.
 *
 * More precisely the zoom value requested by gwy_data_view_set_zoom(), if it's in use (real zoom may differ a bit due
 * to pixel rounding).  If zoom was set by explicite widget size change, real and requested zoom are considered to be
 * the same.
 *
 * When a resize is queued, the new zoom value is returned.
 *
 * In other words, this is the zoom @view would like to have.  Use gwy_data_view_get_real_zoom() to get the real
 * zoom.
 *
 * Returns: The zoom as a ratio between ideal displayed size and base data field size.
 **/
gdouble
gwy_data_view_get_zoom(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    return view->priv->newzoom;
}

/**
 * gwy_data_view_get_real_zoom:
 * @view: A data view.
 *
 * Returns current real zoom of a data view.
 *
 * This is the zoom value a data view may not wish to have, but was imposed by window manager or other constraints.
 * Unlike ideal zoom set by gwy_data_view_set_zoom(), this value cannot be set.
 *
 * When a resize is queued, the current (old) value is returned.
 *
 * Returns: The zoom as a ratio between real displayed size and base data field size.
 **/
gdouble
gwy_data_view_get_real_zoom(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    return view->priv->zoom;
}

/**
 * gwy_data_view_get_xmeasure:
 * @view: A data view.
 *
 * Returns the ratio between horizontal physical lengths and horizontal screen lengths in pixels.
 *
 * Note that screen pixels are not image pixels. The ratio between real horizontal distances and image pixel distances
 * can be obtained using gwy_field_get_dx() on the displayed #GwyField or by gwy_data_view_get_real_pixel_sizes().
 *
 * Returns: The horizontal measure.
 **/
gdouble
gwy_data_view_get_xmeasure(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    return view->priv->xmeasure;
}

/**
 * gwy_data_view_get_ymeasure:
 * @view: A data view.
 *
 * Returns the ratio between vertical physical lengths and vertical screen lengths in pixels.
 *
 * Note that screen pixels are not image pixels. The ratio between real vertical distances and image pixel distances
 * can be obtained using gwy_field_get_dy() on the displayed #GwyField or by gwy_data_view_get_real_pixel_sizes().
 *
 * Returns: The vertical measure.
 **/
gdouble
gwy_data_view_get_ymeasure(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    return view->priv->ymeasure;
}

/**
 * gwy_data_view_coords_widget_clamp:
 * @view: A data view.
 * @xscr: A screen x-coordinate relative to widget origin.
 * @yscr: A screen y-coordinate relative to widget origin.
 *
 * Fixes screen coordinates @xscr and @yscr to be inside the data-displaying area (which can be smaller than widget
 * size).
 *
 * Returns: %TRUE if coordinates are inside the widget area.
 **/
gboolean
gwy_data_view_coords_widget_clamp(GwyDataView *view,
                                  gdouble *xscr, gdouble *yscr)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), FALSE);

    GwyDataViewPrivate *priv = view->priv;
    gint size;
    gdouble t;
    gboolean retval = TRUE;

    if (xscr) {
        size = priv->scwidth;
        t = fmin(fmax(*xscr, priv->xoff), priv->xoff + size-1);
        if (t != *xscr) {
            *xscr = t;
            retval = FALSE;
        }
    }
    if (yscr) {
        size = priv->scheight;
        t = fmin(fmax(*yscr, priv->yoff), priv->yoff + size-1);
        if (t != *yscr) {
            *yscr = t;
            retval = FALSE;
        }
    }

    return retval;
}

/* FIXME: There is a gwymath function now. It either does the same, or it should be corrected to do the same. */
/**
 * gwy_data_view_coords_widget_cut_line:
 * @view: A data view.
 * @x0scr: First point screen x-coordinate relative to widget origin.
 * @y0scr: First point screen y-coordinate relative to widget origin.
 * @x1scr: Second point screen x-coordinate relative to widget origin.
 * @y1scr: Second point screen y-coordinate relative to widget origin.
 *
 * Fixes screen coordinates of line endpoints to be inside the data-displaying area (which can be smaller than widget
 * size).
 *
 * Returns: %TRUE if line was entirely inside the area.
 **/
gboolean
gwy_data_view_coords_widget_cut_line(GwyDataView *view,
                                     gdouble *x0scr, gdouble *y0scr,
                                     gdouble *x1scr, gdouble *y1scr)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), FALSE);

    GwyDataViewPrivate *priv = view->priv;
    gdouble xsize, ysize, x0s, y0s, x1s, y1s;

    xsize = priv->scwidth;
    x0s = CLAMP(*x0scr, priv->xoff, priv->xoff + xsize-1);
    x1s = CLAMP(*x1scr, priv->xoff, priv->xoff + xsize-1);
    ysize = priv->scheight;
    y0s = CLAMP(*y0scr, priv->yoff, priv->yoff + ysize-1);
    y1s = CLAMP(*y1scr, priv->yoff, priv->yoff + ysize-1);

    /* All inside */
    if (x0s == *x0scr && y0s == *y0scr && x1s == *x1scr && y1s == *y1scr)
        return TRUE;

    /* Horizontal/vertical lines */
    if (*x1scr == *x0scr) {
        if (*x0scr != x0s)
            goto fail;
        *y0scr = y0s;
        *y1scr = y1s;
        return FALSE;
    }
    if (*y1scr == *y0scr) {
        if (*y0scr != y0s)
            goto fail;
        *x0scr = x0s;
        *x1scr = x1s;
        return FALSE;
    }

    /* The hard case */
    x0s = *x0scr;
    x1s = *x1scr;
    y0s = *y0scr;
    y1s = *y1scr;

    gdouble t[6];
    t[0] = -(x0s)/(gdouble)(x1s - x0s);
    t[1] = (xsize - 1 -(x0s))/(gdouble)(x1s - x0s);
    t[2] = -(y0s)/(gdouble)(y1s - y0s);
    t[3] = (ysize - 1 -(y0s))/(gdouble)(y1s - y0s);
    /* Include the endpoints */
    t[4] = 0.0;
    t[5] = 1.0;

    gwy_math_sort(t, G_N_ELEMENTS(t));

    gint i0 = -1, i1 = -1;
    for (gint i = 0; i < G_N_ELEMENTS(t); i++) {
        gdouble xy;

        if (t[i] < -EPS || t[i] > 1.0 + EPS)
            continue;

        xy = x0s + t[i]*(x1s - x0s);
        if (xy < -EPS || xy > xsize-1 + EPS)
            continue;

        xy = y0s + t[i]*(y1s - y0s);
        if (xy < -EPS || xy > ysize-1 + EPS)
            continue;

        /* i0 is the first index, once i0 != -1, do not change it any more */
        if (i0 == -1)
            i0 = i;

        /* i1 is the last index, move it as long as we are inside */
        i1 = i;
    }

    if (i0 == -1)
        goto fail;

    *x0scr = x0s + t[i0]*(x1s - x0s);
    *x1scr = x0s + t[i1]*(x1s - x0s);
    *y0scr = y0s + t[i0]*(y1s - y0s);
    *y1scr = y0s + t[i1]*(y1s - y0s);
    return FALSE;

fail:
    /* The line does not intersect the boundary at all.  Just return something and pray... */
    *x0scr = *x1scr = xsize/2;
    *y0scr = *y1scr = ysize/2;
    return FALSE;
}

/**
 * gwy_data_view_coords_widget_to_real:
 * @view: A data view.
 * @xscr: A screen x-coordinate relative to widget origin.
 * @yscr: A screen y-coordinate relative to widget origin.
 * @xreal: Where the physical x-coordinate in the data sample should be stored.
 * @yreal: Where the physical y-coordinate in the data sample should be stored.
 *
 * Recomputes screen coordinates relative to widget origin to physical coordinates in the sample.
 *
 * Note that data fields offsets are <emphasis>not</emphasis> taken into account.  Coordinates @xreal, @yreal are just
 * relative coordinates to the top left field corner.
 **/
void
gwy_data_view_coords_widget_to_real(GwyDataView *view,
                                    gdouble xscr, gdouble yscr,
                                    gdouble *xreal, gdouble *yreal)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    if (xreal)
        *xreal = (xscr + 0.5 - priv->xoff) * priv->xmeasure;
    if (yreal)
        *yreal = (yscr + 0.5 - priv->yoff) * priv->ymeasure;
}

/**
 * gwy_data_view_coords_real_to_widget:
 * @view: A data view.
 * @xreal: A physical x-coordinate in the data sample.
 * @yreal: A physical y-coordinate in the data sample.
 * @xscr: Where the screen x-coordinate relative to widget origin should be stored.
 * @yscr: Where the screen y-coordinate relative to widget origin should be stored.
 *
 * Recomputes physical coordinate in the sample to screen coordinate relative to widget origin, keeping them as
 * floating point values.
 *
 * Note that data fields offsets are <emphasis>not</emphasis> taken into account.  Coordinates @xreal, @yreal are just
 * relative coordinates to the top left field corner.
 **/
void
gwy_data_view_coords_real_to_widget(GwyDataView *view,
                                    gdouble xreal, gdouble yreal,
                                    gdouble *xscr, gdouble *yscr)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    if (xscr)
        *xscr = xreal/priv->xmeasure + priv->xoff;
    if (yscr)
        *yscr = yreal/priv->ymeasure + priv->yoff;
}

/**
 * gwy_data_view_get_xzoom:
 * @view: A data view.
 *
 * Gets the ratio between screen and image horizontal coordinates in a data view.
 *
 * Returns: The ratio between horizontal image and screen pixel sizes.
 **/
gdouble
gwy_data_view_get_xzoom(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    GwyDataViewPrivate *priv = view->priv;
    /* xmeasure = xreal/scrsize, so the entire fraction reduces to scrsize/xres */
    return priv->xreal/priv->xres/priv->xmeasure;
}

/**
 * gwy_data_view_get_yzoom:
 * @view: A data view.
 *
 * Gets the ratio between screen and image vertical coordinates in a data view.
 *
 * Returns: The ratio between vertical image and screen pixel sizes.
 **/
gdouble
gwy_data_view_get_yzoom(GwyDataView *view)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), 1.0);
    GwyDataViewPrivate *priv = view->priv;
    /* xmeasure = xreal/scrsize, so the entire fraction reduces to scrsize/xres */
    return priv->yreal/priv->yres/priv->ymeasure;
}

/**
 * gwy_data_view_set_resizable:
 * @view: A data view.
 * @resizable: %TRUE to make the data view user-resizeable; %FALSE to request fixed size according to the zoom.
 *
 * Sets the resizeability of a data view.
 *
 * By default, data views adapt to any size they get allocated and request very small minimum size. This is useful in
 * data windows which should be freely resizeable, for instance.
 *
 * Module dialogs usually have small fixed-size previews. The data view never grows, but could be still shrinked. In
 * such case @resizable should be set to %FALSE to prevent the shrinking. The data view will then request minimum size
 * according to zoom.
 **/
void
gwy_data_view_set_resizable(GwyDataView *view,
                            gboolean resizable)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    GwyDataViewPrivate *priv = view->priv;
    if (!resizable == !priv->resizable)
        return;

    priv->resizable = !!resizable;
    gtk_widget_queue_resize(GTK_WIDGET(view));
}

/**
 * gwy_data_view_get_pixel_data_sizes:
 * @view: A data view.
 * @xres: Location to store x-resolution of displayed data (or %NULL).
 * @yres: Location to store y-resolution of displayed data (or %NULL).
 *
 * Obtains pixel dimensions of data displayed by a data view.
 *
 * This is a convenience method, the same values could be obtained by gwy_field_get_xres() and gwy_field_get_yres() of
 * the data field.
 **/
void
gwy_data_view_get_pixel_data_sizes(GwyDataView *view,
                                   gint *xres,
                                   gint *yres)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    if (xres)
        *xres = priv->xres;
    if (yres)
        *yres = priv->yres;
}

/**
 * gwy_data_view_get_real_data_sizes:
 * @view: A data view.
 * @xreal: Location to store physical x-dimension of the displayed data without excess (or %NULL).
 * @yreal: Location to store physical y-dimension of the displayed data without excess (or %NULL).
 *
 * Obtains physical dimensions of data displayed by a data view.
 *
 * Physical coordinates are always taken from base data field.  This is a convenience method, the same values could be
 * obtained by gwy_field_get_xreal() and gwy_field_get_yreal() of the data field.
 **/
void
gwy_data_view_get_real_data_sizes(GwyDataView *view,
                                  gdouble *xreal,
                                  gdouble *yreal)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    if (xreal)
        *xreal = priv->xreal;
    if (yreal)
        *yreal = priv->yreal;
}

/**
 * gwy_data_view_get_real_pixel_sizes:
 * @view: A data view.
 * @dx: Location to store physical sampling step along the x-axis of the displayed image (or %NULL).
 * @dy: Location to store physical sampling step along the y-axis of the displayed image (or %NULL).
 *
 * Obtains physical sampling steps of data displayed by a data view.
 *
 * Physical coordinates are always taken from base data field.  This is a convenience method, the same values could be
 * obtained by gwy_field_get_dx() and gwy_field_get_dy() of the data field.
 **/
void
gwy_data_view_get_real_pixel_sizes(GwyDataView *view,
                                   gdouble *dx,
                                   gdouble *dy)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    if (dx)
        *dx = priv->xreal/priv->xres;
    if (dy)
        *dy = priv->yreal/priv->xres;
}

/**
 * gwy_data_view_get_metric:
 * @view: A data view.
 * @metric: Metric matrix 2x2 (stored in sequentially by rows: m11, m12, m12, m22).
 *
 * Fills metric matrix for a data view.
 *
 * The metric matrix essentially transforms distances in physical coordinates to screen distances.  It is to be used
 * with functions like gwy_math_find_nearest_point() and gwy_math_find_nearest_line() when the distance should be
 * screen-Euclidean.
 **/
void
gwy_data_view_get_metric(GwyDataView *view,
                         gdouble *metric)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));
    g_return_if_fail(metric);

    GwyDataViewPrivate *priv = view->priv;
    metric[0] = 1.0/(priv->xmeasure*priv->xmeasure);
    metric[1] = metric[2] = 0.0;
    metric[3] = 1.0/(priv->ymeasure*priv->ymeasure);
}

/**
 * gwy_data_view_get_pixbuf:
 * @view: A data view.
 * @max_width: Pixbuf width that should not be exceeeded.  Value smaller than 1 means unlimited size.
 * @max_height: Pixbuf height that should not be exceeeded.  Value smaller than 1 means unlimited size.
 *
 * Creates and returns a pixbuf from the data view.
 *
 * If the data is not square, the resulting pixbuf is also nonsquare. The returned pixbuf also never has an alpha
 * channel.
 *
 * Returns: (transfer full):
 *          The pixbuf as a newly created #GdkPixbuf, it should be freed when no longer needed.  It is never larger
 *          than the actual data size, as @max_width and @max_height are only upper limits.
 **/
GdkPixbuf*
gwy_data_view_get_pixbuf(GwyDataView *view,
                         gint max_width,
                         gint max_height)
{
    g_return_val_if_fail(GWY_IS_DATA_VIEW(view), NULL);
    GwyDataViewPrivate *priv = view->priv;
    g_return_val_if_fail(priv->field_pixbuf, NULL);

    gint width = gdk_pixbuf_get_width(priv->field_pixbuf);
    gint height = gdk_pixbuf_get_height(priv->field_pixbuf);
    gdouble xscale = (max_width > 0) ? (gdouble)max_width/width : 1.0;
    gdouble yscale = (max_height > 0) ? (gdouble)max_height/height : 1.0;
    gdouble scale = fmin(fmin(xscale, yscale), 1.0);
    gint width_scaled = (gint)(scale*width);
    gint height_scaled = (gint)(scale*height);
    if (max_width)
        width_scaled = CLAMP(width_scaled, 1, max_width);
    if (max_height)
        height_scaled = CLAMP(height_scaled, 1, max_height);

    if (!priv->mask_pixbuf) {
        GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width_scaled, height_scaled);
        gdk_pixbuf_scale(priv->field_pixbuf, pixbuf, 0, 0, width_scaled, height_scaled, 0.0, 0.0,
                         scale, scale, GDK_INTERP_TILES);
        return pixbuf;
    }

    cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, width_scaled, height_scaled);
    cairo_t *cr = cairo_create(surface);
    cairo_scale(cr, (gdouble)width_scaled/width, (gdouble)height_scaled/height);
    gdk_cairo_set_source_pixbuf(cr, priv->field_pixbuf, 0, 0);
    cairo_paint(cr);
    gdk_cairo_set_source_pixbuf(cr, priv->mask_pixbuf, 0, 0);
    cairo_paint(cr);
    cairo_destroy(cr);

    GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, width_scaled, height_scaled);
    cairo_surface_destroy(surface);

    return pixbuf;
}

/**
 * gwy_data_view_set_named_cursor:
 * @view: A data view.
 * @name: A cursor name, or %NULL.
 *
 * Sets the pointer cursor for a data view to a named cursor.
 *
 * The cursor name is the same as one would pass to gdk_cursor_new_from_name().
 *
 * The data view needs to be realised.
 *
 * This function is intended for the implementation of editable data view layers. If an application sets the cursor,
 * a data view layer will likely change it again soon.
 **/
void
gwy_data_view_set_named_cursor(GwyDataView *view,
                               const gchar *name)
{
    g_return_if_fail(GWY_IS_DATA_VIEW(view));

    GwyDataViewPrivate *priv = view->priv;
    /* This may be called very often, so just compare strings and quit if there is nothing to change. */
    if (!gwy_assign_string(&priv->cursor_name, name))
        return;
    if (priv->event_window)
        set_cursor(view);
}

static void
set_cursor(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    const gchar *name = priv->cursor_name;
    GdkDisplay *display = gtk_widget_get_display(GTK_WIDGET(view));

    g_return_if_fail(display);
    GdkCursor *cursor = name ? gdk_cursor_new_from_name(display, name) : NULL;
    gdk_window_set_cursor(priv->event_window, cursor);
    g_clear_object(&cursor);
}

static void
field_changed(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    priv->field_needs_repaint = TRUE;
    update(view);
}

static void
mask_changed(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    priv->mask_needs_repaint = TRUE;
    update(view);
}

static void
gradient_changed(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    if (priv->field) {
        priv->field_needs_repaint = TRUE;
        update(view);
    }
}

static void
follow_default_gradient(GwyDataView *view, gboolean follow)
{
    GwyDataViewPrivate *priv = view->priv;
    GwyInventory *inventory = gwy_gradients();
    if (!follow) {
        g_clear_signal_handler(&priv->default_gradient_changed_id, inventory);
        return;
    }
    if (priv->default_gradient_changed_id)
        return;
    priv->default_gradient_changed_id = g_signal_connect_swapped(inventory, "default-changed",
                                                                 G_CALLBACK(default_gradient_changed), view);
}

static void
default_gradient_changed(GwyDataView *view)
{
    /* That should do it. */
    gwy_data_view_set_gradient(view, NULL);
}

static GdkPixbuf*
ensure_pixbuf(gpointer field, GdkPixbuf *pixbuf, gboolean has_alpha)
{
    gint xres, yres;
    if (GWY_IS_FIELD(field)) {
        xres = gwy_field_get_xres(field);
        yres = gwy_field_get_yres(field);
    }
    else if (GWY_IS_NIELD(field)) {
        xres = gwy_nield_get_xres(field);
        yres = gwy_nield_get_yres(field);
    }
    else {
        g_return_val_if_reached(NULL);
    }

    if (pixbuf && gdk_pixbuf_get_width(pixbuf) == xres && gdk_pixbuf_get_height(pixbuf) == yres)
        return pixbuf;

    g_clear_object(&pixbuf);
    return gdk_pixbuf_new(GDK_COLORSPACE_RGB, has_alpha, 8, xres, yres);
}

static void
paint_field(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    if (!priv->field_needs_repaint)
        return;

    GwyField *field = priv->field;
    GwyGradient *gradient = priv->gradient;
    GwyColorMappingType color_mapping = priv->color_mapping;

    g_return_if_fail(field);

    GdkPixbuf *pixbuf = priv->field_pixbuf = ensure_pixbuf(field, priv->field_pixbuf, FALSE);
    /* Special-case full range, as gwy_draw_field_pixbuf_full() is simplier, it doesn't have to deal with
     * outliers. */
    if (color_mapping == GWY_COLOR_MAPPING_FULL || color_mapping == GWY_COLOR_MAPPING_ADAPT
        || (color_mapping == GWY_COLOR_MAPPING_FIXED && !priv->fixed_range_set)) {
        gwy_field_min_max(field, &priv->range_min, &priv->range_max);
        if (color_mapping == GWY_COLOR_MAPPING_ADAPT)
            gwy_draw_field_pixbuf_adaptive(pixbuf, field, gradient);
        else
            gwy_draw_field_pixbuf_full(pixbuf, field, gradient);
    }
    else {
        /* FIXME GTK3: The ‘Ignore fixed range for presentations’ logic used to be here. But we are simply
         * displaying some field now.  */
        priv->range_min = priv->fixed_min;
        priv->range_max = priv->fixed_max;
        if (color_mapping == GWY_COLOR_MAPPING_AUTO)
            gwy_field_autorange(field, &priv->range_min, &priv->range_max);
        gwy_draw_field_pixbuf_with_range(pixbuf, field, gradient, priv->range_min, priv->range_max);
    }
}

static void
paint_mask(GwyDataView *view)
{
    GwyDataViewPrivate *priv = view->priv;
    if (!priv->field_needs_repaint)
        return;

    GwyNield *mask = priv->mask;

    g_return_if_fail(mask);

    GdkPixbuf *pixbuf = priv->mask_pixbuf = ensure_pixbuf(mask, priv->mask_pixbuf, TRUE);
    gwy_draw_nield_pixbuf_mask(pixbuf, mask, &priv->mask_color);
}

/**
 * SECTION: data-view
 * @title: GwyDataView
 * @short_description: Data field displaying area
 * @see_also: #GwyDataWindow -- window combining data view with other controls,
 *            #GwyDataViewLayer -- layers a data view is composed of,
 *            <link linkend="libgwyui-pixbuf-render">pixbuf-render</link> --
 *            low level functions for painting data fields,
 *            #Gwy3DView -- OpenGL 3D data display
 *
 * #GwyDataView is a basic two-dimensional data display widget.  The actual rendering is performed by one or more
 * #GwyDataViewLayer's, pluggable into the data view.  Each layer generally displays different data field from the
 * container supplied to gwy_data_view_new().
 *
 * The size of a data view is affected by two factors: zoom and outer constraints. If an explicit size set by window
 * manager or by Gtk+ means, the view scales the displayed data to fit into this size (while keeping x/y ratio). Zoom
 * controlls the size a data view requests, and can be set with gwy_data_view_set_zoom().
 *
 * Several helper functions are available for transformation between screen coordinates in the view and physical
 * coordinates in the displayed data field: gwy_data_view_coords_widget_to_real(), gwy_data_view_get_xmeasure(),
 * gwy_data_view_get_hexcess(), and others. Physical coordinates are always taken from data field displayed by base
 * layer.
 **/

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