/*
 *  $Id: app-image-window.c 30028 2026-06-09 13:45:30Z yeti-dn $
 *  Copyright (C) 2025-2026 David Necas (Yeti)
 *  E-mail: yeti@gwyddion.net
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyui/utils.h"

#include "libgwyapp/gwyapp.h"
#include "libgwyapp/internal.h"

struct _GwyAppImageWindowPrivate {
    GtkWidget *menubar;
    GtkWidget *infobar;
    GtkWidget *info_label;
    GwySensitivityGroup *sens_group;

    GtkWidget *meta_widget;
    gulong meta_destroyed_id;

    GtkWidget *log_widget;
    gulong log_destroyed_id;

    GwyFile *file;
    gulong item_changed_id;

    GObject *object;
    gulong object_changed_id;
    gulong xy_changed_id;

    GObject *mask;
    gulong mask_changed_id;

    GwyDataKind data_kind;
    gint id;
    const gchar *accel_root;

    GwyParams *params;
    gulong preview_sid;
};

static void         app_data_window_init          (GwyAppDataWindowInterface *iface);
static GwyFile*     app_data_window_get_file      (GwyAppDataWindow *window);
static GwyDataKind  app_data_window_get_data_kind (GwyAppDataWindow *window);
static gint         app_data_window_get_id        (GwyAppDataWindow *window);
static const gchar* app_data_window_get_accel_root(GwyAppDataWindow *window);
static void         finalize                      (GObject *object);
static void         dispose                       (GObject *object);
static gboolean     configured                    (GtkWidget *widget,
                                                   GdkEventConfigure *event);
static void         destroyed                     (GtkWidget *widget);
static gboolean     key_pressed                   (GtkWidget *widget,
                                                   GdkEventKey *event);
static void         size_allocated                (GtkWidget *widget,
                                                   GdkRectangle *allocation);
static void         save_size                     (GwyAppImageWindow *window,
                                                   const GdkRectangle *allocation);
static void         add_menubar                   (GwyAppImageWindow *window,
                                                   GtkBox *vbox);
static void         add_infobar                   (GwyAppImageWindow *window,
                                                   GtkBox *vbox);
static GtkWidget*   create_view_menu              (GwyAppImageWindow *window,
                                                   GtkAccelGroup *accel_group);
static void         change_mask_color             (GwyAppImageWindow *window);
static void         show_meta_browser             (GwyAppImageWindow *window);
static void         meta_destroyed                (GwyAppImageWindow *window);
static void         show_log_browser              (GwyAppImageWindow *window);
static void         log_destroyed                 (GwyAppImageWindow *window);
static void         reset_zoom                    (GwyDataWindow *window);
static void         zoom_in                       (GwyDataWindow *window);
static void         zoom_out                      (GwyDataWindow *window);
static void         realsquare_toggled            (GwyAppImageWindow *window,
                                                   GtkCheckMenuItem *item);
static void         item_changed                  (GwyDict *container,
                                                   GQuark key,
                                                   GwyAppImageWindow *window);
static void         update_color_mapping          (GwyAppImageWindow *window);
static void         choose_gradient               (GwyAppImageWindow *window);
static void         unset_gradient                (GwyAppImageWindow *window);
static void         dataview_gradient_changed     (GwyDataView *dataview,
                                                   const GParamSpec *pspec,
                                                   GwyAppImageWindow *window);
static void         object_changed                (GObject *object,
                                                   GwyAppImageWindow *window);
static void         mask_changed                  (GObject *object,
                                                   GwyAppImageWindow *window);
static gboolean     preview_gsource               (gpointer user_data);
static void         initial_setup                 (GwyAppImageWindow *window);
static void         update_infobar                (GwyAppImageWindow *window);
static void         update_window_title           (GwyAppImageWindow *window);
static void         update_xyz_preview            (GwyAppImageWindow *window);
static void         maybe_add_xyz_preview         (GwyAppImageWindow *window);
static void         restore_zoom                  (GwyAppImageWindow *window);
static gboolean     data_kind_has_pixels          (GwyDataKind data_kind);

static GtkWindowClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyAppImageWindow, gwy_app_image_window, GWY_TYPE_DATA_WINDOW,
                        G_ADD_PRIVATE(GwyAppImageWindow)
                        G_IMPLEMENT_INTERFACE(GWY_TYPE_APP_DATA_WINDOW, app_data_window_init))

static void
app_data_window_init(GwyAppDataWindowInterface *iface)
{
    iface->get_file       = app_data_window_get_file;
    iface->get_data_kind  = app_data_window_get_data_kind;
    iface->get_id         = app_data_window_get_id;
    iface->get_accel_root = app_data_window_get_accel_root;
}

static void
gwy_app_image_window_class_init(GwyAppImageWindowClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_app_image_window_parent_class;

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

    widget_class->configure_event = configured;
    widget_class->key_press_event = key_pressed;
    widget_class->size_allocate = size_allocated;
    widget_class->destroy = destroyed;
    /* TODO: Various button/key press events, popup-menu event (which we might not need if we have a menubar). */
}

static void
gwy_app_image_window_init(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv;

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

    priv->sens_group = gwy_sensitivity_group_new();

    GtkWindow *gtkwindow = GTK_WINDOW(window);
    gtk_window_add_accel_group(gtkwindow, gwy_app_main_accel_group());
    GtkAccelGroup *accel_group = gwy_data_browser_accel_group();
    if (accel_group)
        gtk_window_add_accel_group(gtkwindow, accel_group);
}

