/*
 *  $Id: menu.c 30116 2026-06-22 15:28:59Z 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.
 */

/* FIXME: some of this belongs to gwyddion, not libgwyapp.
 *
 * keyboard shortcuts are broken
 *
 * each window should have its own sensitivity group – we can even use GwyMenuSensFlags as the flags, why not – but
 * not the group itself
 */

#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/utils.h"
#include "libgwyui/gwyui.h"

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

/* FIXME: We are currently using GwyDataKind to distinguishing module types. But that is mixing categories. We need
 * some generic module function handling, but module types do not need to map to data types 1:1. */

enum {
    RECENT_FUNC_NUM = 8,
    RECENT_MODE_SHIFT = 12,
    RECENT_NUM_MASK = (1u << RECENT_MODE_SHIFT) - 1,
};

// FIXME: Cannot generalise with graph functions being different.
//typedef void (*RunFuncDefault)(const gchar *name);
//typedef void (*RunFuncInMode)(const gchar *name, GwyRunModeFlags mode);

typedef struct {
    GPtrArray *menus;
    GPtrArray *names;
} RecentFuncKeeper;

typedef struct {
    guint (*get_flags)(const gchar *name);
    GtkAccelGroup *accel_group;
    GwySensitivityGroup *sens_group;
    GCallback callback;
    const gchar *prefix;
} WidgetCreationData;

typedef struct {
    const gchar *name;
    const gchar *icon_name;
    gchar *path;
    gchar *path_translated;
    gchar *item_canonical;
    gchar *item_translated;
    gchar *item_translated_canonical;
    gchar *item_collated;
    gchar *accel_path;
    guint sens_flags;
    GtkWidget *widget;
} MenuNodeData;

static void     recent_menu_destroyed  (gpointer user_data,
                                        GObject *where_the_object_was);
static void     update_recent_func     (GwyDataKind data_kind,
                                        const gchar *name);
static gboolean update_recent_names    (RecentFuncKeeper *keeper,
                                        GwyDataKind data_kind,
                                        const gchar *name);
static void     update_recent_menu     (GtkMenuShell *shell,
                                        GPtrArray *names,
                                        GwyDataKind data_kind);
static void     prefill_recent_funcions(GPtrArray *names,
                                        GwyDataKind data_kind);
static void     run_recent             (GtkWidget *item,
                                        gpointer user_data);

static RecentFuncKeeper recent_funcs_keepers[GWY_FILE_N_KINDS];
static GwySensitivityGroup *app_sensgroup = NULL;

/*****************************************************************************
 *
 * Module function menu building
 *
 *****************************************************************************/

/**
 * add_tree_node:
 * @root: Module function menu root.
 * @name: The name of the function to add.
 * @path: Menu path of this function.
 *
 * Inserts a module function to menu tree.
 *
 * This is stage 1, to sort out the information that gwy_foo_func_foreach() gives us to a tree.
 **/
static void
add_tree_node(GNode *root,
              const gchar *name,
              const gchar *path,
              const gchar *icon_name)
{
    MenuNodeData *data = NULL;
    GNode *node, *child;
    gchar **segments, **segments_canonical;
    gchar *s;
    guint n, i;

    g_return_if_fail(path && path[0] == '/');
    segments = g_strsplit(path, "/", 0);

    /* Canonicalize */
    n = g_strv_length(segments);
    segments_canonical = g_new0(gchar*, n+1);
    for (i = 0; i < n; i++)
        segments_canonical[i] = gwy_strip_menu_item_label(g_strdup(segments[i]));

    /* Find node in the tree to branch off */
    node = root;
    i = 1;
    while (segments_canonical[i]) {
        gwy_debug("Searching for <%s> in <%s>", segments_canonical[i], ((MenuNodeData*)node->data)->path);
        for (child = node->children; child; child = child->next) {
            data = (MenuNodeData*)child->data;
            if (gwy_strequal(data->item_canonical, segments_canonical[i])) {
                gwy_debug("Found <%s>, descending", segments_canonical[i]);
                break;
            }
        }
        if (child) {
            node = child;
            i++;
        }
        else {
            gwy_debug("Not found <%s>, stopping search", segments_canonical[i]);
            break;
        }
    }
    if (!segments[i]) {
        g_warning("Item with path `%s' already exists", path);
        goto fail;
    }
    if (i > 1 && (!data || ((MenuNodeData*)node->data)->name)) {
        g_warning("Item with path `%s' cannot be both leaf and branch", path);
        goto fail;
    }

    /* Now recursively create new children till segments[] is exhausted */
    gwy_debug("Branching off new child of <%s>",
              ((MenuNodeData*)node->data)->path);
    while (segments[i]) {
        data = g_new0(MenuNodeData, 1);
        s = segments[i+1];
        segments[i+1] = NULL;
        data->path = g_strjoinv("/", segments);
        segments[i+1] = s;

        data->item_canonical = segments_canonical[i];
        segments_canonical[i] = NULL;
        gwy_debug("Created <%s> with full path <%s>", data->item_canonical, data->path);
        node = g_node_prepend_data(node, data);
        i++;
    }
    /* The leaf node is the real item */
    data->name = name;
    data->icon_name = icon_name;
    s = _(path);
    if (!gwy_strequal(s, path)) {
        data->path_translated = g_strdup(s);
    }

fail:
    g_strfreev(segments);
    g_strfreev(segments_canonical);
}

/**
 * resolve_translations:
 * @node: Module function menu tree node to process.
 * @user_data: Unused.
 *
 * Resolves partial translations of menu paths, calculates collation keys.
 *
 * Must be called on nodes in %G_POST_ORDER.
 *
 * This is stage 2, translations of particular items are extracted and translations are propagated from non-leaf nodes
 * up to braches.
 *
 * FIXME: We should better deal with situations like missing accelerators in some translations by prefering those with
 * accelerators.  Or at least print some warning.
 *
 * Returns: Always %FALSE.
 **/
