/*
 *  $Id: gwyglwindow.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2004-2024 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 <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

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

#include "libgwyui/option-menus.h"
#include "libgwyui/radio-buttons.h"
#include "libgwyui/combo-boxes.h"
#include "libgwyui/utils.h"
#include "libgwyui/icons.h"
#include "libgwyui/gwyglwindow.h"

#define ZOOM_FACTOR 1.3195

enum {
    DEFAULT_WIDTH = 620,
    DEFAULT_HEIGHT = 360,
};

enum {
    N_BUTTONS = GWY_GL_MOVEMENT_LIGHT + 1
};

struct _GwyGLWindowPrivate {
    GtkWidget *view;
    GtkWidget *gradient_menu;
    GtkWidget *material_menu;
    GtkWidget *material_label;
    GtkWidget *lights_spin1;
    GtkWidget *lights_spin2;
    GtkWidget **buttons;
    GSList    *visual_mode_group;

    GtkWidget *labels_menu;
    GtkWidget *labels_text;
    GtkWidget *labels_delta_x;
    GtkWidget *labels_delta_y;
    GtkWidget *labels_size;
    GtkWidget *labels_autosize;

    GtkWidget *notebook;
    GtkWidget *actions;
    GtkWidget *vbox_small;
    GtkWidget *vbox_large;

    GtkWidget *dataov_menu;
    GtkWidget *physcale_entry;
    GtkWidget *label_size_equal;

    GtkWidget *fmscale_reserve_space;
    GtkAdjustment *fmscale_size;
    GtkAdjustment *fmscale_yalign;

    gboolean in_update;
    gboolean controls_full;

    GwyGLSetup *setup;
    gulong setup_id;
};

static void       finalize                    (GObject *object);
static void       dispose                     (GObject *object);
static gboolean   key_pressed                 (GtkWidget *widget,
                                               GdkEventKey *event);
static void       resize                      (GwyGLWindow *window,
                                               gint zoomtype);
static void       pack_buttons                (GwyGLWindow *window,
                                               guint offset,
                                               GtkBox *box);
static void       setup_adj_changed           (GtkAdjustment *adj,
                                               GwyGLSetup *setup);
static void       adj_setup_changed           (GwyGLSetup *setup,
                                               GParamSpec *pspec,
                                               GtkAdjustment *adj);
static GtkWidget* build_basic_tab             (GwyGLWindow *window);
static GtkWidget* build_visual_tab            (GwyGLWindow *window);
static GtkWidget* build_label_tab             (GwyGLWindow *window);
static GtkWidget* build_colorbar_tab          (GwyGLWindow *window);
static void       set_mode                    (gpointer userdata,
                                               GtkWidget *button);
static void       set_gradient                (GtkTreeSelection *selection,
                                               GwyGLWindow *window);
static void       set_material                (GtkTreeSelection *selection,
                                               GwyGLWindow *window);
static void       select_controls             (gpointer data,
                                               GtkWidget *button);
static void       set_labels                  (GtkWidget *combo,
                                               GwyGLWindow *window);
static void       label_adj_changed           (GtkAdjustment *adj,
                                               GwyGLWindow *window);
static void       projection_changed          (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       show_axes_changed           (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       hide_masked_changed         (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       show_labels_changed         (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       fmscale_rspace_changed      (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       show_fmscale_changed        (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       display_mode_changed        (GtkWidget *item,
                                               GwyGLWindow *window);
static void       set_visualization           (GwyGLWindow *window,
                                               GwyGLVisualization visual);
static void       visualization_changed       (GwyGLSetup *setup,
                                               GParamSpec *pspec,
                                               GwyGLWindow *window);
static void       auto_scale_changed          (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       label_size_eq_changed       (GtkToggleButton *check,
                                               GwyGLWindow *window);
static void       labels_entry_activate       (GtkEntry *entry,
                                               GwyGLWindow *window);
static void       labels_reset_clicked        (GwyGLWindow *window);
static void       reset_visualisation         (GwyGLWindow *window);
static gboolean   view_clicked                (GwyGLWindow *window,
                                               GdkEventButton *event,
                                               GtkWidget *view);
static void       visual_selected             (GtkWidget *item,
                                               GwyGLWindow *window);
static void       gradient_selected           (GtkWidget *item,
                                               GwyGLWindow *window);
static void       material_selected           (GtkWidget *item,
                                               GwyGLWindow *window);
static void       set_zscale                  (GwyGLWindow *window);
static void       update_physcale_entry       (GwyGLWindow *window,
                                               GtkAdjustment *adj);
static void       sync_other_labels_to_current(GwyGLWindow *window);
static void       read_gllabel_properties     (GwyGLLabel *label,
                                               gdouble *delta_x,
                                               gdouble *delta_y,
                                               gdouble *size,
                                               gboolean *fixed_size);
static void       update_controls_for_label   (GwyGLWindow *window,
                                               GwyGLLabel *label);

static GtkWindowClass *parent_class = NULL;

/* FIXME: There are all kinds of odd object data, mostly existing because we had not private struct originally. Get
 * rid of what we can? */
static G_DEFINE_QUARK(gwy-gl-window-label-property-id, adj_property)
static G_DEFINE_QUARK(gwy-gl-window-setup-property, setup_property)
static G_DEFINE_QUARK(gwy-gl-window-setup-rad2deg, setup_rad2deg)
static G_DEFINE_QUARK(gwy-gl-window, glwindow)

G_DEFINE_TYPE_WITH_CODE(GwyGLWindow, gwy_gl_window, GTK_TYPE_WINDOW,
                        G_ADD_PRIVATE(GwyGLWindow))

static void
gwy_gl_window_class_init(GwyGLWindowClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_gl_window_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->finalize = finalize;

    widget_class->key_press_event = key_pressed;
}

