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

#include "config.h"
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyui/icons.h"
#include "libgwyui/data-window.h"
#include "libgwyui/utils.h"
#include "libgwyapp/gwyapp.h"
#include "libgwyapp/sanity.h"

#include "gwyddion/gwyddion.h"
#include "gwyddion/toolbox.h"
#include "gwyddion/mac_integration.h"

#define CTRL GDK_CONTROL_MASK
#define CTRLSH (GDK_CONTROL_MASK | GDK_SHIFT_MASK)

enum {
    DND_TARGET_STRING = 1,
};

typedef struct {
    GtkBox *box;
    GtkWidget *group;
    gint width;
    gint pos;
    GtkRadioButton *first_tool;
    const gchar *first_tool_func;
    GtkRadioButton *current_tool;
    const gchar *current_tool_func;
    GPtrArray *unseen_tools;
    GtkAccelGroup *accel_group;
    gboolean seen_unseen_tools;
} GwyAppToolboxBuilder;

typedef struct {
    GCallback callback;
    GQuark func;
    GQuark icon_name;
    GwyAppActionType type;
    GwyRunModeFlags mode;
    GwyMenuSensFlags sens;
    const gchar *tooltip;
} Action;

/*
 * FIXME GTK3: Should we use GMenu and GAction and GtkApplication and all that circus? They are verbose and do not do
 * a good job at information locality, e.g. accelerators have to be added later.
 *
 * And we do not actually want to run any actions over DBus and so on. Direct file operations are done locally,
 * without ever trying to contact other instances. Opening files in a running instance only needs a very trivial
 * GApplication usage.
 *
 * So, maybe later.
 */
typedef struct {
    const gchar *label;
    const gchar *icon_name;
    guint accel_key;
    GdkModifierType accel_mods;
    gpointer function;
    GwyMenuSensFlags sensitivity_flags;
    gint is_checkbox;
    GtkWidget *widget;
} SimpleMenuItem;

static GtkWidget*   create_info_menu               (GtkAccelGroup *accel_group);
static GtkWidget*   create_file_menu               (GtkAccelGroup *accel_group);
static GtkWidget*   create_edit_menu               (GtkAccelGroup *accel_group);
static gboolean     toolbox_mapped                 (GtkWidget *toolbox);
static gboolean     toolbox_key_pressed            (GtkWidget *toolbox,
                                                    GdkEventKey *event);
static void         finalise_toolbox               (GtkWidget *toolbox);
static gboolean     gwy_toolbox_fill_builtin_action(Action *action);
static const gchar* gwy_toolbox_builtin_accel_path (const gchar *name);
static void         gwy_app_toolbox_showhide       (GtkWidget *expander);
static void         show_user_guide                (void);
static void         show_message_log               (void);
static GtkWindow*   create_message_log_window      (void);
static void         toolbox_dnd_data_received      (GtkWidget *widget,
                                                    GdkDragContext *context,
                                                    gint x,
                                                    gint y,
                                                    GtkSelectionData *data,
                                                    guint info,
                                                    guint time_,
                                                    gpointer user_data);
static void         delete_app_window              (void);
static void         action_undo                    (void);
static void         action_redo                    (void);
static void         remove_all_logs                (void);
static void         toggle_logging_enabled         (GtkCheckMenuItem *item);
static void         gwy_app_tool_use               (const gchar *toolname,
                                                    GtkToggleButton *button);
static void         edit_default_mask_color        (void);
static void         action_display_3d              (void);

static gulong toolbox_map_event_id = 0;

/* Translatability hack, intltool seems overkill at this point. */
#define GWY_TOOLBOX_IGNORE(x) /* */
GWY_TOOLBOX_IGNORE((_("Image Process"), _("Tools")))

static void
dummy_submenu_marker(void)
{
}

static void
toolbox_start_group(GwyAppToolboxBuilder *builder,
                    const GwyToolboxGroupSpec *gspec,
                    guint *groupno)
{
    GwyDict *settings;
    GtkWidget *expander;
    gboolean visible = TRUE;
    gchar *s, *key;
    const gchar *translated_name;
    GQuark quark;

    builder->group = gtk_grid_new();
    gtk_grid_set_row_homogeneous(GTK_GRID(builder->group), TRUE);
    gtk_grid_set_column_homogeneous(GTK_GRID(builder->group), TRUE);
    gtk_widget_set_halign(builder->group, GTK_ALIGN_START);
    gtk_widget_set_hexpand(builder->group, FALSE);

    settings = gwy_app_settings_get();
    key = g_strconcat("/app/toolbox/visible/", g_quark_to_string(gspec->id), NULL);
    quark = g_quark_from_string(key);
    g_free(key);
    gwy_dict_gis_boolean(settings, quark, &visible);

    translated_name = gspec->translatable ? _(gspec->name) : gspec->name;
    s = g_strconcat("<small>", translated_name, "</small>", NULL);
    expander = gtk_expander_new(s);
    gtk_expander_set_use_markup(GTK_EXPANDER(expander), TRUE);
    g_free(s);
    g_object_set_data(G_OBJECT(expander), "key", GUINT_TO_POINTER(quark));
    g_object_set_data(G_OBJECT(expander), "gwy-toolbox-ui-constructed", GUINT_TO_POINTER(TRUE));
    gtk_container_add(GTK_CONTAINER(expander), builder->group);
    gtk_expander_set_expanded(GTK_EXPANDER(expander), visible);
    gtk_box_pack_start(builder->box, expander, FALSE, FALSE, 0);
    gtk_box_reorder_child(builder->box, expander, *groupno);
    (*groupno)++;
    g_signal_connect_after(expander, "activate", G_CALLBACK(gwy_app_toolbox_showhide), NULL);
    builder->pos = 0;
}