static gboolean
resolve_translations(GNode *node, G_GNUC_UNUSED gpointer user_data)
{
    MenuNodeData *data = (MenuNodeData*)node->data;
    MenuNodeData *pdata;
    const gchar *p;
    gchar *s;

    if (G_NODE_IS_ROOT(node))
        return FALSE;

    pdata = (MenuNodeData*)node->parent->data;
    if (!data->path_translated) {
        gwy_debug("Path <%s> is untranslated", data->path);
        data->path_translated = g_strdup(data->path);
    }
    else {
        gwy_debug("Path <%s> is translated", data->path);
    }

    p = strrchr(data->path_translated, '/');
    g_return_val_if_fail(p, FALSE);
    data->item_translated = g_strdup(p+1);
    data->item_translated_canonical = gwy_strip_menu_item_label(g_strdup(data->item_translated));
    /* This seems silly but I am seeing identical collation keys for different strings (sort_submenus
     * prints a warning).  Perhaps it only happens when translation language is selected by LANGUAGE env variable but
     * all other locales are different (g_utf8_colllate() is locale-dependent). Anyway, it should not disrupt the
     * order. */
    s = g_utf8_collate_key(data->item_translated_canonical, -1);
    data->item_collated = g_strconcat(s, " ", data->item_translated_canonical, NULL);
    g_free(s);

    if (!pdata->path_translated) {
        pdata->path_translated = g_strndup(data->path_translated, p - data->path_translated);
        gwy_debug("Deducing partial translation: <%s> from <%s>", pdata->path, data->path);
    }

    return FALSE;
}

/**
 * sort_submenus:
 * @node: Module function menu tree node to process.
 * @user_data: Unused.
 *
 * Sorts module function submenus alphabetically.
 *
 * Must be called on nodes in %G_PRE_ORDER.
 *
 * This is stage 3, childrens of each node are sorted according to collation keys (calculated in stage 2).
 *
 * Returns: Always %FALSE.
 **/
static gboolean
sort_submenus(GNode *node, G_GNUC_UNUSED gpointer user_data)
{
    GNode *c1, *c2;
    gboolean ok = FALSE;

    if (G_NODE_IS_LEAF(node))
        return FALSE;

    /* This is bubble sort. */
    while (!ok) {
        ok = TRUE;
        for (c1 = node->children, c2 = c1->next; c2; c1 = c2, c2 = c2->next) {
            MenuNodeData *data1 = (MenuNodeData*)c1->data;
            MenuNodeData *data2 = (MenuNodeData*)c2->data;
            gint cmpresult;

            cmpresult = strcmp(data1->item_collated, data2->item_collated);
            if (cmpresult < 0)
                continue;

            if (cmpresult == 0 && data1 != data2) {
                g_warning("Menu items <%s> and <%s> are identical", data1->item_canonical, data2->item_canonical);
                continue;
            }

            c1->next = c2->next;
            c2->prev = c1->prev;
            if (c1->prev)
                c1->prev->next = c2;
            if (c1 == node->children)
                node->children = c2;
            if (c2->next)
                c2->next->prev = c1;
            c1->prev = c2;
            c2->next = c1;
            c1 = c1->prev;
            c2 = c2->next;
            ok = FALSE;
        }
    }

    return FALSE;
}

static gboolean
gather_attributes(GNode *node, gpointer user_data)
{
    MenuNodeData *data = (MenuNodeData*)node->data;
    WidgetCreationData *wcr = (WidgetCreationData*)user_data;

    data->sens_flags = wcr->get_flags ? wcr->get_flags(data->name) : 0;

    if (G_NODE_IS_LEAF(node))
        data->accel_path = gwy_strip_menu_item_label(g_strconcat(wcr->prefix, data->path, NULL));

    return FALSE;
}

/**
 * create_widgets:
 * @node: Module function menu tree node to process.
 * @callback: Callback function to connect to "activate" signal of the created menu items, swapped.  Function name is
 *            used as callback data.
 *
 * Creates widgets from module function tree.
 *
 * Must be called on nodes in %G_POST_ORDER.
 *
 * This is stage 4, menu items are created from leaves and branches, submenus are attached to branches (with titles,
 * and everything).  Each node data gets its @widget field filled with corresponding menu item, except root node that
 * gets it @widget field filled with the top-level menu.
 *
 * Returns: Always %FALSE.
 **/
static gboolean
create_widgets(GNode *node, gpointer user_data)
{
    MenuNodeData *cdata, *data = (MenuNodeData*)node->data;
    WidgetCreationData *wcr = (WidgetCreationData*)user_data;
    GtkWidget *item = NULL;

    if (!G_NODE_IS_ROOT(node)) {
        if (data->icon_name)
            item = gwy_create_image_menu_item(data->item_translated, data->icon_name, FALSE);
        else
            item = gtk_menu_item_new_with_mnemonic(data->item_translated);

        data->widget = item;
        if (data->accel_path)
            gtk_menu_item_set_accel_path(GTK_MENU_ITEM(item), data->accel_path);
    }

    if (G_NODE_IS_LEAF(node)) {
        if (data->sens_flags)
            gwy_sensitivity_group_add_widget(wcr->sens_group, data->widget, data->sens_flags);
        g_signal_connect_swapped(data->widget, "activate", G_CALLBACK(wcr->callback), (gpointer)data->name);
        return FALSE;
    }

    GtkWidget *menu = gtk_menu_new();
    // XXX: It is crucial to set the accel group first, then add the items. If items are added before setting the
    // group, GTK+ does not set up the item accelerators, gtk_accel_map_add_entry() does not work and items do not
    // actually get accelerators from the map.
    if (wcr->accel_group)
        gtk_menu_set_accel_group(GTK_MENU(menu), wcr->accel_group);
    for (GNode *child = node->children; child; child = child->next) {
        cdata = (MenuNodeData*)child->data;
        gtk_menu_shell_append(GTK_MENU_SHELL(menu), cdata->widget);
    }

    if (G_NODE_IS_ROOT(node))
        data->widget = menu;
    else
        gtk_menu_item_set_submenu(GTK_MENU_ITEM(data->widget), menu);
    gtk_widget_show_all(menu);

    return FALSE;
}

