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

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

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/gradient.h"
#include "libgwyddion/gl-material.h"

#include "libgwyui/option-menus.h"
#include "libgwyui/inventory-store.h"
#include "libgwyui/cell-renderer-gradient.h"
#include "libgwyui/gradient-swatch.h"
#include "libgwyui/widget-impl-utils.h"

/* TODO: We have GwyCellRendererGradient and GwyGradientSwatch, but we do not have anything similar for GL materials
 * and other resources. Dedicated widgets would be needed for each to avoid rendering through pixbufs. */

/*
 * Stuff set on objects:
 * selection button:
 *     "active-resource": name of active resource
 *     "treeview": corresponding treeview (if exists)
 *     "resource-info": ResourceInfo for this resource type
 *
 * tree model:
 *     "resource-info": ResourceInfo for this resource type
 *
 * packed item:
 *     "image": image with resource sample
 *     "label": label with resource name
 */

typedef void (*ResourceSamplerFunc)(GwyResource *resource,
                                    GdkPixbuf *pixbuf);

typedef struct {
    GwyInventory *inventory;
    /* Specialised renderers. */
    GType swatch_widget_type;
    GType cell_renderer_type;
    const gchar *property;
    /* Fallback rendering to pixbufs. */
    ResourceSamplerFunc sampler;
    const gchar *human_name;
    const gchar *window_title;
    const gchar *key;
} ResourceInfo;

/* FIXME: It would be cleaner to use GClosures, but then all involved funcs would have to use closures instead of
 * callback/cbdata arguments. */
typedef struct {
    GCallback callback;
    gpointer cbdata;
} CallbackInfo;

static const ResourceInfo* gl_material_resource_info(void);
static const ResourceInfo* gradient_resource_info   (void);

static GtkWidget* gwy_menu_resource                     (const ResourceInfo *rinfo,
                                                         GCallback callback,
                                                         gpointer cbdata);
static void       pack_resource_box                     (GtkContainer *container,
                                                         const ResourceInfo *rinfo,
                                                         const gchar *name,
                                                         gint width,
                                                         gint height);
static GtkWidget* gwy_resource_tree_view_new            (const ResourceInfo *rinfo,
                                                         GCallback callback,
                                                         gpointer cbdata,
                                                         const gchar *active);
static GtkWidget* gwy_resource_selection_new            (const ResourceInfo *rinfo,
                                                         GCallback callback,
                                                         gpointer cbdata,
                                                         const gchar *active);
static void       gwy_resource_selection_set_active     (GtkWidget *widget,
                                                         const gchar *active);
static void       gwy_resource_selection_cell_pixbuf    (GtkTreeViewColumn *column,
                                                         GtkCellRenderer *renderer,
                                                         GtkTreeModel *model,
                                                         GtkTreeIter *iter,
                                                         gpointer user_data);
static void       gwy_resource_selection_cell_sample    (GtkTreeViewColumn *column,
                                                         GtkCellRenderer *renderer,
                                                         GtkTreeModel *model,
                                                         GtkTreeIter *iter,
                                                         gpointer user_data);
static void       gwy_resource_selection_cell_name      (GtkTreeViewColumn *column,
                                                         GtkCellRenderer *renderer,
                                                         GtkTreeModel *model,
                                                         GtkTreeIter *iter,
                                                         gpointer user_data);
static void       gwy_resource_selection_prefer_toggled (GtkTreeModel *model,
                                                         const gchar *spath);
static void       gwy_resource_button_toggled           (GtkWidget *button,
                                                         CallbackInfo *cbinfo);
static void       gwy_resource_selection_changed        (GtkTreeSelection *selection,
                                                         GtkWidget *button);
static void       gwy_resource_button_update            (GtkWidget *button,
                                                         GwyResource *resource);
static void       gwy_resource_selection_default_changed(GwyInventory *inventory,
                                                         GwyInventoryStore *store);
static void       gwy_resource_button_treeview_destroy  (GtkWidget *treeview,
                                                         GtkWidget *button);
