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

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

#include "libgwyddion/gwyddion.h"
#include "libgwyui/gwyui.h"
#include "libgwyapp/gwyapp.h"
#include "libgwyapp/internal.h"
#include "libgwyapp/data-browser-internal.h"
#include "libgwyapp/sanity.h"

enum {
    IMPORTANT_MODS = (GDK_CONTROL_MASK | GDK_MOD1_MASK | GDK_RELEASE_MASK),
};

typedef enum {
    COLUMN_THUMBNAIL = 0,
    COLUMN_VISIBLE   = 1,
    COLUMN_TITLE     = 2,
    COLUMN_INFO      = 3,
    NUM_COLUMNS
} DataListColumn;

typedef struct {
    GtkWidget *page_widget;
    GtkWidget *treeview;
    GtkTreeViewColumn *columns[NUM_COLUMNS];
    GtkCellRenderer *renderers[NUM_COLUMNS];
    const gchar *tabname;
    guint column_flags;
    gint info_width_chars;
    gint pageno;
} BrowserDataList;

struct _DataBrowserGUI {
    GtkWidget *main_vbox;
    GtkAccelGroup *accel_group;

    GtkWidget *notebook;
    BrowserDataList lists[GWY_FILE_N_KINDS];
    gint switching_pages;

    GtkWidget *filename;
    GtkWidget *messages_button;
    GLogLevelFlags button_log_level;
    GtkWidget *file_close_button;

    gdouble edit_timestamp;
    gboolean doubleclick;

    GtkWidget *buttonbox;
};

static void       create_browser_gui            (DataBrowser *browser);
static void       create_data_list              (DataBrowser *browser,
                                                 BrowserDataList *datalist,
                                                 GwyDataKind data_kind);
static void       ensure_page_visible           (DataBrowser *browser,
                                                 GwyDataKind data_kind);
static void       render_visible                (GtkTreeViewColumn *column,
                                                 GtkCellRenderer *renderer,
                                                 GtkTreeModel *model,
                                                 GtkTreeIter *iter,
                                                 gpointer user_data);
static void       render_info                   (GtkTreeViewColumn *column,
                                                 GtkCellRenderer *renderer,
                                                 GtkTreeModel *model,
                                                 GtkTreeIter *iter,
                                                 gpointer user_data);
static void       render_title                  (GtkTreeViewColumn *column,
                                                 GtkCellRenderer *renderer,
                                                 GtkTreeModel *model,
                                                 GtkTreeIter *iter,
                                                 gpointer user_data);
static void       render_thumbnail              (GtkTreeViewColumn *column,
                                                 GtkCellRenderer *renderer,
                                                 GtkTreeModel *model,
                                                 GtkTreeIter *iter,
                                                 gpointer user_data);
static void       visible_toggled               (GtkCellRendererToggle *renderer,
                                                 gchar *path_str,
                                                 gpointer user_data);
static void       get_title_column_and_renderer (GtkTreeView *treeview,
                                                 GtkTreeViewColumn **column,
                                                 GtkCellRenderer **renderer);
static gboolean   data_list_key_pressed         (GtkTreeView *treeview,
                                                 GdkEventKey *event,
                                                 gpointer user_data);
static gboolean   data_list_start_rename        (GtkTreeView *treeview);
static gboolean   data_list_button_pressed      (GtkTreeView *treeview,
                                                 GdkEventButton *event,
                                                 gpointer user_data);
static gboolean   data_list_button_released     (GtkTreeView *treeview,
                                                 GdkEventButton *event,
                                                 gpointer user_data);
static void       disable_name_edit             (GtkCellRenderer *renderer,
                                                 gpointer check_time);
static void       name_edited                   (GtkCellRenderer *renderer,
                                                 const gchar *strpath,
                                                 const gchar *text,
                                                 gpointer user_data);
static void       selection_changed             (GtkTreeSelection *selection,
                                                 DataBrowser *browser);
static GtkWidget* create_action_buttons         (DataBrowser *browser);
static void       set_action_buttons_sensitivity(DataBrowser *browser,
                                                 gboolean sensitive);
static void       extract_object_as_new         (DataBrowser *browser);
static void       duplicate_object              (DataBrowser *browser);
static void       delete_object                 (DataBrowser *browser);
static void       rename_object                 (DataBrowser *browser);
static FileProxy* get_selected_item             (DataBrowser *browser,
                                                 GwyDataKind *data_kind,
                                                 GtkTreeModel **model,
                                                 GtkTreeIter *iter);
static GtkWidget* create_filename               (DataBrowser *browser);
static void       show_hide_messages            (DataBrowser *browser,
                                                 GtkToggleButton *toggle);
static void       create_message_log_window     (FileProxy *proxy);
static void       message_log_window_destroyed  (gpointer user_data,
                                                 GObject *where_the_object_was);
static void       message_log_updated           (GtkTextBuffer *textbuf,
                                                 GtkTextView *textview);
static gboolean   message_log_key_pressed       (GtkWidget *window,
                                                 GdkEventKey *event);
static void       update_messages_textbuf_since (FileProxy *proxy,
                                                 guint from);
static void       update_message_button         (void);
static void       close_file                    (DataBrowser *browser);
static void       page_switched                 (DataBrowser *browser,
                                                 GtkWidget *page_widget,
                                                 guint pageno);
static void       destroyed                     (DataBrowser *browser,
                                                 GtkWidget *main_vbox);

/* These are for DnD where we only get the model and must find out what it belongs to. Must keep them in sync with
 * data-browser.c */
static G_DEFINE_QUARK(gwy-data-browser-proxy, proxy)
static G_DEFINE_QUARK(gwy-data-browser-data-kind, data_kind)

static const GtkTargetEntry dnd_target_table[] = { GTK_TREE_MODEL_ROW };

#define GWYCOLFLG(x) (1u << COLUMN_##x)
#define GWYCOLFLG_ALL (1u << NUM_COLUMNS) - 1

static struct {
    const gchar *tabname;
    GwyDataKind data_kind;
    guint info_width;
    guint column_flags;
} kindspecs[] = {
    { N_("Image"),     GWY_FILE_IMAGE,   3, GWYCOLFLG_ALL, },
    { N_("Graph"),     GWY_FILE_GRAPH,   5, GWYCOLFLG_ALL, },
    { N_("Spectra"),   GWY_FILE_SPECTRA, 7, GWYCOLFLG_ALL, },
    { N_("Volume"),    GWY_FILE_VOLUME,  7, GWYCOLFLG_ALL, },
    { N_("XYZ"),       GWY_FILE_XYZ,     7, GWYCOLFLG_ALL, },
    { N_("Curve Map"), GWY_FILE_CMAP,    5, GWYCOLFLG_ALL, },
};

