/*
 *  $Id: data-browser.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 <stdarg.h>
#include <glib/gi18n-lib.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"

/* TODO:
 * Who is responsible for sensitivity and when to update it? It would be probably easier to just queue it to an idle
 * function upon update. The program must then survive the user possibly (e.g. over slow network) being able to click
 * on thing before they are disabled. Should not be such a hassle.
 */

enum {
    SCATTER_PREVIEW_MAX_SIZE = 512,
};

typedef struct {
    GwyDataWatchFunc function;
    GDestroyNotify destroy;
    gpointer user_data;
    gulong wid;
    GwyDataKind data_kind;
} WatcherData;

static void       ensure_browser    (void);
static FileProxy* embrace_file      (GwyFile *file);
static void       abandon_file      (FileProxy *proxy);
static gboolean   make_visible_right(FileProxy *proxy,
                                     GwyDataKind data_kind,
                                     gint id);
static void       close_window      (PieceInfo *info);
static void       create_window     (PieceInfo *info);
static void       window_destroyed  (GtkWindow *window,
                                     PieceInfo *info);
static void       disconnect_window (PieceInfo *info);
static void       select_window     (GtkWindow *window,
                                     GdkEventFocus *event,
                                     PieceInfo *info);
static void       object_changed    (PieceInfo *info);
static void       file_item_changed (GwyFile *file,
                                     GQuark key,
                                     FileProxy *proxy);
static void       notify_tree_model (PieceInfo *info);
static void       notify_watchers   (GwyFile *file,
                                     GwyDataKind data_kind,
                                     gint id,
                                     GwyDataWatchEventType event);
static void       select_data_piece (const PieceInfo *info);
static void       really_switch_to  (const SwitchTo *swto);

static G_DEFINE_QUARK(gwy-data-browser-object-info, object_info)
/* 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--gui.c */
static G_DEFINE_QUARK(gwy-data-browser-proxy, proxy)
static G_DEFINE_QUARK(gwy-data-browser-data-kind, data_kind)

static DataBrowser browser = {
    .gui_enabled = TRUE,
    .current_kind = GWY_FILE_NONE,
};

DataBrowser*
_gwy_data_browser(void)
{
    return &browser;
}

static inline PieceInfo*
get_piece_info(gpointer object)
{
    if (G_UNLIKELY(!browser.objpieces))
        return NULL;
    return g_hash_table_lookup(browser.objpieces, object);
}

static inline FileProxy*
proxy_for_file(GwyFile *file, GList **link)
{
    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;
}

/* FIXME: We will have derived classes for app-level data windows which will know what they display. We may not need
 * to a hash table/object data to get the information about the widget. */
static inline PieceInfo*
lookup_widget_object_info(GtkWidget *widget)
{
    if (!GTK_IS_WINDOW(widget))
        widget = gtk_widget_get_toplevel(widget);
    if (!GTK_IS_WINDOW(widget))
        return NULL;

    return g_object_get_qdata(G_OBJECT(widget), object_info_quark());
}

static inline PieceInfo*
lookup_primary_info(FileProxy *proxy, GwyDataKind data_kind, gint id)
{
    GQuark key = gwy_file_key_data(data_kind, id);
    return g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key));
}

/* Responding to changes:
 * - Item in file: swapping object → view, thumbnail, browser info, watchers.
 * - The primary data object: view, thumbnail, browser info, watchers.
 * - Gradient item: view, thumbnail.
 * - Gradient object: thumbnail.
 * - Graph properties: thumbnail, browser info, watchers.
 * - Graph curve properties/data: thumbnail, browser info, watchers(?).
 * - Units: watchers (may need refilter). Views should update automatically.
 * - Volume calibration: browser info, watchers. Should have a signal?
 * - Lawn segmentation, curve labels, etc.: browser info, watchers. Should have a signal?
 * - Volume and lawn preview; image presentation object: swapping object → view, thumbnails.
 * - Volume and lawn preview; image presentation: view, thumbnails.
 * - Mask & colour: → view, thumbnails.
 * - 3D stuff: later.
 * - Mapping and range: view, thumbnails.
 */
static gboolean
connect_piece(PieceInfo *info, gdouble timestamp)
{
    if (!info->key)
        info->key = gwy_form_file_key(&info->parsed);

    FileProxy *proxy = info->proxy;
    GwyDict *container = GWY_DICT(proxy->file);
    if (!gwy_dict_gis_object(container, info->key, &info->object)) {
        /* Main objects must exist. Other things are optional. */
        if (info->parsed.piece == GWY_FILE_PIECE_NONE)
            g_critical("Connecting to non-existent object %s.", g_quark_to_string(info->key));
        g_free(info);
        return FALSE;
    }

    info->changed_timestamp = (timestamp < 0 ? get_timestamp_now() : timestamp);

    GwyFilePiece piece = info->parsed.piece;
    GwyDataKind data_kind = info->parsed.data_kind;
    /* FIXME: It would be nice to have separate signals like "calibration-changed", but if everything just emits
     * "data-changed" that is probably good enough. */
    if (piece == GWY_FILE_PIECE_NONE) {
        if (data_kind == GWY_FILE_GRAPH) {
            info->signal_id = g_signal_connect_swapped(info->object, "curve-data-changed",
                                                       G_CALLBACK(object_changed), info);
            info->signal_id2 = g_signal_connect_swapped(info->object, "curve-notify",
                                                        G_CALLBACK(object_changed), info);
            info->signal_id3 = g_signal_connect_swapped(info->object, "notify",
                                                        G_CALLBACK(object_changed), info);
        }
        else {
            info->signal_id = g_signal_connect_swapped(info->object, "data-changed", G_CALLBACK(object_changed), info);
        }
    }
    else if (piece == GWY_FILE_PIECE_PICTURE || piece == GWY_FILE_PIECE_MASK) {
        info->signal_id = g_signal_connect_swapped(info->object, "data-changed", G_CALLBACK(object_changed), info);
    }

    return TRUE;
}

static void
disconnect_piece(PieceInfo *info)
{
    GObject *object = info->object;
    g_return_if_fail(object);
    g_clear_signal_handler(&info->signal_id3, object);
    g_clear_signal_handler(&info->signal_id2, object);
    g_clear_signal_handler(&info->signal_id, object);
}

/* TODO: This is nice, but it needs to be more refined. We want to connect to objects such as previews to update
 * the thumbnails. Even editing a gradient means the thumbnail should be updated (2.x misses this one). Such objects
 * are not directly interesting to the lists and watchers. But once we create a PieceInfo, we need to manage it. */
static PieceInfo*
add_piece(FileProxy *proxy, GwyDataKind data_kind, gint id, GwyFilePiece piece, gdouble timestamp)
{
    PieceInfo *primary = NULL;
    if (piece != GWY_FILE_PIECE_NONE) {
        /* Ignore added secondary objects unless the primary object exists. If they are added early, we will take them
         * when the primary object is added. */
        primary = lookup_primary_info(proxy, data_kind, id);
        if (!primary)
            return NULL;
    }

    PieceInfo *info = g_new0(PieceInfo, 1);
    info->proxy = proxy;
    info->parsed = (GwyFileKeyParsed){ .data_kind = data_kind, .id = id, .piece = piece, .suffix = NULL };
    if (!connect_piece(info, timestamp))
        return NULL;

    g_hash_table_insert(proxy->qpieces, GUINT_TO_POINTER(info->key), info);
    g_hash_table_insert(browser.objpieces, info->object, info);

    if (primary)
        info->iter = primary->iter;

    return info;
}