static void       gwy_resource_button_destroy           (GtkWidget *button,
                                                         CallbackInfo *cbinfo);
static void       gwy_resource_store_finalized          (gpointer data,
                                                         GObject *exobject);
static gboolean   resource_window_key_pressed           (GtkWidget *window,
                                                         GdkEventKey *event);

/************************** Pop-up menus ****************************/

/**
 * gwy_menu_gradient:
 * @callback: A callback called when a menu item is activated (or %NULL for none).
 * @cbdata: User data passed to the callback.
 *
 * Creates a pop-up gradient menu.
 *
 * Object data <literal>"gradient-name"</literal> is set to gradient name for each menu item.
 *
 * Returns: The newly created pop-up menu as #GtkWidget.
 **/
GtkWidget*
gwy_menu_gradient(GCallback callback,
                  gpointer cbdata)
{
    return gwy_menu_resource(gradient_resource_info(), callback, cbdata);
}

/**
 * gwy_menu_gl_material:
 * @callback: A callback called when a menu item is activated (or %NULL for none).
 * @cbdata: User data passed to the callback.
 *
 * Creates a pop-up GL material menu.
 *
 * Object data <literal>"gl-material-name"</literal> is set to GL material name for each menu item.
 *
 * Returns: The newly created pop-up menu as #GtkWidget.
 **/
GtkWidget*
gwy_menu_gl_material(GCallback callback,
                     gpointer cbdata)
{
    return gwy_menu_resource(gl_material_resource_info(), callback, cbdata);
}

static GtkWidget*
gwy_menu_resource(const ResourceInfo *rinfo,
                  GCallback callback,
                  gpointer cbdata)
{
    gint width, height;
    gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
    width = 5*height;

    GtkWidget *menu = gtk_menu_new();
    g_object_set(menu, "reserve-toggle-size", 0, NULL);

    GwyResource *resource;
    for (gint i = 0; (resource = gwy_inventory_get_nth_item(rinfo->inventory, i)); i++) {
        if (!gwy_resource_get_preferred(resource))
            continue;

        GtkWidget *item = gtk_menu_item_new();
        const gchar *name = gwy_resource_get_name(resource);
        gwy_debug("<%s>", name);
        pack_resource_box(GTK_CONTAINER(item), rinfo, name, width, height);
        g_object_set_data(G_OBJECT(item), rinfo->key, (gpointer)name);
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
        if (callback)
            g_signal_connect(item, "activate", callback, cbdata);
    }

    return menu;
}

/************************** Tree views ****************************/

/**
 * gwy_gradient_tree_view_new:
 * @callback: Callback to connect to "changed" signal of tree view selection (or %NULL for none).
 * @cbdata: User data passed to @callback.
 * @active: Gradient name to be shown as currently selected (or %NULL for default).
 *
 * Creates a tree view with gradient list.
 *
 * Returns: The newly created gradient tree view as #GtkWidget.
 **/
GtkWidget*
gwy_gradient_tree_view_new(GCallback callback,
                           gpointer cbdata,
                           const gchar *active)
{
    return gwy_resource_tree_view_new(gradient_resource_info(), callback, cbdata, active);
}

/**
 * gwy_gradient_tree_view_set_active:
 * @treeview: A gradient selector tree view.
 * @active: Gradient name to be shown as currently selected.
 *
 * Selects a gradient in a gradient list and scrolls to make it visible.
 *
 * Returns: %TRUE if @active was selected, %FALSE if there is no such gradient.
 **/
gboolean
gwy_gradient_tree_view_set_active(GtkWidget *treeview,
                                  const gchar *active)
{
    return gwy_resource_tree_view_set_active(treeview, active);
}

/**
 * gwy_gl_material_tree_view_new:
 * @callback: Callback to connect to "changed" signal of tree view selection (or %NULL for none).
 * @cbdata: User data passed to @callback.
 * @active: GL material name to be shown as currently selected (or %NULL for default).
 *
 * Creates a tree view with GL material list.
 *
 * Returns: The newly created GL material tree view as #GtkWidget.
 **/