#undef GWYCOLFLG
#undef GWYCOLFLG_ALL

static inline FileProxy*
proxy_for_file(GwyFile *file, GList **link)
{
    DataBrowser *browser = _gwy_data_browser();
    for (GList *l = browser->proxies; l; l = g_list_next(l)) {
        FileProxy *proxy = (FileProxy*)l->data;
        if (proxy->file == file) {
            if (link)
                *link = l;
            return proxy;
        }
    }
    return NULL;
}

/**
 * gwy_data_browser_get_gui_enabled:
 *
 * Reports whether creation of windows by the data-browser is enabled.
 *
 * See gwy_data_browser_set_gui_enabled() for discussion.
 *
 * Returns: %TRUE if the data-browser is permitted to create windows, %FALSE if it is not.
 **/
gboolean
gwy_data_browser_get_gui_enabled(void)
{
    DataBrowser *browser = _gwy_data_browser();
    return browser->gui_enabled;
}

/**
 * gwy_data_browser_set_gui_enabled:
 * @setting: %TRUE to enable creation of widgets by the data-browser, %FALSE to disable it.
 *
 * Globally enables or disables creation of widgets by the data-browser.
 *
 * At present, the browser GUI state needs to be decided before any files are added to the data browser. Changing the
 * GUI state later is not supported. Usually, this function is not called explicitly. The state is set by
 * gwy_app_init() according to its @for_gui flag. By default, the GUI is enabled.
 *
 * If GUI is enabled, the data-browser creates windows for data objects automatically, for instance when
 * reconstructing the view of a loaded file or after a module function creates a new image or graph and marks it
 * visible. Non-GUI applications that run module functions usually wish to disable such behaviour.
 *
 * If GUI is disabled the data browser never creates windows showing data objects and also its own GUI becomes
 * unavailable. This mode has some utility, in particular when processing multiple files, thanks to function such
 * as gwy_data_browser_foreach(). Nevertheless, #GwyFile already offers a number of data management functions that
 * are sufficient for many non-GUI use cases.
 **/
void
gwy_data_browser_set_gui_enabled(gboolean setting)
{
    DataBrowser *browser = _gwy_data_browser();
    /* Changing the setting when there are currently no files may be kind-of-OK. Definitely complain when there are
     * any files. */
    g_return_if_fail(!browser->proxies);
    g_return_if_fail(setting || !browser->gui);
    browser->gui_enabled = !!setting;
}

/**
 * gwy_data_browser_widget:
 *
 * Obtains the data browser widget.
 *
 * The data browser is a singleton. If browser GUI is enabled and the widget already exists, it is simply returned.
 * It may be newly created as necessary. In any case, the widget is owned by the browser. Destroy it to disable the
 * shut down the GUI, but keep the data browser itself.
 *
 * Use gwy_data_browser_shut_down() when tearing down the program in an arbitrary manner.
 *
 * If browser GUI is disabled, the function returns %NULL.
 *
 * Returns: (transfer none) (nullable): The browser widget, possibly %NULL.
 **/
GtkWidget*
gwy_data_browser_widget(void)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;
    g_return_val_if_fail(browser->gui_enabled || !gui, NULL);
    if (!browser->gui_enabled)
        return NULL;
    if (!gui) {
        create_browser_gui(browser);
        g_assert(browser->gui);
        if (browser->current_file)
            _gwy_data_browser_gui_switch_current();
    }
    return browser->gui->main_vbox;
}

/**
 * gwy_data_browser_accel_group:
 *
 * Obtains the data browser GUI accelerator group.
 *
 * The group contains accelerators for actions such as data duplication or deletion, which are generally expected to
 * work also in various data windows.
 *
 * The data browser is a singleton; see gwy_data_browser_widget(). When the browser GUI is destroyed, it releases its
 * own reference on the accelerator group and forgets it.
 *
 * If browser GUI is disabled or not yet created, the function returns %NULL. Just calling this function does not
 * force the creation of the accelerator group.
 *
 * Returns: (transfer none) (nullable): The accelerator group, possibly %NULL.
 **/
GtkAccelGroup*
gwy_data_browser_accel_group(void)
{
    DataBrowser *browser = _gwy_data_browser();
    return browser->gui->accel_group;
}

void
_gwy_data_browser_gui_switch_current(void)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;
    if (!gui)
        return;

    FileProxy *proxy = browser->current_file;
    GwyFile *file = proxy ? proxy->file : NULL;
    gwy_debug("switching lists to file %p (id %d, kind %d)", file, gwy_file_get_id(file), browser->current_kind);
    gboolean switching_files = FALSE;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        BrowserDataList *datalist = gui->lists + data_kind;
        if (!datalist->treeview)
            continue;

        GtkTreeView *treeview = GTK_TREE_VIEW(datalist->treeview);
        GtkTreeModel *model = proxy ? proxy->lists[data_kind] : NULL;
        if (model != gtk_tree_view_get_model(treeview)) {
            switching_files = TRUE;
            gtk_tree_view_set_model(treeview, model);
        }
    }
    gwy_debug("switching files: %d", switching_files);

    /* First set the current notebook page because selection changes in non-current pages are more or less
     * automatically no-op. */
    if (proxy && browser->current_kind != GWY_FILE_NONE) {
        ensure_page_visible(browser, browser->current_kind);
        gui->switching_pages++;
        gtk_notebook_set_current_page(GTK_NOTEBOOK(gui->notebook), gui->lists[browser->current_kind].pageno);
        gui->switching_pages--;
    }

    if (switching_files) {
        update_message_button();
        _gwy_data_browser_update_filename();
    }

    if (!proxy)
        return;

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        BrowserDataList *datalist = gui->lists + data_kind;
        if (!datalist->treeview)
            continue;

        GtkTreeView *treeview = GTK_TREE_VIEW(datalist->treeview);
        GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
        /* Select something. Preferably we select the current item. If there is no current item but data items exist,
         * choose something to select from them. */
        gint id = proxy->current_items[data_kind];
        if (id == -1 && gwy_file_get_n_data(file, data_kind)) {
            gint *ids = gwy_file_get_ids(file, data_kind);
            for (gint j = 0; ids[j] >= 0; j++) {
                GQuark key = gwy_file_key_data(data_kind, ids[j]);
                PieceInfo *info = g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key));
                if (info->window) {
                    id = ids[j];
                    break;
                }
            }
            if (id == -1)
                id = ids[0];
            g_free(ids);
        }

        if (id == -1) {
            gtk_tree_selection_unselect_all(selection);
            continue;
        }

        GQuark key = gwy_file_key_data(data_kind, id);
        PieceInfo *info = g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key));
        gtk_tree_selection_select_iter(selection, &info->iter);
        GtkTreePath *path = gtk_tree_model_get_path(proxy->lists[data_kind], &info->iter);
        gtk_tree_view_scroll_to_cell(treeview, path, NULL, FALSE, 0.0, 0.0);
    }
    /* TODO */
}

