/*
 *  $Id: data-window.c 30028 2026-06-09 13:45:30Z 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 <string.h>
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/stats.h"

#include "libgwyui/data-window.h"
#include "libgwyui/ruler.h"
#include "libgwyui/color-axis.h"
#include "libgwyui/option-menus.h"
#include "libgwyui/inventory-store.h"
#include "libgwyui/utils.h"

enum {
    PROP_0,
    PROP_DATA_NAME,
    PROP_UL_CORNER,
    PROP_DATA_VIEW,
    PROP_COLOR_AXIS,
    NUM_PROPERTIES,
};

struct _GwyDataWindowPrivate {
    GtkWidget *grid;
    GtkWidget *dataview;
    GtkWidget *hruler;
    GtkWidget *vruler;
    GtkWidget *statusbar;
    GtkWidget *coloraxis;
    GtkWidget *grad_selector;

    GdkRectangle old_allocation;
    gint statusbar_height;

    GwyValueFormat *coord_format;
    GwyValueFormat *value_format;

    GtkWidget *ul_corner;
    GString *data_name;

    GString *str;
    gchar *value_unit;
};

static void     finalize                   (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     destroy                    (GtkWidget *widget);
static void     size_allocate              (GtkWidget *widget,
                                            GdkRectangle *alc);
static void     update_units               (GwyDataWindow *window);
static gboolean data_view_motion_notify    (GwyDataView *dataview,
                                            GdkEventMotion *event,
                                            GwyDataWindow *window);
static void     update_title               (GwyDataWindow *window);
static void     resize_view                (GwyDataWindow *window);
static void     resize_to_natural          (GwyDataWindow *window);
static void     zoom_changed               (GwyDataWindow *window);
static gboolean key_pressed                (GtkWidget *widget,
                                            GdkEventKey *event);
static gboolean color_axis_clicked         (GtkWidget *window,
                                            GdkEventButton *event);
static void     show_more_gradients        (GwyDataWindow *window);
static void     unset_gradient             (GwyDataWindow *window);
static void     gradient_selected          (GtkWidget *item,
                                            GwyDataWindow *window);
static void     gradient_changed           (GtkTreeSelection *selection,
                                            GwyDataWindow *window);
static gboolean gradient_window_key_pressed(GtkWidget *window,
                                            GdkEventKey *event);
static void     gradient_update            (GwyDataWindow *window,
                                            GwyGradient *gradient);
static void     data_view_updated          (GwyDataWindow *window);

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWindowClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyDataWindow, gwy_data_window, GTK_TYPE_WINDOW,
                        G_ADD_PRIVATE(GwyDataWindow))

static void
gwy_data_window_class_init(GwyDataWindowClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_data_window_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    widget_class->destroy = destroy;

    widget_class->size_allocate = size_allocate;
    widget_class->key_press_event = key_pressed;

    properties[PROP_DATA_NAME] = g_param_spec_string("data-name", NULL,
                                                     "Data name used in window title",
                                                     "",
                                                     GWY_GPARAM_RWE);
    properties[PROP_UL_CORNER] = g_param_spec_object("ul-corner", NULL,
                                                     "Widget in the upper left corner",
                                                     GTK_TYPE_WIDGET,
                                                     GWY_GPARAM_RWE);
    properties[PROP_DATA_VIEW] = g_param_spec_object("data-view", NULL,
                                                     "Data view widget displayed in the window",
                                                     GWY_TYPE_DATA_VIEW,
                                                     G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    properties[PROP_COLOR_AXIS] = g_param_spec_object("color-axis", NULL,
                                                      "Color axis widget displayed in the window",
                                                      GWY_TYPE_COLOR_AXIS,
                                                      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
statusbar_size_allocated(GtkWidget *statusbar, G_GNUC_UNUSED GtkAllocation *alloc, gpointer user_data)
{
    /* Clear the strut (so that users do not see it) and disconnect self. */
    gtk_label_set_markup(GTK_LABEL(statusbar), "");
    g_signal_handlers_disconnect_matched(statusbar, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA,
                                         0, 0, NULL, statusbar_size_allocated, user_data);
}