/**
 * free_node_data:
 * @node: Module function menu tree node to process.
 * @user_data: Unused.
 *
 * Frees module function menu tree auxiliary data.
 *
 * This is stage 6, clean-up.
 *
 * Returns: Always %FALSE.
 **/
static gboolean
free_node_data(GNode *node, G_GNUC_UNUSED gpointer user_data)
{
    MenuNodeData *data = (MenuNodeData*)node->data;

    g_free(data->path);
    g_free(data->path_translated);
    g_free(data->item_canonical);
    g_free(data->item_collated);
    g_free(data->item_translated);
    g_free(data->item_translated_canonical);
    g_free(data->accel_path);
    g_free(data);

    return FALSE;
}

/* XXX: A cleanup function. */
G_GNUC_UNUSED
static void
destroy_menu_node_tree(GNode *root)
{
    g_node_traverse(root, G_POST_ORDER, G_TRAVERSE_ALL, -1, &free_node_data, NULL);
    g_node_destroy(root);
}

static gboolean
free_temporary_node_data(GNode *node, G_GNUC_UNUSED gpointer user_data)
{
    MenuNodeData *data = (MenuNodeData*)node->data;

    GWY_FREE(data->path);
    GWY_FREE(data->path_translated);
    GWY_FREE(data->item_canonical);
    GWY_FREE(data->item_collated);
    GWY_FREE(data->item_translated_canonical);

    return FALSE;
}

static GNode*
make_root_node(const gchar *label)
{
    MenuNodeData *data = g_new0(MenuNodeData, 1);
    data->path = g_strdup("");
    data->item_translated = g_strdup(label);
    return g_node_new(data);
}

/**
 * build_module_func_menu_tree:
 * @root: Module function menu root.
 * @prefix: Accel path prefix.
 * @get_flags: Optional function to get sensitivity flags for each item.
 *
 * Massages a raw menu tree to a better shape by resolving translations and sorting submenus.
 *
 * It also collects item data such as sensitivity flags and accel paths to avoid doing it every time the widgets are
 * constructed.
 *
 * Transitional item data which are not needed to construct widgets are freed afterwards.
 **/
static void
build_module_func_menu_tree(GNode *root,
                            const gchar *prefix,
                            guint (*get_flags)(const gchar *name))
{
    WidgetCreationData wcr;
    gwy_clear1(wcr);
    wcr.prefix = prefix;
    wcr.get_flags = get_flags;

    g_node_traverse(root, G_POST_ORDER, G_TRAVERSE_ALL, -1, &resolve_translations, NULL);
    g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_ALL, -1, &sort_submenus, NULL);
    g_node_traverse(root, G_PRE_ORDER, G_TRAVERSE_LEAVES, -1, &gather_attributes, &wcr);
    g_node_traverse(root, G_POST_ORDER, G_TRAVERSE_ALL, -1, &free_temporary_node_data, NULL);
}

/**
 * build_gtk_menu_from_tree:
 * @root: Module function menu root.
 * @accel_group: Acceleration group to be associated with the menu.
 * @callback: The callback function to connect to leaves.
 *
 * Creates menu widgets from a constructed menu tree.
 *
 * Returns: A newly created menu widget.
 **/
static GtkWidget*
build_gtk_menu_from_tree(GNode *root,
                         GtkAccelGroup *accel_group,
                         GwySensitivityGroup *sens_group,
                         GCallback callback)
{
    /* If root is leaf, the menu is empty */
    if (G_NODE_IS_LEAF(root))
        return gtk_menu_new();

    WidgetCreationData wcr;
    gwy_clear1(wcr);
    wcr.accel_group = accel_group;
    wcr.sens_group = sens_group;
    wcr.callback = callback;

    g_node_traverse(root, G_POST_ORDER, G_TRAVERSE_ALL, -1, &create_widgets, &wcr);
    GtkWidget *menu = ((MenuNodeData*)root->data)->widget;
    if (accel_group)
        gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);

    return menu;
}

static void
gwy_app_menu_add_proc_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_image_func_get_menu_path(name), gwy_image_func_get_icon_name(name));
}

/**
 * gwy_app_image_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>Data Process</guimenu> menu.
 *
 * The menu is created from data processing functions registered by modules, therefore module registration has to be
 * done first for this function to make sense.
 *
 * Returns: (transfer full): Newly created image data processing menu.
 **/
GtkWidget*
gwy_app_image_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_Image"));
        gwy_image_func_foreach(gwy_app_menu_add_proc_func, root);
        build_module_func_menu_tree(root, "<image>/Process", gwy_image_func_get_sensitivity_mask);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_image_func));

    return menu;
}

static void
gwy_app_menu_add_graph_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_graph_func_get_menu_path(name), gwy_graph_func_get_icon_name(name));
}

/**
 * gwy_app_graph_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>Graph</guimenu> menu.
 *
 * The menu is created from graph functions registered by modules, therefore module registration has to be done first
 * for this function to make sense.
 *
 * Returns: (transfer full): Newly created graph data processing menu.
 **/
GtkWidget*
gwy_app_graph_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_Graph"));
        gwy_graph_func_foreach(gwy_app_menu_add_graph_func, root);
        build_module_func_menu_tree(root, "<graph>/Process", gwy_graph_func_get_sensitivity_mask);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_graph_func));

    return menu;
}

static void
gwy_app_menu_add_volume_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_volume_func_get_menu_path(name), gwy_volume_func_get_icon_name(name));
}

/**
 * gwy_app_volume_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>Volume Data</guimenu> menu.
 *
 * The menu is created from volume data processing functions registered by modules, therefore module registration has
 * to be done first for this function to make sense.
 *
 * Returns: (transfer full): Newly created volume data processing menu.
 **/
GtkWidget*
gwy_app_volume_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_Volume"));
        gwy_volume_func_foreach(gwy_app_menu_add_volume_func, root);
        build_module_func_menu_tree(root, "<volume>/Process", gwy_volume_func_get_sensitivity_mask);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_volume_func));

    return menu;
}

static void
gwy_app_menu_add_xyz_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_xyz_func_get_menu_path(name), gwy_xyz_func_get_icon_name(name));
}