static void
dispose(GObject *object)
{
    GwyAppImageWindow *window = GWY_APP_IMAGE_WINDOW(object);
    GwyAppImageWindowPrivate *priv = window->priv;

    if (priv->data_kind == GWY_FILE_XYZ) {
        gwy_set_member_object(window, NULL, GWY_TYPE_FIELD, &priv->object,
                              "xy-changed", G_CALLBACK(object_changed), &priv->xy_changed_id, 0,
                              "data-changed", G_CALLBACK(object_changed), &priv->object_changed_id, 0,
                              NULL);
    }
    else {
        gwy_set_member_object(window, NULL, GWY_TYPE_FIELD, &priv->object,
                              "data-changed", G_CALLBACK(object_changed), &priv->object_changed_id, 0,
                              NULL);
        gwy_set_member_object(window, NULL, GWY_TYPE_NIELD, &priv->mask,
                              "data-changed", G_CALLBACK(mask_changed), &priv->mask_changed_id, 0,
                              NULL);
    }
    g_clear_signal_handler(&priv->item_changed_id, priv->file);

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

static void
finalize(GObject *object)
{
    GwyAppImageWindow *window = GWY_APP_IMAGE_WINDOW(object);
    GwyAppImageWindowPrivate *priv = window->priv;

    g_clear_object(&priv->params);
    g_clear_object(&priv->sens_group);

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

/**
 * gwy_app_image_window_new:
 * @file: (transfer none): A data file container.
 * @data_kind: Type of the data object.
 * @id: Data item id.
 *
 * Creates a new app image data window.
 *
 * The arguments must correspond to a valid data object in @file which can be visualised as an image. Possible data
 * kinds include @GWY_FILE_IMAGE, @GWY_FILE_VOLUME, @GWY_FILE_XYZ and @GWY_FILE_CMAP.
 *
 * Use #GwyAppDataWindow methods to query the parameters later.
 *
 * Returns: (transfer full) (constructor): A newly created window.
 **/
GtkWindow*
gwy_app_image_window_new(GwyFile *file,
                         GwyDataKind data_kind,
                         gint id)
{
    static const GwyEnum help_sections[] = {
        { "data-windows", GWY_FILE_IMAGE,  },
        { "volume-data",  GWY_FILE_VOLUME, },
        { "xyz-data",     GWY_FILE_XYZ,    },
        { "curve-maps",   GWY_FILE_CMAP,   },
    };
    static const GwyEnum menu_prefixes[] = {
        { "<image>",  GWY_FILE_IMAGE,  },
        { "<volume>", GWY_FILE_VOLUME, },
        { "<xyz>",    GWY_FILE_XYZ,    },
        { "<cmap>",   GWY_FILE_CMAP,   },
    };

    GwyAppImageWindow *window = g_object_new(GWY_TYPE_APP_IMAGE_WINDOW, NULL);
    GwyAppImageWindowPrivate *priv = window->priv;

    g_assert(GWY_IS_FILE(file));
    g_assert(data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME
             || data_kind == GWY_FILE_XYZ || data_kind == GWY_FILE_CMAP);
    g_assert(id >= 0);
    priv->file = file;
    priv->data_kind = data_kind;
    priv->id = id;
    priv->accel_root = gwy_enum_to_string(data_kind, menu_prefixes, G_N_ELEMENTS(menu_prefixes));
    g_assert(priv->accel_root);

    const gchar *helploc = gwy_enum_to_string(data_kind, help_sections, G_N_ELEMENTS(help_sections));
    if (helploc)
        gwy_help_add_to_window(GTK_WINDOW(window), helploc, NULL, GWY_HELP_DEFAULT);

    GtkBox *vbox = GTK_BOX(gtk_bin_get_child(GTK_BIN(window)));
    g_assert(GTK_IS_BOX(vbox));
    add_infobar(window, vbox);
    add_menubar(window, vbox);

    /* TODO: There are various popups and stuff. */

    initial_setup(window);
    priv->item_changed_id = g_signal_connect(file, "item-changed", G_CALLBACK(item_changed), window);

    GwyDataView *dataview = GWY_DATA_VIEW(gwy_data_window_get_data_view(GWY_DATA_WINDOW(window)));
    g_signal_connect(dataview, "notify::gradient", G_CALLBACK(dataview_gradient_changed), window);

    return (GtkWindow*)window;
}

static void
item_changed(GwyDict *container, GQuark key, GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyFileKeyParsed parsed;

    const gchar *strkey = g_quark_to_string(key);
    gwy_debug("<%s>", strkey);
    gboolean parsed_ok = gwy_parse_file_key(strkey, &parsed);
    g_return_if_fail(parsed_ok);

    GwyDataKind data_kind = parsed.data_kind;
    gint id = parsed.id;
    GwyFilePiece piece = parsed.piece;
    if (data_kind == GWY_FILE_NONE && piece == GWY_FILE_PIECE_FILENAME) {
        update_window_title(window);
        return;
    }

    if (data_kind != priv->data_kind || id != priv->id)
        return;

    GwyFile *file = GWY_FILE(container);
    g_assert(file == priv->file);
    GwyDataWindow *datawindow = GWY_DATA_WINDOW(window);
    GwyDataView *dataview = GWY_DATA_VIEW(gwy_data_window_get_data_view(datawindow));
    if (data_kind == GWY_FILE_IMAGE && piece == GWY_FILE_PIECE_MASK) {
        GwyMenuSensFlags sensflags = GWY_MENU_FLAG_IMAGE_MASK;
        GwyNield *mask = gwy_file_get_image_mask(file, id);
        gwy_set_member_object(window, mask, GWY_TYPE_NIELD, &priv->mask,
                              "data-changed", G_CALLBACK(mask_changed), &priv->mask_changed_id, 0,
                              NULL);
        gwy_data_view_set_mask(dataview, mask);
        gwy_sensitivity_group_set_state(priv->sens_group, sensflags, mask ? sensflags : 0);
    }
    else if (data_kind == GWY_FILE_IMAGE && piece == GWY_FILE_PIECE_MASK_COLOR) {
        GwyRGBA color;
        if (!gwy_dict_gis_boxed(container, GWY_TYPE_RGBA, key, &color))
            gwy_app_settings_get_default_mask_color(&color);
        gwy_data_view_set_mask_color(dataview, &color);
    }
    else if (data_kind == GWY_FILE_IMAGE && (piece == GWY_FILE_PIECE_NONE
                                             || (piece == GWY_FILE_PIECE_PICTURE && !parsed.suffix))) {
        GwyField *show = NULL, *field = gwy_file_get_image(file, id);
        gwy_dict_gis_object(container, gwy_file_key_image_picture(id), &show);
        if (show)
            gwy_data_view_set_field(dataview, show);
        else if (field)
            gwy_data_view_set_field(dataview, field);
        else
            gwy_data_view_set_field(dataview, NULL);
        gwy_sensitivity_group_set_state(priv->sens_group,
                                        GWY_MENU_FLAG_IMAGE | GWY_MENU_FLAG_IMAGE_SHOW,
                                        (field ? GWY_MENU_FLAG_IMAGE : 0) | (show ? GWY_MENU_FLAG_IMAGE_SHOW : 0));
        update_color_mapping(window);
    }
    else if (piece == GWY_FILE_PIECE_NONE) {
        GType type = 0;
        GwyMenuSensFlags sensflags = 0;
        if (data_kind == GWY_FILE_VOLUME) {
            type = GWY_TYPE_BRICK;
            sensflags = GWY_MENU_FLAG_VOLUME;
        }
        else if (data_kind == GWY_FILE_XYZ) {
            type = GWY_TYPE_SCATTER;
            sensflags = GWY_MENU_FLAG_XYZ;
            if (!priv->preview_sid) {
                priv->preview_sid = g_idle_add_full(G_PRIORITY_LOW, preview_gsource, window, NULL);
                gwy_debug("added preview gsource %lu after item-changed", priv->preview_sid);
            }
        }
        else if (data_kind == GWY_FILE_CMAP) {
            type = GWY_TYPE_LAWN;
            sensflags = GWY_MENU_FLAG_CMAP;
        }
        else {
            g_return_if_reached();
        }

        GObject *object = NULL;
        gwy_dict_gis_object(container, key, &object);
        gboolean objset = FALSE;
        if (data_kind == GWY_FILE_XYZ) {
            objset = gwy_set_member_object(window, object, type, &priv->object,
                                           "xy-changed", G_CALLBACK(object_changed), &priv->xy_changed_id, 0,
                                           "data-changed", G_CALLBACK(object_changed), &priv->object_changed_id, 0,
                                           NULL);
        }
        else {
            objset = gwy_set_member_object(window, object, type, &priv->object,
                                           "data-changed", G_CALLBACK(object_changed), &priv->object_changed_id, 0,
                                           NULL);
        }
        if (objset)
            update_infobar(window);

        gwy_sensitivity_group_set_state(priv->sens_group, sensflags, priv->object ? sensflags : 0);
    }
    else if (piece == GWY_FILE_PIECE_PICTURE) {
        if (!parsed.suffix) {
            GwyField *preview = NULL;
            gwy_dict_gis_object(container, key, &preview);
            gwy_data_view_set_field(dataview, preview);
        }
    }
    else if (piece == GWY_FILE_PIECE_PALETTE) {
        GwyColorAxis *coloraxis = GWY_COLOR_AXIS(gwy_data_window_get_color_axis(datawindow));
        const gchar *name = NULL;
        gwy_dict_gis_string(container, key, &name);
        GwyGradient *gradient = name ? gwy_inventory_get_item(gwy_gradients(), name) : NULL;
        /* XXX: This is somewhat inconsistent. When someone sets the name to a non-existent gradient we set NULL here
         * because it does not exist. But if it is created later we do not notice. This would not happen if widgets
         * like GwyColorAxis held the name and got the object from the inventory each time they were redrawing
         * themselves. But on the other hand the widget still needs to watch the gradient object because otherwise it
         * does not know if its data change. Anyway, this is an edge case. Worry about it later. */
        gwy_data_view_set_gradient(dataview, gradient);
        gwy_color_axis_set_gradient(coloraxis, gradient);
    }
    else if (piece == GWY_FILE_PIECE_COLOR_MAPPING)
        update_color_mapping(window);
    else if (piece == GWY_FILE_PIECE_RANGE) {
        gdouble min, max;
        if (gwy_file_get_fixed_range(file, data_kind, id, &min, &max))
            gwy_data_view_set_fixed_color_range(dataview, min, max);
        else
            gwy_data_view_unset_fixed_color_range(dataview);
    }
    else if (piece == GWY_FILE_PIECE_REAL_SQUARE) {
        gboolean realsquare = FALSE;
        gwy_dict_gis_boolean(container, key, &realsquare);
        gwy_data_view_set_real_square(dataview, realsquare);
    }
    else if (piece == GWY_FILE_PIECE_TITLE) {
        update_window_title(window);
    }
    /* TODO: There are lots of other things: selections, … */

    /* TODO: If we get file piece NONE, update infobar. We should also connect to the main data object and update
     * infobar when it changes. */
}

static void
adaptive_color_axis_map_func(G_GNUC_UNUSED GwyColorAxis *axis,
                             const gdouble *z,
                             gdouble *mapped,
                             guint n,
                             gpointer user_data)
{
    GwyField *field = gwy_data_view_get_field(GWY_DATA_VIEW(user_data));
    if (!field) {
        gwy_clear(mapped, n);
        return;
    }

    gwy_map_field_adaptive(field, z, mapped, n);
}

static void
update_color_mapping(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataWindow *datawindow = GWY_DATA_WINDOW(window);
    GwyDataView *dataview = GWY_DATA_VIEW(gwy_data_window_get_data_view(datawindow));
    GwyColorAxis *coloraxis = GWY_COLOR_AXIS(gwy_data_window_get_color_axis(datawindow));
    GwyDict *container = GWY_DICT(priv->file);

    GwyColorMappingType color_mapping = GWY_COLOR_MAPPING_FULL;
    GwyColorAxisMapFunc map_func = NULL;
    gpointer map_func_data = NULL;
    GwyTicksStyle ticks_style = GWY_TICKS_STYLE_AUTO;
    gboolean show_labels = TRUE;

    if (gwy_dict_contains(container, gwy_file_key_image_picture(priv->id))) {
        ticks_style = GWY_TICKS_STYLE_CENTER;
        show_labels = FALSE;
    }
    else {
        color_mapping = gwy_file_get_color_mapping(priv->file, priv->data_kind, priv->id);
        if (color_mapping == GWY_COLOR_MAPPING_ADAPT) {
            ticks_style = GWY_TICKS_STYLE_UNLABELLED;
            map_func = adaptive_color_axis_map_func;
            map_func_data = dataview;
        }
    }

    gwy_data_view_set_color_mapping(dataview, color_mapping);
    gwy_color_axis_set_ticks_style(coloraxis, ticks_style);
    gwy_color_axis_set_labels_visible(coloraxis, show_labels);
    gwy_color_axis_set_tick_map_func(coloraxis, map_func, map_func_data);
}

static void
dataview_gradient_changed(GwyDataView *dataview,
                          G_GNUC_UNUSED const GParamSpec *pspec,
                          GwyAppImageWindow *window)
{
    GwyGradient *gradient = gwy_data_view_get_gradient(dataview);
    GwyAppImageWindowPrivate *priv = window->priv;
    gwy_file_set_palette(priv->file, priv->data_kind, priv->id,
                         gradient ? gwy_resource_get_name(GWY_RESOURCE(gradient)) : NULL);
}

static void
object_changed(G_GNUC_UNUSED GObject *object,
               GwyAppImageWindow *window)
{
    update_infobar(window);

    GwyAppImageWindowPrivate *priv = window->priv;
    if (priv->data_kind == GWY_FILE_XYZ && !priv->preview_sid) {
        priv->preview_sid = g_idle_add_full(G_PRIORITY_LOW, preview_gsource, window, NULL);
        gwy_debug("added preview gsource %lu after invalidation", priv->preview_sid);
    }
}

static void
mask_changed(G_GNUC_UNUSED GObject *object,
             GwyAppImageWindow *window)
{
    update_infobar(window);
}

static gboolean
preview_gsource(gpointer user_data)
{
    GwyAppImageWindow *window = (GwyAppImageWindow*)user_data;

    /* Do we it before or after? */
    gwy_debug("clearing preview gsource %lu and running the preview", window->priv->preview_sid);
    window->priv->preview_sid = 0;
    update_xyz_preview(window);

    return G_SOURCE_REMOVE;
}

static void
initial_setup(GwyAppImageWindow *window)
{
    static const GwyFilePiece common_pieces[] = {
        GWY_FILE_PIECE_PICTURE,
        GWY_FILE_PIECE_PALETTE,
        GWY_FILE_PIECE_COLOR_MAPPING,
    };

    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataKind data_kind = priv->data_kind;
    gint id = priv->id;
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_NONE, .suffix = NULL };
    GwyDict *container = GWY_DICT(priv->file);

    item_changed(container, gwy_form_file_key(&parsed), window);
    for (guint i = 0; i < G_N_ELEMENTS(common_pieces); i++) {
        parsed.piece = common_pieces[i];
        if (data_kind == GWY_FILE_XYZ && parsed.piece == GWY_FILE_PIECE_PICTURE)
            maybe_add_xyz_preview(window);
        item_changed(container, gwy_form_file_key(&parsed), window);
    }
    if (data_kind_has_pixels(data_kind)) {
        parsed.piece = GWY_FILE_PIECE_REAL_SQUARE;
        item_changed(container, gwy_form_file_key(&parsed), window);
    }
    parsed.piece = GWY_FILE_PIECE_RANGE;
    parsed.suffix = "min";
    item_changed(container, gwy_form_file_key(&parsed), window);
    parsed.suffix = "max";
    item_changed(container, gwy_form_file_key(&parsed), window);

    if (data_kind == GWY_FILE_IMAGE) {
        item_changed(container, gwy_file_key_image_mask(id), window);
        item_changed(container, gwy_file_key_image_mask_color(id), window);
    }
    update_window_title(window);
    /* TODO: There are lots of other things: selections, … */

    restore_zoom(window);
}

static void
add_menubar(GwyAppImageWindow *window, GtkBox *vbox)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataKind data_kind = priv->data_kind;
    GtkWidget *funcmenu = NULL, *item;
    const gchar *label = NULL;
    if (data_kind == GWY_FILE_IMAGE) {
        funcmenu = gwy_app_image_menu(priv->sens_group);
        label = _("_Image");
    }
    else if (data_kind == GWY_FILE_VOLUME) {
        funcmenu = gwy_app_volume_menu(priv->sens_group);
        label = _("_Volume");
    }
    else if (data_kind == GWY_FILE_XYZ) {
        funcmenu = gwy_app_xyz_menu(priv->sens_group);
        label = _("_XYZ");
    }
    else if (data_kind == GWY_FILE_CMAP) {
        funcmenu = gwy_app_cmap_menu(priv->sens_group);
        label = _("_Curve Map");
    }

    GtkAccelGroup *accel_group = NULL;
    if (funcmenu) {
        accel_group = gtk_menu_get_accel_group(GTK_MENU(funcmenu));
        if (accel_group)
            gtk_window_add_accel_group(GTK_WINDOW(window), accel_group);
    }

    priv->menubar = gtk_menu_bar_new();
    gtk_box_pack_start(vbox, priv->menubar, FALSE, FALSE, 0);
    gtk_box_reorder_child(vbox, priv->menubar, 0);

    if (funcmenu) {
        item = gtk_menu_item_new_with_mnemonic(label);
        gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), funcmenu);
        gtk_menu_shell_append(GTK_MENU_SHELL(priv->menubar), item);
    }

    if (funcmenu) {
        GtkWidget *recentmenu = gwy_app_create_recent_func_menu(GWY_APP_DATA_WINDOW(window),
                                                                accel_group, priv->sens_group);
        item = gtk_menu_item_new_with_mnemonic(("_Recent"));
        gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), recentmenu);
        gtk_menu_shell_append(GTK_MENU_SHELL(priv->menubar), item);
    }

    item = gtk_menu_item_new_with_mnemonic(("Vie_w"));
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), create_view_menu(window, accel_group));
    gtk_menu_shell_append(GTK_MENU_SHELL(priv->menubar), item);
}