static void
gwy_data_window_init(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv;

    priv = window->priv = gwy_data_window_get_instance_private(window);
    priv->data_name = g_string_new(NULL);
    priv->str = g_string_new(NULL);

    /* FIXME GTK3 can't we just make the entire thing one 3×3 GtkGrid? Does splitting into hboxes and vboxes achieve
     * anything? */
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(window), vbox);

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, TRUE, 0);

    priv->statusbar = gtk_label_new(NULL);
    gtk_widget_set_hexpand(priv->statusbar, TRUE);
    gtk_label_set_xalign(GTK_LABEL(priv->statusbar), 0.0);
    /* TODO GTK3 at some point we need to get the required height and do not allow it to shrink. */
    gtk_label_set_markup(GTK_LABEL(priv->statusbar), "(|)<sup>(0)</sup><sub>(0)</sub>");
    gtk_label_set_ellipsize(GTK_LABEL(priv->statusbar), PANGO_ELLIPSIZE_END);
    gtk_box_pack_start(GTK_BOX(vbox), priv->statusbar, FALSE, FALSE, 0);
    g_signal_connect(priv->statusbar, "size-allocate", G_CALLBACK(statusbar_size_allocated), priv->statusbar);

    priv->grid = gtk_grid_new();
    gtk_box_pack_start(GTK_BOX(hbox), priv->grid, TRUE, TRUE, 0);

    priv->dataview = gwy_data_view_new();
    GwyDataView *dataview = GWY_DATA_VIEW(priv->dataview);
    gtk_widget_set_hexpand(priv->dataview, TRUE);
    gtk_widget_set_vexpand(priv->dataview, TRUE);
    /* FIXME GTK3 we used to have GTK_SHRINK here. It might not be shrinking any more? We also had SHRINK for all the
     * rulers so they could all become very tiny (even though they may be requesting larger sizes).
     * Currently GwyDataView requests minimum size 2×2. And in future we want to make it scollable anyway, so the
     * point may be moot. */
    gtk_grid_attach(GTK_GRID(priv->grid), priv->dataview, 1, 1, 1, 1);

    priv->hruler = gwy_ruler_new(GTK_ORIENTATION_HORIZONTAL);
    gwy_ruler_set_units_placement(GWY_RULER(priv->hruler), GWY_UNITS_PLACEMENT_AT_ZERO);
    gtk_grid_attach(GTK_GRID(priv->grid), priv->hruler, 1, 0, 1, 1);

    priv->vruler = gwy_ruler_new(GTK_ORIENTATION_VERTICAL);
    gwy_ruler_set_units_placement(GWY_RULER(priv->vruler), GWY_UNITS_PLACEMENT_AT_ZERO);
    gtk_grid_attach(GTK_GRID(priv->grid), priv->vruler, 0, 1, 1, 1);

    priv->coloraxis = gwy_color_axis_new(GTK_ORIENTATION_VERTICAL);
    gtk_box_pack_start(GTK_BOX(hbox), priv->coloraxis, FALSE, FALSE, 0);

    gwy_data_window_fit_to_screen(window);

    g_signal_connect_swapped(priv->coloraxis, "button-press-event", G_CALLBACK(color_axis_clicked), window);
    g_signal_connect_swapped(dataview, "resized", G_CALLBACK(resize_view), window);
    g_signal_connect_data(dataview, "size-allocate", G_CALLBACK(zoom_changed), window, NULL,
                          G_CONNECT_AFTER | G_CONNECT_SWAPPED);
    g_signal_connect_swapped(dataview, "redrawn", G_CALLBACK(data_view_updated), window);
    g_signal_connect(dataview, "motion-notify-event", G_CALLBACK(data_view_motion_notify), window);

    gtk_widget_show_all(gtk_bin_get_child(GTK_BIN(window)));
}