GtkWidget*
gwy_gl_material_tree_view_new(GCallback callback,
                              gpointer cbdata,
                              const gchar *active)
{
    return gwy_resource_tree_view_new(gl_material_resource_info(), callback, cbdata, active);
}

/**
 * gwy_gl_material_tree_view_set_active:
 * @treeview: A GL material selector tree view.
 * @active: GL material name to be shown as currently selected.
 *
 * Selects a GL material in a GL material list and scrolls to make it visible.
 *
 * Returns: %TRUE if @active was selected, %FALSE if there is no such GL material.
 **/
gboolean
gwy_gl_material_tree_view_set_active(GtkWidget *treeview,
                                     const gchar *active)
{
    return gwy_resource_tree_view_set_active(treeview, active);
}

/**
 * gwy_gradient_selection_new:
 * @callback: Callback to connect to "changed" signal of tree view selection (or %NULL for none).
 * @cbdata: User data passed to @callback.
 * @active: Gradient name to be shown as currently selected (or %NULL for default).
 *
 * Creates a gradient selection button.
 *
 * Returns: The newly created gradient selection button as #GtkWidget.
 **/
GtkWidget*
gwy_gradient_selection_new(GCallback callback,
                           gpointer cbdata,
                           const gchar *active)
{
    return gwy_resource_selection_new(gradient_resource_info(), callback, cbdata, active);
}

/**
 * gwy_gradient_selection_get_active:
 * @selection: Gradient selection button.
 *
 * Gets the name of currently selected gradient of a selection button.
 *
 * Returns: Name as a string owned by the selected gradient.
 **/
const gchar*
gwy_gradient_selection_get_active(GtkWidget *selection)
{
    return g_object_get_data(G_OBJECT(selection), "active-resource");
}

/**
 * gwy_gradient_selection_set_active:
 * @selection: Gradient selection button.
 * @active: Gradient name to be shown as currently selected.
 *
 * Sets the currently selected gradient of a selection button.
 **/
void
gwy_gradient_selection_set_active(GtkWidget *selection,
                                  const gchar *active)
{
    gwy_resource_selection_set_active(selection, active);
}

/**
 * gwy_gl_material_selection_new:
 * @callback: Callback to connect to "changed" signal of tree view selection (or %NULL for none).
 * @cbdata: User data passed to @callback.
 * @active: GL material name to be shown as currently selected (or %NULL for default).
 *
 * Creates a GL material selection button.
 *
 * Returns: The newly created GL material selection button as #GtkWidget.
 **/
GtkWidget*
gwy_gl_material_selection_new(GCallback callback,
                              gpointer cbdata,
                              const gchar *active)
{
    return gwy_resource_selection_new(gl_material_resource_info(), callback, cbdata, active);
}

/**
 * gwy_gl_material_selection_get_active:
 * @selection: GL material selection button.
 *
 * Gets the name of currently selected GL material of a selection button.
 *
 * Returns: Name as a string owned by the selected GL material.
 **/
const gchar*
gwy_gl_material_selection_get_active(GtkWidget *selection)
{
    return g_object_get_data(G_OBJECT(selection), "active-resource");
}

/**
 * gwy_gl_material_selection_set_active:
 * @selection: GL material selection button.
 * @active: GL material name to be shown as currently selected.
 *
 * Sets the currently selected GL material of a selection button.
 **/
void
gwy_gl_material_selection_set_active(GtkWidget *selection,
                                     const gchar *active)
{
    gwy_resource_selection_set_active(selection, active);
}

/**
 * gwy_resource_tree_view_set_active:
 * @treeview: A resource selector tree view.
 * @active: Resource name to be shown as currently selected.
 *
 * Selects a resource in a list and scrolls to make it visible.
 *
 * Returns: %TRUE if @active was selected, %FALSE if there is no such resource.
 **/