static void
create_browser_gui(DataBrowser *browser)
{
    DataBrowserGUI *gui = g_new0(DataBrowserGUI, 1);
    browser->gui = gui;

    gui->accel_group = gtk_accel_group_new();

    GtkWidget *vbox = gui->main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    g_object_ref_sink(gui->main_vbox);
    g_signal_connect_swapped(gui->main_vbox, "destroy", G_CALLBACK(destroyed), browser);

    GtkWidget *filename_row = create_filename(browser);
    gtk_box_pack_start(GTK_BOX(vbox), filename_row, FALSE, FALSE, 0);

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++)
        gui->lists[data_kind].pageno = -1;

    gui->notebook = gtk_notebook_new();
    gtk_widget_set_size_request(gui->notebook, -1, 240);
    gtk_box_pack_start(GTK_BOX(vbox), gui->notebook, TRUE, TRUE, 0);
    for (guint i = 0; i < G_N_ELEMENTS(kindspecs); i++) {
        BrowserDataList *datalist = gui->lists + kindspecs[i].data_kind;
        datalist->info_width_chars = kindspecs[i].info_width;
        datalist->column_flags = kindspecs[i].column_flags;

        GtkWidget *scwin = gtk_scrolled_window_new(NULL, NULL);
        gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scwin), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
        create_data_list(browser, datalist, kindspecs[i].data_kind);
        gtk_container_add(GTK_CONTAINER(scwin), datalist->treeview);
        gtk_widget_show_all(scwin);
        datalist->page_widget = scwin;
    }
    ensure_page_visible(browser, GWY_FILE_IMAGE);
    ensure_page_visible(browser, GWY_FILE_GRAPH);
    g_signal_connect_swapped(gui->notebook, "switch-page", G_CALLBACK(page_switched), browser);

    gui->buttonbox = create_action_buttons(browser);
    set_action_buttons_sensitivity(browser, FALSE);
    gtk_box_pack_end(GTK_BOX(vbox), gui->buttonbox, FALSE, FALSE, 0);

    gtk_widget_show_all(vbox);
}

static void
ensure_page_visible(DataBrowser *browser, GwyDataKind data_kind)
{
    DataBrowserGUI *gui = browser->gui;
    g_return_if_fail(gui);
    BrowserDataList *datalist = gui->lists + data_kind;
    if (datalist->pageno >= 0)
        return;

    guint i, insert_pageno = 0;
    for (i = 0; i < G_N_ELEMENTS(kindspecs); i++) {
        if (kindspecs[i].data_kind == data_kind)
            break;
        if (gui->lists[kindspecs[i].data_kind].pageno >= 0)
            insert_pageno++;
    }
    g_return_if_fail(i < G_N_ELEMENTS(kindspecs));

    GtkWidget *label = gtk_label_new(kindspecs[i].tabname);
    /* Move all following tabs one pageno up. */
    while (i < G_N_ELEMENTS(kindspecs)) {
        BrowserDataList *otherlist = gui->lists + kindspecs[i].data_kind;
        if (otherlist->pageno >= 0)
            otherlist->pageno++;
        i++;
    }
    datalist->pageno = insert_pageno;
    gtk_notebook_insert_page(GTK_NOTEBOOK(gui->notebook), datalist->page_widget, label, insert_pageno);
}

static void
create_data_list(DataBrowser *browser, BrowserDataList *datalist, GwyDataKind data_kind)
{
    gpointer dkpointer = GINT_TO_POINTER(data_kind);

    /* Construct the GtkTreeView that will display data items. */
    gwy_debug("creating treeview for %s", _gwy_data_kind_name(data_kind));
    datalist->treeview = gtk_tree_view_new();
    GtkTreeView *treeview = GTK_TREE_VIEW(datalist->treeview);
    g_signal_connect(treeview, "key-press-event", G_CALLBACK(data_list_key_pressed), NULL);
    g_signal_connect(treeview, "button-press-event", G_CALLBACK(data_list_button_pressed), NULL);
    g_signal_connect(treeview, "button-release-event", G_CALLBACK(data_list_button_released), NULL);
    g_signal_connect(treeview, "drag-begin", G_CALLBACK(gwy_data_browser_block_switching), NULL);
    g_signal_connect(treeview, "drag-end", G_CALLBACK(gwy_data_browser_unblock_switching), NULL);

    /* Thumbnail column */
    if (datalist->column_flags & (1u << COLUMN_THUMBNAIL)) {
        GtkCellRenderer *renderer = gtk_cell_renderer_pixbuf_new();
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Thumbnail", renderer, NULL);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_thumbnail, NULL, NULL);
        gtk_tree_view_append_column(treeview, column);
        datalist->columns[COLUMN_THUMBNAIL] = column;
        datalist->renderers[COLUMN_THUMBNAIL] = renderer;
    }

    /* Visibility column */
    if (datalist->column_flags & (1u << COLUMN_VISIBLE)) {
        GtkCellRenderer *renderer = gtk_cell_renderer_toggle_new();
        g_object_set(renderer, "activatable", TRUE, NULL);
        g_signal_connect(renderer, "toggled", G_CALLBACK(visible_toggled), dkpointer);
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Visible", renderer, NULL);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_visible, NULL, NULL);
        gtk_tree_view_append_column(treeview, column);
        datalist->columns[COLUMN_VISIBLE] = column;
        datalist->renderers[COLUMN_VISIBLE] = renderer;
    }

    /* Title column. */
    if (datalist->column_flags & (1u << COLUMN_TITLE)) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer,
                     "ellipsize", PANGO_ELLIPSIZE_END,
                     "ellipsize-set", TRUE,
                     "editable", FALSE,
                     "editable-set", TRUE,
                     NULL);
        g_signal_connect(renderer, "edited", G_CALLBACK(name_edited), dkpointer);
        g_signal_connect(renderer, "editing-canceled", G_CALLBACK(disable_name_edit), GUINT_TO_POINTER(FALSE));
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Title", renderer, NULL);
        gtk_tree_view_column_set_expand(column, TRUE);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_title, NULL, NULL);
        gtk_tree_view_append_column(treeview, column);
        datalist->columns[COLUMN_TITLE] = column;
        datalist->renderers[COLUMN_TITLE] = renderer;
    }

    /* Flags column. */
    if (datalist->column_flags & (1u << COLUMN_INFO)) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer, "width-chars", datalist->info_width_chars, NULL);
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Info", renderer, NULL);
        gtk_tree_view_column_set_cell_data_func(column, renderer, render_info, NULL, NULL);
        gtk_tree_view_append_column(treeview, column);
        datalist->columns[COLUMN_INFO] = column;
        datalist->renderers[COLUMN_INFO] = renderer;
    }

    gtk_tree_view_set_headers_visible(treeview, FALSE);

    /* Selection. */
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_BROWSE);
    g_signal_connect(selection, "changed", G_CALLBACK(selection_changed), browser);

    /* DnD. */
    gtk_tree_view_enable_model_drag_source(treeview, GDK_BUTTON1_MASK,
                                           dnd_target_table, G_N_ELEMENTS(dnd_target_table), GDK_ACTION_COPY);
}