static void
finalize(GObject *object)
{
    GwyDataWindow *window = GWY_DATA_WINDOW(object);
    GwyDataWindowPrivate *priv = window->priv;

    g_string_free(priv->data_name, TRUE);
    g_string_free(priv->str, TRUE);
    g_free(priv->value_unit);
    GWY_FREE_VALUE_FORMAT(priv->coord_format);
    GWY_FREE_VALUE_FORMAT(priv->value_format);

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyDataWindow *window = GWY_DATA_WINDOW(object);

    switch (prop_id) {
        case PROP_DATA_NAME:
        gwy_data_window_set_data_name(window, g_value_get_string(value));
        break;

        case PROP_UL_CORNER:
        gwy_data_window_set_ul_corner_widget(window, GTK_WIDGET(g_value_get_object(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)
{
    GwyDataWindowPrivate *priv = GWY_DATA_WINDOW(object)->priv;

    switch (prop_id) {
        case PROP_DATA_NAME:
        g_value_set_string(value, priv->data_name->str);
        break;

        case PROP_UL_CORNER:
        g_value_set_object(value, priv->ul_corner);
        break;

        case PROP_DATA_VIEW:
        g_value_set_object(value, priv->dataview);
        break;

        case PROP_COLOR_AXIS:
        g_value_set_object(value, priv->coloraxis);
        break;

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

static void
destroy(GtkWidget *widget)
{
    GwyDataWindowPrivate *priv = GWY_DATA_WINDOW(widget)->priv;

    if (priv->grad_selector) {
        gtk_widget_destroy(gtk_widget_get_toplevel(priv->grad_selector));
        priv->grad_selector = NULL;
    }

    GTK_WIDGET_CLASS(parent_class)->destroy(widget);
}

/**
 * gwy_data_window_new: (constructor)
 *
 * Creates a new data displaying window.
 *
 * Returns: (transfer full):
 *          A newly created widget, as #GtkWidget.
 **/
GtkWidget*
gwy_data_window_new(void)
{
    return gtk_widget_new(GWY_TYPE_DATA_WINDOW, NULL);
}

/**
 * gwy_data_window_get_data_view:
 * @window: A data view window.
 *
 * Returns the data view widget an image window currently shows.
 *
 * Returns: (transfer none): The currently shown data view.
 **/
GtkWidget*
gwy_data_window_get_data_view(GwyDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_DATA_WINDOW(window), NULL);
    return window->priv->dataview;
}

/**
 * gwy_data_window_get_color_axis:
 * @window: A data view window.
 *
 * Returns the color axis widget part of an image window.
 *
 * Returns: (transfer none): The color axis widget.
 **/
GtkWidget*
gwy_data_window_get_color_axis(GwyDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_DATA_WINDOW(window), NULL);
    return window->priv->coloraxis;
}

/**
 * gwy_data_window_get_ruler:
 * @window: A data view window.
 * @orientation: Ruler orientation.
 *
 * Gets a ruler part of an image window.
 *
 * Returns: (transfer none): The ruler widget.
 **/
GtkWidget*
gwy_data_window_get_ruler(GwyDataWindow *window,
                          GtkOrientation orientation)
{
    g_return_val_if_fail(GWY_IS_DATA_WINDOW(window), NULL);
    return (orientation == GTK_ORIENTATION_HORIZONTAL ? window->priv->hruler : window->priv->vruler);
}

static void
size_allocate(GtkWidget *widget,
              GdkRectangle *allocation)
{
    GwyDataWindow *window = GWY_DATA_WINDOW(widget);
    GwyDataWindowPrivate *priv = window->priv;

    /* FIXME GTK3 we are calling this with NULL allocation for some misguided reason (see below). It is probably
     * to update various scalings and offsets. But the function doing that should not be called size_allocate(). */
    if (allocation) {
        if (priv->old_allocation.x == allocation->x
            && priv->old_allocation.y == allocation->y
            && priv->old_allocation.width == allocation->width
            && priv->old_allocation.height == allocation->height)
            return;

        GTK_WIDGET_CLASS(parent_class)->size_allocate(widget, allocation);
        priv->old_allocation = *allocation;
    }

    GwyDataView *dataview = GWY_DATA_VIEW(priv->dataview);
    GwyField *field = gwy_data_view_get_field(dataview);

    gdouble excess, pos, real = 1.0, offset = 0.0;

    /* horizontal */
    if (field) {
        real = gwy_field_get_xreal(field);
        offset = gwy_field_get_xoffset(field);
    }
    excess = real * gwy_data_view_get_hexcess(dataview)/2.0;
    gwy_ruler_get_range(GWY_RULER(priv->hruler), NULL, NULL, &pos, NULL);
    gwy_ruler_set_range(GWY_RULER(priv->hruler), offset - excess, real + offset + excess, pos, real);

    /* vertical */
    if (field) {
        real = gwy_field_get_yreal(field);
        offset = gwy_field_get_yoffset(field);
    }
    excess = real * gwy_data_view_get_vexcess(dataview)/2.0;
    gwy_ruler_get_range(GWY_RULER(priv->vruler), NULL, NULL, &pos, NULL);
    gwy_ruler_set_range(GWY_RULER(priv->vruler), offset - excess, real + offset + excess, pos, real);

    update_title(window);
}

/**
 * gwy_data_window_fit_to_screen:
 * @window: A data view window.
 *
 * Sets the zoom of data windows's data view in an attempt to make the window fit to the screen.
 **/
void
gwy_data_window_fit_to_screen(GwyDataWindow *window)
{
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));

    GwyDataWindowPrivate *priv = window->priv;
    if (!priv->dataview) {
        g_warning("Trying to fit data window with no data view to screen.");
        return;
    }

    GtkWidget *widget = GTK_WIDGET(window);
    gint scrwidth, scrheight;
    gwy_get_screen_size(widget, &scrwidth, &scrheight);

    gdouble zoom = gwy_data_view_get_zoom(GWY_DATA_VIEW(priv->dataview));
    GtkRequisition request;
    gtk_widget_get_preferred_size(priv->dataview, &request, NULL);
    gdouble z = MAX(request.width/(gdouble)scrwidth, request.height/(gdouble)scrheight);
    if (z > 0.9) {
        zoom *= 0.9/z;
        gwy_data_view_set_zoom(GWY_DATA_VIEW(priv->dataview), zoom);
        resize_to_natural(window);
    }
}

/**
 * gwy_data_window_set_zoom:
 * @window: An image window.
 * @izoom: The new zoom value (as an integer).
 *
 * Sets the zoom of an image window to @izoom.
 *
 * When @izoom is -1 it zooms out; when @izoom is 1 it zooms out. Otherwise the new zoom value is set to @izoom/10000.
 **/
void
gwy_data_window_set_zoom(GwyDataWindow *window,
                         gint izoom)
{
    static const gdouble factor = 0.5;    /* Half-pixel zoom */
    gdouble rzoom;
    gint curzoom = 0;

    gwy_debug("%d", izoom);
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));
    g_return_if_fail(izoom == -1 || izoom == 1 || (izoom >= 625 && izoom <= 160000));

    GwyDataWindowPrivate *priv = window->priv;

    rzoom = gwy_data_view_get_zoom(GWY_DATA_VIEW(priv->dataview));
    switch (izoom) {
        case -1:
        case 1:
        if (rzoom >= 1)
            curzoom = floor((rzoom - 1.0)/factor + 0.5);
        else
            curzoom = -floor((1.0/rzoom - 1.0)/factor + 0.5);
        curzoom += izoom;
        if (curzoom >= 0)
            rzoom = 1.0 + curzoom*factor;
        else
            rzoom = 1.0/(1.0 - curzoom*factor);
        break;

        default:
        rzoom = izoom/10000.0;
        break;
    }
    rzoom = CLAMP(rzoom, 1/12.0, 12.0);
    gwy_data_view_set_zoom(GWY_DATA_VIEW(priv->dataview), rzoom);

    resize_to_natural(window);
}