/**
 * gwy_app_xyz_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>XYZ Data</guimenu> menu.
 *
 * The menu is created from XYZ data processing functions registered by modules, therefore module registration has to
 * be done first for this function to make sense.
 *
 * Returns: (transfer full): Newly created XYZ data processing menu.
 **/
GtkWidget*
gwy_app_xyz_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_XYZ Data"));
        gwy_xyz_func_foreach(gwy_app_menu_add_xyz_func, root);
        build_module_func_menu_tree(root, "<xyz>/Process", gwy_xyz_func_get_sensitivity_mask);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_xyz_func));

    return menu;
}

static void
gwy_app_menu_add_cmap_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_cmap_func_get_menu_path(name), gwy_cmap_func_get_icon_name(name));
}

/**
 * gwy_app_cmap_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>Curve Maps</guimenu> menu.
 *
 * The menu is created from curve map data processing functions registered by modules, therefore module registration
 * has to be done first for this function to make sense.
 *
 * Returns: (transfer full): Newly created curve map data processing menu.
 **/
GtkWidget*
gwy_app_cmap_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_Curve Maps"));
        gwy_cmap_func_foreach(gwy_app_menu_add_cmap_func, root);
        build_module_func_menu_tree(root, "<cmap>/Process", gwy_cmap_func_get_sensitivity_mask);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_cmap_func));

    return menu;
}

static void
gwy_app_menu_add_synth_func(const gchar *name, gpointer user_data)
{
    GNode *root = (GNode*)user_data;
    add_tree_node(root, name, gwy_synth_func_get_menu_path(name), gwy_synth_func_get_icon_name(name));
}

/**
 * gwy_app_synth_menu:
 * @sens_group: Sensitvity group for the menu.
 *
 * Constructs the application <guimenu>Curve Maps</guimenu> menu.
 *
 * The menu is created from curve map data processing functions registered by modules, therefore module registration
 * has to be done first for this function to make sense.
 *
 * Returns: (transfer full): Newly created curve map data processing menu.
 **/
GtkWidget*
gwy_app_synth_menu(GwySensitivityGroup *sens_group)
{
    static GtkAccelGroup *accel_group = NULL;
    static GNode *root = NULL;

    if (!root) {
        accel_group = gtk_accel_group_new();
        root = make_root_node(_("_Synthetic"));
        gwy_synth_func_foreach(gwy_app_menu_add_synth_func, root);
        build_module_func_menu_tree(root, "<synth>", NULL);
    }
    GtkWidget *menu = build_gtk_menu_from_tree(root, accel_group, sens_group, G_CALLBACK(gwy_app_run_synth_func));

    return menu;
}

/**
 * gwy_app_run_image_func:
 * @name: A data processing function name.
 *
 * Runs a data processing function on the current data.
 *
 * From the run modes function @name supports, the most interactive one is
 * selected.
 *
 * Returns: The actually used mode (nonzero), or 0 on failure.
 **/
GwyRunModeFlags
gwy_app_run_image_func(const gchar *name)
{
    GwyRunModeFlags run_types[] = { GWY_RUN_INTERACTIVE, GWY_RUN_IMMEDIATE, };
    GwyRunModeFlags available_run_modes;
    gsize i;

    gwy_debug("`%s'", name);
    available_run_modes = gwy_image_func_get_run_types(name);
    g_return_val_if_fail(available_run_modes, 0);
    for (i = 0; i < G_N_ELEMENTS(run_types); i++) {
        if (run_types[i] & available_run_modes) {
            gwy_app_run_image_func_in_mode(name, run_types[i]);
            return run_types[i];
        }
    }
    return 0;
}

/**
 * gwy_app_run_image_func_in_mode:
 * @name: A data processing function name.
 * @run: A run mode.
 *
 * Runs a data processing function on current data in specified mode.
 **/
void
gwy_app_run_image_func_in_mode(const gchar *name,
                                 GwyRunModeFlags run)
{
    GwyDict *data;

    gwy_debug("`%s'", name);
    if (!(run & gwy_image_func_get_run_types(name)))
        return;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    g_return_if_fail(data);
    gwy_image_func_run(name, GWY_FILE(data), run);
    gwy_recent_funcs_record_use("image", name);
    update_recent_func(GWY_FILE_IMAGE, name);
}

/**
 * gwy_app_run_graph_func:
 * @name: A graph function name.
 *
 * Runs a graph function on the current graph.
 **/
void
gwy_app_run_graph_func(const gchar *name)
{
    GwyGraph *graph;

    gwy_debug("`%s'", name);
    gwy_data_browser_get_current(GWY_APP_GRAPH, &graph, 0);
    g_return_if_fail(graph);
    g_return_if_fail(GWY_IS_GRAPH(graph));
    gwy_graph_func_run(name, graph);
    gwy_recent_funcs_record_use("graph", name);
    update_recent_func(GWY_FILE_GRAPH, name);
}

/**
 * gwy_app_run_volume_func:
 * @name: A volume data processing function name.
 *
 * Runs a volume data processing function on the current data.
 *
 * From the run modes function @name supports, the most interactive one is selected.
 *
 * Returns: The actually used mode (nonzero), or 0 on failure.
 **/
GwyRunModeFlags
gwy_app_run_volume_func(const gchar *name)
{
    GwyRunModeFlags run_types[] = { GWY_RUN_INTERACTIVE, GWY_RUN_IMMEDIATE, };
    GwyRunModeFlags available_run_modes;
    gsize i;

    gwy_debug("`%s'", name);
    available_run_modes = gwy_volume_func_get_run_types(name);
    g_return_val_if_fail(available_run_modes, 0);
    for (i = 0; i < G_N_ELEMENTS(run_types); i++) {
        if (run_types[i] & available_run_modes) {
            gwy_app_run_volume_func_in_mode(name, run_types[i]);
            return run_types[i];
        }
    }
    return 0;
}

/**
 * gwy_app_run_volume_func_in_mode:
 * @name: A volume data processing function name.
 * @run: A run mode.
 *
 * Runs a volume data processing function on current data in specified mode.
 **/