static void
render_visible(G_GNUC_UNUSED GtkTreeViewColumn *column,
               GtkCellRenderer *renderer,
               GtkTreeModel *model,
               GtkTreeIter *iter,
               G_GNUC_UNUSED gpointer user_data)
{
    PieceInfo *info;
    gtk_tree_model_get(model, iter, 0, &info, -1);
    g_object_set(renderer, "active", !!info->window, NULL);
}

static void
render_title(G_GNUC_UNUSED GtkTreeViewColumn *column,
             GtkCellRenderer *renderer,
             GtkTreeModel *model,
             GtkTreeIter *iter,
             G_GNUC_UNUSED gpointer user_data)
{
    PieceInfo *info;
    gtk_tree_model_get(model, iter, 0, &info, -1);
    g_return_if_fail(info && info->proxy);
    gchar *title = gwy_file_get_display_title(info->proxy->file, info->parsed.data_kind, info->parsed.id);
    g_object_set(renderer, "text", title, NULL);
    g_free(title);
}

static void
render_info(G_GNUC_UNUSED GtkTreeViewColumn *column,
            GtkCellRenderer *renderer,
            GtkTreeModel *model,
            GtkTreeIter *iter,
            G_GNUC_UNUSED gpointer user_data)
{
    PieceInfo *info;
    gtk_tree_model_get(model, iter, 0, &info, -1);
    g_return_if_fail(info && info->proxy);
    GwyDict *container = GWY_DICT(info->proxy->file);
    GwyDataKind data_kind = info->parsed.data_kind;
    gint id = info->parsed.id;
    gchar *text = NULL;

    /* XXX: This could be abstracted to some render-info function and we would not need any ifs then. */
    if (data_kind == GWY_FILE_IMAGE) {
        gboolean has_mask = gwy_dict_contains(container, gwy_file_key_image_mask(id));
        gboolean has_show = gwy_dict_contains(container, gwy_file_key_image_picture(id));
        text = g_strdup_printf("%s%s",
                               has_mask ? "M" : "",
                               has_show ? "P" : "");
    }
    else if (data_kind == GWY_FILE_GRAPH) {
        GwyGraphModel *gmodel = GWY_GRAPH_MODEL(info->object);
        text = g_strdup_printf("%d", gwy_graph_model_get_n_curves(gmodel));
    }
    else if (data_kind == GWY_FILE_SPECTRA) {
        GwySpectra *spectra = GWY_SPECTRA(info->object);
        text = g_strdup_printf("%d", gwy_spectra_get_n_spectra(spectra));
    }
    else if (data_kind == GWY_FILE_VOLUME) {
        GwyBrick *brick = GWY_BRICK(info->object);
        gboolean has_zcal = !!gwy_brick_get_zcalibration(brick);
        text = g_strdup_printf("%d%s", gwy_brick_get_zres(brick), has_zcal ? " Z" : "");
    }
    else if (data_kind == GWY_FILE_XYZ) {
        GwyScatter *scatter = GWY_SCATTER(info->object);
        text = g_strdup_printf("%d:%lu", gwy_scatter_get_n_values(scatter), (gulong)gwy_scatter_get_n_points(scatter));
    }
    else if (data_kind == GWY_FILE_CMAP) {
        GwyLawn *lawn = GWY_LAWN(info->object);
        gint ncurves = gwy_lawn_get_n_curves(lawn), nsegments = gwy_lawn_get_n_segments(lawn);
        if (nsegments)
            text = g_strdup_printf("%d:%d", ncurves, nsegments);
        else
            text = g_strdup_printf("%d", ncurves);
    }

    g_object_set(renderer, "text", text ? text : "", NULL);
    g_free(text);
}

static void
render_thumbnail(G_GNUC_UNUSED GtkTreeViewColumn *column,
                 GtkCellRenderer *renderer,
                 GtkTreeModel *model,
                 GtkTreeIter *iter,
                 G_GNUC_UNUSED gpointer user_data)
{
    PieceInfo *info;
    gtk_tree_model_get(model, iter, 0, &info, -1);
    g_return_if_fail(info && info->proxy);

    if (info->thumbnail && info->thumbnail_timestamp >= info->changed_timestamp) {
        g_object_set(renderer, "pixbuf", info->thumbnail, NULL);
        return;
    }

    GwyFile *file = info->proxy->file;
    GwyDataKind data_kind = info->parsed.data_kind;
    gint id = info->parsed.id;
    gwy_debug("updating thumbnail (%p, %s, %d)", file, _gwy_data_kind_name(data_kind), id);
    GdkPixbuf *pixbuf = NULL;
    g_clear_object(&info->thumbnail);
    if (data_kind == GWY_FILE_IMAGE)
        pixbuf = gwy_render_image_thumbnail(file, id, THUMB_SIZE, THUMB_SIZE);
    else if (data_kind == GWY_FILE_GRAPH)
        pixbuf = gwy_render_graph_thumbnail(file, id, 500*THUMB_SIZE/433, 433*THUMB_SIZE/500);
    else if (data_kind == GWY_FILE_SPECTRA)
        pixbuf = gwy_render_spectra_thumbnail(file, id, 500*THUMB_SIZE/433, 433*THUMB_SIZE/500);
    else if (data_kind == GWY_FILE_VOLUME)
        pixbuf = gwy_render_volume_thumbnail(file, id, THUMB_SIZE, THUMB_SIZE);
    else if (data_kind == GWY_FILE_XYZ)
        pixbuf = gwy_render_xyz_thumbnail(file, id, THUMB_SIZE, THUMB_SIZE);
    else if (data_kind == GWY_FILE_CMAP)
        pixbuf = gwy_render_cmap_thumbnail(file, id, THUMB_SIZE, THUMB_SIZE);

    g_object_set(renderer, "pixbuf", pixbuf, NULL);
    info->thumbnail = pixbuf;
    if (pixbuf) {
        info->thumbnail_timestamp = get_timestamp_now();
        if (info->window) {
            // TODO
            //update_window_icon(model, iter);
        }
    }
}