gboolean
gwy_resource_tree_view_set_active(GtkWidget *treeview,
                                  const gchar *active)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(treeview));
    GtkTreeIter iter;
    if (!gwy_inventory_store_get_iter(GWY_INVENTORY_STORE(model), active, &iter))
        return FALSE;

    gtk_tree_selection_select_iter(selection, &iter);
    GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
    gtk_tree_view_scroll_to_cell(GTK_TREE_VIEW(treeview), path, NULL, TRUE, 0.5, 0.0);
    gtk_tree_path_free(path);

    return TRUE;
}

/************************** Private methods ****************************/

static GtkWidget*
gwy_resource_tree_view_new(const ResourceInfo *rinfo,
                           GCallback callback,
                           gpointer cbdata,
                           const gchar *active)
{
    gint i;

    /* Assure active exists */
    GwyResource *resource = gwy_inventory_get_item_or_default(rinfo->inventory, active);
    active = gwy_resource_get_name(resource);

    GwyInventoryStore *store = gwy_inventory_store_new(rinfo->inventory);
    g_object_set_data(G_OBJECT(store), "resource-info", (gpointer)rinfo);
    GtkWidget *treeview_widget = gtk_tree_view_new_with_model(GTK_TREE_MODEL(store));
    GtkTreeView *treeview = GTK_TREE_VIEW(treeview_widget);
    g_object_unref(store);
    gtk_tree_view_set_headers_visible(treeview, FALSE);
    g_signal_connect(rinfo->inventory, "default-changed", G_CALLBACK(gwy_resource_selection_default_changed), store);
    g_object_weak_ref(G_OBJECT(store), gwy_resource_store_finalized, rinfo->inventory);

    GtkCellRenderer *sample_renderer;
    if (rinfo->cell_renderer_type)
        sample_renderer = g_object_new(rinfo->cell_renderer_type, NULL);
    else
        sample_renderer = gtk_cell_renderer_pixbuf_new();
    GtkTreeViewColumn *sample_column = gtk_tree_view_column_new_with_attributes(rinfo->human_name, sample_renderer,
                                                                                NULL);
    gtk_tree_view_append_column(treeview, sample_column);

    /* preferred */
    GtkCellRenderer *checkbox_renderer = gtk_cell_renderer_toggle_new();
    i = gwy_inventory_store_get_column_by_name(store, "preferred");
    g_assert(i > 0);
    /* TRANSLATORS: Preferred resource. */
    GtkTreeViewColumn *checkbox_column = gtk_tree_view_column_new_with_attributes(_("Preferred"), checkbox_renderer,
                                                                                  "active", i,
                                                                                  NULL);
    g_object_set(checkbox_renderer, "activatable", TRUE, NULL);
    g_signal_connect_swapped(checkbox_renderer, "toggled", G_CALLBACK(gwy_resource_selection_prefer_toggled), store);
    gtk_tree_view_append_column(treeview, checkbox_column);

    /* name */
    GtkCellRenderer *name_renderer = gtk_cell_renderer_text_new();
    i = gwy_inventory_store_get_column_by_name(store, "name");
    g_assert(i > 0);
    GtkTreeViewColumn *name_column = gtk_tree_view_column_new_with_attributes(_("Name"), name_renderer,
                                                                              "text", i,
                                                                              NULL);
    gtk_tree_view_column_set_cell_data_func(name_column, name_renderer,
                                            gwy_resource_selection_cell_name, rinfo->inventory, NULL);
    g_object_set(name_renderer, "weight-set", TRUE, NULL);
    gtk_tree_view_append_column(treeview, name_column);

    /* Ask the other renderers about reasonable sizes and match them. */
    gint name_natural, checkbox_natural;
    gtk_cell_renderer_get_preferred_height(checkbox_renderer, treeview_widget, NULL, &checkbox_natural);
    gtk_cell_renderer_get_preferred_height(name_renderer, treeview_widget, NULL, &name_natural);
    gint height = MAX(name_natural, checkbox_natural);
    gint width = 6*height;

    /* Sample. */
    if (rinfo->cell_renderer_type) {
        g_assert(g_type_is_a(rinfo->cell_renderer_type, GTK_TYPE_CELL_RENDERER));
        gtk_tree_view_column_set_cell_data_func(sample_column, sample_renderer,
                                                gwy_resource_selection_cell_sample, NULL, NULL);
    }
    else {
        GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width, height);
        gtk_tree_view_column_set_cell_data_func(sample_column, sample_renderer,
                                                gwy_resource_selection_cell_pixbuf, pixbuf, g_object_unref);
    }

    gtk_cell_renderer_set_fixed_size(sample_renderer, width, height);
    gtk_cell_renderer_set_fixed_size(checkbox_renderer, -1, height);
    gtk_cell_renderer_set_fixed_size(name_renderer, -1, height);

    gtk_tree_view_set_search_column(treeview, i);
    gtk_tree_view_set_enable_search(treeview, TRUE);

    /* Selection. */
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
    gwy_resource_tree_view_set_active(treeview_widget, active);

    if (callback)
        g_signal_connect(selection, "changed", callback, cbdata);

    return treeview_widget;
}