/**
 * gwy_data_window_run_gradient_chooser:
 * @window: An image window.
 *
 * Programmatically invokes the false colour gradient chooser of an image window.
 *
 * If the chooser window is already shown, it is brought to focus. Otherwise it is created and shown as if the user
 * selected the full gradient chooser from the GUI.
 **/
void
gwy_data_window_run_gradient_chooser(GwyDataWindow *window)
{
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));
    show_more_gradients(window);
}

static void
update_units(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GwyDataView *dataview = GWY_DATA_VIEW(priv->dataview);
    GwyUnit *xyunit, *zunit;
    GwyField *field = gwy_data_view_get_field(dataview);

    xyunit = gwy_field_get_unit_xy(field);
    zunit = gwy_field_get_unit_z(field);
    gwy_debug("before: coord_format = %p, value_format = %p",
              priv->coord_format, priv->value_format);
    priv->coord_format = gwy_field_get_value_format_xy(field, GWY_UNIT_FORMAT_VFMARKUP, priv->coord_format);
    priv->value_format = gwy_field_get_value_format_z(field, GWY_UNIT_FORMAT_VFMARKUP, priv->value_format);
    g_free(priv->value_unit);
    priv->value_unit = gwy_unit_get_string(zunit, GWY_UNIT_FORMAT_VFMARKUP);
    gwy_debug("after: coord_format = %p, value_format = %p",
              priv->coord_format, priv->value_format);
    gwy_debug("after: coord_format = {%d, %g, %s}, value_format = {%d, %g, %s}",
              priv->coord_format->precision, priv->coord_format->magnitude, priv->coord_format->units,
              priv->value_format->precision, priv->value_format->magnitude, priv->value_format->units);
    gwy_unit_assign(gwy_ruler_get_unit(GWY_RULER(priv->hruler)), xyunit);
    gwy_unit_assign(gwy_ruler_get_unit(GWY_RULER(priv->vruler)), xyunit);
    gwy_unit_assign(gwy_color_axis_get_unit(GWY_COLOR_AXIS(priv->coloraxis)), zunit);
}