static PieceInfo*
add_primary_object(FileProxy *proxy, GwyDataKind data_kind, gint id, gdouble timestamp)
{
    timestamp = (timestamp < 0 ? get_timestamp_now() : timestamp);

    PieceInfo *info = add_piece(proxy, data_kind, id, GWY_FILE_PIECE_NONE, timestamp);
    g_return_val_if_fail(info, NULL);

    GtkTreeModel *model = proxy->lists[data_kind];
    if (model)
        gtk_list_store_insert_with_values(GTK_LIST_STORE(model), &info->iter, -1, 0, info, -1);

    /* Mirrors removals in remove_primary_object(). */
    if (data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP) {
        add_piece(proxy, data_kind, id, GWY_FILE_PIECE_MASK, timestamp);
        add_piece(proxy, data_kind, id, GWY_FILE_PIECE_PICTURE, timestamp);
    }
    else if (data_kind == GWY_FILE_XYZ)
        add_piece(proxy, data_kind, id, GWY_FILE_PIECE_PICTURE, timestamp);

    notify_watchers(proxy->file, data_kind, id, GWY_DATA_WATCH_EVENT_ADDED);
    return info;
}

static void
remove_piece(PieceInfo *info)
{
    g_clear_object(&info->thumbnail);
    disconnect_piece(info);
    g_hash_table_remove(browser.objpieces, info->object);
    g_hash_table_remove(info->proxy->qpieces, GUINT_TO_POINTER(info->key));
    /* If we removed the primary piece, do not notify the tree view because the row is gone. */
    if (info->parsed.piece != GWY_FILE_PIECE_NONE)
        notify_tree_model(info);
    g_free(info);
}

static void
maybe_remove_piece(FileProxy *proxy, GwyDataKind data_kind, gint id, GwyFilePiece piece)
{
    GwyFileKeyParsed parsed = { .data_kind = data_kind, .id = id, .piece = piece, .suffix = NULL };
    GQuark key = gwy_form_file_key(&parsed);
    PieceInfo *info;
    if ((info = g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key))))
        remove_piece(info);
}

/* Returns FALSE if proxy is no longer valid (file is gone now). */
static gboolean
remove_primary_object(PieceInfo *info)
{
    GwyDataKind data_kind = info->parsed.data_kind;
    gint id = info->parsed.id;
    GwyFilePiece piece = info->parsed.piece;
    FileProxy *proxy = info->proxy;

    g_assert(piece == GWY_FILE_PIECE_NONE);
    if (info->window) {
        gint visible_count = proxy->visible_count - 1;
        gtk_widget_destroy(GTK_WIDGET(info->window));
        /* If this removed the last visible object in the file, bye bye the file. In such case all the code below
         * would be trying to access data structures which no longer exist. Checking if visible count is 1 is not
         * nice, but probably fine. */
        if (!visible_count)
            return FALSE;
    }

    /* Mirrors additions in add_primary_object(). */
    if (data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME || data_kind == GWY_FILE_CMAP) {
        maybe_remove_piece(proxy, data_kind, id, GWY_FILE_PIECE_MASK);
        maybe_remove_piece(proxy, data_kind, id, GWY_FILE_PIECE_PICTURE);
    }
    else if (data_kind == GWY_FILE_XYZ)
        maybe_remove_piece(proxy, data_kind, id, GWY_FILE_PIECE_PICTURE);

    GtkTreeModel *model = proxy->lists[data_kind];
    if (model)
        gtk_list_store_remove(GTK_LIST_STORE(model), &info->iter);

    g_clear_object(&info->thumbnail);
    notify_watchers(proxy->file, data_kind, id, GWY_DATA_WATCH_EVENT_REMOVED);
    remove_piece(info);

    return TRUE;
}

static void
replace_object(PieceInfo *info)
{
    GwyDataKind data_kind = info->parsed.data_kind;
    gint id = info->parsed.id;
    FileProxy *proxy = info->proxy;

    g_hash_table_remove(browser.objpieces, info->object);
    disconnect_piece(info);
    if (!connect_piece(info, -1.0)) {
        g_critical("Cannot reconnect to object.");
        return;
    }
    g_hash_table_insert(browser.objpieces, info->object, info);

    notify_tree_model(info);
    notify_watchers(proxy->file, data_kind, id, GWY_DATA_WATCH_EVENT_CHANGED);
}

// NB: We are connecting callback with varying signatures. So the only consistent way is connect-swapped.
static void
object_changed(PieceInfo *info)
{
    notify_tree_model(info);
}

static void
notify_tree_model(PieceInfo *info)
{
    if (browser.in_shutdown)
        return;

    FileProxy *proxy = info->proxy;
    GwyDataKind data_kind = info->parsed.data_kind;
    GtkTreeModel *model = proxy->lists[data_kind];
    if (!model)
        return;

    info->changed_timestamp = get_timestamp_now();
    if (info->parsed.piece != GWY_FILE_PIECE_NONE) {
        PieceInfo *primary = lookup_primary_info(proxy, data_kind, info->parsed.id);
        if (primary)
            primary->changed_timestamp = fmax(primary->changed_timestamp, info->changed_timestamp);
        else {
            /* There should be a primary data piece if we are doing updates on the secondary one. */
            g_warn_if_reached();
        }
    }
    GtkTreePath *path = gtk_tree_model_get_path(model, &info->iter);
    gtk_tree_model_row_changed(model, path, &info->iter);
    gtk_tree_path_free(path);
}

static void
file_item_changed(GwyFile *file, GQuark key, FileProxy *proxy)
{
    if (browser.in_shutdown)
        return;

    GwyFileKeyParsed parsed;
    gboolean key_parsed_ok = gwy_parse_file_key(g_quark_to_string(key), &parsed);
    g_return_if_fail(key_parsed_ok);

    GwyDataKind data_kind = parsed.data_kind;
    if (data_kind == GWY_FILE_NONE) {
        if (browser.gui_enabled)
            _gwy_data_browser_update_filename();
        return;
    }

    GwyDict *container = GWY_DICT(file);
    GwyFilePiece piece = parsed.piece;
    gint id = parsed.id;
    gboolean item_present = gwy_dict_contains(container, key);
    /* Info exists and item is present: Item was replaced with a different object. We could check if the object
     * really differs or someone is pulling a fast one. But generally, disconnect, update object, reconnect. Update
     * all kinds of stuff (watchers). Windows will update themselves? Tools need updating.
     *
     * Info exist but item is not present: Item was deleted (Container helpfully unrefs the object after emitting
     * the signal, so we can use info->object). Disconnect, remove iter from GtkListStore, remove info from hash
     * tables, free it. Who will close windows for primary data? Will they close themselves? Tools may need updating.
     *
     * Info does not exists but item is present: Item was added. Connect, update watchers. Window is not visible so
     * tools do not need updating.
     *
     * Neither info nor item exists: Impossible. Panic.
     */
#ifdef DEBUG
    gwy_debug("%p:item-changed at %s (%s, %s, %d, %s)",
              file, g_quark_to_string(key),
              _gwy_data_kind_name(data_kind), _gwy_file_piece_name(piece), id, parsed.suffix);
#endif

    gboolean handled_object = FALSE;
    if (piece == GWY_FILE_PIECE_NONE) {
        PieceInfo *info = g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key));
        gwy_debug("primary data object (info %p, exists %d)", info, item_present);
        g_return_if_fail(info || item_present);
        if (info && item_present)
            replace_object(info);
        else if (item_present) {
            add_primary_object(proxy, data_kind, id, -1.0);
            if (browser.gui_enabled)
                make_visible_right(proxy, data_kind, id);
        }
        else if (info) {
            if (!remove_primary_object(info))
                proxy = NULL;
        }
        handled_object = TRUE;
    }

    /* Non-primary objects. We ignore them when added until the primary object is added (and it does the corresponding
     * add_piece() when being added). Replacement and removal can then assume the primary object exists. */
    if (piece == GWY_FILE_PIECE_PICTURE || piece == GWY_FILE_PIECE_MASK) {
        PieceInfo *info = g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key));
        gwy_debug("secondary data object (info %p, exists %d)", info, item_present);
        if (info && item_present)
            replace_object(info);
        else if (item_present)
            info = add_piece(proxy, data_kind, id, piece, -1.0);
        else if (info)
            remove_piece(info);
        else {
            /* Everything should just ignore the cleanup of auxiliary objects after the main object has been removed.
             * Says gwy_file_remove(). */
            return;
        }
        handled_object = TRUE;
    }

    /* Everything else is either visibility (creating and destroying windows) or should update thumbnails or other
     * data info. None of it is relevant when we have no GUI. */
    if (!browser.gui_enabled)
        return;

    if (handled_object) {
        /* FIXME: Notify the tree model? replace_object() already does it. What about the other cases? */
        PieceInfo *info = proxy ? g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key)) : NULL;
        _gwy_data_browser_update_sensitivity_flags(data_kind, info);
        return;
    }

    PieceInfo *info = lookup_primary_info(proxy, data_kind, id);
    if (!info) {
        gwy_debug("changed item does not correspond to a known primary data object, ignoring");
        return;
    }

    if (piece == GWY_FILE_PIECE_VISIBLE) {
        g_assert(!parsed.suffix);
        /* Visibility should cause its own update. It can also make the entire file go poof. So do not try any updates
         * after updating visibility. */
        make_visible_right(proxy, data_kind, id);
        return;
    }

    if (piece == GWY_FILE_PIECE_PALETTE || piece == GWY_FILE_PIECE_COLOR_MAPPING || piece == GWY_FILE_PIECE_RANGE
        || piece == GWY_FILE_PIECE_MASK_COLOR || piece == GWY_FILE_PIECE_TITLE) {
        /* TODO: Notify watchers? */
        notify_tree_model(info);
    }
}