GtkWidget*
gwy_resource_selection_new(const ResourceInfo *rinfo,
                           GCallback callback,
                           gpointer cbdata,
                           const gchar *active)
{
    /* Assure active exists */
    GwyResource *resource = gwy_inventory_get_item_or_default(rinfo->inventory, active);
    active = gwy_resource_get_name(resource);

    gint width, height;
    gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
    width = 5*height;

    GtkWidget *button = gtk_toggle_button_new();
    pack_resource_box(GTK_CONTAINER(button), rinfo, active, width, height);
    g_object_set_data(G_OBJECT(button), "active-resource", resource);
    g_object_set_data(G_OBJECT(button), "resource-info", (gpointer)rinfo);

    CallbackInfo *cbinfo = g_new(CallbackInfo, 1);
    cbinfo->callback = callback;
    cbinfo->cbdata = cbdata;
    g_signal_connect(button, "toggled", G_CALLBACK(gwy_resource_button_toggled), cbinfo);
    g_signal_connect(button, "destroy", G_CALLBACK(gwy_resource_button_destroy), cbinfo);

    return button;
}

/* FIXME: If there is no treeview, no signal is emitted.  Must define a proper class for the buttons. */
static void
gwy_resource_selection_set_active(GtkWidget *widget,
                                  const gchar *active)
{
    const ResourceInfo *rinfo;
    GtkWidget *treeview;
    const gchar *current;

    rinfo = g_object_get_data(G_OBJECT(widget), "resource-info");
    g_return_if_fail(rinfo);

    current = g_object_get_data(G_OBJECT(widget), "active-resource");
    if (current && active && gwy_strequal(active, current))
        return;

    treeview = g_object_get_data(G_OBJECT(widget), "treeview");
    if (!treeview) {
        GwyResource *resource = gwy_inventory_get_item_or_default(rinfo->inventory, active);
        gwy_resource_button_update(widget, resource);
        return;
    }

    g_return_if_fail(GTK_IS_TREE_VIEW(treeview));
    gwy_resource_tree_view_set_active(treeview, active);
}

static void
gwy_resource_button_toggled(GtkWidget *button,
                            CallbackInfo *cbinfo)
{
    ResourceInfo *rinfo;
    GwyResource *resource;
    GtkTreeSelection *selection;
    GtkWidget *window, *scwin, *treeview;

    if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button))) {
        /* Pop down */
        treeview = g_object_get_data(G_OBJECT(button), "treeview");
        if (treeview)
            gtk_widget_destroy(gtk_widget_get_toplevel(treeview));
        return;
    }

    rinfo = g_object_get_data(G_OBJECT(button), "resource-info");

    /* Pop up */
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), rinfo->window_title);
    gtk_window_set_default_size(GTK_WINDOW(window), -1, 400);
    gtk_window_set_transient_for(GTK_WINDOW(window), GTK_WINDOW(gtk_widget_get_toplevel(button)));

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

    resource = g_object_get_data(G_OBJECT(button), "active-resource");
    gwy_debug("active-resource = <%s>", gwy_resource_get_name(resource));
    treeview = gwy_resource_tree_view_new(rinfo, cbinfo->callback, cbinfo->cbdata, gwy_resource_get_name(resource));
    g_object_set_data(G_OBJECT(button), "treeview", treeview);
    gtk_container_add(GTK_CONTAINER(scwin), treeview);
    g_signal_connect(treeview, "destroy", G_CALLBACK(gwy_resource_button_treeview_destroy), button);
    g_signal_connect_swapped(treeview, "row-activated", G_CALLBACK(gtk_widget_destroy), window);
    g_signal_connect_swapped(treeview, "key-press-event", G_CALLBACK(resource_window_key_pressed), window);

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(treeview));
    g_signal_connect(selection, "changed", G_CALLBACK(gwy_resource_selection_changed), button);

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