static gboolean
data_view_motion_notify(GwyDataView *dataview,
                        GdkEventMotion *event,
                        GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GwyField *field;
    gdouble xreal, yreal, xoff, yoff, value;
    GwyValueFormat *xyvf, *zvf;
    gint h = 0;

    /* Do it at the beginning, assuming the oldest request (from the initial strut) is the largest anyway. */
    gtk_widget_get_preferred_height(priv->statusbar, &h, NULL);
    priv->statusbar_height = MAX(priv->statusbar_height, h);
    gtk_widget_set_size_request(priv->statusbar, -1, priv->statusbar_height);

    if (!dataview) {
        gtk_label_set_markup(GTK_LABEL(priv->statusbar), "");
        return FALSE;
    }

    if (priv->hruler) {
        GwyRuler *ruler = GWY_RULER(priv->hruler);
        gwy_ruler_move_marker(ruler, gwy_ruler_coord_widget_to_real(ruler, event->x));
    }
    if (priv->vruler) {
        GwyRuler *ruler = GWY_RULER(priv->vruler);
        gwy_ruler_move_marker(ruler, gwy_ruler_coord_widget_to_real(ruler, event->y));
    }

    gdouble x = event->x, y = event->y;
    gwy_data_view_coords_widget_clamp(dataview, &x, &y);
    if (x != event->x || y != event->y)
        return FALSE;

    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    field = gwy_data_view_get_field(dataview);
    xoff = gwy_field_get_xoffset(field);
    yoff = gwy_field_get_yoffset(field);
    gwy_debug("xreal = %g, yreal = %g, xr = %g, yr = %g, xi = %g, yi = %g",
              gwy_field_get_xreal(field), gwy_field_get_yreal(field), xreal, yreal, x, y);
    value = gwy_field_get_dval_real(field, xreal, yreal, GWY_INTERPOLATION_ROUND);

    if (!priv->coord_format)
        update_units(window);

    xyvf = priv->coord_format;
    zvf = priv->value_format;
    g_string_printf(priv->str, "(%.*f%s%s, %.*f%s%s): %.*f%s%s",
                    xyvf->precision, (xreal + xoff)/xyvf->magnitude,
                    strlen(xyvf->units) ? " " : "", xyvf->units,
                    xyvf->precision, (yreal + yoff)/xyvf->magnitude,
                    strlen(xyvf->units) ? " " : "", xyvf->units,
                    zvf->precision, value/zvf->magnitude,
                    strlen(zvf->units) ? " " : "", zvf->units);
    if (fabs(log(zvf->magnitude)) > 1e-12) {
        g_string_append_printf(priv->str, " = %.3e %s", value, priv->value_unit);
    }
    gtk_label_set_markup(GTK_LABEL(priv->statusbar), priv->str->str);

    return FALSE;
}