static void
gwy_gl_window_init(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv;

    priv = window->priv = gwy_gl_window_get_instance_private(window);

    priv->buttons = g_new0(GtkWidget*, 2*N_BUTTONS);
    priv->in_update = FALSE;

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_container_add(GTK_CONTAINER(window), hbox);

    /* Small toolbar */
    priv->vbox_small = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_end(GTK_BOX(hbox), priv->vbox_small, FALSE, FALSE, 0);
    gtk_container_set_border_width(GTK_CONTAINER(priv->vbox_small), 4);

    GtkWidget *more = gtk_button_new();
    gtk_box_pack_start(GTK_BOX(priv->vbox_small), more, FALSE, FALSE, 0);
    gtk_container_add(GTK_CONTAINER(more),
                      gtk_image_new_from_icon_name(GWY_ICON_MORE, GTK_ICON_SIZE_LARGE_TOOLBAR));
    gtk_widget_set_tooltip_text(more, _("Show full controls"));
    g_object_set_qdata(G_OBJECT(more), glwindow_quark(), window);
    g_signal_connect_swapped(more, "clicked", G_CALLBACK(select_controls), GINT_TO_POINTER(FALSE));

    pack_buttons(window, 0, GTK_BOX(priv->vbox_small));

    /* Large toolbar */
    priv->vbox_large = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_box_pack_end(GTK_BOX(hbox), priv->vbox_large, FALSE, FALSE, 0);
    gtk_container_set_border_width(GTK_CONTAINER(priv->vbox_large), 4);
    gtk_widget_set_no_show_all(priv->vbox_large, TRUE);

    GtkWidget *hbox2 = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(priv->vbox_large), hbox2, FALSE, FALSE, 0);

    GtkWidget *less = gtk_button_new();
    gtk_box_pack_end(GTK_BOX(hbox2), less, FALSE, FALSE, 0);

    gtk_container_add(GTK_CONTAINER(less),
                      gtk_image_new_from_icon_name(GWY_ICON_LESS, GTK_ICON_SIZE_LARGE_TOOLBAR));
    gtk_widget_set_tooltip_text(less, _("Hide full controls"));
    g_object_set_qdata(G_OBJECT(less), glwindow_quark(), window);
    g_signal_connect_swapped(less, "clicked", G_CALLBACK(select_controls), GINT_TO_POINTER(TRUE));

    pack_buttons(window, N_BUTTONS, GTK_BOX(hbox2));

    priv->notebook = gtk_notebook_new();
    GtkNotebook *notebook = GTK_NOTEBOOK(priv->notebook);
    gtk_box_pack_start(GTK_BOX(priv->vbox_large), priv->notebook, TRUE, TRUE, 0);

    gtk_notebook_append_page(notebook, build_basic_tab(window), gtk_label_new(C_("adjective", "Basic")));
    gtk_notebook_append_page(notebook, build_visual_tab(window), gtk_label_new(_("Light & Material")));
    gtk_notebook_append_page(notebook, build_label_tab(window), gtk_label_new(_("Labels")));
    gtk_notebook_append_page(notebook, build_colorbar_tab(window), gtk_label_new(_("Colorbar")));

    gtk_widget_show_all(hbox);

    gtk_window_set_default_size(GTK_WINDOW(window), DEFAULT_WIDTH, DEFAULT_HEIGHT);
}

static void
finalize(GObject *object)
{
    GwyGLWindowPrivate *priv = GWY_GL_WINDOW(object)->priv;

    g_free(priv->buttons);

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

static void
dispose(GObject *object)
{
    GwyGLWindowPrivate *priv = GWY_GL_WINDOW(object)->priv;

    g_clear_signal_handler(&priv->setup_id, priv->setup);
    g_clear_object(&priv->setup);

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

static void
copy_to_clipboard(GwyGLWindow *window)
{
    GwyGLWindowPrivate *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_gl_view_get_pixbuf(GWY_GL_VIEW(priv->view));
    gtk_clipboard_set_image(clipboard, pixbuf);
    g_object_unref(pixbuf);
}

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    GwyGLWindowPrivate *priv = GWY_GL_WINDOW(widget)->priv;
    enum {
        important_mods = GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_RELEASE_MASK
    };
    GwyGLWindow *window;
    guint state, key;

    gwy_debug("state = %u, keyval = %u", event->state, event->keyval);
    window = GWY_GL_WINDOW(widget);
    state = event->state & important_mods;
    key = event->keyval;
    if (state == GDK_CONTROL_MASK && (key == GDK_KEY_C || key == GDK_KEY_c)) {
        copy_to_clipboard(window);
        return TRUE;
    }

    if (!priv->controls_full && !state) {
        GwyGLMovement movement = GWY_GL_MOVEMENT_NONE;

        if (key == GDK_KEY_R || key == GDK_KEY_r)
            movement = GWY_GL_MOVEMENT_ROTATION;
        else if (key == GDK_KEY_S || key == GDK_KEY_s)
            movement = GWY_GL_MOVEMENT_SCALE;
        else if (key == GDK_KEY_V || key == GDK_KEY_v)
            movement = GWY_GL_MOVEMENT_DEFORMATION;
        else if (key == GDK_KEY_L || key == GDK_KEY_l)
            movement = GWY_GL_MOVEMENT_LIGHT;

        if (movement != GWY_GL_MOVEMENT_NONE) {
            gtk_button_clicked(GTK_BUTTON(priv->buttons[movement]));
            return TRUE;
        }

        if (key == GDK_KEY_minus || key == GDK_KEY_KP_Subtract) {
            resize(window, -1);
            return TRUE;
        }
        else if (key == GDK_KEY_equal || key == GDK_KEY_KP_Equal || key == GDK_KEY_plus || key == GDK_KEY_KP_Add) {
            resize(window, 1);
            return TRUE;
        }
        else if (key == GDK_KEY_Z || key == GDK_KEY_z || key == GDK_KEY_KP_Divide) {
            resize(window, 0);
            return TRUE;
        }
    }

    if (GTK_WIDGET_CLASS(parent_class)->key_press_event)
        return GTK_WIDGET_CLASS(parent_class)->key_press_event(widget, event);
    return FALSE;
}

static void
resize(GwyGLWindow *window, gint zoomtype)
{
    GtkWidget *widget = GTK_WIDGET(window);
    gint w, h;

    gtk_window_get_size(GTK_WINDOW(window), &w, &h);
    if (zoomtype > 0) {
        gint scrwidth, scrheight;
        gwy_get_screen_size(widget, &scrwidth, &scrheight);

        w = GWY_ROUND(ZOOM_FACTOR*w);
        h = GWY_ROUND(ZOOM_FACTOR*h);
        if (w > 0.9*scrwidth || h > 0.9*scrheight) {
            if ((gdouble)w/scrwidth > (gdouble)h/scrheight) {
                h = GWY_ROUND(0.9*scrwidth*h/w);
                w = GWY_ROUND(0.9*scrwidth);
            }
            else {
                w = GWY_ROUND(0.9*scrheight*w/h);
                h = GWY_ROUND(0.9*scrheight);
            }
        }
    }
    else if (zoomtype < 0) {
        GtkRequisition req;
        gtk_widget_get_preferred_size(widget, NULL, &req);

        w = GWY_ROUND(w/ZOOM_FACTOR);
        h = GWY_ROUND(h/ZOOM_FACTOR);
        if (w < req.width || h < req.height) {
            if ((gdouble)w/req.width < (gdouble)h/req.height) {
                h = GWY_ROUND((gdouble)req.width*h/w);
                w = req.width;
            }
            else {
                w = GWY_ROUND((gdouble)req.height*w/h);
                h = req.height;
            }
        }
    }
    else {
        w = DEFAULT_WIDTH;
        h = DEFAULT_HEIGHT;
    }

    gtk_window_resize(GTK_WINDOW(window), w, h);
}

static void
pack_buttons(GwyGLWindow *window, guint offset, GtkBox *box)
{
    static struct {
        GwyGLMovement mode;
        const gchar *icon_name;
        const gchar *tooltip;
    }
    const buttons[] = {
        { GWY_GL_MOVEMENT_ROTATION,    GWY_ICON_ROTATE_3D,        N_("Rotate view (R)"),           },
        { GWY_GL_MOVEMENT_SCALE,       GWY_ICON_SCALE,            N_("Scale view as a whole (S)"), },
        { GWY_GL_MOVEMENT_DEFORMATION, GWY_ICON_SCALE_VERTICALLY, N_("Scale value range (V)"),     },
        { GWY_GL_MOVEMENT_LIGHT,       GWY_ICON_LIGHT_ROTATE,     N_("Move light source (L)"),     },
    };
    GtkRadioButton *group = NULL;
    GwyGLWindowPrivate *priv = window->priv;

    for (guint i = 0; i < G_N_ELEMENTS(buttons); i++) {
        GtkWidget *button = gtk_radio_button_new_from_widget(group);
        gtk_box_pack_start(GTK_BOX(box), button, FALSE, FALSE, 0);
        g_object_set(button, "draw-indicator", FALSE, NULL);
        gtk_container_add(GTK_CONTAINER(button),
                          gtk_image_new_from_icon_name(buttons[i].icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR));
        g_signal_connect_swapped(button, "clicked", G_CALLBACK(set_mode), GINT_TO_POINTER(buttons[i].mode));
        g_object_set_qdata(G_OBJECT(button), glwindow_quark(), window);
        gtk_widget_set_tooltip_text(button, _(buttons[i].tooltip));
        priv->buttons[offset + buttons[i].mode] = button;
        if (!group)
            group = GTK_RADIO_BUTTON(button);
    }
}

/**
 * gwy_gl_window_new:
 * @data: A #GwyDict containing the data to display.
 *
 * Creates a new OpenGL 3D data displaying window.
 *
 * Returns: A newly created widget, as #GtkWidget.
 **/
GtkWidget*
gwy_gl_window_new(GwyDict *data)
{
    GwyGLWindow *window = g_object_new(GWY_TYPE_GL_WINDOW, NULL);

    g_return_val_if_fail(GWY_IS_DICT(data), (GtkWidget*)window);

    GwyGLWindowPrivate *priv = window->priv;
    GtkWidget *hbox = gtk_bin_get_child(GTK_BIN(window));
    priv->view = gwy_gl_view_new(data);
    gtk_box_pack_start(GTK_BOX(hbox), priv->view, TRUE, TRUE, 0);
    gtk_box_reorder_child(GTK_BOX(hbox), priv->view, 0);
    g_signal_connect_swapped(priv->view, "button-press-event", G_CALLBACK(view_clicked), window);
    gwy_gl_view_set_movement_type(GWY_GL_VIEW(priv->view), GWY_GL_MOVEMENT_ROTATION);

    return GTK_WIDGET(window);
}

static void
setup_adj_changed(GtkAdjustment *adj, GwyGLSetup *setup)
{
    const gchar *property = g_object_get_qdata(G_OBJECT(adj), setup_property_quark());
    g_return_if_fail(property);

    gboolean rad2deg = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(adj), setup_rad2deg_quark()));
    gulong id = g_signal_handler_find(setup, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA,
                                      0, 0, 0, adj_setup_changed, adj);
    gdouble v =gtk_adjustment_get_value(adj);
    g_signal_handler_block(setup, id);
    g_object_set(setup, property, rad2deg ? gwy_deg2rad(v) : v, NULL);
    g_signal_handler_unblock(setup, id);
}