static GtkWidget*
toolbox_make_tool_button(GwyAppToolboxBuilder *builder,
                         GwyToolClass *tool_class,
                         Action *action)
{
    GtkWidget *button;
    const gchar *name, *icon_name;
    gchar *accel_path;

    icon_name = gwy_tool_class_get_icon_name(tool_class);
    action->icon_name = g_quark_from_static_string(icon_name);
    button = gtk_radio_button_new_from_widget(builder->first_tool);
    name = g_type_name(G_TYPE_FROM_CLASS(tool_class));
    action->func = g_quark_from_static_string(name);
    if (!builder->first_tool) {
        builder->first_tool = GTK_RADIO_BUTTON(button);
        builder->first_tool_func = name;
    }
    if (builder->current_tool_func && gwy_strequal(name, builder->current_tool_func))
        builder->current_tool = GTK_RADIO_BUTTON(button);
    gtk_toggle_button_set_mode(GTK_TOGGLE_BUTTON(button), FALSE);
    accel_path = g_strconcat("<tool>/", name, NULL);
    gtk_widget_set_accel_path(button, accel_path, builder->accel_group);
    g_free(accel_path);

    action->tooltip = gwy_tool_class_get_tooltip(tool_class);

    return button;
}

static void
toolbox_action_run(Action *action, GtkButton *button)
{
    const gchar *name = g_quark_to_string(action->func);

    if (action->type == GWY_APP_ACTION_TYPE_BUILTIN)
        action->callback();
    else if (action->type == GWY_APP_ACTION_TYPE_TOOL)
        gwy_app_tool_use(name, GTK_TOGGLE_BUTTON(button));
    else if (action->type == GWY_APP_ACTION_TYPE_IMAGE)
        gwy_app_run_image_func_in_mode(name, action->mode);
    else if (action->type == GWY_APP_ACTION_TYPE_GRAPH)
        gwy_app_run_graph_func(name);
    else if (action->type == GWY_APP_ACTION_TYPE_SYNTH)
        gwy_app_run_synth_func_in_mode(name, action->mode);
    else if (action->type == GWY_APP_ACTION_TYPE_VOLUME)
        gwy_app_run_volume_func_in_mode(name, action->mode);
    else if (action->type == GWY_APP_ACTION_TYPE_XYZ)
        gwy_app_run_xyz_func_in_mode(name, action->mode);
    else if (action->type == GWY_APP_ACTION_TYPE_CMAP)
        gwy_app_run_cmap_func_in_mode(name, action->mode);
    else {
        g_assert_not_reached();
    }
}

static void
check_run_mode(GwyAppActionType type, const gchar *name,
               GwyRunModeFlags available_modes, GwyRunModeFlags *mode)
{
    GwyRunModeFlags first_mode = GWY_RUN_INTERACTIVE;

    if (available_modes & GWY_RUN_INTERACTIVE)
        first_mode = GWY_RUN_INTERACTIVE;
    else if (available_modes & GWY_RUN_IMMEDIATE)
        first_mode = GWY_RUN_IMMEDIATE;
    else if (available_modes & GWY_RUN_NONINTERACTIVE)
        first_mode = GWY_RUN_NONINTERACTIVE;

    if (!*mode) {
        *mode = first_mode;
        return;
    }

    if (available_modes & *mode)
        return;

    g_warning("Function %s::%s cannot be run in mode %d", gwy_toolbox_action_type_name(type), name, *mode);
    *mode = first_mode;
}