/**
 * gwy_data_window_update_title:
 * @window: An image window.
 *
 * Updates the title of @window to reflect current state.
 **/
static void
update_title(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GwyDataView *dataview = GWY_DATA_VIEW(priv->dataview);
    g_return_if_fail(GWY_IS_DATA_VIEW(dataview));

    gdouble zoom = gwy_data_view_get_real_zoom(dataview);
    gwy_debug("%g", zoom);
    gint prec;

    if (zoom >= 1.0) {
        prec = (zoom/floor(zoom) < 1.0 + 1e-12) ? 0 : 1;
        g_string_printf(priv->str,
                        "%s %.*f:1 (%s)",
                        priv->data_name->str,
                        prec, zoom, g_get_application_name());
    }
    else {
        zoom = 1.0/zoom;
        prec = (zoom/floor(zoom) < 1.0 + 1e-12) ? 0 : 1;
        g_string_printf(priv->str,
                        "%s 1:%.*f (%s)",
                        priv->data_name->str,
                        prec, zoom, g_get_application_name());
    }
    gtk_window_set_title(GTK_WINDOW(window), priv->str->str);
}

/**
 * gwy_data_window_get_data_name:
 * @window: An image window.
 *
 * Gets the data name part of an image window's title.
 *
 * Returns: The data name as a string owned by the window.
 **/
const gchar*
gwy_data_window_get_data_name(GwyDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_DATA_WINDOW(window), NULL);
    return window->priv->data_name->str;
}

/**
 * gwy_data_window_set_data_name:
 * @window: An image window.
 * @data_name: New data name.
 *
 * Sets the data name of an image window.
 *
 * The data name is used in the window's title.
 **/
void
gwy_data_window_set_data_name(GwyDataWindow *window,
                              const gchar *data_name)
{
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));

    GwyDataWindowPrivate *priv = window->priv;

    if (!data_name)
        data_name = "";

    if (!gwy_strequal(data_name, priv->data_name->str)) {
        g_string_assign(priv->data_name, data_name);
        update_title(window);
        g_object_notify_by_pspec(G_OBJECT(window), properties[PROP_DATA_NAME]);
    }
}

/**
 * gwy_data_window_get_ul_corner_widget:
 * @window: An image window.
 *
 * Returns the upper left corner widget of @window.
 *
 * Returns: The upper left corner widget as a #GtkWidget, %NULL if there is
 *          no such widget.
 **/
GtkWidget*
gwy_data_window_get_ul_corner_widget(GwyDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_DATA_WINDOW(window), NULL);
    return window->priv->ul_corner;
}

/**
 * gwy_data_window_set_ul_corner_widget:
 * @window: An image window.
 * @corner: A widget to set as upper left corner widget, many be %NULL to
 *          just remove any eventual existing one.
 *
 * Sets the widget in upper left corner of an image window to @corner.
 **/
void
gwy_data_window_set_ul_corner_widget(GwyDataWindow *window,
                                     GtkWidget *corner)
{
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));
    g_return_if_fail(!corner || GTK_IS_WIDGET(corner));

    GwyDataWindowPrivate *priv = window->priv;

    if (corner == priv->ul_corner)
        return;

    if (priv->ul_corner)
        gtk_widget_unparent(priv->ul_corner);

    if (corner)
        gtk_grid_attach(GTK_GRID(priv->grid), corner, 0, 0, 1, 1);
    g_object_notify_by_pspec(G_OBJECT(window), properties[PROP_UL_CORNER]);
}

static void
resize_view(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    gdouble min, max;
    gwy_data_view_get_color_range(GWY_DATA_VIEW(priv->dataview), &min, &max);
    gwy_color_axis_set_range(GWY_COLOR_AXIS(priv->coloraxis), min, max);
    resize_to_natural(window);
}

static void
resize_to_natural(GwyDataWindow *window)
{
    GtkWidget *widget = GTK_WIDGET(window);
    gint width, height, dummy;
    GTK_WIDGET_CLASS(parent_class)->get_preferred_width(widget, &dummy, &width);
    GTK_WIDGET_CLASS(parent_class)->get_preferred_height(widget, &dummy, &height);
    if (width && height) {
        gtk_window_resize(GTK_WINDOW(window), width, height);
    }
}