static void
adj_setup_changed(GwyGLSetup *setup, GParamSpec *pspec, GtkAdjustment *adj)
{
    gboolean rad2deg = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(adj), setup_rad2deg_quark()));
    gdouble value;
    g_object_get(setup, pspec->name, &value, NULL);

    gulong id = g_signal_handler_find(adj, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA,
                                      0, 0, 0, setup_adj_changed, setup);
    g_signal_handler_block(adj, id);
    if (rad2deg)
        value = gwy_rad2deg(gwy_canonicalize_angle(value, FALSE, TRUE));
    gtk_adjustment_set_value(adj, value);
    g_signal_handler_unblock(adj, id);
}

static GtkAdjustment*
make_setup_adj(GwyGLSetup *setup,
               const gchar *property,
               gdouble min,
               gdouble max,
               gdouble step,
               gdouble page,
               gboolean rad2deg)
{
    GtkAdjustment *adj;
    gdouble value;

    g_object_get(setup, property, &value, NULL);
    if (rad2deg)
        value = gwy_rad2deg(value);
    adj = gtk_adjustment_new(value, min, max, step, page, 0.0);
    g_object_set_qdata(G_OBJECT(adj), setup_property_quark(), (gpointer)property);
    g_object_set_qdata(G_OBJECT(adj), setup_rad2deg_quark(), GINT_TO_POINTER(rad2deg));
    g_signal_connect(adj, "value-changed", G_CALLBACK(setup_adj_changed), setup);

    gchar *detail = g_strconcat("notify::", property, NULL);
    g_signal_connect(setup, detail, G_CALLBACK(adj_setup_changed), adj);
    g_free(detail);

    return adj;
}

static GtkWidget*
build_basic_tab(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GtkWidget *spin, *check, *button, *label;
    GtkAdjustment *adj;
    gint row;

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);
    gwy_set_widget_margins(GTK_WIDGET(grid), 4, 4, 4, 4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(grid), TRUE, TRUE, 0);
    row = 0;

    adj = make_setup_adj(setup, "rotation-x", -180.0, 180.0, 1.0, 15.0, TRUE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("φ:"), _("deg"), G_OBJECT(adj), GWY_HSCALE_LINEAR);

    adj = make_setup_adj(setup, "rotation-y", -180.0, 180.0, 1.0, 15.0, TRUE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("θ:"), _("deg"), G_OBJECT(adj), GWY_HSCALE_LINEAR);

    adj = make_setup_adj(setup, "scale", 0.05, 10.0, 0.01, 0.1, FALSE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("_Scale:"), NULL, G_OBJECT(adj), GWY_HSCALE_LOG);
    gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 2);

    adj = make_setup_adj(setup, "z-scale", 0.001, 100.0, 0.001, 1.0, FALSE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("_Value scale:"), NULL, G_OBJECT(adj), GWY_HSCALE_LOG);
    gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 5);
    g_signal_connect_swapped(adj, "value-changed", G_CALLBACK(update_physcale_entry), window);

    label = gtk_label_new_with_mnemonic(_("Ph_ysical scale:"));
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_grid_attach(GTK_GRID(grid), label, 0, row, 1, 1);

    priv->physcale_entry = gtk_entry_new();
    gtk_entry_set_width_chars(GTK_ENTRY(priv->physcale_entry), 8);
    gtk_grid_attach(GTK_GRID(grid), priv->physcale_entry, 1, row, 1, 1);
    gtk_label_set_mnemonic_widget(GTK_LABEL(label), priv->physcale_entry);
    update_physcale_entry(window, GTK_ADJUSTMENT(adj));
    g_signal_connect_swapped(priv->physcale_entry, "activate", G_CALLBACK(set_zscale), window);

    button = gtk_button_new_with_mnemonic(C_("verb", "Set"));
    gtk_grid_attach(GTK_GRID(grid), button, 2, row, 1, 1);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(set_zscale), window);
    row++;

    /* The range and step is what seems typically supported. */
    adj = make_setup_adj(setup, "line-width", 1.0, 10.0, 0.1, 1.0, FALSE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("Line _width:"), _("px"), G_OBJECT(adj), GWY_HSCALE_LINEAR);
    gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 1);

    gboolean axes_visible, labels_visible, hide_masked;
    GwyGLProjection projection;
    g_object_get(setup,
                 "axes-visible", &axes_visible,
                 "labels-visible", &labels_visible,
                 "hide_masked", &hide_masked,
                 "projection", &projection,
                 NULL);

    check = gtk_check_button_new_with_mnemonic(_("Show _axes"));
    gwy_set_widget_margins(check, -1, -1, 8, -1);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), axes_visible);
    gtk_grid_attach(GTK_GRID(grid), check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(show_axes_changed), window);
    row++;

    check = gtk_check_button_new_with_mnemonic(_("Show _labels"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), labels_visible);
    gtk_grid_attach(GTK_GRID(grid), check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(show_labels_changed), window);
    row++;

    check = gtk_check_button_new_with_mnemonic(_("_Orthographic projection"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), !projection);
    gtk_grid_attach(GTK_GRID(grid), check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(projection_changed), window);
    row++;

    check = gtk_check_button_new_with_mnemonic(_("_Hide masked"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), hide_masked);
    gtk_grid_attach(GTK_GRID(grid), check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(hide_masked_changed), window);
    row++;

    return vbox;
}