static void
gwy_resource_selection_cell_pixbuf(G_GNUC_UNUSED GtkTreeViewColumn *column,
                                   GtkCellRenderer *renderer,
                                   GtkTreeModel *model,
                                   GtkTreeIter *iter,
                                   gpointer user_data)
{
    GdkPixbuf *pixbuf = (GdkPixbuf*)user_data;

    GwyResource *resource;
    gtk_tree_model_get(model, iter, 0, &resource, -1);
    const ResourceInfo *rinfo = g_object_get_data(G_OBJECT(model), "resource-info");
    rinfo->sampler(resource, pixbuf);
    /* This looks like noop because we use one pixbuf all the time, but it apparently makes the cell rendered to
     * really render it */
    g_object_set(renderer, "pixbuf", pixbuf, NULL);
}

static void
gwy_resource_selection_cell_sample(G_GNUC_UNUSED GtkTreeViewColumn *column,
                                   GtkCellRenderer *renderer,
                                   GtkTreeModel *model,
                                   GtkTreeIter *iter,
                                   G_GNUC_UNUSED gpointer user_data)
{
    GwyResource *resource;
    gtk_tree_model_get(model, iter, 0, &resource, -1);
    const ResourceInfo *rinfo = g_object_get_data(G_OBJECT(model), "resource-info");
    gpointer item;
    gtk_tree_model_get(model, iter, 0, &item, -1);
    g_object_set(renderer, rinfo->property, item, NULL);
}

static void
gwy_resource_selection_cell_name(G_GNUC_UNUSED GtkTreeViewColumn *column,
                                 GtkCellRenderer *renderer,
                                 GtkTreeModel *model,
                                 GtkTreeIter *iter,
                                 gpointer user_data)
{
    GwyInventory *inventory = (GwyInventory*)user_data;
    gpointer item, defitem;

    gtk_tree_model_get(model, iter, 0, &item, -1);
    defitem = gwy_inventory_get_default_item(inventory);
    g_object_set(renderer, "weight", (item == defitem) ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL, NULL);
}

static void
gwy_resource_selection_changed(GtkTreeSelection *selection,
                               GtkWidget *button)
{
    GwyResource *resource;
    GtkTreeModel *model;
    GtkTreeIter iter;

    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return;
    gtk_tree_model_get(model, &iter, 0, &resource, -1);
    gwy_resource_button_update(button, resource);
}

static void
gwy_resource_button_update(GtkWidget *button,
                           GwyResource *resource)
{
    const ResourceInfo *rinfo;
    GtkWidget *label;

    gwy_debug("updating button to: %s", gwy_resource_get_name(resource));
    rinfo = g_object_get_data(G_OBJECT(button), "resource-info");
    GtkWidget *sample = g_object_get_data(G_OBJECT(button), "image");
    if (rinfo->swatch_widget_type) {
        g_object_set(sample, rinfo->property, resource, NULL);
    }
    else {
        GdkPixbuf *pixbuf = gtk_image_get_pixbuf(GTK_IMAGE(sample));
        rinfo->sampler(resource, pixbuf);
        gtk_image_set_from_pixbuf(GTK_IMAGE(sample), pixbuf);
    }

    label = g_object_get_data(G_OBJECT(button), "label");
    gtk_label_set_text(GTK_LABEL(label), gwy_resource_get_name(resource));

    g_object_set_data(G_OBJECT(button), "active-resource", resource);
}