static GtkWidget*
append_menu_item(GwyAppImageWindow *window, GtkMenuShell *menushell,
                 const gchar *label, const gchar *icon_name, gboolean is_check,
                 const gchar *accel_path_tail,
                 GCallback callback)
{
    g_assert(!icon_name || !is_check);
    GwyAppImageWindowPrivate *priv = window->priv;
    GtkWidget *item;
    if (icon_name)
        item = gwy_create_image_menu_item(label, icon_name, FALSE);
    else if (is_check)
        item = gtk_check_menu_item_new_with_mnemonic(label);
    else
        item = gtk_menu_item_new_with_mnemonic(label);

    if (accel_path_tail) {
        gchar *path = g_strconcat(priv->accel_root, accel_path_tail, NULL);
        gtk_menu_item_set_accel_path(GTK_MENU_ITEM(item), path);
        g_free(path);
    }
    gtk_menu_shell_append(menushell, item);
    if (callback)
        g_signal_connect_swapped(item, "activate", callback, window);

    return item;
}

/* FIXME: This should probably be a small toolbar, which could include the ‘Change Preview’ functions for volume
 * and curve map data. */
static GtkWidget*
create_view_menu(GwyAppImageWindow *window, GtkAccelGroup *accel_group)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataKind data_kind = priv->data_kind;
    gint id = priv->id;

    GtkWidget *menu = gtk_menu_new();
    GtkMenuShell *shell = GTK_MENU_SHELL(menu);
    if (accel_group)
        gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);

    GtkWidget *item;
    if (data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP)
        append_menu_item(window, shell, _("Zoom _1:1 (Z)"), GWY_ICON_ZOOM_1_1, FALSE, NULL, G_CALLBACK(reset_zoom));
    append_menu_item(window, shell, _("Zoom _In (+)"), GWY_ICON_ZOOM_IN, FALSE, NULL, G_CALLBACK(zoom_in));
    append_menu_item(window, shell, _("Zoom _Out (-)"), GWY_ICON_ZOOM_OUT, FALSE, NULL, G_CALLBACK(zoom_out));

    if (data_kind_has_pixels(data_kind)) {
        item = append_menu_item(window, shell, _("_Physical Aspect Ratio"), NULL, TRUE,
                                "/View/Physical Aspect Ratio", NULL);
        GwyFileKeyParsed parsed = {
            .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_REAL_SQUARE, .suffix = NULL
        };
        gboolean realsquare = FALSE;
        if (gwy_dict_gis_boolean(GWY_DICT(priv->file), gwy_form_file_key(&parsed), &realsquare) && realsquare)
            gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
        g_signal_connect_swapped(item, "activate", G_CALLBACK(realsquare_toggled), window);
    }

    append_menu_item(window, shell, _("Color _Gradient..."), GWY_ICON_PALETTES, FALSE,
                     "/View/Color Gradient", G_CALLBACK(choose_gradient));
    append_menu_item(window, shell, _("_Unset Color Gradient"), NULL, FALSE,
                     "/View/Unset Color Gradient", G_CALLBACK(unset_gradient));

    if (data_kind == GWY_FILE_IMAGE) {
        item = append_menu_item(window, shell, _("Mask _Color..."), GWY_ICON_MASK, FALSE,
                                "/View/Mask Color", G_CALLBACK(change_mask_color));
        gwy_sensitivity_group_add_widget(priv->sens_group, item, GWY_MENU_FLAG_IMAGE_MASK);
    }

    if (data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP || data_kind == GWY_FILE_XYZ) {
        item = append_menu_item(window, shell, _("_Change Preview"), NULL, FALSE, "/View/Change Preview", NULL);
        if (data_kind == GWY_FILE_VOLUME)
            g_signal_connect_swapped(item, "activate", G_CALLBACK(_gwy_app_change_volume_preview), window);
        else if (data_kind == GWY_FILE_CMAP)
            g_signal_connect_swapped(item, "activate", G_CALLBACK(_gwy_app_change_cmap_preview), window);
        else
            g_signal_connect_swapped(item, "activate", G_CALLBACK(_gwy_app_change_xyz_preview), window);
    }

    if (data_kind == GWY_FILE_XYZ) {
        append_menu_item(window, shell, _("_Update Preview"), NULL, FALSE,
                         "/View/Update Preview", G_CALLBACK(update_xyz_preview));
    }

    gtk_menu_shell_append(shell, gtk_separator_menu_item_new());

    item = append_menu_item(window, shell, _("_Metadata..."), NULL, FALSE,
                            "/View/Metadata", G_CALLBACK(show_meta_browser));
    const gchar *path = gtk_menu_item_get_accel_path(GTK_MENU_ITEM(item));
    gtk_accel_map_add_entry(path, GDK_KEY_B, GDK_CONTROL_MASK | GDK_SHIFT_MASK);
    gtk_accel_map_lock_path(path);

    append_menu_item(window, shell, _("_Log..."), NULL, FALSE, "/View/Log", G_CALLBACK(show_log_browser));

    return menu;
}