static gboolean
toolbox_start_item(GwyAppToolboxBuilder *builder,
                   const GwyToolboxItemSpec *ispec)
{
    const gchar *func = NULL, *icon_name = NULL, *accel_path;
    GtkWidget *button = NULL;
    GwyToolClass *tool_class;
    GType gtype;
    Action action, *a;
    guint i;

    g_return_val_if_fail(builder->group, FALSE);

    gwy_clear1(action);
    action.type = ispec->type;
    action.func = ispec->function;
    action.mode = ispec->mode;
    action.icon_name = ispec->icon;
    action.sens = -1;

    func = action.func ? g_quark_to_string(action.func) : NULL;

    switch (action.type) {
        case GWY_APP_ACTION_TYPE_PLACEHOLDER:
        builder->pos++;
        return TRUE;
        break;

        case GWY_APP_ACTION_TYPE_BUILTIN:
        if (!gwy_toolbox_fill_builtin_action(&action)) {
            g_warning("Function builtin::%s does not exist", func);
            return FALSE;
        }
        if (action.mode)
            g_warning("Function builtin::%s does not have run modes", func);
        break;

        case GWY_APP_ACTION_TYPE_IMAGE:
        if (!gwy_image_func_exists(func)) {
            g_warning("Function image::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_image_func_get_icon_name(func);
        action.tooltip = gwy_image_func_get_tooltip(func);
        action.sens = gwy_image_func_get_sensitivity_mask(func);
        check_run_mode(action.type, func, gwy_image_func_get_run_types(func), &action.mode);
        break;

        case GWY_APP_ACTION_TYPE_GRAPH:
        if (!gwy_graph_func_exists(func)) {
            g_warning("Function graph::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_graph_func_get_icon_name(func);
        action.tooltip = gwy_graph_func_get_tooltip(func);
        action.sens = gwy_graph_func_get_sensitivity_mask(func);
        if (action.mode)
            g_warning("Function graph::%s does not have run modes", func);
        break;

        case GWY_APP_ACTION_TYPE_SYNTH:
        if (!gwy_synth_func_exists(func)) {
            g_warning("Function cmap::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_synth_func_get_icon_name(func);
        action.tooltip = gwy_synth_func_get_tooltip(func);
        action.sens = 0;
        check_run_mode(action.type, func, gwy_synth_func_get_run_modes(func), &action.mode);
        break;

        case GWY_APP_ACTION_TYPE_VOLUME:
        if (!gwy_volume_func_exists(func)) {
            g_warning("Function volume::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_volume_func_get_icon_name(func);
        action.tooltip = gwy_volume_func_get_tooltip(func);
        action.sens = gwy_volume_func_get_sensitivity_mask(func);
        check_run_mode(action.type, func, gwy_volume_func_get_run_types(func), &action.mode);
        break;

        case GWY_APP_ACTION_TYPE_XYZ:
        if (!gwy_xyz_func_exists(func)) {
            g_warning("Function xyz::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_xyz_func_get_icon_name(func);
        action.tooltip = gwy_xyz_func_get_tooltip(func);
        action.sens = gwy_xyz_func_get_sensitivity_mask(func);
        check_run_mode(action.type, func, gwy_xyz_func_get_run_types(func), &action.mode);
        break;

        case GWY_APP_ACTION_TYPE_CMAP:
        if (!gwy_cmap_func_exists(func)) {
            g_warning("Function cmap::%s does not exist", func);
            return FALSE;
        }
        icon_name = gwy_cmap_func_get_icon_name(func);
        action.tooltip = gwy_cmap_func_get_tooltip(func);
        action.sens = gwy_cmap_func_get_sensitivity_mask(func);
        check_run_mode(action.type, func, gwy_cmap_func_get_run_types(func), &action.mode);
        break;

        case GWY_APP_ACTION_TYPE_TOOL:
        /* Handle unseen tools */
        if (!func) {
            if (builder->seen_unseen_tools) {
                g_warning("Unseen tools placeholder present multiple times.");
                return FALSE;
            }
            for (i = 0; i < builder->unseen_tools->len; i++) {
                const gchar *name = g_ptr_array_index(builder->unseen_tools, i);
                GwyToolboxItemSpec iispec;

                gwy_clear1(iispec);
                iispec.type = GWY_APP_ACTION_TYPE_TOOL;
                iispec.function = g_quark_from_static_string(name);
                toolbox_start_item(builder, &iispec);
            }
            builder->seen_unseen_tools = TRUE;
            return TRUE;
        }
        if (!(gtype = g_type_from_name(func))) {
            g_warning("Function tool::%s does not exist", func);
            return FALSE;
        }
        tool_class = g_type_class_peek(gtype);
        if (!GWY_IS_TOOL_CLASS(tool_class)) {
            g_warning("Type %s is not a GwyTool", func);
            return FALSE;
        }
        button = toolbox_make_tool_button(builder, tool_class, &action);
        break;

        default:
        g_return_val_if_reached(FALSE);
        break;
    }

    if (!button)
        button = gtk_button_new();

    if (!action.icon_name && icon_name)
        action.icon_name = g_quark_from_string(icon_name);

    if (action.type == GWY_APP_ACTION_TYPE_BUILTIN) {
        accel_path = gwy_toolbox_builtin_accel_path(func);
        if (accel_path)
            gtk_widget_set_accel_path(button, accel_path, builder->accel_group);
    }

    if (!action.icon_name) {
        g_warning("Function %s::%s has no icon set", gwy_toolbox_action_type_name(action.type), func);
        icon_name = GWY_ICON_GTK_MISSING_IMAGE;
        action.icon_name = g_quark_from_static_string(icon_name);
    }
    else {
        icon_name = g_quark_to_string(action.icon_name);
        if (!gtk_icon_theme_has_icon(gtk_icon_theme_get_default(), icon_name)) {
            g_warning("Function %s::%s icon %s not found",
                      gwy_toolbox_action_type_name(action.type), func, g_quark_to_string(action.icon_name));
            icon_name = GWY_ICON_GTK_MISSING_IMAGE;
            action.icon_name = g_quark_from_static_string(icon_name);
        }
    }

    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    gtk_widget_set_can_default(button, FALSE);
    gtk_container_set_border_width(GTK_CONTAINER(button), 0);
    gtk_widget_set_name(button, "toolboxbutton");
    gtk_grid_attach(GTK_GRID(builder->group), button, builder->pos % builder->width, builder->pos/builder->width, 1, 1);
    gtk_container_add(GTK_CONTAINER(button), gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_LARGE_TOOLBAR));
    if (action.tooltip)
        gtk_widget_set_tooltip_text(button, _(action.tooltip));

    a = g_memdup2(&action, sizeof(Action));
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(toolbox_action_run), a);
    g_signal_connect_swapped(button, "destroy", G_CALLBACK(g_free), a);

    if (action.sens != (GwyMenuSensFlags)-1)
        gwy_app_sensitivity_add_widget(button, action.sens);

    builder->pos++;

    return TRUE;
}

static void
gather_tools(const gchar *name, gpointer user_data)
{
    GPtrArray *tools = (GPtrArray*)user_data;
    g_ptr_array_add(tools, (gpointer)name);
}

/* XXX: Move to toolbox-spec probably.  It can keep the file name for itself... */
static void
remove_seen_unseen_tools(GwyAppToolboxBuilder *builder,
                         const GwyToolboxSpec *spec)
{
    GPtrArray *unseen_tools = builder->unseen_tools;
    const GwyToolboxGroupSpec *gspec;
    const GwyToolboxItemSpec *ispec;
    GArray *group, *item;
    const gchar *name;
    guint i, j, k;

    group = spec->group;
    for (i = 0; i < group->len; i++) {
        gspec = &g_array_index(group, GwyToolboxGroupSpec, i);
        item = gspec->item;
        for (j = 0; j < item->len; j++) {
            ispec = &g_array_index(item, GwyToolboxItemSpec, j);
            if (!ispec->function || ispec->type != GWY_APP_ACTION_TYPE_TOOL)
                continue;

            name = g_quark_to_string(ispec->function);
            for (k = 0; k < unseen_tools->len; k++) {
                if (gwy_strequal(name, g_ptr_array_index(unseen_tools, k))) {
                    g_ptr_array_remove_index(unseen_tools, k);
                    break;
                }
            }
        }
    }
}

static void
closed_expander_realized(GtkExpander *expander, gulong *p)
{
    g_signal_handler_disconnect(expander, *p);
    gtk_expander_set_expanded(expander, FALSE);
    g_free(p);
}

static void
gwy_app_toolbox_build(GwyToolboxSpec *spec,
                      GtkBox *vbox,
                      GtkAccelGroup *accel_group)
{
    GwyAppToolboxBuilder builder;
    const GwyToolboxGroupSpec *gspec;
    const GwyToolboxItemSpec *ispec;
    GtkWidget *important_widget = NULL;

    gwy_clear1(builder);
    builder.width = spec->width >= 8 ? spec->width : 8;
    builder.box = vbox;
    builder.unseen_tools = g_ptr_array_new();
    builder.accel_group = accel_group;
    builder.current_tool_func = gwy_app_current_tool_name();

    gwy_tool_func_foreach(gather_tools, builder.unseen_tools);
    remove_seen_unseen_tools(&builder, spec);

    GArray *group = spec->group;
    guint groupno = 1;
    for (guint i = 0; i < group->len; i++) {
        gspec = &g_array_index(group, GwyToolboxGroupSpec, i);
        toolbox_start_group(&builder, gspec, &groupno);
        GArray *item = gspec->item;
        for (guint j = 0; j < item->len; j++) {
            ispec = &g_array_index(item, GwyToolboxItemSpec, j);
            /* When the construction fails remove the item also from the spec so *if* we edit and save the spec it is
             * corrected. */
            if (!toolbox_start_item(&builder, ispec))
                gwy_toolbox_spec_remove_item(spec, i, j);
        }
        builder.group = NULL;
    }

    if (builder.current_tool) {
        guint handler_id = g_signal_handler_find(builder.current_tool, G_SIGNAL_MATCH_FUNC,
                                                 0, 0, NULL, toolbox_action_run, NULL);
        g_signal_handler_block(builder.current_tool, handler_id);
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(builder.current_tool), TRUE);
        g_signal_handler_unblock(builder.current_tool, handler_id);
        important_widget = GTK_WIDGET(builder.current_tool);
    }
    else if (builder.first_tool) {
        gwy_app_switch_tool(builder.first_tool_func);
        gtk_widget_grab_focus(GTK_WIDGET(builder.first_tool));
        important_widget = GTK_WIDGET(builder.first_tool);
    }

    /* XXX: Start the expander with the first tool as expanded and collapse it only after it has been realised.
     * Otherwise we get CRITICAL GTK+ error gtk_widget_event: assertion 'WIDGET_REALIZED_FOR_EVENT (widget, event)'
     * failed */
    if (important_widget) {
        GtkExpander *expander = GTK_EXPANDER(gtk_widget_get_ancestor(important_widget, GTK_TYPE_EXPANDER));
        if (!gtk_expander_get_expanded(GTK_EXPANDER(expander))) {
            gulong *p = g_new(gulong, 1);

            gtk_expander_set_expanded(expander, TRUE);
            *p = g_signal_connect_after(expander, "realize", G_CALLBACK(closed_expander_realized), p);
        }
    }

    g_ptr_array_free(builder.unseen_tools, TRUE);
}

static void
toolbox_add_menu(GtkMenuShell *menushell,
                 GtkWidget *submenu,
                 const gchar *item_label)
{
    GtkWidget *item = gtk_menu_item_new_with_mnemonic(item_label);
    gtk_menu_shell_append(menushell, item);
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), submenu);
}

GtkWidget*
create_toolbox_window(void)
{
    static GtkTargetEntry dnd_target_table[] = {
        { "STRING",        0, DND_TARGET_STRING, },
        { "text/plain",    0, DND_TARGET_STRING, },
        { "text/uri-list", 0, DND_TARGET_STRING, },
    };

    GtkWidget *toolbox = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gwy_app_init_widget_styles(gtk_widget_get_screen(toolbox));
    GtkWindow *window = GTK_WINDOW(toolbox);
    gtk_window_set_title(window, g_get_application_name());
    gtk_window_set_role(window, GWY_TOOLBOX_WM_ROLE);
    // FIXME: We need the data browser part to be resizeable at least vertically. Allow free resizing for now, even
    // though it we do not reorganise the icons. Use GtkFlowBox? It has some weird quirks like its size requests and
    // the fact that everything is inside a FlowBoxChild, which itself can also have focus and be activated, not
    // playing nicely with buttons. However, something like the following seems to be working almost as intended,
    // including sane keyboard navigation between the buttons.
    //
    // #!/usr/bin/python3
    // import gi
    // gi.require_version('Gtk', '3.0')
    // from gi.repository import Gtk
    //
    // win = Gtk.Window()
    // vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
    // win.add(vbox)
    //
    // fb = Gtk.FlowBox(min_children_per_line=5, max_children_per_line=100, activate_on_single_click=True,
    //                  homogeneous=True,
    //                  selection_mode=Gtk.SelectionMode.NONE)
    //
    // vbox.pack_start(fb, False, False, 0)
    // vbox.pack_end(Gtk.Label(label='The rest'), True, True, 0)
    //
    // def clicked(button, i):
    //     print('button', i, 'clicked')
    //
    // for i in range(15):
    //     button = Gtk.Button(label='%d' % (i+1))
    //     button.connect('clicked', clicked, i+1)
    //     fb.add(button)
    //
    // for c in fb.get_children():
    //     c.set_can_focus(False)
    //
    // def app_quit(*args):
    //     Gtk.main_quit()
    //
    // win.set_default_size(300,200)
    // win.show_all()
    // win.connect('destroy', app_quit)
    // Gtk.main()

    //gtk_window_set_resizable(window, FALSE);
    gwy_help_add_to_window(window, "main-window", NULL, GWY_HELP_DEFAULT);
    // This also sets up the main accel group. Do not do it again.
    gwy_app_set_main_window(window);

    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_container_add(GTK_CONTAINER(toolbox), vbox);

    GtkWidget *menubar = gtk_menu_bar_new();
    gtk_container_add(GTK_CONTAINER(vbox), menubar);
    gtk_widget_set_name(menubar, "toolboxmenubar");

    GtkMenuShell *shell = GTK_MENU_SHELL(menubar);
    GtkAccelGroup *accel_group = gwy_app_main_accel_group();
    toolbox_add_menu(shell, create_file_menu(accel_group), _("_File"));
    toolbox_add_menu(shell, create_edit_menu(accel_group), _("_Edit"));

    GwySensitivityGroup *sens_group = gwy_app_sensitivity_get_group();
    GtkWidget *menu = gwy_app_synth_menu(sens_group);
    toolbox_add_menu(shell, menu, _("S_ynthetic"));
    GtkAccelGroup *menu_accel_group = gtk_menu_get_accel_group(GTK_MENU(menu));
    if (menu_accel_group)
        gtk_window_add_accel_group(window, menu_accel_group);
    toolbox_add_menu(shell, create_info_menu(accel_group), _("_Info"));

    /***************************************************************/

    GwyToolboxSpec *spec = gwy_parse_toolbox_ui(FALSE);
    if (spec) {
        gwy_app_toolbox_build(spec, GTK_BOX(vbox), accel_group);
        g_object_set_data(G_OBJECT(toolbox), "gwy-app-toolbox-spec", spec);
    }

    gtk_box_pack_start(GTK_BOX(vbox), gwy_data_browser_widget(), TRUE, TRUE, 0);

    /***************************************************************/
    gtk_drag_dest_set(toolbox, GTK_DEST_DEFAULT_ALL, dnd_target_table, G_N_ELEMENTS(dnd_target_table), GDK_ACTION_COPY);
    g_signal_connect(toolbox, "drag-data-received", G_CALLBACK(toolbox_dnd_data_received), NULL);

    /***************************************************************/
    /* XXX */
    g_signal_connect(toolbox, "delete-event", G_CALLBACK(gwy_app_quit), NULL);
    g_signal_connect(toolbox, "key-press-event", G_CALLBACK(toolbox_key_pressed), NULL);
    g_signal_connect(toolbox, "destroy", G_CALLBACK(finalise_toolbox), NULL);
    toolbox_map_event_id = g_signal_connect_after(toolbox, "map-event", G_CALLBACK(toolbox_mapped), NULL);
    gtk_widget_show_all(toolbox);

    gwyddion_mac_build_menu(vbox);

    return toolbox;
}

static gboolean
toolbox_mapped(GtkWidget *toolbox)
{
    g_return_val_if_fail(toolbox_map_event_id, FALSE);
    g_signal_handler_disconnect(toolbox, toolbox_map_event_id);
    toolbox_map_event_id = 0;
    return FALSE;
}

static gboolean
toolbox_key_pressed(G_GNUC_UNUSED GtkWidget *toolbox, GdkEventKey *event)
{
    GwyTool *current_tool;

    if (event->keyval != GDK_KEY_F3 || (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)))
        return FALSE;

    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 void
finalise_toolbox(GtkWidget *toolbox)
{
    GwyToolboxSpec *spec;

    if ((spec = g_object_get_data(G_OBJECT(toolbox), "gwy-app-toolbox-spec"))) {
        gwy_toolbox_spec_free(spec);
        g_object_set_data(G_OBJECT(toolbox), "gwy-app-toolbox-spec", NULL);
    }
}

void
gwy_toolbox_rebuild_to_spec(GwyToolboxSpec *spec)
{
    GtkWindow* toolbox = gwy_app_main_window();
    GwyToolboxSpec *oldspec = g_object_get_data(G_OBJECT(toolbox), "gwy-app-toolbox-spec");
    if (oldspec != spec) {
        gwy_toolbox_spec_free(oldspec);
        g_object_set_data(G_OBJECT(toolbox), "gwy-app-toolbox-spec", spec);
    }

    GtkBox *vbox = GTK_BOX(gtk_bin_get_child(GTK_BIN(toolbox)));
    GList *children = gtk_container_get_children(GTK_CONTAINER(vbox));

    for (GList *l = children; l; l = g_list_next(l)) {
        if (g_object_get_data(G_OBJECT(l->data), "gwy-toolbox-ui-constructed"))
            gtk_widget_destroy(GTK_WIDGET(l->data));
    }
    g_list_free(children);

    gwy_app_toolbox_build(spec, vbox, gwy_app_main_accel_group());
    gtk_widget_show_all(GTK_WIDGET(vbox));
}

const GwyToolboxBuiltinSpec*
gwy_toolbox_get_builtins(guint *nspec)
{
    static const GwyToolboxBuiltinSpec spec[] = {
        {
            "display_3d", GWY_ICON_3D_BASE, &action_display_3d,
            N_("Display a 3D view of data"), N_("Display a 3D view of data"),
        },
        {
            "undo", GWY_ICON_GTK_UNDO, &action_undo,
            N_("Undo"), N_("Undo last action"),
        },
        {
            "redo", GWY_ICON_GTK_REDO, &action_redo,
            N_("Redo"), N_("Redo again last undone action"),
        },
    };

    *nspec = G_N_ELEMENTS(spec);
    return spec;
}

const GwyToolboxBuiltinSpec*
gwy_toolbox_find_builtin_spec(const gchar *name)
{
    const GwyToolboxBuiltinSpec* spec;
    guint i, n;

    spec = gwy_toolbox_get_builtins(&n);
    for (i = 0; i < n; i++) {
        if (gwy_strequal(name, spec[i].name))
            return spec + i;
    }
    return NULL;
}

static gboolean
gwy_toolbox_fill_builtin_action(Action *action)
{
    const GwyToolboxBuiltinSpec *spec;
    const gchar *name;

    name = g_quark_to_string(action->func);
    if (!(spec = gwy_toolbox_find_builtin_spec(name)))
        return FALSE;

    action->type = GWY_APP_ACTION_TYPE_BUILTIN;
    action->sens = GWY_MENU_FLAG_IMAGE;
    action->callback = spec->callback;
    action->tooltip = spec->tooltip;
    action->icon_name = g_quark_from_static_string(spec->icon_name);

    return TRUE;
}

static const gchar*
gwy_toolbox_builtin_accel_path(const gchar *name)
{
    static const gchar *paths[] = {
        "display_3d", "<builtin>/Display 3D",
    };
    guint i;

    for (i = 0; i < G_N_ELEMENTS(paths); i += 2) {
        if (gwy_strequal(paths[i], name))
            return paths[i+1];
    }
    return NULL;
}

/*************************************************************************/
static void
remove_underscore_and_dots(GString *str)
{
    guint i, j;
    gchar *label = str->str;

    for (i = j = 0; label[i]; i++) {
        label[j] = label[i];
        if (label[i] != '_' || label[i+1] == '_')
            j++;
    }
    /* If the label *ends* with an underscore, just kill it */
    g_string_truncate(str, j);

    if (g_str_has_suffix(str->str, "..."))
        g_string_truncate(str, str->len-3);
    else if (g_str_has_suffix(str->str, "…"))
        g_string_truncate(str, str->len-3);
}

static GtkWidget*
build_simple_menu(SimpleMenuItem *items, guint nitems,
                  const gchar *prefix,
                  GtkAccelGroup *accel_group)
{
    GtkWidget *menu = gtk_menu_new();
    GtkMenuShell *shell = GTK_MENU_SHELL(menu);
    GString *str = g_string_new(NULL);
    GwySensitivityGroup *sensgroup = gwy_app_sensitivity_get_group();
    gtk_menu_set_accel_group(GTK_MENU(menu), accel_group);

    for (guint i = 0; i < nitems; i++) {
        SimpleMenuItem *item = items + i;
        GtkWidget *menuitem = NULL;

        if (!item->function) {
            menuitem = gtk_separator_menu_item_new();
        }
        else if (item->is_checkbox) {
            g_assert(item->label);

            gboolean has_underscore = !!strchr(item->label, '_');
            if (has_underscore)
                menuitem = gtk_check_menu_item_new_with_mnemonic(_(item->label));
            else
                menuitem = gtk_check_menu_item_new_with_label(_(item->label));

            g_signal_connect(menuitem, "toggled", G_CALLBACK(item->function), NULL);
        }
        else {
            g_assert(item->label);

            gboolean has_underscore = !!strchr(item->label, '_');
            if (item->icon_name)
                menuitem = gwy_create_image_menu_item(_(item->label), item->icon_name, FALSE);
            else
                menuitem = gtk_menu_item_new_with_label(_(item->label));

            if (has_underscore)
                gtk_menu_item_set_use_underline(GTK_MENU_ITEM(menuitem), TRUE);
            if (item->function != dummy_submenu_marker)
                g_signal_connect(menuitem, "activate", G_CALLBACK(item->function), NULL);
        }

        if (item->label) {
            g_string_assign(str, item->label);
            remove_underscore_and_dots(str);
            g_string_prepend_c(str, '/');
            g_string_prepend(str, prefix);
            gtk_menu_item_set_accel_path(GTK_MENU_ITEM(menuitem), str->str);
            if (item->accel_key) {
                gtk_accel_map_add_entry(str->str, item->accel_key, item->accel_mods);
                gtk_accel_map_lock_path(str->str);
            }
        }

        gtk_menu_shell_append(shell, menuitem);
        item->widget = menuitem;

        if (item->sensitivity_flags)
            gwy_sensitivity_group_add_widget(sensgroup, menuitem, item->sensitivity_flags);
    }

    g_string_free(str, TRUE);

    return menu;
}

static GtkWidget*
create_info_menu(GtkAccelGroup *accel_group)
{
    static SimpleMenuItem menu_items[] = {
        { N_("Module _Browser"),   NULL,               0,          0, gwy_module_browser, 0, FALSE, NULL, },
        { N_("Program _Messages"), NULL,               0,          0, show_message_log,   0, FALSE, NULL, },
        { NULL,                    NULL,               0,          0, NULL,               0, FALSE, NULL, },
        { N_("_User Guide"),       GWY_ICON_GTK_HELP,  GDK_KEY_F1, 0, show_user_guide,    0, FALSE, NULL, },
        { N_("_About Gwyddion"),   GWY_ICON_GTK_ABOUT, 0,          0, show_about_dialog,      0, FALSE, NULL, },
    };

    return build_simple_menu(menu_items, G_N_ELEMENTS(menu_items), "<meta>", accel_group);
}

static GtkWidget*
create_file_menu(GtkAccelGroup *accel_group)
{
    static SimpleMenuItem menu_items[] = {
        { N_("_Open..."),         GWY_ICON_GTK_OPEN,    GDK_KEY_o, CTRL,   gwy_app_file_open,    0,                  FALSE, NULL },
        { N_("Open _Recent"),     NULL,                 0,         0,      dummy_submenu_marker, 0,                  FALSE, NULL },
        { N_("_Merge..."),        NULL,                 GDK_KEY_m, CTRLSH, gwy_app_file_merge,   GWY_MENU_FLAG_FILE, FALSE, NULL },
        { N_("_Save"),            GWY_ICON_GTK_SAVE,    GDK_KEY_s, CTRL,   gwy_app_file_save,    GWY_MENU_FLAG_FILE, FALSE, NULL },
        { N_("Save _As..."),      GWY_ICON_GTK_SAVE_AS, GDK_KEY_s, CTRLSH, gwy_app_file_save_as, GWY_MENU_FLAG_FILE, FALSE, NULL },
        { N_("_Close"),           GWY_ICON_GTK_CLOSE,   GDK_KEY_w, CTRL,   gwy_app_file_close,   GWY_MENU_FLAG_FILE, FALSE, NULL },
        { N_("Remo_ve All Logs"), NULL,                 0,         0,      remove_all_logs,      GWY_MENU_FLAG_FILE, FALSE, NULL },
        { NULL,                   NULL,                 0,         0,      NULL,                 0,                  FALSE, NULL },
        { N_("_Quit"),            GWY_ICON_GTK_QUIT,    GDK_KEY_q, CTRL,   delete_app_window,    0,                  FALSE, NULL },
    };

    GtkWidget *menu = build_simple_menu(menu_items, G_N_ELEMENTS(menu_items), "<file>", accel_group);
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_items[1].widget), gwy_app_menu_recent_files_get());

    return menu;
}

GtkWidget*
create_edit_menu(GtkAccelGroup *accel_group)
{
    static SimpleMenuItem menu_items[] = {
        { N_("_Undo"),                  GWY_ICON_GTK_UNDO,    GDK_KEY_z, CTRL, action_undo,                GWY_MENU_FLAG_UNDO, FALSE, NULL },
        { N_("_Redo"),                  GWY_ICON_GTK_REDO,    GDK_KEY_y, CTRL, action_redo,                GWY_MENU_FLAG_REDO, FALSE, NULL },
        { NULL,                         NULL,                 0,         0,    NULL,                       0,                  FALSE, NULL },
        { N_("Default Mask _Color..."), GWY_ICON_MASK,        0,         0,    edit_default_mask_color,    0,                  FALSE, NULL },
        { N_("Color _Gradients..."),    GWY_ICON_PALETTES,    0,         0,    gwy_app_gradient_editor,    0,                  FALSE, NULL },
        { N_("G_L Materials..."),       GWY_ICON_GL_MATERIAL, 0,         0,    gwy_app_gl_material_editor, 0,                  FALSE, NULL },
        { N_("_Toolbox..."),            NULL,                 0,         0,    gwy_toolbox_editor,         0,                  FALSE, NULL },
        { N_("_Keyboard Shortcuts..."), NULL,                 0,         0,    gwy_shortcut_editor,        0,                  FALSE, NULL },
        { N_("_Logging Enabled"),       NULL,                 0,         0,    toggle_logging_enabled,     0,                  TRUE,  NULL },
    };

    GtkWidget *menu = build_simple_menu(menu_items, G_N_ELEMENTS(menu_items), "<edit>", accel_group);

    gboolean enable_logging = gwy_log_get_enabled();
    gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(menu_items[8].widget), enable_logging);

    return menu;
}

static void
gwy_app_toolbox_showhide(GtkWidget *expander)
{
    GwyDict *settings;
    gboolean visible;
    GQuark quark;

    settings = gwy_app_settings_get();
    quark = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(expander), "key"));
    visible = gtk_expander_get_expanded(GTK_EXPANDER(expander));
    gwy_dict_set_boolean(settings, quark, visible);
}