/**
 * gwy_data_browser_add:
 * @file: (transfer full): A data file container
 *
 * Adds a data file container to the application data browser.
 *
 * The file becomes managed by the data browser. Use gwy_data_browser_remove() to remove it. The data browser takes
 * a reference on the file; you can usually release yours.
 *
 * If the file is still marked in construction, the construction is finished. If some data objects that need an image
 * representation (e.g. volume or XYZ data) lack previews, default previews are created for them.
 *
 * If GUI is running, windows are created, showing @file's data according to visibility flags. If nothing is marked
 * visible, the first found data are displayed.
 **/
void
gwy_data_browser_add(GwyFile *file)
{
    g_return_if_fail(GWY_IS_FILE(file));
    g_return_if_fail(gwy_file_get_id(file) == GWY_FILE_ID_NONE);
    g_object_ref(file);

    GwyDict *container = GWY_DICT(file);
    if (gwy_dict_is_being_constructed(container))
        gwy_dict_finish_construction(container);

    gint id;
    ensure_browser();
    if ((id = gwy_file_get_id(file)) == GWY_FILE_ID_NONE)
        _gwy_file_set_id(file, (id = browser.last_fileid++));
    g_hash_table_insert(browser.files, GINT_TO_POINTER(id), file);
    _gwy_file_set_managed(file, TRUE);

    FileProxy *proxy = embrace_file(file);
    browser.proxies = g_list_prepend(browser.proxies, proxy);

    if (browser.gui_enabled) {
        gwy_data_browser_reset_visibility(file, GWY_VISIBILITY_RESET_DEFAULT);
        /* FIXME: Does need to be added to browser GUI somehow? Proxy GUI is created lazily. */
    }
}

/**
 * gwy_data_browser_remove:
 * @file: (transfer none): A data file container
 *
 * Removes a data file container to the application data browser.
 *
 * If GUI is running, all windows displaying @file's data are closed. The file ceases being managed by the data
 * browser. The data browser releases its reference on the file. This often leads to @file's destruction.
 *
 * The current file may not be removed while switching is blocked with gwy_data_browser_block_switching(). It would
 * lead to a contradiction: the browser must switch away from @file but it is not allowed to change the current file.
 **/
void
gwy_data_browser_remove(GwyFile *file)
{
    g_return_if_fail(GWY_IS_FILE(file));
    GList *link;
    FileProxy *proxy = proxy_for_file(file, &link);
    g_return_if_fail(proxy);

    gwy_debug("removing file %p (proxy %p)", file, proxy);
    /* Switch the current data away from @file (including browser GUI). This should not happen when switching is
     * blocked. If someone blocks switching and removes the current file he rightfully gets the crash he deserves.
     *
     * 2.x makes it NULL and it also works (there is an unused function desperately_switch_to_something() which may be
     * helpful at the end here. */
    if (proxy == browser.current_file) {
        select_data_piece(NULL);
        g_return_if_fail(browser.current_file != proxy);
    }

    /* TODO: Kill the proxy GUI for the file. */
    if (proxy->message_window)
        gtk_widget_destroy(proxy->message_window);
    g_clear_object(&proxy->message_textbuf);
    GWY_FREE_ARRAY(proxy->messages);

    g_clear_signal_handler(&proxy->item_changed_id, file);
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++)
        g_clear_object(proxy->lists + data_kind);
    abandon_file(proxy);
    g_hash_table_destroy(proxy->qpieces);
    /* NB: Remove id and any object file. Make the file pristine again. */

    g_hash_table_remove(browser.files, GINT_TO_POINTER(gwy_file_get_id(file)));
    browser.proxies = g_list_remove_link(browser.proxies, link);
    g_list_free_1(link);
    g_free(proxy);
    _gwy_file_set_managed(file, FALSE);

    g_object_unref(file);

    gwy_debug("finished removing file %p", file);
    /* TODO desperately_switch_to_something(); */
}

/**
 * gwy_data_browser_get_file:
 * @id: Identifier of a data file container.
 *
 * Finds a file by its numerical id.
 *
 * Only files managed by the data browser have an id. It is not an error to request a file which does not exist. In
 * such case, %NULL is returned. See also gwy_file_get_id().
 *
 * Returns: (transfer none) (nullable): The file identified by @id, or %NULL.
 **/
GwyFile*
gwy_data_browser_get_file(gint id)
{
    /* Calling this function when we have no files yet is a weird but valid use case. */
    if (!browser.files)
        return NULL;
    return g_hash_table_lookup(browser.files, GINT_TO_POINTER(id));
}

/**
 * gwy_data_browser_copy_data:
 * @source: Source data file container.
 * @data_kind: Type of data to copy.
 * @id: Data item id.
 * @dest: Target data file container.
 *
 * Adds a new data items as a duplicate of existing data including all auxiliary information.
 *
 * The function differs from gwy_file_copy_data() in two primary aspects: It adds a log entry and it makes the new data
 * visible. Therefore, it suitable for data duplication in a running GUI, as opposed to mere file manipulation
 * function.
 *
 * Returns: The id of copied data in @dest.
 **/
gint
gwy_data_browser_copy_data(GwyFile *source,
                           GwyDataKind data_kind,
                           gint id,
                           GwyFile *dest)
{
    gint newid = gwy_file_copy_data(source, data_kind, id, dest);
    g_return_val_if_fail(newid >= 0, newid);
    _gwy_log_builtin_duplicate(source, dest, data_kind, id, newid);
    gwy_file_set_visible(dest, data_kind, newid, TRUE);
    return newid;
}