static void
visible_toggled(GtkCellRendererToggle *renderer,
                gchar *path_str,
                gpointer user_data)
{
    GwyDataKind data_kind = GPOINTER_TO_INT(user_data);
    DataBrowser *browser = _gwy_data_browser();
    FileProxy *proxy = browser->current_file;
    g_return_if_fail(proxy);

    GtkTreePath *path = gtk_tree_path_new_from_string(path_str);
    GtkTreeModel *model = proxy->lists[data_kind];
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, path);
    gtk_tree_path_free(path);

    gboolean visible = gtk_cell_renderer_toggle_get_active(renderer);
    /* We got the current state. Revert it. */
    visible = !visible;
    gwy_debug("visible toggled to %d", visible);

    PieceInfo *info;
    gtk_tree_model_get(model, &iter, 0, &info, -1);
    g_return_if_fail(info && info->proxy == proxy && info->parsed.data_kind == data_kind);
    gwy_file_set_visible(proxy->file, data_kind, info->parsed.id, visible);
    /* Now the file can be already closed. */
}

static void
get_title_column_and_renderer(GtkTreeView *treeview, GtkTreeViewColumn **column, GtkCellRenderer **renderer)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;
    GtkWidget *widget = GTK_WIDGET(treeview);
    for (guint i = 0; i < GWY_FILE_N_KINDS; i++) {
        if (gui->lists[i].treeview == widget) {
            *column = gui->lists[i].columns[COLUMN_TITLE];
            *renderer = gui->lists[i].renderers[COLUMN_TITLE];
            return;
        }
    }
    g_assert_not_reached();
}

static gboolean
data_list_key_pressed(GtkTreeView *treeview,
                      GdkEventKey *event,
                      G_GNUC_UNUSED gpointer user_data)
{
    if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter || event->keyval == GDK_KEY_F2)
        return data_list_start_rename(treeview);
    return FALSE;
}

static gboolean
data_list_start_rename(GtkTreeView *treeview)
{
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    GtkTreeModel *model;
    GtkTreeIter iter;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return FALSE;

    GtkTreeViewColumn *column;
    GtkCellRenderer *renderer;
    get_title_column_and_renderer(treeview, &column, &renderer);

    gboolean editable;
    g_object_get(renderer, "editable", &editable, NULL);
    if (editable)
        return FALSE;

    gtk_widget_grab_focus(GTK_WIDGET(treeview));
    GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
    g_object_set(renderer, "editable", TRUE, NULL);
    gtk_tree_view_set_cursor(treeview, path, column, TRUE);
    gtk_tree_path_free(path);
    return TRUE;
}

static gboolean
data_list_button_pressed(G_GNUC_UNUSED GtkTreeView *treeview,
                         GdkEventButton *event,
                         G_GNUC_UNUSED gpointer user_data)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;

    if (event->type == GDK_2BUTTON_PRESS && event->button == 1)
        gui->doubleclick = TRUE;
    return FALSE;
}

static gboolean
data_list_button_released(GtkTreeView *treeview,
                          GdkEventButton *event,
                          G_GNUC_UNUSED gpointer user_data)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;

    if (gui->doubleclick) {
        gui->doubleclick = FALSE;
        GtkTreeViewColumn *column, *eventcolumn;
        GtkTreePath *path;
        GtkCellRenderer *renderer;
        get_title_column_and_renderer(treeview, &column, &renderer);
        if (gtk_tree_view_get_path_at_pos(treeview, event->x, event->y, &path, &eventcolumn, NULL, NULL)
            && eventcolumn == column) {
            gboolean editable;
            g_object_get(renderer, "editable", &editable, NULL);
            if (!editable) {
                gwy_debug("enabling editable");
                gtk_widget_grab_focus(GTK_WIDGET(treeview));
                g_object_set(renderer, "editable", TRUE, NULL);
                gtk_tree_view_set_cursor(treeview, path, column, TRUE);
            }
            gtk_tree_path_free(path);
        }
    }
    return FALSE;
}

static void
disable_name_edit(GtkCellRenderer *renderer,
                  gpointer check_time)
{
    if (GPOINTER_TO_UINT(check_time)) {
        DataBrowser *browser = _gwy_data_browser();
        DataBrowserGUI *gui = browser->gui;
        if (get_timestamp_now() - gui->edit_timestamp < 0.1)
            return;
    }

    gwy_debug("disabling title editable (%p)", renderer);
    g_object_set(renderer, "editable", FALSE, NULL);
}

static void
name_edited(GtkCellRenderer *renderer,
            const gchar *strpath,
            const gchar *text,
            gpointer user_data)
{
    GwyDataKind data_kind = GPOINTER_TO_INT(user_data);
    DataBrowser *browser = _gwy_data_browser();
    FileProxy *proxy = browser->current_file;
    g_return_if_fail(proxy);
    GtkTreeModel *model = proxy->lists[data_kind];

    GtkTreePath *path = gtk_tree_path_new_from_string(strpath);
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, path);
    gtk_tree_path_free(path);

    PieceInfo *info;
    gtk_tree_model_get(model, &iter, 0, &info, -1);
    g_return_if_fail(info);
    g_return_if_fail(info->parsed.data_kind == data_kind);

    gchar *title = g_strstrip(g_strdup(text));
    if (!*title) {
        g_free(title);
        gwy_file_set_title(proxy->file, data_kind, info->parsed.id, NULL, TRUE);
    }
    else
        gwy_file_pass_title(proxy->file, data_kind, info->parsed.id, title);

    disable_name_edit(renderer, GUINT_TO_POINTER(TRUE));
}