void
gwy_app_run_volume_func_in_mode(const gchar *name,
                                GwyRunModeFlags run)
{
    GwyDict *data;

    gwy_debug("`%s'", name);
    if (!(run & gwy_volume_func_get_run_types(name)))
        return;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    g_return_if_fail(data);
    gwy_volume_func_run(name, GWY_FILE(data), run);
    gwy_recent_funcs_record_use("volume", name);
    update_recent_func(GWY_FILE_VOLUME, name);
}

/**
 * gwy_app_run_xyz_func:
 * @name: A XYZ data processing function name.
 *
 * Runs a XYZ data processing function on the current data.
 *
 * From the run modes function @name supports, the most interactive one is selected.
 *
 * Returns: The actually used mode (nonzero), or 0 on failure.
 **/
GwyRunModeFlags
gwy_app_run_xyz_func(const gchar *name)
{
    GwyRunModeFlags run_types[] = { GWY_RUN_INTERACTIVE, GWY_RUN_IMMEDIATE, };
    GwyRunModeFlags available_run_modes;
    gsize i;

    gwy_debug("`%s'", name);
    available_run_modes = gwy_xyz_func_get_run_types(name);
    g_return_val_if_fail(available_run_modes, 0);
    for (i = 0; i < G_N_ELEMENTS(run_types); i++) {
        if (run_types[i] & available_run_modes) {
            gwy_app_run_xyz_func_in_mode(name, run_types[i]);
            return run_types[i];
        }
    }
    return 0;
}

/**
 * gwy_app_run_xyz_func_in_mode:
 * @name: A XYZ data processing function name.
 * @run: A run mode.
 *
 * Runs a XYZ data processing function on current data in specified mode.
 **/
void
gwy_app_run_xyz_func_in_mode(const gchar *name,
                             GwyRunModeFlags run)
{
    GwyDict *data;

    gwy_debug("`%s'", name);
    if (!(run & gwy_xyz_func_get_run_types(name)))
        return;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    g_return_if_fail(data);
    gwy_xyz_func_run(name, GWY_FILE(data), run);
    gwy_recent_funcs_record_use("xyz", name);
    update_recent_func(GWY_FILE_XYZ, name);
}

/**
 * gwy_app_run_cmap_func:
 * @name: A curve map processing function name.
 *
 * Runs a curve map processing function on the current data.
 *
 * From the run modes function @name supports, the most interactive one is selected.
 *
 * Returns: The actually used mode (nonzero), or 0 on failure.
 **/
GwyRunModeFlags
gwy_app_run_cmap_func(const gchar *name)
{
    GwyRunModeFlags run_types[] = { GWY_RUN_INTERACTIVE, GWY_RUN_IMMEDIATE, };
    GwyRunModeFlags available_run_modes;
    gsize i;

    gwy_debug("`%s'", name);
    available_run_modes = gwy_cmap_func_get_run_types(name);
    g_return_val_if_fail(available_run_modes, 0);
    for (i = 0; i < G_N_ELEMENTS(run_types); i++) {
        if (run_types[i] & available_run_modes) {
            gwy_app_run_cmap_func_in_mode(name, run_types[i]);
            return run_types[i];
        }
    }
    return 0;
}

/**
 * gwy_app_run_cmap_func_in_mode:
 * @name: A curve map processing function name.
 * @run: A run mode.
 *
 * Runs a curve map processing function on current data in specified mode.
 **/
void
gwy_app_run_cmap_func_in_mode(const gchar *name,
                              GwyRunModeFlags run)
{
    GwyDict *data;

    gwy_debug("`%s'", name);
    if (!(run & gwy_cmap_func_get_run_types(name)))
        return;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    g_return_if_fail(data);
    gwy_cmap_func_run(name, GWY_FILE(data), run);
    gwy_recent_funcs_record_use("cmap", name);
    update_recent_func(GWY_FILE_CMAP, name);
}

/**
 * gwy_app_run_synth_func:
 * @name: A synthetic data function name.
 *
 * Runs a synthetic data function on the current data.
 *
 * From the run modes function @name supports, the most interactive one is selected.
 *
 * Returns: The actually used mode (nonzero), or 0 on failure.
 **/
GwyRunModeFlags
gwy_app_run_synth_func(const gchar *name)
{
    GwyRunModeFlags run_types[] = { GWY_RUN_INTERACTIVE, GWY_RUN_IMMEDIATE, };
    GwyRunModeFlags available_run_modes;
    gsize i;

    gwy_debug("`%s'", name);
    available_run_modes = gwy_synth_func_get_run_modes(name);
    g_return_val_if_fail(available_run_modes, 0);
    for (i = 0; i < G_N_ELEMENTS(run_types); i++) {
        if (run_types[i] & available_run_modes) {
            gwy_app_run_synth_func_in_mode(name, run_types[i]);
            return run_types[i];
        }
    }
    return 0;
}

/**
 * gwy_app_run_synth_func_in_mode:
 * @name: A synthetic data function name.
 * @run: A run mode.
 *
 * Runs a synthetic data function on current data in specified mode.
 **/
void
gwy_app_run_synth_func_in_mode(const gchar *name,
                               GwyRunModeFlags run)
{
    gwy_debug("`%s'", name);
    if (!(run & gwy_synth_func_get_run_modes(name)))
        return;

    // NB: Unlike other functions, synth functions can be run with no data.
    GwyFile *data = NULL;
    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    gwy_synth_func_run(name, data, run);
    gwy_recent_funcs_record_use("synth", name);
    // FIXME: Can we get synthetic data functions to the recent menu somehow? It would have to be able to contain
    // different function types.
    //update_recent_func(GWY_FILE_CMAP, name);
}

static GtkWidget*
create_recent_menu_main_item(const gchar *accel_root, const gchar *accel_path_leaf, const gchar *label,
                             GtkMenuShell *menushell, guint accel_key, GdkModifierType accel_mods,
                             GwyRunModeFlags mode)
{
    gchar *accel_path = g_strconcat(accel_root, "/Recent/", accel_path_leaf, NULL);

    GtkWidget *item = gtk_menu_item_new_with_mnemonic(label);
    gtk_widget_set_sensitive(item, FALSE);
    gtk_menu_item_set_accel_path(GTK_MENU_ITEM(item), accel_path);
    gtk_accel_map_add_entry(accel_path, accel_key, accel_mods);
    gtk_menu_shell_append(menushell, item);
    g_signal_connect(item, "activate", G_CALLBACK(run_recent), GUINT_TO_POINTER(0 + (mode << RECENT_MODE_SHIFT)));
    g_free(accel_path);

    return item;
}