static GtkWidget*
build_visual_tab(GwyGLWindow *window)
{
    static const GwyEnum display_modes[] = {
        { N_("_Lighting"),           GWY_GL_VISUALIZATION_LIGHTING,         },
        { N_("_Gradient"),           GWY_GL_VISUALIZATION_GRADIENT,         },
        { N_("_Overlay"),            GWY_GL_VISUALIZATION_OVERLAY,          },
        { N_("_Overlay – no light"), GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT, },
    };

    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GwyDict *data = gwy_gl_view_get_data(view);
    GwyGLVisualization visualization;
    g_object_get(setup, "visualization", &visualization, NULL);

    gboolean is_material = FALSE, is_gradient = FALSE, is_overlay = FALSE, light = FALSE;
    GtkWidget *spin, *menu, *label, *button;
    GtkAdjustment *adj;

    if (visualization == GWY_GL_VISUALIZATION_GRADIENT) {
        is_gradient = TRUE;
        light = FALSE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_LIGHTING) {
        is_material = TRUE;
        light = TRUE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_OVERLAY) {
        is_overlay  = TRUE;
        light = TRUE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT) {
        is_overlay  = TRUE;
        light = FALSE;
    }
    else {
        g_warning("Unknown visualization mode %d.", visualization);
        is_gradient = TRUE;
    }

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(grid), TRUE, TRUE, 0);
    gint row = 0;

    priv->visual_mode_group = gwy_radio_buttons_create(display_modes, G_N_ELEMENTS(display_modes),
                                                       G_CALLBACK(display_mode_changed), window,
                                                       visualization);

    row = gwy_radio_buttons_attach_to_grid(priv->visual_mode_group, grid, 2, row);

    label = gtk_label_new_with_mnemonic(_("_Material:"));
    priv->material_label = label;
    gtk_label_set_xalign(GTK_LABEL(label), 0);
    gtk_widget_set_sensitive(label, is_material);
    gtk_grid_attach(grid, label, 0, row, 2, 1);
    row++;

    const gchar *name = NULL;
    gwy_dict_gis_string_by_name(data, gwy_gl_view_get_material_key(view), &name);
    menu = gwy_gl_material_selection_new(G_CALLBACK(set_material), window, name);
    priv->material_menu = menu;
    gtk_widget_set_sensitive(menu, is_material);
    gtk_grid_attach(grid, menu, 0, row, 2, 1);
    row++;

    adj = make_setup_adj(setup, "light-phi", -180.0, 180.0, 1.0, 15.0, TRUE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("_Light φ:"), _("deg"), G_OBJECT(adj), GWY_HSCALE_LINEAR);
    priv->lights_spin1 = spin;
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), light);

    adj = make_setup_adj(setup, "light-theta", -180.0, 180.0, 1.0, 15.0, TRUE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("L_ight θ:"), _("deg"), G_OBJECT(adj), GWY_HSCALE_LINEAR);
    priv->lights_spin2 = spin;
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), light);

    gtk_widget_set_sensitive(priv->buttons[GWY_GL_MOVEMENT_LIGHT], light);
    gtk_widget_set_sensitive(priv->buttons[N_BUTTONS + GWY_GL_MOVEMENT_LIGHT], light);
    row++;

    name = NULL;
    gwy_dict_gis_string_by_name(data, gwy_gl_view_get_gradient_key(view), &name);
    menu = gwy_gradient_selection_new(G_CALLBACK(set_gradient), window, name);
    gtk_widget_set_sensitive(menu, is_gradient || is_overlay);
    priv->gradient_menu = menu;
    gwy_set_widget_margins(menu, -1, -1, 8, -1);
    gtk_grid_attach(grid, menu, 0, row, 2, 1);
    row++;

    priv->dataov_menu = gtk_label_new(NULL);
    gtk_grid_attach(grid, priv->dataov_menu, 0, row, 2, 1);
    row++;

    button = gtk_button_new_with_mnemonic(_("_Reset"));
    gwy_set_widget_margins(button, -1, -1, 8, -1);
    gtk_grid_attach(grid, button, 0, row, 1, 1);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(reset_visualisation), window);
    row++;

    priv->setup_id = g_signal_connect(setup, "notify::visualization", G_CALLBACK(visualization_changed), window);

    return vbox;
}

/**
 * gwy_gl_window_set_overlay_chooser:
 * @window: An OpenGL data view window.
 * @chooser: Overlay chooser widget.
 *
 * Sets the overlay chooser widget of a 3D window.
 *
 * Once set, the overlay chooser widget cannot be changed.
 *
 * The 3D window does not use the provided widget in any way, it just places it in an appropriate place in the user
 * interface.  It is expected that the caller will set up the layer and call gwy_gl_view_set_ovlay() appropriately
 * upon selection of data in the chooser.
 **/
void
gwy_gl_window_set_overlay_chooser(GwyGLWindow *window,
                                  GtkWidget *chooser)
{
    g_return_if_fail(GWY_IS_GL_WINDOW(window));
    g_return_if_fail(GTK_IS_WIDGET(chooser));

    GwyGLWindowPrivate *priv = window->priv;

    /* XXX: Switching the choosers would be nice from API completness point of view but of limited practical use. */
    if (chooser == priv->dataov_menu)
        return;
    g_return_if_fail(GTK_IS_LABEL(priv->dataov_menu));

    GtkWidget *vbox = gtk_notebook_get_nth_page(GTK_NOTEBOOK(priv->notebook), 1);
    GList *list = gtk_container_get_children(GTK_CONTAINER(vbox));
    GtkWidget *grid = GTK_WIDGET(list->data);
    g_list_free(list);
    g_return_if_fail(GTK_IS_GRID(grid));

    gint row;
    gtk_container_child_get(GTK_CONTAINER(grid), priv->dataov_menu, "top-attach", &row, NULL);
    gtk_widget_destroy(priv->dataov_menu);
    gtk_grid_attach(GTK_GRID(grid), chooser, 0, row, 3, 1);
    priv->dataov_menu = chooser;

    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GwyGLVisualization visualization;
    g_object_get(setup, "visualization", &visualization, NULL);
    gtk_widget_set_sensitive(chooser, visualization == GWY_GL_VISUALIZATION_OVERLAY);
}

/* gwy_gl_window_get_overlay_chooser:
 * @window: An OpenGL data view window.
 *
 * Obtains the overlay chooser widget.
 *
 * It is the widget previously set with gwy_gl_window_set_overlay_chooser(). The function returns some widget also
 * when none has been set previously, but no assumption should be made what it is in such case.
 *
 * Returns: The data overlay chooser widget.
 **/
GtkWidget*
gwy_gl_window_get_overlay_chooser(GwyGLWindow *window)
{
    g_return_val_if_fail(GWY_IS_GL_WINDOW(window), NULL);
    return window->priv->dataov_menu;
}