static void
zoom_in(GwyDataWindow *window)
{
    gwy_data_window_set_zoom(window, 1);
}

static void
zoom_out(GwyDataWindow *window)
{
    gwy_data_window_set_zoom(window, -1);
}

static void
reset_zoom(GwyDataWindow *window)
{
    gwy_data_window_set_zoom(window, 10000);
}

static void
realsquare_toggled(GwyAppImageWindow *window, GtkCheckMenuItem *item)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataKind data_kind = priv->data_kind;
    gint id = priv->id;
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_REAL_SQUARE, .suffix = NULL };
    GQuark key = gwy_form_file_key(&parsed);
    gboolean active = gtk_check_menu_item_get_active(item);
    if (active)
        gwy_dict_set_boolean(GWY_DICT(priv->file), key, TRUE);
    else
        gwy_dict_remove(GWY_DICT(priv->file), key);
}

static void
change_mask_color(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDict *data = GWY_DICT(priv->file);

    GwyRGBA rgba;
    GQuark quark = gwy_file_key_image_mask_color(priv->id);
    if (!gwy_dict_gis_boxed(data, GWY_TYPE_RGBA, quark, &rgba)) {
        gwy_app_settings_get_default_mask_color(&rgba);
        gwy_dict_set_boxed(data, GWY_TYPE_RGBA, quark, &rgba);
    }
    gwy_mask_color_selector_run(NULL, NULL, NULL, GWY_DICT(data), quark);
}