static GtkWidget*
create_action_buttons(DataBrowser *browser)
{
    static const struct {
        const gchar *icon_name;
        const gchar *tooltip;
        GCallback callback;
        guint accelkey;
        GdkModifierType accelmods;
        gboolean add_accel;
    }
    actions[] = {
        {
            GWY_ICON_GTK_COPY, N_("Duplicate data"),
            G_CALLBACK(duplicate_object), GDK_KEY_d, GDK_CONTROL_MASK, TRUE,
        },
        {
            GWY_ICON_GTK_DELETE, N_("Delete data"),
            G_CALLBACK(delete_object), GDK_KEY_Delete, GDK_CONTROL_MASK, TRUE,
        },
        {
            // Do not set an accelerator for Rename. Keep F2 handled by the list, i.e. when it has the keyboard focus.
            GWY_ICON_GTK_EDIT, N_("Rename data"),
            G_CALLBACK(rename_object), GDK_KEY_F2, 0, FALSE,
        },
        {
            GWY_ICON_GTK_NEW, N_("Extract to a new file"),
            G_CALLBACK(extract_object_as_new), GDK_KEY_Insert, GDK_CONTROL_MASK, TRUE,
        },
    };

    DataBrowserGUI *gui = browser->gui;
    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);

    for (guint i = 0; i < G_N_ELEMENTS(actions); i++) {
        GtkWidget *button = gwy_create_stock_like_button(NULL, actions[i].icon_name);
        gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
        // TODO: Update the button sensitivity correctly.
        //gwy_sensitivity_group_add_widget(browser->sensgroup, button, SENS_OBJECT);
        g_signal_connect_swapped(button, "clicked", actions[i].callback, browser);

        if (actions[i].add_accel) {
            gtk_widget_add_accelerator(button, "clicked", gui->accel_group,
                                       actions[i].accelkey, actions[i].accelmods, 0);
        }
        gchar *accel_text = gtk_accelerator_get_label(actions[i].accelkey, actions[i].accelmods);
        gchar *tooltip_with_accel = g_strconcat(_(actions[i].tooltip), " (", accel_text, ")", NULL);
        g_free(accel_text);
        gtk_widget_set_tooltip_text(button, tooltip_with_accel);
        g_free(tooltip_with_accel);
    }

    return hbox;
}

static void
set_action_buttons_sensitivity(DataBrowser *browser, gboolean sensitive)
{
    gtk_widget_set_sensitive(browser->gui->buttonbox, sensitive);
}

static void
extract_object_as_new(DataBrowser *browser)
{
    GtkTreeModel *model;
    GtkTreeIter iter;
    GwyDataKind data_kind;
    FileProxy *proxy;
    if (!(proxy = get_selected_item(browser, &data_kind, &model, &iter)))
        return;

    PieceInfo *info;
    gtk_tree_model_get(model, &iter, 0, &info, -1);
    g_assert(info && info->parsed.data_kind == data_kind && info->proxy == proxy);

    // Avoid adding an empty file to the brower which then has to fight against being closed becaus it has no data.
    // Add the file once it already contains the data.
    GwyFile *newfile = gwy_file_new();
    gint newid = gwy_file_copy_data(proxy->file, data_kind, info->parsed.id, newfile);
    _gwy_log_builtin_duplicate(proxy->file, newfile, data_kind, info->parsed.id, newid);
    gwy_file_set_visible(newfile, data_kind, newid, TRUE);
    gwy_data_browser_add(newfile);
    g_object_unref(newfile);
}

static void
duplicate_object(DataBrowser *browser)
{
    GtkTreeModel *model;
    GtkTreeIter iter;
    GwyDataKind data_kind;
    FileProxy *proxy;
    if (!(proxy = get_selected_item(browser, &data_kind, &model, &iter)))
        return;

    PieceInfo *info;
    gtk_tree_model_get(model, &iter, 0, &info, -1);
    g_assert(info && info->parsed.data_kind == data_kind && info->proxy == proxy);
    gwy_data_browser_copy_data(proxy->file, data_kind, info->parsed.id, proxy->file);
}

static void
delete_object(DataBrowser *browser)
{
    GtkTreeModel *model;
    GtkTreeIter iter;
    GwyDataKind data_kind;
    FileProxy *proxy;
    if (!(proxy = get_selected_item(browser, &data_kind, &model, &iter)))
        return;

    PieceInfo *info;
    gtk_tree_model_get(model, &iter, 0, &info, -1);
    g_assert(info && info->parsed.data_kind == data_kind && info->proxy == proxy);
    /* Keep file object itself alive during the removal (which may close the last window and thus close the file) and
     * let its destruction finish at the end. */
    GwyFile *file = g_object_ref(proxy->file);
    gwy_file_remove(file, data_kind, info->parsed.id);

    /* XXX: Somehow we get stuck in ‘have not data’ when we do not do this. */
    DataBrowserGUI *gui = browser->gui;
    BrowserDataList *datalist = gui->lists + data_kind;
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(datalist->treeview));
    selection_changed(selection, browser);

    g_object_unref(file);
}

static void
rename_object(DataBrowser *browser)
{
    GwyDataKind data_kind = browser->current_kind;
    if (!browser->current_file || data_kind == GWY_FILE_NONE || !browser->gui)
        return;

    data_list_start_rename(GTK_TREE_VIEW(browser->gui->lists[data_kind].treeview));
}

static FileProxy*
get_selected_item(DataBrowser *browser, GwyDataKind *data_kind,
                  GtkTreeModel **model, GtkTreeIter *iter)
{
    DataBrowserGUI *gui = browser->gui;
    FileProxy *proxy = browser->current_file;
    *data_kind = browser->current_kind;
    if (!proxy || *data_kind == GWY_FILE_NONE)
        return NULL;

    GtkTreeView *treeview = GTK_TREE_VIEW(gui->lists[*data_kind].treeview);
    GtkTreeSelection *selection = gtk_tree_view_get_selection(treeview);
    if (!gtk_tree_selection_get_selected(selection, model, iter))
        return NULL;
    return proxy;
}

static GtkWidget*
create_filename(DataBrowser *browser)
{
    DataBrowserGUI *gui = browser->gui;
    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

    /* Filename. */
    gui->filename = gtk_label_new(NULL);
    gtk_label_set_ellipsize(GTK_LABEL(gui->filename), PANGO_ELLIPSIZE_END);
    gtk_label_set_xalign(GTK_LABEL(gui->filename), 0.0);
    gwy_set_widget_margins(gui->filename, 4, 4, 2, 2);
    gtk_box_pack_start(GTK_BOX(hbox), gui->filename, TRUE, TRUE, 0);

    /* Messages button. */
    GtkWidget *button, *image;
    gui->messages_button = button = gtk_toggle_button_new();
    gui->button_log_level = G_LOG_LEVEL_INFO;
    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    image = gtk_image_new_from_icon_name(GWY_ICON_LOAD_INFO, GTK_ICON_SIZE_BUTTON);
    gtk_container_add(GTK_CONTAINER(button), image);
    gtk_widget_set_tooltip_text(button, _("Show file messages"));
    gtk_widget_set_no_show_all(gui->messages_button, TRUE);
    gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
    g_signal_connect_swapped(button, "toggled", G_CALLBACK(show_hide_messages), browser);
    /* TODO: Update toggle state according to log window being shown when switching files. */

    /* Close button. */
    gui->file_close_button = button = gtk_button_new();
    gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
    image = gtk_image_new_from_icon_name(GWY_ICON_GTK_CLOSE, GTK_ICON_SIZE_BUTTON);
    gtk_container_add(GTK_CONTAINER(button), image);
    gtk_widget_set_tooltip_text(button, _("Close file"));
    gtk_box_pack_start(GTK_BOX(hbox), button, FALSE, FALSE, 0);
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(close_file), browser);

    return hbox;
}