static guint
all_labels_have_equal_size(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);

    guint size_x = gwy_gl_label_get_size(gwy_gl_view_get_label(view, GWY_GL_VIEW_LABEL_X));
    guint size_y = gwy_gl_label_get_size(gwy_gl_view_get_label(view, GWY_GL_VIEW_LABEL_Y));
    guint size_min = gwy_gl_label_get_size(gwy_gl_view_get_label(view, GWY_GL_VIEW_LABEL_MIN));
    guint size_max = gwy_gl_label_get_size(gwy_gl_view_get_label(view, GWY_GL_VIEW_LABEL_MAX));
    if (size_x == size_y && size_y == size_min && size_min == size_max)
        return size_x;

    return 0;
}

static GtkWidget*
build_label_tab(GwyGLWindow *window)
{
    static const GwyEnum label_entries[] = {
        { N_("X-axis"),          GWY_GL_VIEW_LABEL_X, },
        { N_("Y-axis"),          GWY_GL_VIEW_LABEL_Y, },
        { N_("Minimum z value"), GWY_GL_VIEW_LABEL_MIN, },
        { N_("Maximum z value"), GWY_GL_VIEW_LABEL_MAX, }
    };

    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GtkWidget *spin, *combo, *movelabel, *entry, *check, *button;
    GtkAdjustment *adj;

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);
    gwy_set_widget_margins(GTK_WIDGET(grid), 4, 4, 4, 4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(grid), TRUE, TRUE, 0);
    gint row = 0;

    combo = gwy_enum_combo_box_new(label_entries, G_N_ELEMENTS(label_entries),
                                   G_CALLBACK(set_labels), window,
                                   -1, TRUE);
    gwy_gtkgrid_attach_adjbar(grid, row, _("_Label:"), NULL, G_OBJECT(combo), GWY_HSCALE_WIDGET_NO_EXPAND);
    priv->labels_menu = combo;
    row++;

    GwyGLLabel *label = gwy_gl_view_get_label(view, GWY_GL_VIEW_LABEL_X);
    gdouble delta_x, delta_y, size;
    gboolean fixed_size;
    read_gllabel_properties(label, &delta_x, &delta_y, &size, &fixed_size);

    priv->labels_text = entry = gtk_entry_new();
    gtk_entry_set_max_length(GTK_ENTRY(entry), 100);
    gwy_widget_set_activate_on_unfocus(entry, TRUE);
    g_signal_connect(entry, "activate", G_CALLBACK(labels_entry_activate), window);
    gtk_entry_set_text(GTK_ENTRY(entry), gwy_gl_label_get_text(label));
    gtk_editable_select_region(GTK_EDITABLE(entry), 0, gtk_entry_get_text_length(GTK_ENTRY(entry)));
    gwy_gtkgrid_attach_adjbar(grid, row, _("_Text:"), NULL, G_OBJECT(entry), GWY_HSCALE_WIDGET);
    row++;

    movelabel = gtk_label_new(_("Move label"));
    gwy_set_widget_margins(movelabel, -1, -1, 8, -1);
    gtk_label_set_xalign(GTK_LABEL(movelabel), 0.0);
    gtk_grid_attach(grid, movelabel, 0, row, 1, 1);
    row++;

    adj = gtk_adjustment_new(delta_x, -1000.0, 1000.0, 1.0, 10.0, 0.0);
    g_object_set_qdata(G_OBJECT(adj), adj_property_quark(), "delta-x");
    spin = gwy_gtkgrid_attach_adjbar(grid, row, _("_Horizontally:"), _("px"), G_OBJECT(adj), GWY_HSCALE_SQRT);
    priv->labels_delta_x = spin;
    g_signal_connect(adj, "value-changed", G_CALLBACK(label_adj_changed), window);
    row++;

    adj = gtk_adjustment_new(delta_y, -1000.0, 1000.0, 1.0, 10.0, 0.0);
    g_object_set_qdata(G_OBJECT(adj), adj_property_quark(), "delta-y");
    spin = gwy_gtkgrid_attach_adjbar(grid, row, _("_Vertically:"), _("px"), G_OBJECT(adj), GWY_HSCALE_SQRT);
    priv->labels_delta_y = spin;
    g_signal_connect(adj, "value-changed", G_CALLBACK(label_adj_changed), window);
    row++;

    check = gtk_check_button_new_with_mnemonic(_("A_ll labels have the same size"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), all_labels_have_equal_size(window));
    gwy_set_widget_margins(check, -1, -1, 8, -1);
    gtk_grid_attach(grid, check, 0, row, 2, 1);
    priv->label_size_equal = check;
    g_signal_connect(check, "toggled", G_CALLBACK(label_size_eq_changed), window);
    row++;

    check = gtk_check_button_new_with_mnemonic(_("Scale size _automatically"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), !fixed_size);
    gtk_grid_attach(grid, check, 0, row, 2, 1);
    priv->labels_autosize = check;
    g_signal_connect(check, "toggled", G_CALLBACK(auto_scale_changed), window);
    row++;

    adj = gtk_adjustment_new(size, 1.0, 100.0, 1.0, 10.0, 0.0);
    g_object_set_qdata(G_OBJECT(adj), adj_property_quark(), "size");
    spin = gwy_gtkgrid_attach_adjbar(grid, row, _("Si_ze:"), _("px"), G_OBJECT(adj), GWY_HSCALE_SQRT);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), fixed_size);
    priv->labels_size = spin;
    g_signal_connect(adj, "value-changed", G_CALLBACK(label_adj_changed), window);
    row++;

    button = gtk_button_new_with_mnemonic(_("_Reset"));
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(labels_reset_clicked), window);
    gtk_grid_attach(grid, button, 0, row, 1, 1);
    priv->actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(priv->vbox_large), priv->actions, FALSE, FALSE, 0);

    return vbox;
}

static GtkWidget*
build_colorbar_tab(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GtkWidget *spin, *check;
    GtkAdjustment *adj;

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);

    GtkGrid *grid = GTK_GRID(gtk_grid_new());
    gtk_grid_set_row_spacing(grid, 2);
    gtk_grid_set_column_spacing(grid, 6);
    gtk_container_set_border_width(GTK_CONTAINER(grid), 4);
    gtk_box_pack_start(GTK_BOX(vbox), GTK_WIDGET(grid), TRUE, TRUE, 0);
    gint row = 0;

    check = gtk_check_button_new_with_mnemonic(_("Show false _colorbar"));
    gboolean fmscale_visible, fmscale_reserve_space;
    g_object_get(setup,
                 "fmscale-visible", &fmscale_visible,
                 "fmscale-reserve-space", &fmscale_reserve_space,
                 NULL);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), fmscale_visible);
    gtk_grid_attach(grid, check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(show_fmscale_changed), window);
    row++;

    check = priv->fmscale_reserve_space = gtk_check_button_new_with_mnemonic(_("Reserve space for _colorbar"));
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), fmscale_reserve_space);
    gtk_grid_attach(grid, check, 0, row, 2, 1);
    g_signal_connect(check, "toggled", G_CALLBACK(fmscale_rspace_changed), window);
    gtk_widget_set_sensitive(check, fmscale_visible);
    row++;

    adj = priv->fmscale_size = make_setup_adj(setup, "fmscale-size", 0.0, 1.0, 0.001, 0.1, FALSE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("_Size:"), NULL, G_OBJECT(adj), GWY_HSCALE_LINEAR);
    gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 3);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), fmscale_visible);

    adj = priv->fmscale_yalign = make_setup_adj(setup, "fmscale-y-align", 0.0, 1.0, 0.001, 0.1, FALSE);
    spin = gwy_gtkgrid_attach_adjbar(grid, row++, _("_Vertical alignment:"), NULL, G_OBJECT(adj), GWY_HSCALE_LINEAR);
    gtk_spin_button_set_digits(GTK_SPIN_BUTTON(spin), 3);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), fmscale_visible);

    return vbox;
}