static void
zoom_changed(GwyDataWindow *window)
{
    g_return_if_fail(GWY_IS_DATA_WINDOW(window));
    update_title(window);
}

static void
copy_to_clipboard(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GtkClipboard *clipboard;
    GdkDisplay *display;
    GdkPixbuf *pixbuf;
    GdkAtom atom;

    display = gtk_widget_get_display(GTK_WIDGET(window));
    atom = gdk_atom_intern("CLIPBOARD", FALSE);
    clipboard = gtk_clipboard_get_for_display(display, atom);
    pixbuf = gwy_data_view_get_pixbuf(GWY_DATA_VIEW(priv->dataview), 0, 0);
    gtk_clipboard_set_image(clipboard, pixbuf);
    g_object_unref(pixbuf);
}

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    enum {
        important_mods = GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_RELEASE_MASK
    };

    GwyDataWindow *window = GWY_DATA_WINDOW(widget);
    gboolean (*method)(GtkWidget*, GdkEventKey*);
    guint state, key;

    gwy_debug("state = %u, keyval = %u", event->state, event->keyval);
    state = event->state & important_mods;
    key = event->keyval;
    if (!state && (key == GDK_KEY_minus || key == GDK_KEY_KP_Subtract)) {
        gwy_data_window_set_zoom(window, -1);
        return TRUE;
    }
    else if (!state && (key == GDK_KEY_equal || key == GDK_KEY_KP_Equal
                        || key == GDK_KEY_plus || key == GDK_KEY_KP_Add)) {
        gwy_data_window_set_zoom(window, 1);
        return TRUE;
    }
    else if (!state && (key == GDK_KEY_Z || key == GDK_KEY_z || key == GDK_KEY_KP_Divide)) {
        gwy_data_window_set_zoom(window, 10000);
        return TRUE;
    }
    else if (state == GDK_CONTROL_MASK && (key == GDK_KEY_C || key == GDK_KEY_c)) {
        copy_to_clipboard(window);
        return TRUE;
    }

    method = GTK_WIDGET_CLASS(parent_class)->key_press_event;
    return method ? method(widget, event) : FALSE;
}

static gboolean
color_axis_clicked(GtkWidget *window,
                   GdkEventButton *event)
{
    GtkWidget *menu, *item;

    if (event->button != 3)
        return FALSE;

    menu = gwy_menu_gradient(G_CALLBACK(gradient_selected), window);

    item = gtk_menu_item_new_with_mnemonic(_("_Unset Color Gradient"));
    g_signal_connect_swapped(item, "activate", G_CALLBACK(unset_gradient), window);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);

    /* TRANSLATORS: Countable (more false colour gradients). */
    item = gtk_menu_item_new_with_mnemonic(_("_More..."));
    g_signal_connect_swapped(item, "activate", G_CALLBACK(show_more_gradients), window);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
    g_signal_connect(menu, "selection-done", G_CALLBACK(gtk_widget_destroy), NULL);

    gtk_widget_show_all(menu);
    gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);

    return FALSE;
}

static void
show_more_gradients(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GtkWidget *selwindow, *treeview, *scwin;
    GwyGradient *gradient;

    if (priv->grad_selector) {
        selwindow = gtk_widget_get_toplevel(priv->grad_selector);
        gtk_window_present_with_time(GTK_WINDOW(window), gtk_get_current_event_time());
        return;
    }

    /* Pop up */
    selwindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(selwindow), _("Choose Color Gradient"));
    gtk_window_set_default_size(GTK_WINDOW(selwindow), -1, 400);
    gtk_window_set_transient_for(GTK_WINDOW(selwindow), GTK_WINDOW(window));

    scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_container_add(GTK_CONTAINER(selwindow), scwin);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);

    gradient = gwy_color_axis_get_gradient(GWY_COLOR_AXIS(priv->coloraxis));
    treeview = gwy_gradient_tree_view_new(G_CALLBACK(gradient_changed), window,
                                          gradient ? gwy_resource_get_name(GWY_RESOURCE(gradient)) : NULL);
    g_signal_connect_swapped(treeview, "row-activated", G_CALLBACK(gtk_widget_destroy), selwindow);
    g_signal_connect_swapped(treeview, "key-press-event", G_CALLBACK(gradient_window_key_pressed), selwindow);
    priv->grad_selector = treeview;
    g_object_add_weak_pointer(G_OBJECT(treeview), (gpointer*)&priv->grad_selector);
    gtk_container_add(GTK_CONTAINER(scwin), treeview);

    gtk_widget_show_all(scwin);
    gtk_window_present(GTK_WINDOW(selwindow));
}