void
_gwy_data_browser_update_filename(void)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;
    if (!gui)
        return;

    if (!browser->current_file) {
        gtk_label_set_text(GTK_LABEL(gui->filename), NULL);
        gtk_widget_set_sensitive(gui->file_close_button, FALSE);
        gwy_app_sensitivity_set_state(GWY_MENU_FLAG_FILE, 0);
        return;
    }

    FileProxy *proxy = browser->current_file;
    const gchar *filename;
    if (gwy_dict_gis_string(GWY_DICT(proxy->file), gwy_file_key_filename(), &filename)) {
        gchar *basename = g_path_get_basename(filename);
        gtk_label_set_text(GTK_LABEL(gui->filename), basename);
        g_free(basename);
    }
    else
        gtk_label_set_text(GTK_LABEL(gui->filename), _("Untitled"));

    gtk_widget_set_sensitive(gui->file_close_button, TRUE);
    gwy_app_sensitivity_set_state(GWY_MENU_FLAG_FILE, GWY_MENU_FLAG_FILE);
}

static void
close_file(DataBrowser *browser)
{
    if (browser->in_shutdown)
        return;
    if (browser->current_file)
        gwy_data_browser_remove(browser->current_file->file);
}

static void
show_hide_messages(DataBrowser *browser, GtkToggleButton *toggle)
{
    if (browser->in_shutdown)
        return;

    gboolean active = gtk_toggle_button_get_active(toggle);
    FileProxy *proxy = browser->current_file;
    if (!proxy) {
        if (active)
            gtk_toggle_button_set_active(toggle, FALSE);
        return;
    }

    /* We should not get here when the browser GUI is set up for a file. */
    gboolean have_window = !!proxy->message_window;
    if (!active == !have_window)
        return;

    if (have_window) {
        gtk_widget_destroy(proxy->message_window);
        return;
    }

    create_message_log_window(proxy);
    update_messages_textbuf_since(proxy, 0);
    gtk_window_present(GTK_WINDOW(proxy->message_window));
}

static void
create_message_log_window(FileProxy *proxy)
{
    const gchar *filename;
    gchar *title;
    if (gwy_dict_gis_string(GWY_DICT(proxy->file), gwy_file_key_filename(), &filename)) {
        gchar *bname = g_path_get_basename(filename);
        title = g_strdup_printf(_("Messages for %s"), bname);
        g_free(bname);
    }
    else
        title = g_strdup(_("Messages for Untitled"));

    proxy->message_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    GtkWindow *window = GTK_WINDOW(proxy->message_window);
    gtk_window_set_title(window, title);
    g_free(title);
    gtk_window_set_default_size(window, 480, 320);

    GtkTextBuffer *textbuf = proxy->message_textbuf = _gwy_app_log_create_textbuf();
    GtkWidget *logview = gtk_text_view_new_with_buffer(textbuf);
    gtk_text_view_set_editable(GTK_TEXT_VIEW(logview), FALSE);

    GtkWidget *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, "key-press-event", G_CALLBACK(message_log_key_pressed), NULL);
    g_object_weak_ref(G_OBJECT(window), message_log_window_destroyed, proxy);
}

static void
message_log_window_destroyed(gpointer user_data,
                             G_GNUC_UNUSED GObject *where_the_object_was)
{
    FileProxy *proxy = (FileProxy*)user_data;

    proxy->message_window = NULL;
    g_clear_object(&proxy->message_textbuf);

    DataBrowser *browser = _gwy_data_browser();
    if (proxy == browser->current_file && browser->gui)
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(browser->gui->messages_button), FALSE);
}

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_key_pressed(GtkWidget *window, GdkEventKey *event)
{
    if (event->keyval != GDK_KEY_Escape || (event->state & IMPORTANT_MODS))
        return FALSE;

    gtk_widget_destroy(window);
    return TRUE;
}

static void
update_messages_textbuf_since(FileProxy *proxy, guint from)
{
    GArray *messages = proxy->messages;
    GtkTextBuffer *textbuf = proxy->message_textbuf;

    if (!messages || !textbuf)
        return;

    for (guint i = from; i < messages->len; i++) {
        const GwyAppLogMessage *message = &g_array_index(messages, GwyAppLogMessage, i);
        proxy->log_levels_seen |= message->log_level;
        _gwy_app_log_add_message_to_textbuf(textbuf, message->message, message->log_level);
    }
    update_message_button();
}

static void
clear_log_message(gpointer user_data)
{
    GwyAppLogMessage *message = (GwyAppLogMessage*)user_data;
    g_free(message->message);
}

void
_gwy_data_browser_add_messages(GwyFile *file)
{
    if (!file) {
        _gwy_app_log_discard_captured_messages();
        g_warning("Cannot add messages for NULL data.");
        return;
    }
    g_return_if_fail(GWY_IS_FILE(file));

    DataBrowser *browser = _gwy_data_browser();
    /* If GUI is disabled, do not even remember the messages. If it just isn't shown we may need them later. */
    if (!browser->gui_enabled) {
        _gwy_app_log_discard_captured_messages();
        return;
    }
    FileProxy *proxy = proxy_for_file(file, NULL);
    if (!proxy) {
        _gwy_app_log_discard_captured_messages();
        g_critical("Data file container is not managed by the data browser.");
        return;
    }

    guint nmesg;
    GwyAppLogMessage *messages = _gwy_app_log_get_captured_messages(&nmesg);
    if (!messages)
        return;

    if (!proxy->messages) {
        proxy->messages = g_array_new(FALSE, FALSE, sizeof(GwyAppLogMessage));
        g_array_set_clear_func(proxy->messages, clear_log_message);
    }

    g_array_append_vals(proxy->messages, messages, nmesg);
    g_free(messages);

    update_messages_textbuf_since(proxy, proxy->messages->len - nmesg);
    if (browser->gui)
        update_message_button();
}