/**
 * gwy_gl_window_get_gl_view:
 * @window: An OpenGL data view window.
 *
 * Returns the #GwyGLView widget this 3D window currently shows.
 *
 * Returns: The currently shown #GwyDataView.
 **/
GtkWidget*
gwy_gl_window_get_gl_view(GwyGLWindow *window)
{
    g_return_val_if_fail(GWY_IS_GL_WINDOW(window), NULL);

    return window->priv->view;
}

/**
 * gwy_gl_window_add_action_widget:
 * @window: An OpenGL data view window.
 * @widget: A widget to pack into the action area.
 *
 * Adds a widget (usually a button) to 3D window action area.
 *
 * The action area is located under the parameter notebook.
 **/
void
gwy_gl_window_add_action_widget(GwyGLWindow *window,
                                GtkWidget *widget)
{
    g_return_if_fail(GWY_IS_GL_WINDOW(window));
    g_return_if_fail(GTK_IS_WIDGET(widget));

    gtk_box_pack_start(GTK_BOX(window->priv->actions), widget, FALSE, FALSE, 0);
}

/**
 * gwy_gl_window_add_small_toolbar_button:
 * @window: An OpenGL data view window.
 * @icon_name: Button pixmap icon name, like "gtk-save" or %GWY_ICON_CROP.
 * @tooltip: Button tooltip.
 * @callback: Callback action for "clicked" signal.  It is connected swapped, that is it gets @cbdata as its first
 *            argument, the clicked button as the last.
 * @cbdata: Data to pass to @callback.
 *
 * Adds a button to small @window toolbar.
 *
 * The small toolbar is those visible when full controls are hidden.  Due to space constraints the button must be
 * contain only a pixmap.
 **/
void
gwy_gl_window_add_small_toolbar_button(GwyGLWindow *window,
                                       const gchar *icon_name,
                                       const gchar *tooltip,
                                       GCallback callback,
                                       gpointer cbdata)
{
    g_return_if_fail(GWY_IS_GL_WINDOW(window));
    g_return_if_fail(icon_name);

    GwyGLWindowPrivate *priv = window->priv;
    GtkWidget *button = gtk_button_new();
    gtk_box_pack_start(GTK_BOX(priv->vbox_small), button, FALSE, FALSE, 0);
    gtk_container_add(GTK_CONTAINER(button), gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR));
    gtk_widget_set_tooltip_text(button, tooltip);
    g_object_set_qdata(G_OBJECT(button), glwindow_quark(), window);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(callback), cbdata);
}

static void
set_mode(gpointer userdata, GtkWidget *button)
{
    if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)))
        return;

    GwyGLWindow *window = g_object_get_qdata(G_OBJECT(button), glwindow_quark());
    g_return_if_fail(GWY_IS_GL_WINDOW(window));

    GwyGLWindowPrivate *priv = window->priv;
    if (priv->in_update)
        return;

    priv->in_update = TRUE;
    GwyGLMovement movement = GPOINTER_TO_INT(userdata);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->buttons[movement]), TRUE);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->buttons[movement + N_BUTTONS]), TRUE);
    gwy_gl_view_set_movement_type(GWY_GL_VIEW(priv->view), movement);
    priv->in_update = FALSE;
}

static void
set_gradient(GtkTreeSelection *selection, GwyGLWindow *window)
{
    GtkTreeModel *model;
    GtkTreeIter iter;

    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return;

    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);

    GwyResource *resource;
    gtk_tree_model_get(model, &iter, 0, &resource, -1);
    const gchar *name = gwy_resource_get_name(resource);

    gwy_dict_set_string_by_name(gwy_gl_view_get_data(view),
                                           gwy_gl_view_get_gradient_key(view),
                                           name);
}

static void
set_material(GtkTreeSelection *selection, GwyGLWindow *window)
{
    GtkTreeModel *model;
    GtkTreeIter iter;

    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return;

    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);

    GwyResource *resource;
    gtk_tree_model_get(model, &iter, 0, &resource, -1);
    const gchar *name = gwy_resource_get_name(resource);

    gwy_dict_set_string_by_name(gwy_gl_view_get_data(view),
                                           gwy_gl_view_get_material_key(view),
                                           name);
}

static void
select_controls(gpointer data, GtkWidget *button)
{
    GwyGLWindow *window;
    GtkWidget *show, *hide;

    window = (GwyGLWindow*)g_object_get_qdata(G_OBJECT(button), glwindow_quark());
    g_return_if_fail(GWY_IS_GL_WINDOW(window));
    GwyGLWindowPrivate *priv = window->priv;

    show = data ? priv->vbox_small : priv->vbox_large;
    hide = data ? priv->vbox_large : priv->vbox_small;
    priv->controls_full = !data;
    gtk_widget_hide(hide);
    gtk_widget_set_no_show_all(hide, TRUE);
    gtk_widget_set_no_show_all(show, FALSE);
    gtk_widget_show_all(show);
}

static void
set_labels(GtkWidget *combo, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLViewLabel id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(combo));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);
    update_controls_for_label(window, label);
}

static void
label_adj_changed(GtkAdjustment *adj, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    gint id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->labels_menu));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);
    const gchar *key = g_object_get_qdata(G_OBJECT(adj), adj_property_quark());
    gdouble oldval, newval;
    g_object_get(label, key, &oldval, NULL);

    newval = gtk_adjustment_get_value(adj);
    if (oldval != newval)
        g_object_set(label, key, newval, NULL);

    GtkToggleButton *check = GTK_TOGGLE_BUTTON(priv->label_size_equal);
    if (gtk_toggle_button_get_active(check))
        sync_other_labels_to_current(window);
}

static void
projection_changed(GtkToggleButton *check,
                   GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLSetup *setup = gwy_gl_view_get_setup(GWY_GL_VIEW(priv->view));
    GwyGLProjection projection = (gtk_toggle_button_get_active(check)
                                  ? GWY_GL_PROJECTION_ORTHOGRAPHIC
                                  : GWY_GL_PROJECTION_PERSPECTIVE);
    GwyGLProjection setup_projection;

    g_object_get(setup, "projection", &setup_projection, NULL);
    if (projection != setup_projection)
        g_object_set(setup, "projection", projection, NULL);
}

static void
hide_masked_changed(GtkToggleButton *check,
                    GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLSetup *setup = gwy_gl_view_get_setup(GWY_GL_VIEW(priv->view));
    gboolean hide_masked = gtk_toggle_button_get_active(check);
    gboolean setup_hide_masked;

    g_object_get(setup, "hide-masked", &setup_hide_masked, NULL);
    if (hide_masked != setup_hide_masked)
        g_object_set(setup, "hide-masked", hide_masked, NULL);
}

static void
show_axes_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    g_object_set(gwy_gl_view_get_setup(GWY_GL_VIEW(window->priv->view)),
                 "axes-visible", gtk_toggle_button_get_active(check),
                 NULL);
}