/**
 * gwy_app_create_recent_func_menu:
 * @window: Data window the menu will be added to. It determines the data kind and thus the menu type.
 * @accel_group: Acceleration group to be associated with the menu.
 * @sens_group: Sensitvity group for the menu.
 *
 * Creates a recent function menu for a data window.
 *
 * The menu content is updated automatically when function such as gwy_app_run_volume_func() is used.
 *
 * Returns: (transfer full): New recent function menu as a #GtkWidget.
 **/
GtkWidget*
gwy_app_create_recent_func_menu(GwyAppDataWindow *window,
                                GtkAccelGroup *accel_group,
                                GwySensitivityGroup *sens_group)
{
    GtkWidget *menu = gtk_menu_new();
    g_return_val_if_fail(GWY_IS_APP_DATA_WINDOW(window), menu);

    if (accel_group)
        gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);
    GwyDataKind data_kind = gwy_app_data_window_get_data_kind(window);
    g_object_set_data(G_OBJECT(menu), "gwy-menu-data-kind", GUINT_TO_POINTER(data_kind));
    g_object_set_data(G_OBJECT(menu), "gwy-menu-sens-group", sens_group);

    const gchar *accel_root = gwy_app_data_window_get_accel_root(window);

    create_recent_menu_main_item(accel_root, "Repeat Last", _("Repeat Last"), GTK_MENU_SHELL(menu),
                                 GDK_KEY_f, GDK_CONTROL_MASK, GWY_RUN_IMMEDIATE);
    create_recent_menu_main_item(accel_root, "Rw-show Last", _("Re-show Last..."), GTK_MENU_SHELL(menu),
                                 GDK_KEY_f, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GWY_RUN_INTERACTIVE);

    RecentFuncKeeper *keeper = recent_funcs_keepers + data_kind;
    if (!keeper->names) {
        keeper->names = g_ptr_array_new();
        prefill_recent_funcions(keeper->names, data_kind);
    }
    if (!keeper->menus)
        keeper->menus = g_ptr_array_new();
    g_ptr_array_add(keeper->menus, menu);
    g_object_weak_ref(G_OBJECT(menu), recent_menu_destroyed, keeper);

    update_recent_menu(GTK_MENU_SHELL(menu), keeper->names, data_kind);

    return menu;
}

static void
recent_menu_destroyed(gpointer user_data, GObject *where_the_object_was)
{
    RecentFuncKeeper *keeper = (RecentFuncKeeper*)user_data;
    g_ptr_array_remove_fast(keeper->menus, where_the_object_was);
}

/* NB: We rely on runtype=0 failing the condition (runtype & available_modes) and running the function in the default
 * mode. */
static void
repeat_last_func(GtkWidget *item, GwyRunModeFlags runtype, guint i)
{
    GtkWidget *menu = gtk_widget_get_parent(item);
    GwyDataKind data_kind = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(menu), "gwy-menu-data-kind"));

    g_return_if_fail((guint)data_kind < GWY_FILE_N_KINDS);
    RecentFuncKeeper *keeper = recent_funcs_keepers + data_kind;
    GPtrArray *names = keeper->names;
    g_return_if_fail(names && names->len > i);

    const gchar *name = g_ptr_array_index(names, i);
    if (data_kind == GWY_FILE_IMAGE) {
        if (runtype & gwy_image_func_get_run_types(name))
            gwy_app_run_image_func_in_mode(name, runtype);
        else
            gwy_app_run_image_func(name);
    }
    else if (data_kind == GWY_FILE_GRAPH) {
        /* TODO: Unify graph functions with other types. */
        gwy_app_run_graph_func(name);
    }
    else if (data_kind == GWY_FILE_SPECTRA) {
        /* FIXME: How did we get here? Do we have spectra functions now? */
    }
    else if (data_kind == GWY_FILE_VOLUME) {
        if (runtype & gwy_volume_func_get_run_types(name))
            gwy_app_run_volume_func_in_mode(name, runtype);
        else
            gwy_app_run_volume_func(name);
    }
    else if (data_kind == GWY_FILE_XYZ) {
        if (runtype & gwy_xyz_func_get_run_types(name))
            gwy_app_run_xyz_func_in_mode(name, runtype);
        else
            gwy_app_run_xyz_func(name);
    }
    else if (data_kind == GWY_FILE_CMAP) {
        if (runtype & gwy_cmap_func_get_run_types(name))
            gwy_app_run_cmap_func_in_mode(name, runtype);
        else
            gwy_app_run_cmap_func(name);
    }
}

static void
run_recent(GtkWidget *item, gpointer user_data)
{
    guint mode_and_num = GPOINTER_TO_UINT(user_data);
    GwyRunModeFlags mode = mode_and_num >> RECENT_MODE_SHIFT;
    guint num = (mode_and_num & RECENT_NUM_MASK);
    repeat_last_func(item, mode, num);
}

// Update recent functions menu for data kind @data_kind, making function @name the last used function.
static void
update_recent_func(GwyDataKind data_kind, const gchar *name)
{
    RecentFuncKeeper *keeper = recent_funcs_keepers + data_kind;
    if (!update_recent_names(keeper, data_kind, name))
        return;

    GPtrArray *menus = keeper->menus;
    if (!menus)
        return;

    for (guint i = 0; i < menus->len; i++) {
        GtkMenuShell *menu = g_ptr_array_index(menus, i);
        update_recent_menu(menu, keeper->names, data_kind);
    }
}