static void
gwy_resource_selection_prefer_toggled(GtkTreeModel *model,
                                      const gchar *spath)
{
    GwyInventory *inventory;
    GwyResource *resource;
    GtkTreePath *path;
    GtkTreeIter iter;
    gboolean is_preferred;
    gint i;

    inventory = gwy_inventory_store_get_inventory(GWY_INVENTORY_STORE(model));

    path = gtk_tree_path_new_from_string(spath);
    gtk_tree_model_get_iter(model, &iter, path);
    gtk_tree_model_get(model, &iter, 0, &resource, -1);
    is_preferred = gwy_resource_get_preferred(resource);
    gwy_debug("res: %d", is_preferred);
    gwy_resource_set_preferred(resource, !is_preferred);
    i = gtk_tree_path_get_indices(path)[0];
    gtk_tree_path_free(path);

    gwy_inventory_nth_item_updated(inventory, i);
}

static void
gwy_resource_selection_default_changed(GwyInventory *inventory,
                                       GwyInventoryStore *store)
{
    GtkTreeModel *model;
    GwyResource *resource;
    GtkTreePath *path;
    GtkTreeIter iter;

    resource = gwy_inventory_get_default_item(inventory);
    if (!resource)
        return;

    /* FIXME: This is somewhat crude.  In fact it does not work, but the tree view is redrawn anyway so no one notices
     * it.  Must emit "row-changed" on old default in InventoryStore.  */
    model = GTK_TREE_MODEL(store);
    gwy_inventory_store_get_iter(store, gwy_resource_get_name(resource), &iter);
    path = gtk_tree_model_get_path(model, &iter);
    gtk_tree_model_row_changed(model, path, &iter);
    gtk_tree_path_free(path);
}

static void
gwy_resource_button_treeview_destroy(G_GNUC_UNUSED GtkWidget *treeview,
                                     GtkWidget *button)
{
    g_object_set_data(G_OBJECT(button), "treeview", NULL);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), FALSE);
}

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

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

static void
gwy_resource_button_destroy(GtkWidget *button,
                            CallbackInfo *cbinfo)
{
    GtkWidget *widget;

    widget = g_object_get_data(G_OBJECT(button), "treeview");
    gwy_debug("widget: %p", widget);
    if (widget) {
        g_object_set_data(G_OBJECT(button), "treeview", NULL);
        gtk_widget_destroy(gtk_widget_get_toplevel(widget));
    }

    g_free(cbinfo);
}

static void
gwy_resource_store_finalized(gpointer data,
                             GObject *exobject)
{
    g_signal_handlers_disconnect_by_func(data, gwy_resource_selection_default_changed, exobject);
}

/************************** Common subroutines ****************************/

static void
pack_resource_box(GtkContainer *container,
                  const ResourceInfo *rinfo,
                  const gchar *name,
                  gint width,
                  gint height)
{
    GtkWidget *hbox, *sample, *label;

    GwyResource *resource = gwy_inventory_get_item_or_default(rinfo->inventory, name);
    if (rinfo->swatch_widget_type) {
        sample = g_object_new(rinfo->swatch_widget_type,
                              rinfo->property, resource,
                              NULL);
        gtk_widget_set_size_request(sample, width, height);
    }
    else {
        GdkPixbuf *pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, width, height);
        rinfo->sampler(resource, pixbuf);
        sample = gtk_image_new_from_pixbuf(pixbuf);
        g_object_unref(pixbuf);
    }

    name = gwy_resource_get_name(resource);
    hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
    label = gtk_label_new(name);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_box_pack_start(GTK_BOX(hbox), sample, FALSE, FALSE, 0);
    gtk_box_pack_start(GTK_BOX(hbox), label, TRUE, TRUE, 0);
    gtk_container_add(container, hbox);

    g_object_set_data(G_OBJECT(container), "image", sample);
    g_object_set_data(G_OBJECT(container), "label", label);
}