// Helper for forking and updating the log correctly for data duplication.
void
_gwy_log_builtin_duplicate(GwyFile *srcfile, GwyFile *destfile,
                           GwyDataKind data_kind, gint oldid, gint newid)
{
    if (!gwy_log_get_enabled())
        return;

    GwyFileKeyParsed parsed = { .id = oldid, .data_kind = data_kind, .piece = GWY_FILE_PIECE_LOG, .suffix = NULL };
    GwyStringList *datalog;
    if (gwy_dict_gis_object(GWY_DICT(srcfile), gwy_form_file_key(&parsed), &datalog)) {
        datalog = gwy_string_list_copy(datalog);
        parsed.id = newid;
        gwy_dict_pass_object(GWY_DICT(destfile), gwy_form_file_key(&parsed), datalog);
    }
    /* This will create a new log if needed. Do not pass oldid here because that requests forking the log and
     * we have already done it here. This dance could be avoided if gwy_log_add_full() had a form with different
     * source and destination files. */
    gwy_log_add_full(destfile, data_kind, newid, newid, "builtin::duplicate", NULL);
}

/**
 * gwy_data_browser_find_object:
 * @object: Data object, presumably in an data file managed the data browser.
 * @key: (out) (nullable): Location to store the key corresponding to @object in returned file. Possibly %NULL.
 *
 * Finds file and the corresponding key for any data object.
 *
 * The function does not actually scan all open files and is relatively cheap. The behaviour during changes in the
 * file, such as data replacement, addition or removal, is currently undefined (it may return values corresponding
 * either to the old or the new state).
 *
 * Returns: (nullable): The file containing @object, if any. Otherwise %NULL.
 **/
GwyFile*
gwy_data_browser_find_object(GObject *object,
                             GQuark *key)
{
    const PieceInfo *info = get_piece_info(object);
    if (!info) {
        if (key)
            *key = 0;
        return NULL;
    }

    if (key)
        *key = info->key;
    return info->proxy->file;
}

static void
ensure_volume_preview(GwyDict *container, gint id)
{
    GQuark key = gwy_file_key_volume_picture(id);
    GwyField *preview;
    if (gwy_dict_gis_object(container, key, &preview))
        return;

    GwyBrick *brick = gwy_file_get_volume(GWY_FILE(container), id);
    gwy_dict_pass_object(container, key, _gwy_app_create_brick_preview_field(brick));
}

static void
ensure_xyz_preview(GwyDict *container, gint id)
{
    GQuark key = gwy_file_key_xyz_picture(id);
    GwyField *preview;
    if (gwy_dict_gis_object(container, key, &preview))
        return;

    GwyScatter *scatter = gwy_file_get_xyz(GWY_FILE(container), id);
    GwyField *raster = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    gint ivalue = gwy_scatter_get_n_values(scatter) ? 0 : -1;
    gwy_preview_scatter_to_field(scatter, ivalue, raster, SCATTER_PREVIEW_MAX_SIZE, SCATTER_PREVIEW_MAX_SIZE, 0);
    gwy_dict_pass_object(container, key, raster);
}

static void
ensure_cmap_preview(GwyDict *container, gint id)
{
    GQuark key = gwy_file_key_cmap_picture(id);
    GwyField *preview;
    if (gwy_dict_gis_object(container, key, &preview))
        return;

    GwyLawn *lawn = gwy_file_get_cmap(GWY_FILE(container), id);
    gwy_dict_pass_object(container, key, _gwy_app_create_lawn_preview_field(lawn));
}

static FileProxy*
embrace_file(GwyFile *file)
{
    FileProxy *proxy = g_new0(FileProxy, 1);
    proxy->file = file;
    proxy->qpieces = g_hash_table_new(g_direct_hash, g_direct_equal);

    gdouble timestamp = get_timestamp_now();
    GwyDict *container = GWY_DICT(file);

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        if (browser.gui_enabled) {
            GtkListStore *store = gtk_list_store_new(1, G_TYPE_POINTER);
            proxy->lists[data_kind] = GTK_TREE_MODEL(store);
            /* We are storing iters long-term. This should not actually ever fail, but if GTK+ for whatever reason
             * changes, fail here noisily. */
            g_assert(gtk_tree_model_get_flags(proxy->lists[data_kind]) & GTK_TREE_MODEL_ITERS_PERSIST);
            g_object_set_qdata(G_OBJECT(store), proxy_quark(), proxy);
            /* This duplicates the data kind name, but we like data_kind_quark() for internal use because it is much
             * more direct. */
            g_object_set_qdata(G_OBJECT(store), data_kind_quark(), GUINT_TO_POINTER(data_kind));
            gwy_app_set_tree_model_kind(proxy->lists[data_kind], _gwy_data_kind_name(data_kind));
        }

        gint *ids = gwy_file_get_ids(file, data_kind);
        proxy->current_items[data_kind] = ids[0];
        for (gint j = 0; ids[j] >= 0; j++) {
            gint id = ids[j];
            if (data_kind == GWY_FILE_VOLUME)
                ensure_volume_preview(container, id);
            else if (data_kind == GWY_FILE_XYZ)
                ensure_xyz_preview(container, id);
            else if (data_kind == GWY_FILE_CMAP)
                ensure_cmap_preview(container, id);

            add_primary_object(proxy, data_kind, id, timestamp);
        }
        g_free(ids);
    }

    proxy->item_changed_id = g_signal_connect(file, "item-changed", G_CALLBACK(file_item_changed), proxy);
    return proxy;
}

/* We want to kill all windows without setting the visibility flags. It is more complicated than in 2.x which assumes
 * the file will be destroyed anyway and just resets the visibility. However, the caller can be still holding its own
 * reference. So do not mess with the file content and close the windows explicitly. */
static void
abandon_file(FileProxy *proxy)
{
    GwyFile *file = proxy->file;
    GHashTable *qpieces = proxy->qpieces;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; 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(qpieces, GUINT_TO_POINTER(key));
            g_return_if_fail(info);
            disconnect_piece(info);
            /* If GUI is not running, checking info->window is fine because it should always be NULL. */
            if (info->window) {
                g_assert(proxy->visible_count);
                disconnect_window(info);
                close_window(info);
                proxy->visible_count--;
                gwy_debug("decremented visible count to %d", proxy->visible_count);
            }
            remove_piece(info);
            notify_watchers(file, data_kind, ids[j], GWY_DATA_WATCH_EVENT_REMOVED);
        }
        g_free(ids);
    }

    /* Whatever reprimarys (and it may be a lot) is non-primary data. */
    GPtrArray *pieces = g_hash_table_get_values_as_ptr_array(qpieces);
    for (guint i = 0; i < pieces->len; i++) {
        remove_piece(g_ptr_array_index(pieces, i));
    }
    g_ptr_array_free(pieces, TRUE);
}

static void
close_window(PieceInfo *info)
{
    gwy_debug("closing window for file %p, data_kind %s, id %d",
              info->proxy->file, _gwy_data_kind_name(info->parsed.data_kind), info->parsed.id);
    gtk_widget_destroy(GTK_WIDGET(info->window));
    /* The rest should happen in our "destroy" signal handler. */
}

static void
create_window(PieceInfo *info)
{
    g_assert(!info->window);

    FileProxy *proxy = info->proxy;
    GwyFile *file = proxy->file;
    gint id = info->parsed.id;
    GwyDataKind data_kind = info->parsed.data_kind;
    if (data_kind == GWY_FILE_IMAGE || data_kind == GWY_FILE_VOLUME
        || data_kind == GWY_FILE_XYZ || data_kind == GWY_FILE_CMAP) {
        gwy_debug("creating window for file %p, data_kind %s, id %d",
                  file, _gwy_data_kind_name(data_kind), id);
        info->window = gwy_app_image_window_new(file, data_kind, id);
    }
    else if (data_kind == GWY_FILE_GRAPH || data_kind == GWY_FILE_SPECTRA) {
        gwy_debug("creating window for file %p, data_kind %s, id %d",
                  file, _gwy_data_kind_name(data_kind), id);
        info->window = gwy_app_graph_window_new(file, data_kind, id);
    }
    else {
        g_warning("Implement me!");
        return;
    }
    g_object_set_qdata(G_OBJECT(info->window), object_info_quark(), info);
    info->focus_event_id = g_signal_connect(info->window, "focus-in-event", G_CALLBACK(select_window), info);
    info->window_destroyed_id = g_signal_connect(info->window, "destroy", G_CALLBACK(window_destroyed), info);
    gtk_widget_show_all(GTK_WIDGET(info->window));
    gtk_window_present(info->window);
}