static void
show_labels_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    g_object_set(gwy_gl_view_get_setup(GWY_GL_VIEW(window->priv->view)),
                 "labels-visible", gtk_toggle_button_get_active(check),
                 NULL);
}

static void
fmscale_rspace_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    g_object_set(gwy_gl_view_get_setup(GWY_GL_VIEW(window->priv->view)),
                 "fmscale-reserve-space", gtk_toggle_button_get_active(check),
                 NULL);
}

static void
show_fmscale_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLSetup *setup = gwy_gl_view_get_setup(GWY_GL_VIEW(priv->view));
    gboolean active = gtk_toggle_button_get_active(check);

    g_object_set(setup, "fmscale-visible", active, NULL);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(priv->fmscale_yalign), active);
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(priv->fmscale_size), active);
    gtk_widget_set_sensitive(priv->fmscale_reserve_space, active);
}

static void
display_mode_changed(GtkWidget *item, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLSetup *setup = gwy_gl_view_get_setup(GWY_GL_VIEW(priv->view));

    if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(item)))
        return;

    GSList *list = gtk_radio_button_get_group(GTK_RADIO_BUTTON(item));
    GwyGLVisualization visualization = gwy_radio_buttons_get_current(list);
    GwyGLVisualization setup_visualization;

    g_object_get(setup, "visualization", &setup_visualization, NULL);
    if (visualization != setup_visualization)
        g_object_set(setup, "visualization", visualization, NULL);
}

static void
set_visualization(GwyGLWindow *window,
                  GwyGLVisualization visualization)
{
    GwyGLWindowPrivate *priv = window->priv;
    gboolean is_material = FALSE, is_gradient = FALSE, is_overlay = FALSE, light = FALSE;
    GtkAdjustment *adj;

    if (visualization == GWY_GL_VISUALIZATION_GRADIENT) {
        is_gradient = TRUE;
        light = FALSE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_LIGHTING) {
        is_material = TRUE;
        light = TRUE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_OVERLAY) {
        is_overlay  = TRUE;
        light = TRUE;
    }
    else if (visualization == GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT) {
        is_overlay  = TRUE;
        light = FALSE;
    }

    gtk_widget_set_sensitive(priv->material_menu, is_material);
    gtk_widget_set_sensitive(priv->material_label, is_material);
    gtk_widget_set_sensitive(priv->gradient_menu, is_gradient || is_overlay);
    adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(priv->lights_spin1));
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), light);
    adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(priv->lights_spin2));
    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), light);
    gtk_widget_set_sensitive(priv->buttons[GWY_GL_MOVEMENT_LIGHT], light);
    gtk_widget_set_sensitive(priv->buttons[N_BUTTONS + GWY_GL_MOVEMENT_LIGHT], light);
    gtk_widget_set_sensitive(priv->dataov_menu, is_overlay);

    if (!light && gwy_gl_view_get_movement_type(GWY_GL_VIEW(priv->view)) == GWY_GL_MOVEMENT_LIGHT)
        g_signal_emit_by_name(priv->buttons[GWY_GL_MOVEMENT_ROTATION], "clicked");
}

static void
visualization_changed(GwyGLSetup *setup,
                      G_GNUC_UNUSED GParamSpec *pspec,
                      GwyGLWindow *window)
{
    GwyGLVisualization visualization;
    g_object_get(setup, "visualization", &visualization, NULL);
    set_visualization(window, visualization);
}

static void
auto_scale_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    /* The check button is for the opposite of "fixed-size". */
    gboolean fixed_size = !gtk_toggle_button_get_active(check);
    GtkAdjustment *adj = gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(priv->labels_size));

    gwy_gtkgrid_hscale_set_sensitive(G_OBJECT(adj), fixed_size);

    gint id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->labels_menu));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);
    if (!gwy_gl_label_get_fixed_size(label) != !fixed_size)
        g_object_set(label, "fixed-size", fixed_size, NULL);

    /* Restore the size the (previously disabled) spin button is showing by explictily running the callback. */
    if (fixed_size)
        label_adj_changed(gtk_spin_button_get_adjustment(GTK_SPIN_BUTTON(priv->labels_size)), window);

    check = GTK_TOGGLE_BUTTON(priv->label_size_equal);
    if (gtk_toggle_button_get_active(check))
        sync_other_labels_to_current(window);
}

static void
label_size_eq_changed(GtkToggleButton *check, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    if (priv->in_update || !gtk_toggle_button_get_active(check))
        return;

    sync_other_labels_to_current(window);
}

static void
labels_entry_activate(GtkEntry *entry, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    gint id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->labels_menu));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);

    gwy_gl_label_set_text(label, gtk_entry_get_text(entry));
}

/* This resets the current one.  Should we add also a reset-all button? */
static void
labels_reset_clicked(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    gint id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->labels_menu));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);
    gwy_gl_label_reset(label);
    update_controls_for_label(window, label);
}

static void
reset_visualisation(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyDict *data = gwy_gl_view_get_data(view);
    const gchar *name, *key;

    /* This sequence ensures gradient changes to the *current* default, even if it unset presently. */
    key = gwy_gl_view_get_gradient_key(view);
    name = gwy_inventory_get_default_item_name(gwy_gradients());
    gwy_dict_set_string_by_name(data, key, name);
    gwy_dict_remove_by_name(data, key);

    key = gwy_gl_view_get_material_key(view);
    name = gwy_inventory_get_default_item_name(gwy_gl_materials());
    gwy_dict_set_string_by_name(data, key, name);
    gwy_dict_remove_by_name(data, key);
}

static gboolean
view_clicked(GwyGLWindow *window, GdkEventButton *event, GtkWidget *view)
{
    GwyGLSetup *setup = gwy_gl_view_get_setup(GWY_GL_VIEW(view));
    GtkWidget *menu, *item, *item2;

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

    GwyGLVisualization visualization;
    g_object_get(setup, "visualization", &visualization, NULL);

    if (visualization == GWY_GL_VISUALIZATION_GRADIENT) {
        menu = gwy_menu_gradient(G_CALLBACK(gradient_selected), window);
        item = gtk_menu_item_new_with_mnemonic(_("S_witch to Lighting Mode"));
        visualization = GWY_GL_VISUALIZATION_LIGHTING;
        g_object_set_data(G_OBJECT(item), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        item2 = gtk_menu_item_new_with_mnemonic(_("S_witch to Overlay Mode"));
        visualization = GWY_GL_VISUALIZATION_OVERLAY;
        g_object_set_data(G_OBJECT(item2), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item2, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item2);
    }
    else if (visualization == GWY_GL_VISUALIZATION_LIGHTING) {
        menu = gwy_menu_gl_material(G_CALLBACK(material_selected), window);
        item = gtk_menu_item_new_with_mnemonic(_("S_witch to Color Gradient Mode"));
        visualization = GWY_GL_VISUALIZATION_GRADIENT;
        g_object_set_data(G_OBJECT(item), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        item2 = gtk_menu_item_new_with_mnemonic(_("S_witch to Overlay Mode"));
        visualization = GWY_GL_VISUALIZATION_OVERLAY;
        g_object_set_data(G_OBJECT(item2), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item2, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item2);
    }
    else if (visualization == GWY_GL_VISUALIZATION_OVERLAY || visualization == GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT) {
        menu = gwy_menu_gradient(G_CALLBACK(gradient_selected), window);
        g_object_set(menu, "reserve-toggle-size", 0, NULL);
        item = gtk_menu_item_new_with_mnemonic(_("S_witch to Color Gradient Mode"));
        visualization = GWY_GL_VISUALIZATION_GRADIENT;
        g_object_set_data(G_OBJECT(item), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        item2 = gtk_menu_item_new_with_mnemonic(_("S_witch to Lighting Mode"));
        visualization = GWY_GL_VISUALIZATION_LIGHTING;
        g_object_set_data(G_OBJECT(item2), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item2, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item2);
        item2 = gtk_menu_item_new_with_mnemonic(_("T_oggle light"));
        visualization = (GWY_GL_VISUALIZATION_OVERLAY
                         ? GWY_GL_VISUALIZATION_OVERLAY_NO_LIGHT
                         : GWY_GL_VISUALIZATION_OVERLAY);
        // XXX: We were doing something like this (but by assigning directly to setup->visualization. Why?
        // g_object_set(setup, "visualization", visualization, NULL);
        g_object_set_data(G_OBJECT(item2), "display-mode", GINT_TO_POINTER(visualization));
        g_signal_connect(item2, "activate", G_CALLBACK(visual_selected), window);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item2);
    }
    else {
        g_return_val_if_reached(FALSE);
    }
    gtk_widget_show_all(menu);
    gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*)event);
    g_signal_connect(menu, "selection-done", G_CALLBACK(gtk_widget_destroy), NULL);
    return FALSE;
}