static void
choose_gradient(GwyAppImageWindow *window)
{
    gwy_data_window_run_gradient_chooser(GWY_DATA_WINDOW(window));
}

static void
unset_gradient(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyFileKeyParsed parsed = {
        .data_kind = priv->data_kind, .id = priv->id, .piece = GWY_FILE_PIECE_PALETTE, .suffix = NULL
    };

    gwy_dict_remove(GWY_DICT(priv->file), gwy_form_file_key(&parsed));
}

static void
show_meta_browser(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    if (!priv->meta_widget) {
        priv->meta_widget = gwy_meta_browser_new(priv->file, priv->data_kind, priv->id);
        priv->meta_destroyed_id = g_signal_connect_swapped(priv->meta_widget, "destroy",
                                                           G_CALLBACK(meta_destroyed), window);
    }
    gtk_window_present(GTK_WINDOW(priv->meta_widget));
}

static void
meta_destroyed(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    g_clear_signal_handler(&priv->meta_destroyed_id, priv->meta_widget);
    priv->meta_widget = NULL;
}

static void
show_log_browser(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    if (!priv->log_widget) {
        priv->log_widget = gwy_log_browser_new(priv->file, priv->data_kind, priv->id);
        priv->log_destroyed_id = g_signal_connect_swapped(priv->log_widget, "destroy",
                                                          G_CALLBACK(log_destroyed), window);
    }
    gtk_window_present(GTK_WINDOW(priv->log_widget));
}