static void
show_user_guide(void)
{
    gwy_help_show("index", NULL);
}

static void
show_message_log(void)
{
    static GtkWindow *window = NULL;

    if (!window)
        window = create_message_log_window();

    gtk_window_present(window);
}

static void
message_log_updated(GtkTextBuffer *textbuf, GtkTextView *textview)
{
    GtkTextIter iter;

    gtk_text_buffer_get_end_iter(textbuf, &iter);
    gtk_text_view_scroll_to_iter(textview, &iter, 0.0, FALSE, 0.0, 1.0);
}

static gboolean
message_log_deleted(GtkWidget *window)
{
    gtk_widget_hide(window);
    return TRUE;
}

static gboolean
message_log_key_pressed(GtkWidget *window, GdkEventKey *event)
{
    if (event->keyval != GDK_KEY_Escape || (event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK | GDK_MOD1_MASK)))
        return FALSE;

    gtk_widget_hide(window);
    return TRUE;
}

static GtkWindow*
create_message_log_window(void)
{
    GtkWindow *window;
    GtkTextBuffer *textbuf;
    GtkWidget *logview, *scwin;

    window = (GtkWindow*)gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(window, _("Program Messages"));
    gtk_window_set_default_size(window, 480, 320);

    textbuf = gwy_app_get_log_text_buffer();
    logview = gtk_text_view_new_with_buffer(textbuf);
    gtk_text_view_set_editable(GTK_TEXT_VIEW(logview), FALSE);

    scwin = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_AUTOMATIC, GTK_POLICY_ALWAYS);
    gtk_container_add(GTK_CONTAINER(scwin), logview);
    gtk_widget_show_all(scwin);

    gtk_container_add(GTK_CONTAINER(window), scwin);

    gtk_window_add_accel_group(window, gwy_app_main_accel_group());
    g_signal_connect(textbuf, "changed", G_CALLBACK(message_log_updated), logview);
    g_signal_connect(window, "delete-event", G_CALLBACK(message_log_deleted), NULL);
    g_signal_connect(window, "key-press-event", G_CALLBACK(message_log_key_pressed), NULL);

    return window;
}