static void
visual_selected(GtkWidget *item, GwyGLWindow *window)
{
    GwyGLVisualization visual = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "display-mode"));
    gwy_radio_buttons_set_current(window->priv->visual_mode_group, visual);
}

static void
gradient_selected(GtkWidget *item, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    const gchar *name = g_object_get_data(G_OBJECT(item), "gradient-name");
    gwy_gradient_selection_set_active(priv->gradient_menu, name);
    /* FIXME: Double update if tree view is visible. Remove once selection buttons can emit signals. */
    gwy_dict_set_string_by_name(gwy_gl_view_get_data(view),
                                           gwy_gl_view_get_gradient_key(view),
                                           name);
}

static void
material_selected(GtkWidget *item, GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    const gchar *name = g_object_get_data(G_OBJECT(item), "gl-material-name");

    gwy_gl_material_selection_set_active(priv->material_menu, name);
    /* FIXME: Double update if tree view is visible. Remove once selection buttons can emit signals. */
    gwy_dict_set_string_by_name(gwy_gl_view_get_data(view),
                                           gwy_gl_view_get_material_key(view),
                                           name);
}

static void
set_zscale(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GwyDict *container = gwy_gl_view_get_data(view);
    const gchar *data_key = gwy_gl_view_get_data_key(view);
    GwyField *dfield = NULL;

    if (!container || !setup || !data_key || !gwy_dict_gis_object_by_name(container, data_key, &dfield))
        return;

    gdouble entryval = g_strtod(gtk_entry_get_text(GTK_ENTRY(priv->physcale_entry)), NULL);

    /* XXX: We need to carefully emulate the code from GL view here. */
    gdouble min, max, xreal, yreal, scale, zscale;
    gwy_field_min_max(dfield, &min, &max);
    xreal = gwy_field_get_xreal(dfield);
    yreal = gwy_field_get_yreal(dfield);
    scale = 2.0/MAX(xreal, yreal);
    zscale = scale*2*(max - min) * entryval;

    g_object_set(setup, "z-scale", zscale, NULL);
}

static void
update_physcale_entry(GwyGLWindow *window, GtkAdjustment *adj)
{
    GwyGLWindowPrivate *priv = window->priv;
    GwyGLView *view = GWY_GL_VIEW(priv->view);
    GwyGLSetup *setup = gwy_gl_view_get_setup(view);
    GwyDict *container = gwy_gl_view_get_data(view);
    const gchar *data_key = gwy_gl_view_get_data_key(view);
    GwyField *dfield = NULL;

    view = GWY_GL_VIEW(priv->view);
    container = gwy_gl_view_get_data(view);
    setup = gwy_gl_view_get_setup(view);
    data_key = gwy_gl_view_get_data_key(view);
    if (!container || !setup || !data_key || !gwy_dict_gis_object_by_name(container, data_key, &dfield))
        return;

    /* XXX: We need to carefully emulate the code from GL view here. */
    gdouble min, max, xreal, yreal, scale, physcale;
    gwy_field_min_max(dfield, &min, &max);
    xreal = gwy_field_get_xreal(dfield);
    yreal = gwy_field_get_yreal(dfield);
    scale = 2.0/MAX(xreal, yreal);
    physcale = (max <= min ? 0.0 : gtk_adjustment_get_value(adj)/(scale*2*(max - min)));

    gchar *buf = g_strdup_printf("%g", physcale);
    gtk_entry_set_text(GTK_ENTRY(priv->physcale_entry), buf);
    g_free(buf);
}

static void
sync_other_labels_to_current(GwyGLWindow *window)
{
    GwyGLWindowPrivate *priv = window->priv;
    gint id = gwy_enum_combo_box_get_active(GTK_COMBO_BOX(priv->labels_menu));
    GwyGLLabel *label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), id);
    gdouble size = gwy_gl_label_get_size(label);
    gboolean fixed_size = gwy_gl_label_get_fixed_size(label);

    for (gint i = 0; i < GWY_GL_VIEW_NLABELS; i++) {
        if (i == id)
            continue;

        label = gwy_gl_view_get_label(GWY_GL_VIEW(priv->view), i);
        if (gwy_gl_label_get_size(label) != size || gwy_gl_label_get_fixed_size(label) != fixed_size) {
            g_object_set(label,
                         "fixed-size", fixed_size,
                         "size", size,
                         NULL);
        }
    }
}

static void
read_gllabel_properties(GwyGLLabel *label,
                        gdouble *delta_x,
                        gdouble *delta_y,
                        gdouble *size,
                        gboolean *fixed_size)
{
    g_object_get(label,
                 "delta-x", &delta_x,
                 "delta-y", &delta_y,
                 "size", &size,
                 "fixed-size", &fixed_size,
                 NULL);
}

static void
update_controls_for_label(GwyGLWindow *window, GwyGLLabel *label)
{
    g_return_if_fail(label);

    GwyGLWindowPrivate *priv = window->priv;
    gdouble delta_x, delta_y, size;
    gboolean fixed_size;
    read_gllabel_properties(label, &delta_x, &delta_y, &size, &fixed_size);

    gtk_entry_set_text(GTK_ENTRY(priv->labels_text), gwy_gl_label_get_text(label));
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->labels_delta_x), delta_x);
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->labels_delta_y), delta_y);
    gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->labels_size), size);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->labels_autosize), !fixed_size);
}

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

/**
 * SECTION:gwyglwindow
 * @title: GwyGLWindow
 * @short_description: OpenGL data display window
 * @see_also: #GwyGLView -- the basic OpenGL 3D data display widget
 *
 * #GwyGLWindow encapsulates a #GwyGLView together with appropriate controls. You can create an OpenGL window for an
 * OpenGL view with gwy_gl_window_new(). It has an `action area' below the controls where additional widgets can be
 * packed with gwy_gl_window_add_action_widget().
 **/

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