static void
log_destroyed(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    g_clear_signal_handler(&priv->log_destroyed_id, priv->log_widget);
    priv->log_widget = NULL;
}

static void
add_infobar(GwyAppImageWindow *window, GtkBox *vbox)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyDataKind data_kind = priv->data_kind;

    if (data_kind == GWY_FILE_IMAGE)
        return;

    priv->infobar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

    priv->info_label = gtk_label_new(NULL);
    GtkLabel *label = GTK_LABEL(priv->info_label);
    gtk_label_set_xalign(label, 0.0);
    gtk_label_set_line_wrap(label, FALSE);
    gtk_label_set_ellipsize(label, PANGO_ELLIPSIZE_END);
    GtkWidget *shrinkwrap = gwy_shrink_wrap_new(GTK_SIZE_GROUP_HORIZONTAL);
    gtk_container_add(GTK_CONTAINER(shrinkwrap), priv->info_label);
    gtk_box_pack_start(GTK_BOX(priv->infobar), shrinkwrap, TRUE, TRUE, 0);

    gtk_box_pack_start(vbox, priv->infobar, FALSE, FALSE, 0);
    gtk_box_reorder_child(vbox, priv->infobar, 0);
}

static void
update_infobar(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    if (!priv->infobar || !priv->info_label)
        return;
    GwyDataKind data_kind = priv->data_kind;
    GwyFile *file = priv->file;

    if (!file || !priv->object) {
        gtk_label_set_text(GTK_LABEL(priv->info_label), NULL);
        return;
    }

    gchar *text = NULL;
    if (data_kind == GWY_FILE_VOLUME) {
        GwyBrick *brick = GWY_BRICK(priv->object);
        if (brick) {
            gchar *unit = gwy_unit_get_string(gwy_brick_get_unit_z(brick), GWY_UNIT_FORMAT_MARKUP);
            text = g_strdup_printf(_("Z levels: %d, Z unit: %s"), gwy_brick_get_zres(brick), *unit ? unit : "1");
            g_free(unit);
        }
    }
    else if (data_kind == GWY_FILE_XYZ) {
        GwyScatter *scatter = GWY_SCATTER(priv->object);
        if (scatter) {
            GString *str = g_string_new(NULL);
            g_string_printf(str, _("Points: %lu"), (gulong)gwy_scatter_get_n_points(scatter));
            gint n;
            if ((n = gwy_scatter_get_n_values(scatter))) {
                g_string_append(str, "   ");
                g_string_append(str, _("Data:"));
                for (gint i = 0; i < n; i++) {
                    if (i)
                        g_string_append_c(str, ',');
                    g_string_append_c(str, ' ');
                    const gchar *name = gwy_scatter_get_data_label(scatter, i);
                    g_string_append(str, name ? name : _("Untitled"));
                }
            }
            text = g_string_free(str, FALSE);
        }
    }
    else if (data_kind == GWY_FILE_CMAP) {
        GwyLawn *lawn = GWY_LAWN(priv->object);
        if (lawn) {
            GString *str = g_string_new(_("Curves:"));
            gint n = gwy_lawn_get_n_curves(lawn);
            for (gint i = 0; i < n; i++) {
                if (i)
                    g_string_append_c(str, ',');
                g_string_append_c(str, ' ');
                const gchar *name = gwy_lawn_get_curve_label(lawn, i);
                g_string_append(str, name ? name : _("Untitled"));
            }

            if ((n = gwy_lawn_get_n_segments(lawn))) {
                g_string_append(str, "   ");
                g_string_append(str, _("Segments:"));
                for (gint i = 0; i < n; i++) {
                    if (i)
                        g_string_append_c(str, ',');
                    g_string_append_c(str, ' ');
                    const gchar *name = gwy_lawn_get_segment_label(lawn, i);
                    g_string_append(str, name ? name : _("Untitled"));
                }
            }
            text = g_string_free(str, FALSE);
        }
    }
    gtk_label_set_text(GTK_LABEL(priv->info_label), text);
    g_free(text);
}

static void
update_window_title(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    gchar *basename, *dataname = gwy_file_get_display_title(priv->file, priv->data_kind, priv->id);
    const gchar *filename;
    if (gwy_dict_gis_string(GWY_DICT(priv->file), gwy_file_key_filename(), &filename))
        basename = g_path_get_basename(filename);
    else
        basename = g_strdup(_("Untitled"));

    gchar *title = g_strdup_printf("%s [%s]", basename, dataname);
    g_free(dataname);
    g_free(basename);
    gwy_data_window_set_data_name(GWY_DATA_WINDOW(window), title);
    g_free(title);
}

static void
maybe_add_xyz_preview(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyScatter *scatter = GWY_SCATTER(priv->object);
    GwyDict *container = GWY_DICT(priv->file);
    GQuark key = gwy_file_key_xyz_picture(priv->id);
    if (gwy_dict_contains(container, key)) {
        /* The caller does not do this when calling us. */
        item_changed(container, key, window);
        return;
    }

    GwyField *raster = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    gint res = GWY_ROUND(sqrt(gwy_scatter_get_n_points(scatter)));
    res = CLAMP(res, 2, 1024);
    gint ivalue = gwy_scatter_get_n_values(scatter) ? 0 : -1;
    gwy_preview_scatter_to_field(scatter, ivalue, raster, res, res, GWY_PREVIEW_SCATTER_FILL);
    gwy_dict_pass_object(container, key, raster);
}