static const ResourceInfo*
gradient_resource_info(void)
{
    static ResourceInfo rinfo;

    if (!rinfo.inventory) {
        rinfo.inventory = gwy_gradients();
        rinfo.cell_renderer_type = GWY_TYPE_CELL_RENDERER_GRADIENT;
        rinfo.swatch_widget_type = GWY_TYPE_GRADIENT_SWATCH;
        rinfo.property = "gradient";
        rinfo.human_name = _("Color Gradient");
        rinfo.window_title = _("Choose Color Gradient");
        rinfo.key = "gradient-name";
    }
    return &rinfo;
}

static const ResourceInfo*
gl_material_resource_info(void)
{
    static ResourceInfo rinfo;

    if (!rinfo.inventory) {
        rinfo.inventory = gwy_gl_materials();
        rinfo.sampler = (ResourceSamplerFunc)&gwy_sample_gl_material_to_pixbuf;
        rinfo.human_name = _("GL Material");
        rinfo.window_title = _("Choose GL Material");
        rinfo.key = "gl-material-name";
    }
    return &rinfo;
}

/**
 * gwy_sample_gl_material_to_pixbuf:
 * @gl_material: A GL material to sample.
 * @pixbuf: A pixbuf to sample gl_material to (in horizontal direction).
 *
 * Samples GL material to a provided pixbuf.
 **/
void
gwy_sample_gl_material_to_pixbuf(GwyGLMaterial *gl_material,
                                 GdkPixbuf *pixbuf)
{
    g_return_if_fail(GWY_IS_GL_MATERIAL(gl_material));
    g_return_if_fail(GDK_IS_PIXBUF(pixbuf));

    const GwyRGBA *ambient = gwy_gl_material_get_ambient(gl_material);
    const GwyRGBA *emission  = gwy_gl_material_get_emission(gl_material);
    const GwyRGBA *specular = gwy_gl_material_get_specular(gl_material);
    const GwyRGBA *diffuse = gwy_gl_material_get_diffuse(gl_material);
    gdouble shininess = gwy_gl_material_get_shininess(gl_material);

    gint width = gdk_pixbuf_get_width(pixbuf);
    gint height = gdk_pixbuf_get_height(pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(pixbuf);
    gboolean has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
    gint bpp = 3 + (has_alpha ? 1 : 0);
    guchar *pdata = gdk_pixbuf_get_pixels(pixbuf);

    gdouble q = (width == 1) ? 0.0 : 1.0/(width - 1.0);
    gdouble p = (height == 1) ? 0.0 : 1.0/(height - 1.0);
    guint8 alpha = float_to_hex(ambient->a);

    for (gint j = 0; j < width; j++) {
        gdouble VRp = j*q*(2.0 - j*q);
        gdouble s = pow(VRp, 128.0*shininess);
        GwyRGBA s0;

        s0.r = emission->r + 0.3*ambient->r + specular->r*s;
        s0.g = emission->g + 0.3*ambient->g + specular->g*s;
        s0.b = emission->b + 0.3*ambient->b + specular->b*s;

        for (gint i = 0; i < height; i++) {
            guchar *row = pdata + i*rowstride + j*bpp;
            gdouble LNp = 1.0 - i*p;
            gdouble v;

            v = s0.r + diffuse->r*LNp;
            *(row++) = float_to_hex(v);

            v = s0.g + diffuse->g*LNp;
            *(row++) = float_to_hex(v);

            v = s0.b + diffuse->b*LNp;
            *(row++) = float_to_hex(v);

            if (has_alpha)
                *(row++) = alpha;
        }
    }
}

/**
 * SECTION: option-menus
 * @title: Option menus
 * @short_description: Resource selectors
 * @see_also: <link linkend="libgwyui-gwyradiobuttons">gwyradiobuttons</link> -- radio button constructors,
 *            <link linkend="libgwyui-gwycheckboxes">gwycheckboxes</link> -- check box constructors,
 *            <link linkend="libgwyui-gwycombobox">gwycombobox</link> -- combo box constructors
 **/

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