static gboolean
toolbox_dnd_open_files(gpointer user_data)
{
    GPtrArray *files = (GPtrArray*)user_data;
    gchar *filename;
    guint i;

    for (i = 0; i < files->len; i++) {
        filename = (gchar*)g_ptr_array_index(files, i);
        gwy_app_file_load(NULL, filename, NULL);
        g_free(filename);
    }
    g_ptr_array_free(files, TRUE);

    return G_SOURCE_REMOVE;
}

static void
toolbox_dnd_data_received(G_GNUC_UNUSED GtkWidget *widget,
                          GdkDragContext *context,
                          G_GNUC_UNUSED gint x,
                          G_GNUC_UNUSED gint y,
                          GtkSelectionData *data,
                          G_GNUC_UNUSED guint info,
                          guint time_,
                          G_GNUC_UNUSED gpointer user_data)
{
    gchar *uri, *filename, *text;
    gchar **file_list;
    gboolean ok = FALSE;
    GPtrArray *files;
    guint i;

    if (gtk_selection_data_get_length(data) <= 0 || gtk_selection_data_get_format(data) != 8) {
        gtk_drag_finish(context, FALSE, FALSE, time_);
        return;
    }

    text = g_strdelimit(g_strdup(gtk_selection_data_get_data(data)), "\r\n", '\n');
    file_list = g_strsplit(text, "\n", 0);
    g_free(text);
    if (!file_list) {
        gtk_drag_finish(context, FALSE, FALSE, time_);
        return;
    }

    files = g_ptr_array_new();
    for (i = 0; file_list[i]; i++) {
        uri = g_strstrip(file_list[i]);
        if (!*uri)
            continue;
        filename = g_filename_from_uri(uri, NULL, NULL);
        if (!filename)
            continue;
        gwy_debug("filename = %s", filename);
        if (gwy_file_detect(filename, FALSE, GWY_FILE_OPERATION_LOAD, NULL)) {
            /* FIXME: what about charset conversion? */
            g_ptr_array_add(files, filename);
            ok = TRUE;    /* FIXME: what if we accept only some? */
        }
        else
            g_free(filename);
    }
    g_strfreev(file_list);
    gtk_drag_finish(context, ok, FALSE, time_);

    if (files->len)
        g_idle_add(toolbox_dnd_open_files, files);
    else
        g_ptr_array_free(files, TRUE);
}