static void
update_xyz_preview(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GtkWidget *dataview = gwy_data_window_get_data_view(GWY_DATA_WINDOW(window));
    if (!priv->object) {
        /* We might be being destroyed anyway here? */
        gwy_data_view_set_field(GWY_DATA_VIEW(dataview), NULL);
        return;
    }

    g_return_if_fail(GWY_IS_SCATTER(priv->object));
    GwyDict *dict = GWY_DICT(priv->file);
    GwyScatter *scatter = GWY_SCATTER(priv->object);
    GQuark key = gwy_file_key_xyz_picture(priv->id);
    GwyField *raster = NULL;
    if (!gwy_dict_gis_object(dict, key, &raster)) {
        raster = gwy_field_new(1, 1, 1.0, 1.0, TRUE);
        gwy_dict_pass_object(dict, key, raster);
    }

    GwyPreviewScatterFlags flags = GWY_PREVIEW_SCATTER_FILL;
    guint preview_type = 0; // XYZPreviewType in change-preview-xyz.c
    gint ivalue = 0;
    gboolean outliers = FALSE;
    _gwy_preview_settings_from_file(priv->file, priv->data_kind, priv->id, &preview_type, &ivalue, &outliers);
    if (ivalue >= gwy_scatter_get_n_values(scatter))
        ivalue = -1;
    if (preview_type)
        flags |= GWY_PREVIEW_SCATTER_DENSITY;
    if (outliers)
        flags |= GWY_PREVIEW_SCATTER_OUTLIERS;

    GdkRectangle allocation;
    gtk_widget_get_allocation(dataview, &allocation);
    gwy_preview_scatter_to_field(scatter, ivalue, raster, allocation.width, allocation.height, flags);
    gwy_data_view_set_zoom(GWY_DATA_VIEW(dataview), 1.0);
    gwy_field_data_changed(raster);
}

static void
destroyed(GtkWidget *widget)
{
    GwyAppImageWindow *window = GWY_APP_IMAGE_WINDOW(widget);
    GwyAppImageWindowPrivate *priv = window->priv;

    if (priv->preview_sid) {
        gwy_debug("removing preview gsource %lu because we are being destroyed", priv->preview_sid);
        g_source_remove(priv->preview_sid);
        priv->preview_sid = 0;
    }

    if (priv->meta_widget)
        gtk_widget_destroy(priv->meta_widget);
    if (priv->log_widget)
        gtk_widget_destroy(priv->log_widget);

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

/* FIXME: This should probably logically go somewhere else. */
static gboolean
key_pressed(GtkWidget *widget,
            GdkEventKey *event)
{
    if (event->keyval != GDK_KEY_F3 || (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK))) {
        return GTK_WIDGET_CLASS(parent_class)->key_press_event(widget, event);
    }

    GwyTool *current_tool = gwy_app_current_tool();
    if (current_tool) {
        if (!gwy_tool_is_visible(current_tool))
            gwy_tool_show(current_tool);
        else
            gwy_tool_hide(current_tool);
    }
    return TRUE;
}

static gboolean
configured(GtkWidget *widget, GdkEventConfigure *event)
{
    GdkRectangle allocation = { .x = event->x, .y = event->y, .width = event->width, .height = event->height };
    save_size(GWY_APP_IMAGE_WINDOW(widget), &allocation);
    return GTK_WIDGET_CLASS(parent_class)->configure_event(widget, event);
}

static void
size_allocated(GtkWidget *widget, GdkRectangle *allocation)
{
    save_size(GWY_APP_IMAGE_WINDOW(widget), allocation);

    GTK_WIDGET_CLASS(parent_class)->size_allocate(widget, allocation);
}

static void
save_size(GwyAppImageWindow *window, const GdkRectangle *allocation)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    /* Someone created us with plain g_object_new(). */
    g_return_if_fail(priv->file);
    GtkWidget *dataview = gwy_data_window_get_data_view(GWY_DATA_WINDOW(window));
    gboolean have_pixels = data_kind_has_pixels(priv->data_kind);
    if (have_pixels) {
        gdouble zoom = gwy_data_view_get_real_zoom(GWY_DATA_VIEW(dataview));
        GwyFileKeyParsed parsed = {
            .id = priv->id, .data_kind = priv->data_kind, .piece = GWY_FILE_PIECE_VIEW, .suffix = "scale"
        };
        gwy_dict_set_double(GWY_DICT(priv->file), gwy_form_file_key(&parsed), zoom);
    }
    _gwy_save_widget_screen_size(dataview, allocation, priv->file, priv->data_kind, priv->id, !have_pixels);
}

/* FIXME: Where should it go? */
void
_gwy_save_widget_screen_size(GtkWidget *widget, const GdkRectangle *allocation,
                             GwyFile *file, GwyDataKind data_kind, gint id,
                             gboolean absolute_too)
{
    gint scw, sch;
    gwy_get_screen_size(widget, &scw, &sch);
    gint w, h;
    if (allocation) {
        w = allocation->width;
        h = allocation->height;
    }
    else {
        GdkRectangle alloc;
        gtk_widget_get_allocation(widget, &alloc);
        w = alloc.width;
        h = alloc.height;
    }
    gdouble relsize = MAX(w/(gdouble)scw, h/(gdouble)sch);
    GwyDict *container = GWY_DICT(file);
    GwyFileKeyParsed parsed = { .id = id, .data_kind = data_kind, .piece = GWY_FILE_PIECE_VIEW };

    parsed.suffix = "relative-size";
    gwy_dict_set_double(container, gwy_form_file_key(&parsed), relsize);
    if (absolute_too) {
        parsed.suffix = "width";
        gwy_dict_set_int32(container, gwy_form_file_key(&parsed), w);
        parsed.suffix = "height";
        gwy_dict_set_int32(container, gwy_form_file_key(&parsed), h);
    }
}