static void
unset_gradient(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GtkTreeView *treeview;
    GtkTreeSelection *selection;

    if (priv->grad_selector) {
        treeview = GTK_TREE_VIEW(priv->grad_selector);
        selection = gtk_tree_view_get_selection(treeview);
        gtk_tree_selection_unselect_all(selection);
    }
    gradient_update(window, NULL);
    gwy_color_axis_set_gradient(GWY_COLOR_AXIS(priv->coloraxis), NULL);
}

static void
gradient_selected(GtkWidget *item,
                  GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;
    GtkTreeView *treeview;
    GtkTreeSelection *selection;
    GtkTreeModel *model;
    GtkTreeIter iter;
    GwyResource *resource;
    const gchar *gradient;

    gradient = g_object_get_data(G_OBJECT(item), "gradient-name");
    if (!priv->grad_selector) {
        gradient_update(window, gradient ? gwy_gradients_get_gradient(gradient) : NULL);
        return;
    }

    treeview = GTK_TREE_VIEW(priv->grad_selector);
    selection = gtk_tree_view_get_selection(treeview);
    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
        gtk_tree_model_get(model, &iter, 0, &resource, -1);
        if (gwy_strequal(gradient, gwy_resource_get_name(resource)))
            return;
    }

    /* This leads to gradient_changed() which actually updates the gradient */
    gwy_gradient_tree_view_set_active(priv->grad_selector, gradient);
}

static void
gradient_changed(GtkTreeSelection *selection,
                 GwyDataWindow *window)
{
    GwyResource *resource;
    GtkTreeModel *model;
    GtkTreeIter iter;

    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
        gtk_tree_model_get(model, &iter, 0, &resource, -1);
        gradient_update(window, GWY_GRADIENT(resource));
    }
}

static gboolean
gradient_window_key_pressed(GtkWidget *window,
                            GdkEventKey *event)
{
    enum {
        important_mods = GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_RELEASE_MASK
    };
    guint state, key;

    state = event->state & important_mods;
    key = event->keyval;
    if (state == 0 && key == GDK_KEY_Escape) {
        gtk_widget_destroy(window);
        return TRUE;
    }
    return FALSE;
}

static void
gradient_update(GwyDataWindow *window,
                GwyGradient *gradient)
{
    GwyDataWindowPrivate *priv = window->priv;
    gwy_data_view_set_gradient(GWY_DATA_VIEW(priv->dataview), gradient);
    gwy_color_axis_set_gradient(GWY_COLOR_AXIS(priv->coloraxis), gradient);
    /* FIXME GTK3 we used to update the gradient name in the file here. However, libgwui now should not mess with the
     * file structure. So it has to be done in libgwyapp by watching DataView::notify signal. */
}

static void
data_view_updated(GwyDataWindow *window)
{
    GwyDataWindowPrivate *priv = window->priv;

    GwyDataView *view = GWY_DATA_VIEW(priv->dataview);
    GwyColorAxis *axis = GWY_COLOR_AXIS(priv->coloraxis);
    gdouble min, max;
    gwy_data_view_get_color_range(view, &min, &max);
    gwy_color_axis_set_range(axis, min, max);
    gwy_color_axis_set_gradient(axis, gwy_data_view_get_gradient(view));
    size_allocate(GTK_WIDGET(window), NULL);
    update_units(window);
}

/**
 * SECTION: data-window
 * @title: GwyDataWindow
 * @short_description: Data display window
 * @see_also: #GwyDataView -- basic data display widget
 *
 * #GwyDataWindow encapsulates a #GwyDataView together with other controls. You can create an image window for a data
 * view with gwy_data_window_new().
 **/

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