static void
update_message_button(void)
{
    DataBrowser *browser = _gwy_data_browser();
    DataBrowserGUI *gui = browser->gui;
    g_return_if_fail(gui);
    FileProxy *proxy = browser->current_file;
    GtkWidget *button = gui->messages_button;

    if (!proxy || !proxy->messages || !proxy->messages->len) {
        gtk_widget_set_no_show_all(button, TRUE);
        gtk_widget_hide(button);
        return;
    }

    if (gui->button_log_level != proxy->log_levels_seen) {
        const gchar *icon_name = GWY_ICON_LOAD_INFO;

        gtk_widget_destroy(gtk_bin_get_child(GTK_BIN(button)));
        if (proxy->log_levels_seen & (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING))
            icon_name = GWY_ICON_LOAD_WARNING;
        else if (proxy->log_levels_seen & (G_LOG_LEVEL_MESSAGE | G_LOG_LEVEL_INFO))
            icon_name = GWY_ICON_LOAD_INFO;
        else if (proxy->log_levels_seen & G_LOG_LEVEL_DEBUG)
            icon_name = GWY_ICON_LOAD_DEBUG;

        GtkWidget *image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON);
        gtk_container_add(GTK_CONTAINER(button), image);
        gui->button_log_level = proxy->log_levels_seen;
    }

    gtk_widget_set_no_show_all(button, FALSE);
    gtk_widget_show_all(button);
    /* The "toggled" handler can deal with setting state to the existing state. */
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(button), !!proxy->message_window);
}

static void
selection_changed(GtkTreeSelection *selection, DataBrowser *browser)
{
    GtkTreeView *treeview = gtk_tree_selection_get_tree_view(selection);
    GtkTreeModel *model = gtk_tree_view_get_model(treeview);
    if (!model) {
        /* No model means no file at all (not just no data of the current kind). */
        set_action_buttons_sensitivity(browser, FALSE);
        gwy_app_sensitivity_set_state(GWY_MENU_FLAG_FILE
                                      | GWY_MENU_FLAG_IMAGE | GWY_MENU_FLAG_IMAGE_MASK | GWY_MENU_FLAG_IMAGE_SHOW
                                      | GWY_MENU_FLAG_GL
                                      | GWY_MENU_FLAG_GRAPH | GWY_MENU_FLAG_GRAPH_CURVE | GWY_MENU_FLAG_SPECTRA
                                      | GWY_MENU_FLAG_VOLUME | GWY_MENU_FLAG_XYZ | GWY_MENU_FLAG_CMAP,
                                      0);
        return;
    }
    GwyDataKind data_kind = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(model), data_kind_quark()));
    FileProxy *proxy = g_object_get_qdata(G_OBJECT(model), proxy_quark());
    gwy_app_sensitivity_set_state(GWY_MENU_FLAG_FILE, GWY_MENU_FLAG_FILE);

    GtkTreeIter iter;
    gboolean any = gtk_tree_selection_get_selected(selection, NULL, &iter);
    gwy_debug("Any: %d (page %d)", any, data_kind);
    if (data_kind == browser->current_kind) {
        /* We have multiple lists but only one of set of button. Ignore changes in inactive lists. */
        set_action_buttons_sensitivity(browser, any);
    }

    PieceInfo *info = NULL;
    if (any) {
        gtk_tree_model_get(model, &iter, 0, &info, -1);
        g_assert(info->proxy == proxy);
        g_assert(info->parsed.data_kind == data_kind);
        proxy->current_items[data_kind] = info->parsed.id;
    }
    else {
        proxy->current_items[data_kind] = -1;
    }
    _gwy_data_browser_update_sensitivity_flags(data_kind, info);
}

void
_gwy_data_browser_update_sensitivity_flags(GwyDataKind data_kind, const PieceInfo *info)
{
    GwyFile *file = info ? info->proxy->file : NULL;
    GwyDict *container = file ? GWY_DICT(file) : NULL;
    gint id = info ? info->parsed.id : -1;
    GwyMenuSensFlags flags = 0, flag_mask = 0;

    if (data_kind == GWY_FILE_IMAGE) {
        flag_mask = GWY_MENU_FLAG_IMAGE | GWY_MENU_FLAG_IMAGE_SHOW | GWY_MENU_FLAG_IMAGE_MASK;
        if (file) {
            flags |= GWY_MENU_FLAG_IMAGE;

            GwyField *field;
            if (gwy_dict_gis_object(container, gwy_file_key_image_picture(id), &field) && GWY_IS_FIELD(field))
                flags |= GWY_MENU_FLAG_IMAGE_SHOW;

            GwyNield *nield;
            if ((nield = gwy_file_get_image_mask(file, id)) && GWY_IS_NIELD(nield))
                flags |= GWY_MENU_FLAG_IMAGE_MASK;
        }
    }
    else if (data_kind == GWY_FILE_GRAPH) {
        flag_mask = GWY_MENU_FLAG_GRAPH | GWY_MENU_FLAG_GRAPH_CURVE;
        if (file) {
            flags |= GWY_MENU_FLAG_GRAPH;

            GwyGraphModel *gmodel;
            if ((gmodel = gwy_file_get_graph(file, id)) && gwy_graph_model_get_n_curves(gmodel))
                flags |= GWY_MENU_FLAG_GRAPH_CURVE;
        }
    }
    else if (data_kind == GWY_FILE_VOLUME) {
        flag_mask = GWY_MENU_FLAG_VOLUME;
        if (file)
            flags |= GWY_MENU_FLAG_VOLUME;
    }
    else if (data_kind == GWY_FILE_XYZ) {
        flag_mask = GWY_MENU_FLAG_XYZ;
        if (file)
            flags |= GWY_MENU_FLAG_XYZ;
    }
    else if (data_kind == GWY_FILE_CMAP) {
        flag_mask = GWY_MENU_FLAG_CMAP;
        if (file)
            flags |= GWY_MENU_FLAG_CMAP;
    }
    else if (data_kind == GWY_FILE_SPECTRA) {
        flag_mask = GWY_MENU_FLAG_SPECTRA;
        if (file)
            flags |= GWY_MENU_FLAG_SPECTRA;
    }

    if (flag_mask) {
        gwy_debug("to_set %04x, flags %04x", flag_mask, flags);
        gwy_app_sensitivity_set_state(flag_mask, flags);
    }
}

static void
page_switched(DataBrowser *browser,
              G_GNUC_UNUSED GtkWidget *page_widget,
              guint pageno)
{
    if (browser->in_shutdown)
        return;

    DataBrowserGUI *gui = browser->gui;
    if (gui->switching_pages)
        return;

    GwyDataKind data_kind = GWY_FILE_NONE;
    for (guint i = 0; i < GWY_FILE_N_KINDS; i++) {
        if (gui->lists[i].pageno == pageno) {
            data_kind = i;
            break;
        }
    }
    g_return_if_fail(data_kind != GWY_FILE_NONE);
    browser->current_kind = data_kind;

    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(gui->lists[data_kind].treeview));
    selection_changed(selection, browser);
}

static void
destroyed(DataBrowser *browser, GtkWidget *main_vbox)
{
    /* TODO */
    DataBrowserGUI *gui = browser->gui;
    g_clear_object(&gui->accel_group);
    GWY_FREE(browser->gui);
    g_object_unref(main_vbox);
}

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