static void
restore_zoom(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    /* Someone created us with plain g_object_new(). */
    g_return_if_fail(priv->file);
    GwyDict *container = GWY_DICT(priv->file);

    GwyFileKeyParsed parsed = { .id = priv->id, .data_kind = priv->data_kind, .piece = GWY_FILE_PIECE_VIEW };
    gdouble scale, relsize;

    parsed.suffix = "relative-size";
    if (!gwy_dict_gis_double(container, gwy_form_file_key(&parsed), &relsize))
        return;
    parsed.suffix = "scale";
    if (!gwy_dict_gis_double(container, gwy_form_file_key(&parsed), &scale))
        return;
    if (scale <= 0.0 || relsize <= 0.0)
        return;

    GtkRequisition req;
    GtkWidget *dataview = gwy_data_window_get_data_view(GWY_DATA_WINDOW(window));
    gtk_widget_get_preferred_size(dataview, NULL, &req);
    gint scw, sch;
    gwy_get_screen_size(GTK_WIDGET(window), &scw, &sch);
    gdouble newrelsize = MAX(scale*req.width/scw, scale*req.height/sch);
    gwy_debug("restoring data window: relsize %g, zoom %g, request %dx%d, newrelsize %g",
              relsize, scale, req.width, req.height, newrelsize);

    /* If the data view will be small we can just apply the saved zoom.  Should it be larger though, we must check if
     * it is not too large and better show it at defaut size than huge. */
    if (newrelsize > 1.2*relsize || newrelsize > 0.85) {
        gwy_debug("window too large for this screen; do not trying to restore the size");
        return;
    }
    gwy_data_view_set_zoom(GWY_DATA_VIEW(dataview), scale);
}

GwyParams*
_gwy_app_image_window_get_params(GwyAppImageWindow *window)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    if (!priv->params)
        priv->params = gwy_params_new();
    return priv->params;
}

// NB: The values may [no longer] be valid for the particular data object! This function does not know.
void
_gwy_preview_settings_from_file(GwyFile *file, GwyDataKind data_kind, gint id,
                                guint *type, gint *idx, gboolean *outliers)
{
    GwyDict *dict = GWY_DICT(file);
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = GWY_FILE_PIECE_PICTURE };

    if (type) {
        parsed.suffix = "type";
        gwy_dict_gis_enum(dict, gwy_form_file_key(&parsed), type);
    }
    if (idx) {
        parsed.suffix = "index";
        gwy_dict_gis_int32(dict, gwy_form_file_key(&parsed), idx);
    }
    if (outliers) {
        parsed.suffix = "skip-outliers";
        gwy_dict_gis_boolean(dict, gwy_form_file_key(&parsed), outliers);
    }
}

void
_gwy_load_preview_params_from_file(GwyAppImageWindow *window,
                                   gint type_id, gint index_id, gint outliers_id)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyParams *params = priv->params;
    GwyDict *dict = GWY_DICT(priv->file);
    GwyFileKeyParsed parsed = { .data_kind = priv->data_kind, .id = priv->id, .piece = GWY_FILE_PIECE_PICTURE };

    if (type_id >= 0) {
        parsed.suffix = "type";
        guint type;
        if (gwy_dict_gis_enum(dict, gwy_form_file_key(&parsed), &type))
            gwy_params_set_enum(params, type_id, type);
    }
    if (index_id >= 0) {
        parsed.suffix = "index";
        gint idx;
        if (gwy_dict_gis_int32(dict, gwy_form_file_key(&parsed), &idx))
            gwy_params_set_int(params, index_id, idx);
    }
    if (outliers_id >= 0) {
        parsed.suffix = "skip-outliers";
        gboolean outliers;
        if (gwy_dict_gis_boolean(dict, gwy_form_file_key(&parsed), &outliers))
            gwy_params_set_boolean(params, outliers_id, outliers);
    }
}

void
_gwy_save_preview_params_to_file(GwyAppImageWindow *window,
                                 gint type_id, gint index_id, gint outliers_id)
{
    GwyAppImageWindowPrivate *priv = window->priv;
    GwyParams *params = priv->params;
    GwyDict *dict = GWY_DICT(priv->file);
    GwyFileKeyParsed parsed = { .data_kind = priv->data_kind, .id = priv->id, .piece = GWY_FILE_PIECE_PICTURE };

    if (type_id >= 0) {
        parsed.suffix = "type";
        gwy_dict_set_enum(dict, gwy_form_file_key(&parsed), gwy_params_get_enum(params, type_id));
    }
    if (index_id >= 0) {
        parsed.suffix = "index";
        gwy_dict_set_int32(dict, gwy_form_file_key(&parsed), gwy_params_get_int(params, index_id));
    }
    if (outliers_id >= 0) {
        parsed.suffix = "skip-outliers";
        gwy_dict_set_boolean(dict, gwy_form_file_key(&parsed), gwy_params_get_boolean(params, outliers_id));
    }
}

static gboolean
data_kind_has_pixels(GwyDataKind data_kind)
{
    return data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP;
}

static GwyFile*
app_data_window_get_file(GwyAppDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_APP_IMAGE_WINDOW(window), NULL);
    return GWY_APP_IMAGE_WINDOW(window)->priv->file;
}

static GwyDataKind
app_data_window_get_data_kind(GwyAppDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_APP_IMAGE_WINDOW(window), GWY_FILE_NONE);
    return GWY_APP_IMAGE_WINDOW(window)->priv->data_kind;
}

static gint
app_data_window_get_id(GwyAppDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_APP_IMAGE_WINDOW(window), -1);
    return GWY_APP_IMAGE_WINDOW(window)->priv->id;
}

static const gchar*
app_data_window_get_accel_root(GwyAppDataWindow *window)
{
    g_return_val_if_fail(GWY_IS_APP_IMAGE_WINDOW(window), "<NONE>");
    return GWY_APP_IMAGE_WINDOW(window)->priv->accel_root;
}

/**
 * SECTION: app-image-window
 * @title: GwyAppImageWindow
 * @short_description: Windows for data shown as images
 *
 * #GwyAppImageWindow wraps #GwyDataWindow and updates it to changes in the file.
 **/

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