static void
window_destroyed(G_GNUC_UNUSED GtkWindow *window, PieceInfo *info)
{
    if (browser.in_shutdown)
        return;

    gwy_debug("destroyed window %p", window);
    g_assert(window == info->window);
    disconnect_window(info);
    info->window = NULL;
    FileProxy *proxy = info->proxy;
    gwy_debug("visible count %d", proxy->visible_count);
    g_assert(proxy->visible_count);
    proxy->visible_count--;
    gwy_debug("decremented visible count to %d", proxy->visible_count);
    /* This will cause a call to make_visible_right(), which will not sync anything because the window
     * is already gone, but may kill the file if this was the last window. */
    gwy_file_set_visible(info->proxy->file, info->parsed.data_kind, info->parsed.id, FALSE);
}

static void
disconnect_window(PieceInfo *info)
{
    g_return_if_fail(info->window);
    g_clear_signal_handler(&info->window_destroyed_id, info->window);
    g_clear_signal_handler(&info->focus_event_id, info->window);
}

static void
select_window(G_GNUC_UNUSED GtkWindow *window,
              G_GNUC_UNUSED GdkEventFocus *event,
              PieceInfo *info)
{
    if (browser.in_shutdown)
        return;

    g_assert(window == info->window);
    select_data_piece(info);
}

static gboolean
make_visible_right(FileProxy *proxy, GwyDataKind data_kind, gint id)
{
    g_assert(browser.gui_enabled);
    gwy_debug("proxy %p (file %p), data_kind %d, id %d", proxy, proxy->file, data_kind, id);
    PieceInfo *info = lookup_primary_info(proxy, data_kind, id);
    gboolean visible = gwy_file_get_visible(proxy->file, data_kind, id);
    gwy_debug("info->window = %p, visible = %d", info->window, visible);
    if (info->window && !visible) {
        g_assert(proxy->visible_count);
        close_window(info);
        // If the window still existed, visible count has been decremented in close_window(). It is, unfortunately,
        // asymmetrical because users can close windows either by window manager means or unchecking the checkbox. We
        // could make it symmetrical if the "delete-event" handler of data windows only toggled "visible" and did not
        // let GTK+ close the window. Not sure if it would make the logic clearer.
        //proxy->visible_count--;
        //gwy_debug("decremented visible count to %d", proxy->visible_count);
    }
    else if (!info->window && visible) {
        create_window(info);
        proxy->visible_count++;
        gwy_debug("incremented visible count to %d", proxy->visible_count);
    }

    /* TODO: Count OpenGL windows into visible count. */
    if (!proxy->visible_count && !proxy->resetting_visibility)
        gwy_data_browser_remove(proxy->file);

    return visible;
}

/**
 * gwy_data_browser_reset_visibility:
 * @file: (transfer none): A data file container.
 * @reset_type: Type of visibility reset.
 *
 * Resets visibility of all data objects in a container.
 *
 * It is possible to perform the reset when GUI is disabled. In such case the visibility flags in the file are set,
 * but no windows are created or closed. The %GWY_VISIBILITY_RESET_RESTORE mode is a no-op in such case because it
 * does not change the visibility flags. %GWY_VISIBILITY_RESET_DEFAULT is often also no-op, but it may set the flag
 * for some data if none are currently marked visible.
 *
 * This function prevents the file from being closed when all windows are closed. Usually, a GUI caller should close
 * the file when getting a zero return value.
 *
 * Returns: The number of data objects marked visible in @file after the reset (if GUI is running, it will be also
 *          the number of data objects shown in windows now).
 **/
gint
gwy_data_browser_reset_visibility(GwyFile *file,
                                  GwyVisibilityResetType reset_type)
{
    gboolean gui_enabled = browser.gui_enabled;
    g_return_val_if_fail(GWY_IS_FILE(file), FALSE);
    FileProxy *proxy = proxy_for_file(file, NULL);
    g_return_val_if_fail(proxy, FALSE);

    gint *all_ids[GWY_FILE_N_KINDS];
    gint marked_visible = 0;
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++)
        all_ids[data_kind] = gwy_file_get_ids(file, data_kind);

    /* Prevent the file being closed. For RESTORE we may close windows before opening others. */
    g_assert(!proxy->resetting_visibility);
    proxy->resetting_visibility = TRUE;

    if (reset_type == GWY_VISIBILITY_RESET_RESTORE || reset_type == GWY_VISIBILITY_RESET_DEFAULT) {
        for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
            gint *ids = all_ids[data_kind];
            for (gint j = 0; ids[j] >= 0; j++) {
                gboolean visible;
                if (gui_enabled)
                    visible = make_visible_right(proxy, data_kind, ids[j]);
                else
                    visible = gwy_file_get_visible(file, data_kind, ids[j]);

                if (visible)
                    marked_visible++;
            }
        }

        /* For RESTORE, we are content even with nothing being displayed and just report it. */
        if (marked_visible || reset_type == GWY_VISIBILITY_RESET_RESTORE)
            goto finish;

        /* Attempt to show something for DEFAULT. */
        for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
            gint *ids = all_ids[data_kind];
            if (ids[0] != -1) {
                /* We know nothing was visible. So calling make_visible_right() is not necessary. */
                gwy_file_set_visible(file, data_kind, ids[0], TRUE);
                marked_visible++;
            }
        }

        goto finish;
    }

    gboolean visible = TRUE;
    if (reset_type == GWY_VISIBILITY_RESET_HIDE_ALL)
        visible = FALSE;
    else if (reset_type == GWY_VISIBILITY_RESET_SHOW_ALL)
        visible = TRUE;
    else {
        g_return_val_if_reached(FALSE);
    }

    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++) {
        gint *ids = all_ids[data_kind];
        for (gint j = 0; ids[j] >= 0; j++) {
            /* The function can be called from the GUI when there is a mismatch between data objects being marked
             * visible and objects actually being shown in windows. So, first mark the objects as requested. It
             * will fix some, but not necessarily all. Then make sure the state matches the flag. */
            gwy_file_set_visible(file, data_kind, ids[j], visible);
            if (gui_enabled)
                make_visible_right(proxy, data_kind, ids[j]);
            if (visible)
                marked_visible++;
        }
    }

finish:
    for (guint data_kind = 0; data_kind < GWY_FILE_N_KINDS; data_kind++)
        g_free(all_ids[data_kind]);
    proxy->resetting_visibility = FALSE;

    return marked_visible;
}

/**
 * gwy_data_browser_foreach:
 * @function: (scope call) (closure user_data) (destroy destroy):
 *            Function to call for each file.
 * @user_data: User data to pass to @function.
 * @destroy: Function to destroy user data when done, possibly %NULL.
 *
 * Calls a function for every file managed by the data browser.
 *
 * The function must not remove files from the data browser nor add new ones to it.
 **/
void
gwy_data_browser_foreach(GwyDataBrowserForeachFunc function,
                         gpointer user_data,
                         GDestroyNotify destroy)
{
    g_return_if_fail(function);

    for (GList *l = browser.proxies; l; l = g_list_next(l)) {
        FileProxy *proxy = (FileProxy*)l->data;
        function(proxy->file, user_data);
    }
    if (destroy)
        destroy(user_data);
}