static void
delete_app_window(void)
{
    gboolean boo;

    g_signal_emit_by_name(gwy_app_main_window(), "delete-event", NULL, &boo);
}

static void
action_undo(void)
{
    GwyDict *data;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    if (data)
        gwy_app_undo_undo_dict(data);
}

static void
action_redo(void)
{
    GwyDict *data;

    gwy_data_browser_get_current(GWY_APP_FILE, &data, 0);
    if (data)
        gwy_app_undo_redo_dict(data);
}

static void
remove_all_logs(void)
{
    GwyFile *file;
    gwy_data_browser_get_current(GWY_APP_FILE, &file, 0);
    g_return_if_fail(file);

    g_object_ref(file);
    gwy_file_remove_logs(file);
    g_object_unref(file);
}

static void
toggle_logging_enabled(GtkCheckMenuItem *item)
{
    gboolean active = gtk_check_menu_item_get_active(item);

    gwy_dict_set_boolean_by_name(gwy_app_settings_get(), "/app/log/disable", !active);
    gwy_log_set_enabled(active);
}

static void
gwy_app_tool_use(const gchar *toolname, GtkToggleButton *button)
{
    /* don't catch deactivations */
    if (button && !gtk_toggle_button_get_active(button)) {
        gwy_debug("deactivation");
    }
    else
        gwy_app_switch_tool(toolname);
}