// Update recent functions list for data kind @data_kind, making function @name the last used function.
static gboolean
update_recent_names(RecentFuncKeeper *keeper, GwyDataKind data_kind, const gchar *name)
{
    if (!keeper->names) {
        keeper->names = g_ptr_array_new();
        prefill_recent_funcions(keeper->names, data_kind);
    }
    GPtrArray *names = keeper->names;

    guint found_at = G_MAXUINT;
    for (guint i = 0; i < names->len; i++) {
        const gchar *othername = g_ptr_array_index(names, i);
        if (!g_strcmp0(name, othername)) {
            found_at = i;
            break;
        }
    }
    /* Nothing to do; the function is already first. */
    if (!found_at)
        return FALSE;

    if (found_at < G_MAXUINT)
        g_ptr_array_remove_index(names, found_at);

    if (g_ptr_array_index(names, 0))
        g_ptr_array_insert(names, 0, (gpointer)name);
    else
        g_ptr_array_index(names, 0) = (gpointer)name;

    if (names->len > RECENT_FUNC_NUM)
        g_ptr_array_set_size(names, RECENT_FUNC_NUM);

#ifdef DEBUG
    for (guint i = 0; i < names->len; i++) {
        gwy_debug("recent[%u] = %s", i, (const gchar*)g_ptr_array_index(names, i));
    }
#endif

    return TRUE;
}

// Prefill recent functions from the global data about function usage. This is a bit awkward because we do not want to
// prefill the special first item, i.e. Run-last, just regular the menu below.
static void
prefill_recent_funcions(GPtrArray *names, GwyDataKind data_kind)
{
    // FIXME: Here we definitely mix data kind name and function name. They are 90% the same thing, but conceptually
    // they are not really the same thing.
    const gchar *kind = _gwy_data_kind_name(data_kind);
    g_ptr_array_set_size(names, RECENT_FUNC_NUM);
    const gchar **names_array = (const gchar**)&g_ptr_array_index(names, 1);
    guint n = gwy_recent_funcs_get_head(kind, names_array, RECENT_FUNC_NUM-1);

    // Remove functions that do not exist. They can be old functions, user's private modules of whatever. */
    guint nn = 0;
    for (guint i = 0; i < n; i++) {
        const gchar *name = names_array[i];
        gboolean exists = FALSE;
        if (data_kind == GWY_FILE_IMAGE)
            exists = gwy_image_func_exists(name);
        else if (data_kind == GWY_FILE_GRAPH)
            exists = gwy_graph_func_exists(name);
        else if (data_kind == GWY_FILE_VOLUME)
            exists = gwy_volume_func_exists(name);
        else if (data_kind == GWY_FILE_XYZ)
            exists = gwy_xyz_func_exists(name);
        else if (data_kind == GWY_FILE_CMAP)
            exists = gwy_cmap_func_exists(name);

        if (exists)
            names_array[nn++] = names_array[i];
    }
    n = nn;

    if (n)
        g_ptr_array_set_size(names, n+1);
    else
        g_ptr_array_set_size(names, 0);
}

static const gchar*
get_func_info(const gchar *name, GwyDataKind data_kind,
              GwyMenuSensFlags *sensflags, GwyRunModeFlags *runtypes, const gchar **icon_name)
{
    if (data_kind == GWY_FILE_IMAGE) {
        *sensflags = gwy_image_func_get_sensitivity_mask(name);
        *runtypes = gwy_image_func_get_run_types(name);
        *icon_name = gwy_image_func_get_icon_name(name);
        return gwy_image_func_get_menu_path(name);
    }
    if (data_kind == GWY_FILE_GRAPH) {
        *sensflags = gwy_graph_func_get_sensitivity_mask(name);
        *runtypes = GWY_RUN_IMMEDIATE;
        *icon_name = gwy_graph_func_get_icon_name(name);
        return gwy_graph_func_get_menu_path(name);
    }
    if (data_kind == GWY_FILE_VOLUME) {
        *sensflags = gwy_volume_func_get_sensitivity_mask(name);
        *runtypes = gwy_volume_func_get_run_types(name);
        *icon_name = gwy_volume_func_get_icon_name(name);
        return gwy_volume_func_get_menu_path(name);
    }
    if (data_kind == GWY_FILE_XYZ) {
        *sensflags = gwy_xyz_func_get_sensitivity_mask(name);
        *runtypes = gwy_xyz_func_get_run_types(name);
        *icon_name = gwy_xyz_func_get_icon_name(name);
        return gwy_xyz_func_get_menu_path(name);
    }
    if (data_kind == GWY_FILE_CMAP) {
        *sensflags = gwy_cmap_func_get_sensitivity_mask(name);
        *runtypes = gwy_cmap_func_get_run_types(name);
        *icon_name = gwy_cmap_func_get_icon_name(name);
        return gwy_cmap_func_get_menu_path(name);
    }
    g_assert_not_reached();
    return NULL;
}

static void
update_last_func_item(GtkWidget *widget, const gchar *name, const gchar *funcname,
                      GwySensitivityGroup *sens_group, GwyMenuSensFlags sensflags)
{
    gchar *labeltext = g_strconcat(name, " (", funcname, ")", NULL);
    gtk_menu_item_set_label(GTK_MENU_ITEM(widget), labeltext);
    g_free(labeltext);

    /* The first time we go through this, the widget is still initially insensitive and not in any sensitivity
     * group. So add it instead. The group will initialise its sensitivity as needed. */
    if (gwy_sensitivity_group_contains_widget(sens_group, widget))
        gwy_sensitivity_group_set_widget_mask(sens_group, widget, sensflags);
    else
        gwy_sensitivity_group_add_widget(sens_group, widget, sensflags);
}