/**
 * gwy_data_browser_select_widget:
 * @widget: (transfer none) (nullable): A data displaying widget.
 *
 * Selects the file and data item corresponding to a data displaying widget as current.
 *
 * Selecting an image window switches the active tool to this window.
 *
 * You can pass %NULL to select no data. Usually when some data exist, some data are selected as current. However, it
 * is possible to have no data selected when data exist.
 *
 * It is an error to pass non-data-displaying widgets.
 **/
void
gwy_data_browser_select_widget(GtkWidget *widget)
{
    if (widget) {
        PieceInfo *info = lookup_widget_object_info(widget);
        g_return_if_fail(info);
        select_data_piece(info);
    }
    else
        select_data_piece(NULL);
}

/**
 * gwy_data_browser_get_window_for_data:
 * @file: (transfer none): A data file container.
 * @data_kind: Type of the data object.
 * @id: Data item id.
 *
 * Gets the top-level window displaying given data.
 *
 * It is not an error to ask for nonexistend windows, even windows which could not exist (for example because @data
 * is not a managed file or @id is negative).
 *
 * Returns: (transfer none) (nullable):
 *          The window displaying data in @data identified by (@data_kind, @id), possibly %NULL.
 **/
GtkWindow*
gwy_data_browser_get_window_for_data(GwyFile *file,
                                     GwyDataKind data_kind,
                                     gint id)
{
    if (!file || id < 0 || (guint)data_kind >= GWY_FILE_N_KINDS)
        return NULL;

    FileProxy *proxy = proxy_for_file(file, NULL);
    if (!proxy)
        return NULL;

    PieceInfo *info = lookup_primary_info(proxy, data_kind, id);
    return info ? info->window : NULL;
}

/**
 * gwy_data_browser_get_data_for_widget:
 * @widget: (transfer none): A data displaying widget.
 * @file: (out) (optional) (transfer none): Location to store the corresponding data file.
 * @data_kind: (out) (optional): Location to store the corresponding data kind.
 * @id: (out) (optional): Location to store the corresponding data id.
 *
 * Identifies the data displayed by a data displaying widget.
 *
 * The preferred argument for @widget is the top-level window. However, there is some liberty. The function also
 * identifies the data correctly when given the data displaying widget such as #GwyDataWindow or #GwyGraph.
 * It is not an error to pass a window not showing any data, although there should not be any reasong for doing so.
 * If the function return %FALSE, the values of @file, @data_kind and @id are kept intact.
 *
 * The returned data kind is the primary data kind for that window.
 *
 * Returns: %TRUE if the data displayed by @widget was identified, %FALSE otherwise.
 **/
gboolean
gwy_data_browser_get_data_for_widget(GtkWidget *widget,
                                     GwyFile **file,
                                     GwyDataKind *data_kind,
                                     gint *id)
{
    PieceInfo *info = lookup_widget_object_info(widget);
    if (!info)
        return FALSE;

    if (file)
        *file = info->proxy->file;
    if (data_kind)
        *data_kind = info->parsed.data_kind;
    if (id)
        *id = info->parsed.id;
    return TRUE;
}

/**
 * gwy_data_browser_select_data:
 * @file: A data file container managed by the data-browser.
 * @data_kind: Type of the data object.
 * @id: Data item id.
 *
 * Selects a file and data item.
 **/
void
gwy_data_browser_select_data(GwyFile *file,
                             GwyDataKind data_kind,
                             gint id)
{
    if (!file || data_kind == GWY_FILE_NONE || id < 0)
        select_data_piece(NULL);

    FileProxy *proxy = proxy_for_file(file, NULL);
    if (!proxy) {
        g_warning("Don't know file %p.", file);
        return;
    }
    select_data_piece(lookup_primary_info(proxy, data_kind, id));
}

/* TODO: Use this function for DnD and also for any modal data processing dialogues for a good measure. */
/**
 * gwy_data_browser_block_switching:
 *
 * Temporarily blocks data switching.
 *
 * Normally, the current data item is switched according to the active window.
 *
 * This may be necessary during operations which would be derailed by data switching. For instance DnD from tree views
 * showing the contents of active data does not work correctly if the window manager switches active windows during
 * the DnD, so at the drop time the tree view content no longer matches the dragged row. While switching is blocked,
 * functions such as gwy_data_browser_select_data() will only queue the change.
 *
 * Use gwy_data_browser_unblock_switching() to restore data switching. It will also switch data to the window that
 * should be currently active, if the active window has changed (but without any intermediate steps if the active
 * window has changed multiple times).
 *
 * Switching must not be blocked permanently because it would get the data browser out of sync.
 **/
void
gwy_data_browser_block_switching(void)
{
    if (!browser.block_switching)
        browser.switch_to = (SwitchTo){ .fileno = GWY_FILE_ID_NONE, .id = -1, .data_kind = GWY_FILE_NONE };
    browser.block_switching++;
}

/**
 * gwy_data_browser_unblock_switching:
 *
 * Stops temporarily blocking of data switching.
 *
 * If some data switching has been blocked, the most recent will be realised now.
 *
 * The blocking is started by gwy_data_browser_block_switching(). See its description for details.
 **/
void
gwy_data_browser_unblock_switching(void)
{
    g_return_if_fail(browser.block_switching);
    browser.block_switching--;
    if (browser.block_switching)
        return;
    if (browser.switch_to.fileno != GWY_FILE_ID_NONE)
        really_switch_to(&browser.switch_to);
}

/**
 * gwy_data_browser_get_current_data_kind:
 *
 * Gets the kind of current data.
 *
 * This is what the active data lists shows, for instance. However, even the inactive lists have their own current
 * data items which you can get using gwy_data_browser_get_current_data_id().
 *
 * Returns: The data kind, possibly %GWY_FILE_NONE if there are no current data.
 **/
GwyDataKind
gwy_data_browser_get_current_data_kind(void)
{
    return browser.current_kind;
}

/**
 * gwy_data_browser_get_current_file:
 *
 * Gets the current file.
 *
 * Returns: (transfer none) (nullable): The current file, possibly %NULL if there is no current file.
 **/
GwyFile*
gwy_data_browser_get_current_file(void)
{
    FileProxy *proxy = browser.current_file;
    return (proxy ? proxy->file : NULL);
}

/**
 * gwy_data_browser_get_current_file_id:
 *
 * Gets the numerical id of the current file.
 *
 * Returns: The numerical id, poissbly %GWY_FILE_ID_NONE if there is no current file.
 **/
gint
gwy_data_browser_get_current_file_id(void)
{
    FileProxy *proxy = browser.current_file;
    return (proxy ? gwy_file_get_id(proxy->file) : GWY_FILE_ID_NONE);
}

/**
 * gwy_data_browser_get_current_data_id:
 * @data_kind: Type of the data object.
 *
 * Gets the numerical id of the current data of given kind.
 *
 * Use in conjuction with gwy_data_browser_get_current_data_kind() to get the selected data in the active data list
 * of the browser.
 *
 * Returns: The numerical id, possible -1 if there are no current data of given kind.
 **/
gint
gwy_data_browser_get_current_data_id(GwyDataKind data_kind)
{
    g_return_val_if_fail((guint)data_kind < GWY_FILE_N_KINDS, -1);
    FileProxy *proxy = browser.current_file;
    if (!proxy)
        return -1;
    return proxy->current_items[data_kind];
}

static void
select_data_piece(const PieceInfo *info)
{
    SwitchTo swto;

    if (!info) {
        swto.fileno = GWY_FILE_ID_NONE;
        swto.id = -1;
        swto.data_kind = GWY_FILE_NONE;
    }
    else {
        FileProxy *proxy = info->proxy;
        swto.fileno = gwy_file_get_id(proxy->file);
        swto.id = info->parsed.id;
        swto.data_kind = info->parsed.data_kind;
    }

    if (browser.block_switching) {
        /* This is what 2.x does. While switching is blocked, queue switching to some data, but not switching away. */
        if (swto.fileno != GWY_FILE_ID_NONE)
            browser.switch_to = swto;
    }
    else
        really_switch_to(&swto);
}