static void
edit_default_mask_color(void)
{
    GwyRGBA color = { 1.0, 0.0, 0.0, 0.5 };
    GwyDict *settings = gwy_app_settings_get();
    gwy_rgba_get_from_dict(&color, settings, "/mask");

    /* The mask colour helper function now works with RGBA boxed directly stored in the file, but settings have it
     * component-wise. Create a temporary Container as a workaround. */
    GwyDict *container = gwy_dict_new();
    GQuark key = gwy_file_key_image_mask_color(0);
    gwy_dict_set_boxed(container, GWY_TYPE_RGBA, key, &color);
    gwy_mask_color_selector_run(_("Change Default Mask Color"), NULL, NULL, container, key);
    GwyRGBA newcolor = color;
    gwy_dict_gis_boxed(container, GWY_TYPE_RGBA, key, &newcolor);
    g_object_unref(container);
    if (!gwy_serializable_boxed_equal(GWY_TYPE_RGBA, &newcolor, &color))
        gwy_rgba_store_to_dict(&newcolor, settings, "/mask");
}

static void
action_display_3d(void)
{
    GwyDict *data;
    gint id;

    gwy_data_browser_get_current(GWY_APP_FILE, &data,
                                 GWY_APP_FIELD_ID, &id,
                                 0);
    g_return_if_fail(data);
    //gwy_app_data_browser_show_gl(data, id);
}

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