static void
update_recent_menu(GtkMenuShell *shell,
                   GPtrArray *names, GwyDataKind data_kind)
{
    if (!names || !names->len)
        return;
    GwySensitivityGroup *sens_group = g_object_get_data(G_OBJECT(shell), "gwy-menu-sens-group");
    GList *children = gtk_container_get_children(GTK_CONTAINER(shell));
    GList *l = children;
    const gchar *firstname = g_ptr_array_index(names, 0);

    // If no function has been run in this sesssion, there can be functions prefilled from global history, but no
    // Re-run last.
    if (firstname) {
        GwyMenuSensFlags sensflags;
        GwyRunModeFlags runtypes;
        const gchar *icon_name;
        const gchar *menu_path = get_func_info(firstname, data_kind, &sensflags, &runtypes, &icon_name);
        g_return_if_fail(menu_path);
        const gchar *last_component = strrchr(menu_path, '/');
        last_component = (last_component ? last_component+1 : menu_path);
        gchar *canlabel = gwy_strip_menu_item_label(g_strdup(last_component));

        update_last_func_item(GTK_WIDGET(l->data), _("Repeat"), canlabel, sens_group, sensflags);

        l = g_list_next(l);
        update_last_func_item(GTK_WIDGET(l->data), _("Re-show"), canlabel, sens_group, sensflags);

        l = g_list_next(l);
        g_free(canlabel);
    }
    else
        l = g_list_next(g_list_next(l));

    if (names->len == 1)
        return;

    /* When we get here and there is nothing more in the menu, add the separator because it is not there either. */
    if (!l) {
        GtkWidget *item = gtk_separator_menu_item_new();
        gtk_menu_shell_append(shell, item);
        gtk_widget_show_all(item);
    }
    else
        l = g_list_next(l);

    for (guint i = 1; i < names->len; i++) {
        const gchar *name = g_ptr_array_index(names, i);
        GwyMenuSensFlags sensflags;
        GwyRunModeFlags runtypes;
        const gchar *icon_name;
        const gchar *menu_path = get_func_info(name, data_kind, &sensflags, &runtypes, &icon_name);
        const gchar *last_component = strrchr(menu_path, '/');
        last_component = (last_component ? last_component+1 : menu_path);
        if (l) {
            GtkWidget *item = (GtkWidget*)l->data;
            gwy_sensitivity_group_set_widget_mask(sens_group, item, sensflags);
            gtk_menu_item_set_label(GTK_MENU_ITEM(item), last_component);
            gwy_set_image_menu_item_icon(GTK_MENU_ITEM(item), icon_name);
            l = g_list_next(l);
        }
        else {
            GtkWidget *item = gwy_create_image_menu_item(last_component, icon_name, FALSE);
            gwy_sensitivity_group_add_widget(sens_group, item, sensflags);
            gtk_menu_shell_append(shell, item);
            gtk_widget_show_all(item);
            // Counting on GWY_RUN_NONE = 0 here and not adding any flags.
            g_signal_connect(item, "activate", G_CALLBACK(run_recent), GUINT_TO_POINTER(i));
        }
    }

    // There can be an extra item when we move a prefilled item to the Re-run position.
    while (l) {
        gtk_widget_destroy(GTK_WIDGET(l->data));
        l = g_list_next(l);
    }

    g_list_free(children);
}

/**
 * gwy_app_sensitivity_get_group:
 *
 * Gets the application-wide widget sensitvity group.
 *
 * The flags to be used with this sensitvity group are defined in #GwyMenuSensFlags.
 *
 * Returns: The global sensitvity group instead.  No reference is added, you can add yours, but the returned object
 *          will exist to the end of program anyway.
 **/
GwySensitivityGroup*
gwy_app_sensitivity_get_group(void)
{
    /* This reference is never released. */
    if (!app_sensgroup)
        app_sensgroup = gwy_sensitivity_group_new();

    return app_sensgroup;
}

/**
 * gwy_app_sensitivity_add_widget:
 * @widget: Widget to add.
 * @mask: Which flags the widget is sensitive to.
 *
 * Adds a widget to the application-wide widget sensitvity group.
 *
 * The semantics of this function is the same as gwy_sensitivity_group_add_widget() (in fact, it's a simple wrapper
 * around it).
 **/
void
gwy_app_sensitivity_add_widget(GtkWidget *widget,
                               GwyMenuSensFlags mask)
{
    gwy_sensitivity_group_add_widget(gwy_app_sensitivity_get_group(), widget, mask);
}

/**
 * gwy_app_sensitivity_set_state:
 * @affected_mask: Which bits in @state to copy to state.
 * @state: The new state (masked with @affected_mask).
 *
 * Sets the state of application-wide widget sensitvity group.
 *
 * The semantics of this function is the same as gwy_sensitivity_group_set_state() (in fact, it's a simple wrapper
 * around it).
 **/
void
gwy_app_sensitivity_set_state(GwyMenuSensFlags affected_mask,
                              GwyMenuSensFlags state)
{
    gwy_sensitivity_group_set_state(gwy_app_sensitivity_get_group(), affected_mask, state);
}

/**
 * SECTION: menu
 * @title: menu
 * @short_description: Menu and sensitivity functions
 *
 * Menu and toolbox item sensitivity is updated by main application whenever its state changes.  Possible states that
 * may affect widget sesitivity are defined in #GwyMenuSensFlags.
 **/

/**
 * GwyMenuSensFlags:
 * @GWY_MENU_FLAG_FILE: A file is open, with any type of data.
 * @GWY_MENU_FLAG_IMAGE: An image window is active on which image functions can be used.
 * @GWY_MENU_FLAG_IMAGE_MASK: There is a mask on the image.
 * @GWY_MENU_FLAG_IMAGE_SHOW: There is a presentation on the image.
 * @GWY_MENU_FLAG_UNDO: There is something to undo (for the current window).
 * @GWY_MENU_FLAG_REDO: There is something to redo (for the current window).
 * @GWY_MENU_FLAG_GRAPH: A graph window is active on which graph functions can be used.
 * @GWY_MENU_FLAG_GRAPH_CURVE: There current graph window contains at least one curve. This ensures a graph function
 *                             will not be run on an empty graph.
 * @GWY_MENU_FLAG_SPECTRA: A spectra graph window is active on which spectra functions can be used.
 * @GWY_MENU_FLAG_GL: An OpenGL 3D view is present.
 * @GWY_MENU_FLAG_VOLUME: A volume image window is active on which volume data functions can be used.
 * @GWY_MENU_FLAG_XYZ: An XYZ image window is active on which XYZ data functions can be used.
 * @GWY_MENU_FLAG_CMAP: A curve map image window is active on which curve map functions can be used.
 *
 * Sensitivity flags indicating the availability of various data kinds.
 *
 * They represent various application states that may be preconditions for widgets to become sensitive.
 **/

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