static void
really_switch_to(const SwitchTo *swto)
{
    if (browser.in_shutdown)
        return;

    GwyFile *file = gwy_data_browser_get_file(swto->fileno);
    FileProxy *proxy = file ? proxy_for_file(file, NULL) : NULL;
    gboolean do_switch_tool = FALSE;

    if (!proxy) {
        /* Switching from NONE to NONE is cheap, so always switch to NONE and do not complicate it. */
        do_switch_tool = TRUE;
        browser.current_file = NULL;
        browser.current_kind = GWY_FILE_NONE;
    }
    else {
        g_return_if_fail(swto->data_kind != GWY_FILE_NONE && swto->id > -1);
        FileProxy *oldproxy = browser.current_file;
        if (swto->data_kind == GWY_FILE_IMAGE) {
            if (proxy != oldproxy)
                do_switch_tool = TRUE;
            else if (swto->id != oldproxy->current_items[GWY_FILE_IMAGE])
                do_switch_tool = TRUE;
        }
        browser.current_file = proxy;
        browser.current_kind = swto->data_kind;
        proxy->current_items[swto->data_kind] = swto->id;
    }

    _gwy_data_browser_gui_switch_current();
    if (do_switch_tool) {
        GwyTool *tool = gwy_app_current_tool();
        if (tool) {
            if (proxy) {
                GtkWindow *window = gwy_data_browser_get_window_for_data(proxy->file, swto->data_kind, swto->id);
                GtkWidget *view = window ? gwy_data_window_get_data_view(GWY_DATA_WINDOW(window)) : NULL;
                gwy_tool_data_switched(tool, view ? GWY_DATA_VIEW(view) : NULL);
            }
            else
                gwy_tool_data_switched(tool, NULL);
        }
    }
}

/* XXX: Only transitional, to be removed once modules get direct data arguments and no longer rely on figuring out
 * what is currently selected in the browser. They can still use gwy_data_browser_get_current_data_id() but that
 * should be rarely needed. */
/**
 * gwy_data_browser_get_current:
 * @what: First information about current objects to obtain.
 * @...: pointer to store the information to (object pointer for objects, #GQuark pointer for keys, #gint pointer for
 *       ids), followed by 0-terminated list of #GwyAppWhat, pointer couples.
 *
 * Gets information about current objects.
 *
 * All output arguments are always set to some value, even if the requested object does not exist.  Object arguments
 * are set to pointer to the object if it exists (no reference is added), or cleared to %NULL if no such object
 * exists.
 *
 * Quark arguments are set to the corresponding key even if no such object is actually present (use object arguments
 * to check for object presence) but the location where it would be stored is known.  This is common with
 * presentations and masks.  They are be set to 0 if no corresponding location exists -- for example, when the current
 * mask key is requested but the current data contains no channel (or there is no current data at all).
 *
 * The rules for id arguments are similar to quarks, except they are set to -1 to indicate undefined result.
 *
 * The current objects can change due to user interaction even during the execution of modal dialogs (typically used
 * by modules).  Therefore to achieve consistency one has to ask for the complete set of current objects at once.
 **/
void
gwy_data_browser_get_current(GwyAppWhat what,
                             ...)
{
    typedef struct {
        GwyAppWhat what_id;
        GwyAppWhat what_key;
        GwyAppWhat what_object;
        GwyDataKind data_kind;
        GQuark (*get_key)(gint id);
    } Assoc;

    static const Assoc assoc[] = {
        { GWY_APP_FIELD_ID,       GWY_APP_FIELD_KEY,       GWY_APP_FIELD,       GWY_FILE_IMAGE,   gwy_file_key_image,         },
        { 0,                      GWY_APP_MASK_FIELD_KEY,  GWY_APP_MASK_FIELD,  GWY_FILE_IMAGE,   gwy_file_key_image_mask,    },
        { 0,                      GWY_APP_SHOW_FIELD_KEY,  GWY_APP_SHOW_FIELD,  GWY_FILE_IMAGE,   gwy_file_key_image_picture, },
        { GWY_APP_GRAPH_MODEL_ID, GWY_APP_GRAPH_MODEL_KEY, GWY_APP_GRAPH_MODEL, GWY_FILE_GRAPH,   gwy_file_key_graph,         },
        { GWY_APP_BRICK_ID,       GWY_APP_BRICK_KEY,       GWY_APP_BRICK,       GWY_FILE_VOLUME,  gwy_file_key_volume,        },
        { GWY_APP_SPECTRA_ID,     GWY_APP_SPECTRA_KEY,     GWY_APP_SPECTRA,     GWY_FILE_SPECTRA, gwy_file_key_spectra,       },
        { GWY_APP_SCATTER_ID,     GWY_APP_SCATTER_KEY,     GWY_APP_SCATTER,     GWY_FILE_XYZ,     gwy_file_key_xyz,           },
        { GWY_APP_LAWN_ID,        GWY_APP_LAWN_KEY,        GWY_APP_LAWN,        GWY_FILE_CMAP,    gwy_file_key_cmap,          },
    };

    if (!what)
        return;

    FileProxy *proxy = browser.current_file;
    GwyDict *container = proxy ? GWY_DICT(proxy->file) : NULL;
    va_list ap;

    va_start(ap, what);
    do {
        gboolean found = FALSE;
        for (guint i = 0; !found && i < G_N_ELEMENTS(assoc); i++) {
            GwyDataKind data_kind = assoc[i].data_kind;
            gint id = (proxy ? proxy->current_items[data_kind] : -1);
            if (what == assoc[i].what_id) {
                gint *target = va_arg(ap, gint*);
                *target = id;
                found = TRUE;
            }
            else if (what == assoc[i].what_key || what == assoc[i].what_object) {
                GQuark key = (id >= 0 ? assoc[i].get_key(id) : 0);
                if (what == assoc[i].what_key) {
                    GQuark *target = va_arg(ap, GQuark*);
                    *target = key;
                }
                else {
                    GObject **target = va_arg(ap, GObject**);
                    *target = NULL;
                    if (key)
                        gwy_dict_gis_object(container, key, target);
                }
                found = TRUE;
            }
        }
        if (found)
            continue;

        if (what == GWY_APP_FILE) {
            GwyFile **target = va_arg(ap, GwyFile**);
            *target = (proxy ? proxy->file : NULL);
        }
        else if (what == GWY_APP_FILE_ID) {
            gint *target = va_arg(ap, gint*);
            *target = (proxy ? gwy_file_get_id(proxy->file) : GWY_FILE_ID_NONE);
        }
        else if (what == GWY_APP_DATA_KIND) {
            GwyDataKind *target = va_arg(ap, GwyDataKind*);
            *target = browser.current_kind;
        }
        else if (what == GWY_APP_DATA_VIEW || what == GWY_APP_VOLUME_VIEW
                 || what == GWY_APP_XYZ_VIEW || what == GWY_APP_CURVE_MAP_VIEW) {
            gint id;
            GQuark key;
            if (what == GWY_APP_DATA_VIEW) {
                id = (proxy ? proxy->current_items[GWY_FILE_IMAGE] : -1);
                key = (id >= 0 ? gwy_file_key_image(id) : 0);
            }
            else if (what == GWY_APP_VOLUME_VIEW) {
                id = (proxy ? proxy->current_items[GWY_FILE_VOLUME] : -1);
                key = (id >= 0 ? gwy_file_key_volume(id) : 0);
            }
            else if (what == GWY_APP_XYZ_VIEW) {
                id = (proxy ? proxy->current_items[GWY_FILE_XYZ] : -1);
                key = (id >= 0 ? gwy_file_key_xyz(id) : 0);
            }
            else if (what == GWY_APP_CURVE_MAP_VIEW) {
                id = (proxy ? proxy->current_items[GWY_FILE_CMAP] : -1);
                key = (id >= 0 ? gwy_file_key_cmap(id) : 0);
            }
            PieceInfo *info = (key ? g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key)) : NULL);
            GtkWidget **target = va_arg(ap, GtkWidget**);
            *target = (info && info->window ? gwy_data_window_get_data_view(GWY_DATA_WINDOW(info->window)) : NULL);
        }
        else if (what == GWY_APP_GRAPH) {
            gint id = (proxy ? proxy->current_items[GWY_FILE_GRAPH] : -1);
            GQuark key = (id >= 0 ? gwy_file_key_graph(id) : 0);
            PieceInfo *info = (key ? g_hash_table_lookup(proxy->qpieces, GUINT_TO_POINTER(key)) : NULL);
            GtkWidget **target = va_arg(ap, GtkWidget**);
            *target = (info && info->window ? gwy_graph_window_get_graph(GWY_GRAPH_WINDOW(info->window)) : NULL);
        }
        else {
            g_assert_not_reached();
            goto fail;
        }
    } while ((what = va_arg(ap, GwyAppWhat)));

fail:
    va_end(ap);
}

/**
 * gwy_data_browser_add_watch:
 * @function: (scope notified) (closure user_data) (destroy destroy):
 *            Function to call when data change, are added or removed.
 * @data_kind: Data kind to watch.
 * @user_data: User data to pass to @function.
 * @destroy: Function to destroy user data when done, possibly %NULL.
 *
 * Adds a function watching a specific kind of data.
 *
 * Returns: Identifier of the watching function to be used with gwy_data_browser_remove_watch().
 **/
gulong
gwy_data_browser_add_watch(GwyDataWatchFunc function,
                           GwyDataKind data_kind,
                           gpointer user_data,
                           GDestroyNotify destroy)
{
    g_return_val_if_fail(function, 0);
    g_return_val_if_fail((guint)data_kind < GWY_FILE_N_KINDS, 0);

    ensure_browser();
    WatcherData wdata = { .function = function, .user_data = user_data, .data_kind = data_kind, .destroy = destroy };
    wdata.wid = ++browser.last_watcherid;
    g_array_append_val(browser.watchers, wdata);

    return wdata.wid;
}

/**
 * gwy_data_browser_remove_watch:
 * @id: Watch function identifier.
 *
 * Removes a function watching a specific kind of data.
 *
 * The identifier is obtained when adding such function with gwy_data_browser_add_watch().
 **/
void
gwy_data_browser_remove_watch(gulong id)
{
    g_return_if_fail(browser.watchers);

    GArray *watchers = browser.watchers;
    guint n = watchers->len;
    for (guint i = 0; i < n; i++) {
        WatcherData *wdata = &g_array_index(watchers, WatcherData, i);
        if (wdata->wid == id) {
            if (wdata->destroy)
                wdata->destroy(wdata->user_data);
            g_array_remove_index_fast(watchers, i);
            return;
        }
    }
    g_warning("Trying to remove non-existent watcher %lu.", id);
}

static void
notify_watchers(GwyFile *file, GwyDataKind data_kind, gint id,
                GwyDataWatchEventType event)
{
    /* We should not get here without browser.watchers existing. */
    g_return_if_fail(browser.watchers);

    GArray *watchers = browser.watchers;
    guint n = watchers->len;
    for (guint i = 0; i < n; i++) {
        WatcherData *wdata = &g_array_index(watchers, WatcherData, i);
        if (wdata->data_kind == data_kind)
            wdata->function(file, id, event, wdata->user_data);
    }
}

/**
 * gwy_data_browser_shut_down:
 *
 * Shuts down the data browser when quitting the application.
 *
 * The function is meant to avoid spurious signals and notifications in a quick shutdown. It does not go through all
 * files and remove them from the browser and does not destroy the GUI. It just makes the data browser ignore any
 * signals and data changes, assuming the program is already terminating.
 **/
void
gwy_data_browser_shut_down(void)
{
    browser.in_shutdown = TRUE;
}

static void
ensure_browser(void)
{
    if (browser.files)
        return;

    browser.files = g_hash_table_new(g_direct_hash, g_direct_equal);
    browser.objpieces = g_hash_table_new(g_direct_hash, g_direct_equal);
    browser.watchers = g_array_new(FALSE, FALSE, sizeof(WatcherData));
}

/**
 * SECTION: data-browser
 * @title: Data browser
 * @short_description: Manage files and data in them
 *
 * The data browser is both an entity that monitors of various data in Gwyddion and the corresponding user interface
 * showing the data lists and letting the user deleting or copying them. The public functions are generally related
 * to the first part.
 *
 * Data browser functions may only be called from the main thread. Therefore, there is no locking.
 *
 * An #GwyFile that represents an SPM file is managed by functions such as gwy_data_browser_add() or
 * gwy_data_browser_remove(). Note that the high-level libgwyapp functions generally call the data browser functions
 * as appropriate. Low-level functions, e.g. #GwyFile functions, do not. If a #GwyFile has not been added to the data
 * browser it is unmanaged and cannot be used with data browser functions (there are, however, quite a few data
 * managing operations #GwyFile itself can do).
 *
 * An important part of the data browser is keeping track which data item is currently selected (to know what a data
 * processing method should process, etc.).  You can obtain the information about various currently selected objects
 * using gwy_data_browser_get_current().  Making a data item currently selected is accomplished either using function
 * such as gwy_data_browser_select_widget(), which corresponds to the user switching windows, or
 * gwy_data_browser_select_data() which selects a data item as current in the browser. The latter is less safe and can
 * result in a strange behaviour because for some purposes only data displayed in a window can really be ‘current’.
 **/

/**
 * GwyDataBrowserForeachFunc:
 * @file: A data container managed by the data-browser.
 * @user_data: (closure): User data passed to gwy_data_browser_foreach().
 *
 * Type of data file for-each function.
 *
 * Such function is passed to gwy_data_browser_foreach().
 **/

/**
 * GwyDataWatchFunc:
 * @file: A data container managed by the data-browser.
 * @id: Object (channel) id in the container.
 * @event: The type of data change event reported.
 * @user_data: (closure): User data passed to gwy_data_browser_add_watch().
 *
 * Type of data watch function.
 *
 * Such function is passed to gwy_data_browser_add_watch().
 **/

/**
 * GwyDataWatchEventType:
 * @GWY_DATA_WATCH_EVENT_ADDED: A new data object has appeared.
 * @GWY_DATA_WATCH_EVENT_CHANGED: A data object has changed.
 * @GWY_DATA_WATCH_EVENT_REMOVED: A data object has been removed.
 *
 * Type of event reported to #GwyDataWatchFunc watcher functions.
 **/

/**
 * GwyVisibilityResetType:
 * @GWY_VISIBILITY_RESET_DEFAULT: Restore visibilities from container and if nothing would be visible, make an
 *                                arbitrary data object visible.
 * @GWY_VISIBILITY_RESET_RESTORE: Restore visibilities from container.
 * @GWY_VISIBILITY_RESET_SHOW_ALL: Show all data objects.
 * @GWY_VISIBILITY_RESET_HIDE_ALL: Hide all data objects.  This normally makes the file inaccessible.
 *
 * Data object visibility reset type.
 *
 * The precise behaviour of @GWY_VISIBILITY_RESET_DEFAULT may be subject of further changes.  It indicates the wish to
 * restore saved visibilities and do something reasonable when there are no visibilities to restore.
 **/

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