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

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

#include "libgwyddion/macros.h"
#include "libgwyddion/utils.h"
#include "libgwyddion/dict.h"
#include "libgwyui/utils.h"

#include "libgwyapp/app.h"
#include "libgwyapp/help.h"
#include "libgwyapp/meta-browser.h"
#include "libgwyapp/module-utils.h"
#include "libgwyapp/internal.h"

enum {
    META_KEY,
    META_VALUE
};

typedef struct {
    GQuark quark;
    gchar *value;
    gboolean isok;
} FixupData;

struct _GwyMetaBrowserPrivate {
    GwyFile *file;
    GwyDataKind data_kind;
    gint id;

    GwyDict *meta;
    gulong changed_id;

    GtkListStore *store;
    GtkWidget *user_filter;
    GtkWidget *treeview;
    GtkWidget *new;
    GtkWidget *delete;
    GtkWidget *close;
    GtkWidget *save;
    gchar *filter_str;
    gboolean filter_is_casesens;
};

static void       finalize          (GObject *object);
static void       destroyed         (GtkWidget *widget);
static GtkWidget* create_treeview   (GwyMetaBrowser *browser);
static GtkWidget* create_buttons    (GwyMetaBrowser *browser);
static void       fill_list_store   (GwyMetaBrowser *browser);
static gboolean   filter_visible    (GtkTreeModel *model,
                                     GtkTreeIter *iter,
                                     gpointer user_data);
static gint       key_sort_func     (GtkTreeModel *model,
                                     GtkTreeIter *a,
                                     GtkTreeIter *b,
                                     gpointer user_data);
static gboolean   key_equal_func    (GtkTreeModel *model,
                                     gint column,
                                     const gchar *key,
                                     GtkTreeIter *iter,
                                     gpointer search_data);
static void       cell_edited       (GtkCellRendererText *renderer,
                                     const gchar *strpath,
                                     const gchar *text,
                                     GwyMetaBrowser *browser);
static void       render_key        (GtkTreeViewColumn *column,
                                     GtkCellRenderer *cell,
                                     GtkTreeModel *model,
                                     GtkTreeIter *iter,
                                     gpointer user_data);
static void       render_value      (GtkTreeViewColumn *column,
                                     GtkCellRenderer *cell,
                                     GtkTreeModel *model,
                                     GtkTreeIter *iter,
                                     gpointer user_data);
static void       add_line          (GQuark quark,
                                     GValue *gvalue,
                                     gpointer user_data);
static void       update_title      (GwyMetaBrowser *browser);
static void       update_sensitivity(GwyMetaBrowser *browser);
static void       item_changed      (GwyDict *container,
                                     GQuark quark,
                                     GwyMetaBrowser *browser);
static void       add_new_item      (GwyMetaBrowser *browser);
static void       delete_item       (GwyMetaBrowser *browser);
static void       data_finalized    (gpointer user_data,
                                     GObject *where_the_object_was);
static void       focus_iter        (GwyMetaBrowser *browser,
                                     GtkTreeIter *iter);
static gboolean   find_key          (GwyMetaBrowser *browser,
                                     GQuark quark,
                                     GtkTreeIter *iter);
static void       filter_changed    (GwyMetaBrowser *browser);
static void       save_items        (GwyMetaBrowser *browser);

static GObjectClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyMetaBrowser, gwy_meta_browser, GTK_TYPE_WINDOW,
                        G_ADD_PRIVATE(GwyMetaBrowser))

static void
gwy_meta_browser_class_init(GwyMetaBrowserClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_meta_browser_parent_class;

    object_class->finalize = finalize;

    widget_class->destroy = destroyed;
}

static void
finalize(GObject *object)
{
    GwyMetaBrowser *browser = GWY_META_BROWSER(object);
    GwyMetaBrowserPrivate *priv = browser->priv;

    g_clear_object(&priv->store);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
gwy_meta_browser_init(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv;

    browser->priv = priv = gwy_meta_browser_get_instance_private(browser);

    GtkWidget *widget = GTK_WIDGET(browser);
    GtkWindow *window = GTK_WINDOW(browser);

    GtkListStore *store = priv->store = gtk_list_store_new(1, G_TYPE_UINT);
    priv->treeview = create_treeview(browser);
    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);

    GtkTreeModel *filtered_store = gtk_tree_model_filter_new(GTK_TREE_MODEL(store), NULL);
    gtk_tree_model_filter_set_visible_func(GTK_TREE_MODEL_FILTER(filtered_store),
                                           filter_visible, browser, NULL);

    gtk_tree_view_set_model(treeview, GTK_TREE_MODEL(filtered_store));
    g_object_unref(filtered_store);

    /* Search column must set whenver the model changes */
    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(store), 0, key_sort_func, NULL, NULL);
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store), META_KEY, GTK_SORT_ASCENDING);
    gtk_tree_view_set_search_column(treeview, META_KEY);
    gtk_tree_view_set_search_equal_func(treeview, key_equal_func, browser, NULL);

    GtkRequisition request;
    gtk_widget_get_preferred_size(priv->treeview, NULL, &request);
    request.width = MAX(request.width, 320);
    request.height = MAX(request.height, 400);
    gint scw, sch;
    gwy_get_screen_size(widget, &scw, &sch);
    gtk_window_set_default_size(window, MIN(request.width + 24, 2*scw/3), MIN(request.height + 32, 2*sch/3));
    gtk_window_add_accel_group(window, gwy_app_main_accel_group());

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

    GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
    gtk_container_add(GTK_CONTAINER(scroll), priv->treeview);

    priv->user_filter = gtk_search_entry_new();
    gtk_widget_set_tooltip_text(priv->user_filter,
                                "Filter metadata keys. "
                                "If all characters are lowercase ASCII a case-insensitive search is performed.");
    gtk_box_pack_start(GTK_BOX(vbox), priv->user_filter, FALSE, FALSE, 0);
    g_signal_connect_swapped(priv->user_filter, "search-changed",
                             G_CALLBACK(filter_changed), browser);

    gtk_box_pack_start(GTK_BOX(vbox), create_buttons(browser), FALSE, FALSE, 0);

    gwy_help_add_to_window(window, "metadata", NULL, GWY_HELP_DEFAULT);

    gtk_widget_show_all(vbox);
}

/**
 * gwy_meta_browser_new:
 * @file: A data file container.
 * @data_kind: Type of data item.
 * @id: Id of data item in @data to show metadata for.
 *
 * Creates a new metadata browser for a data object in a file.
 *
 * If there is currently no metadata for given data, new empty metadata is created.
 *
 * Returns: (transfer full): A new metadata browser window.
 **/
GtkWidget*
gwy_meta_browser_new(GwyFile *file,
                     GwyDataKind data_kind,
                     gint id)
{
    GwyMetaBrowser *browser = g_object_new(GWY_TYPE_META_BROWSER, NULL);
    GwyMetaBrowserPrivate *priv = browser->priv;

    g_return_val_if_fail(GWY_IS_FILE(file), (GtkWidget*)browser);
    g_return_val_if_fail((guint)data_kind < GWY_FILE_N_KINDS, (GtkWidget*)browser);
    g_return_val_if_fail(id >= 0, (GtkWidget*)browser);

    priv->file = file;
    priv->data_kind = data_kind;
    priv->id = id;


    if (!gwy_dict_contains(GWY_DICT(file), gwy_file_key_data(data_kind, id))) {
        g_warning("Creating metadata browser for non-existent data.");
    }

    GwyDict *meta = priv->meta = gwy_file_get_meta(file, data_kind, id);

    if (!meta) {
        priv->meta = meta = gwy_dict_new();
        gwy_file_pass_meta(file, data_kind, id, meta);
    }
    else {
        /* Temporarily unset the model to avoid thousands of signals while filling the data. */
        GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
        GtkTreeModel *model = gtk_tree_view_get_model(treeview);
        g_object_ref(model);
        gtk_tree_view_set_model(treeview, NULL);
        fill_list_store(browser);
        gtk_tree_view_set_model(treeview, model);
        g_object_unref(model);
    }

    g_object_weak_ref(G_OBJECT(meta), data_finalized, browser);
    priv->changed_id = g_signal_connect(meta, "item-changed", G_CALLBACK(item_changed), browser);

    update_title(browser);
    update_sensitivity(browser);

    return (GtkWidget*)browser;
}

static void
fill_list_store(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkListStore *store = priv->store;
    GwyDict *meta = priv->meta;

    GArray *fixlist = g_array_new(FALSE, FALSE, sizeof(FixupData));
    gwy_dict_foreach(meta, NULL, add_line, fixlist);
    /* Commit UTF-8 fixes found by add_line() */
    guint n = fixlist->len;
    for (guint i = 0; i < n; i++) {
        FixupData *fd = &g_array_index(fixlist, FixupData, i);

        if (fd->isok || fd->value) {
            if (!fd->isok)
                gwy_dict_pass_string(meta, fd->quark, fd->value);

            GtkTreeIter iter;
            gtk_list_store_insert_with_values(store, &iter, G_MAXINT, META_KEY, fd->quark, -1);
        }
        else
            gwy_dict_remove(meta, fd->quark);
    }
    g_array_free(fixlist, TRUE);
}

static gint
key_sort_func(GtkTreeModel *model,
              GtkTreeIter *a,
              GtkTreeIter *b,
              G_GNUC_UNUSED gpointer user_data)
{
    GQuark qa, qb;

    gtk_tree_model_get(model, a, META_KEY, &qa, -1);
    gtk_tree_model_get(model, b, META_KEY, &qb, -1);
    return g_utf8_collate(g_quark_to_string(qa), g_quark_to_string(qb));
}

static gboolean
key_equal_func(GtkTreeModel *model,
               G_GNUC_UNUSED gint column,
               const gchar *key,
               GtkTreeIter *iter,
               G_GNUC_UNUSED gpointer search_data)
{
    GQuark quark;

    gtk_tree_model_get(model, iter, META_KEY, &quark, -1);
    return !strstr(g_quark_to_string(quark), key);
}

static gboolean
filter_visible(GtkTreeModel *model,
               GtkTreeIter *iter,
               gpointer user_data)
{
    GwyMetaBrowser *browser = (GwyMetaBrowser*)user_data;
    GwyMetaBrowserPrivate *priv = browser->priv;
    const gchar *key_str, *filter = priv->filter_str;

    if (!filter)
        return TRUE;

    GQuark key;
    gtk_tree_model_get(GTK_TREE_MODEL(model), iter,
                       META_KEY, &key,
                       -1);
    if (!key)
        return FALSE;

    gboolean visible = FALSE;
    key_str = g_quark_to_string(key);
    if (priv->filter_is_casesens) {
        if (strstr(key_str, filter))
            visible = TRUE;
    }
    else {
        gchar *key_uncase = g_utf8_casefold(key_str, -1);
        if (strstr(key_uncase, filter))
            visible = TRUE;

        g_free(key_uncase);
    }

    return visible;
}

static void
update_title(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    gchar *dataname = gwy_file_get_display_title(priv->file, priv->data_kind, priv->id);
    gchar *title = g_strdup_printf(_("Metadata of %s (%s)"), dataname, g_get_application_name());
    gtk_window_set_title(GTK_WINDOW(browser), title);
    g_free(title);
    g_free(dataname);
}

static void
update_sensitivity(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(priv->treeview));
    GtkTreeIter iter;
    if (gtk_tree_model_get_iter_first(model, &iter))
         gtk_widget_set_sensitive(priv->save, TRUE);
    else
         gtk_widget_set_sensitive(priv->save, FALSE);
}

static GtkWidget*
create_treeview(GwyMetaBrowser *browser)
{
    static const struct {
        const gchar *title;
        const guint id;
    }
    columns[] = {
        { N_("Name"),  META_KEY,   },
        { N_("Value"), META_VALUE, },
    };

    GtkTreeSelection *selection;
    gsize i;

    GtkWidget *widget = gtk_tree_view_new();
    GtkTreeView *treeview = GTK_TREE_VIEW(widget);
    gtk_tree_view_set_enable_search(treeview, TRUE);

    for (i = 0; i < G_N_ELEMENTS(columns); i++) {
        GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
        g_object_set(renderer, "editable", TRUE, "editable-set", TRUE, NULL);
        GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes(_(columns[i].title), renderer, NULL);
        if (columns[i].id == META_KEY)
            gtk_tree_view_column_set_cell_data_func(column, renderer, render_key, NULL, NULL);
        else if (columns[i].id == META_VALUE)
            gtk_tree_view_column_set_cell_data_func(column, renderer, render_value, browser, NULL);
        gtk_tree_view_append_column(treeview, column);
        g_object_set_data(G_OBJECT(renderer), "column", GUINT_TO_POINTER(columns[i].id));
        g_signal_connect(renderer, "edited", G_CALLBACK(cell_edited), browser);
    }

    selection = gtk_tree_view_get_selection(treeview);
    gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);

    return widget;
}

static GtkWidget*
create_buttons(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;

    GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);

    priv->save = gwy_create_stock_button(GWY_STOCK_EXPORT);
    gtk_box_pack_start(GTK_BOX(hbox), priv->save, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->save, "clicked", G_CALLBACK(save_items), browser);
    gtk_widget_set_sensitive(priv->save, FALSE);

    priv->new = gwy_create_stock_button(GWY_STOCK_NEW);
    gtk_box_pack_start(GTK_BOX(hbox), priv->new, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->new, "clicked", G_CALLBACK(add_new_item), browser);

    priv->delete = gwy_create_stock_button(GWY_STOCK_DELETE);
    gtk_box_pack_start(GTK_BOX(hbox), priv->delete, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->delete, "clicked", G_CALLBACK(delete_item), browser);

    priv->close = gwy_create_stock_button(GWY_STOCK_CLOSE);
    gtk_box_pack_start(GTK_BOX(hbox), priv->close, TRUE, TRUE, 0);
    g_signal_connect_swapped(priv->close, "clicked", G_CALLBACK(gtk_widget_destroy), browser);

    return hbox;
}

static gboolean
validate_key(const gchar *key)
{
    if (!key || !*key)
        return FALSE;

    while (*key) {
        gchar c = *key;

        if (c < ' ' || c == '/' || c == '<' || c == '>' || c == '&' || c == 127)
            return FALSE;
        key++;
    }
    return TRUE;
}

static void
cell_edited(GtkCellRendererText *renderer,
            const gchar *strpath,
            const gchar *text,
            GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    guint col = GPOINTER_TO_UINT(g_object_get_data(G_OBJECT(renderer), "column"));
    gwy_debug("Column %d edited to <%s> (path %s)", col, text, strpath);

    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(priv->treeview));
    GtkTreePath *path = gtk_tree_path_new_from_string(strpath);
    GtkTreeIter iter;
    gtk_tree_model_get_iter(model, &iter, path);
    gtk_tree_path_free(path);

    GQuark oldkey, quark;
    gtk_tree_model_get(model, &iter, META_KEY, &oldkey, -1);

    if (col == META_KEY) {
        if (validate_key(text)) {
            quark = g_quark_from_string(text);
            gwy_dict_rename(priv->meta, oldkey, quark, FALSE);
        }
    }
    else if (col == META_VALUE) {
        if (pango_parse_markup(text, -1, 0, NULL, NULL, NULL, NULL))
            gwy_dict_pass_string(priv->meta, oldkey, g_strdup(text));
    }
}

static void
render_key(G_GNUC_UNUSED GtkTreeViewColumn *column,
           GtkCellRenderer *renderer,
           GtkTreeModel *model,
           GtkTreeIter *iter,
           G_GNUC_UNUSED gpointer user_data)
{
    GQuark quark;

    gtk_tree_model_get(model, iter, META_KEY, &quark, -1);
    g_object_set(renderer, "text", g_quark_to_string(quark), NULL);
}

static void
render_value(G_GNUC_UNUSED GtkTreeViewColumn *column,
             GtkCellRenderer *renderer,
             GtkTreeModel *model,
             GtkTreeIter *iter,
             gpointer user_data)
{
    GwyMetaBrowser *browser = (GwyMetaBrowser*)user_data;
    GwyMetaBrowserPrivate *priv = browser->priv;
    GQuark quark;

    gtk_tree_model_get(model, iter, META_KEY, &quark, -1);
    g_object_set(renderer, "text", gwy_dict_get_string(priv->meta, quark), NULL);
}

static void
add_line(GQuark quark,
         GValue *gvalue,
         gpointer user_data)
{
    GArray *fixlist = (GArray*)user_data;

    g_return_if_fail(G_VALUE_HOLDS_STRING(gvalue));
    const gchar *val = g_value_get_string(gvalue);

    /* Theoretically, modules should ensure metadata are in UTF-8 when it's stored to container.  But in practice we
     * cannot rely on it. */
    FixupData fd;
    gwy_clear1(fd);
    fd.quark = quark;

    gchar *s;
    if (g_utf8_validate(val, -1, NULL)) {
        fd.value = (gchar*)val;
        fd.isok = TRUE;
    }
    else if ((s = gwy_convert_to_utf8(val, -1, "ISO-8859-1")))
        fd.value = s;
    else {
        g_warning("Bogus metadata <%s> at key <%s>", val, g_quark_to_string(quark));
        return;
    }

    /* The same applies to markup validity.  Fix invalid markup by taking it literally. */
    if (!pango_parse_markup(fd.value, -1, 0, NULL, NULL, NULL, NULL)) {
        s = g_markup_escape_text(fd.value, -1);
        if (!fd.isok)
            g_free(fd.value);
        fd.value = s;
        fd.isok = FALSE;
    }

    g_array_append_val(fixlist, fd);
}

static void
item_changed(GwyDict *container,
             GQuark quark,
             GwyMetaBrowser*browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkListStore *store = priv->store;
    G_GNUC_UNUSED const gchar *key = g_quark_to_string(quark);

    gwy_debug("Meta item <%s> changed", key);
    g_return_if_fail(quark);

    GtkTreeIter iter;
    if (find_key(browser, quark, &iter)) {
        if (gwy_dict_contains(container, quark))
            gwy_list_store_row_changed(store, &iter, NULL, -1);
        else
            gtk_list_store_remove(store, &iter);
        return;
    }

    gtk_list_store_insert_with_values(store, &iter, G_MAXINT, META_KEY, quark, -1);
    focus_iter(browser, &iter);
}

static void
add_new_item(GwyMetaBrowser *browser)
{
    static const gchar *whatever[] = {
        "angary", "bistere", "couchant", "dolerite", "envoy", "figwort", "gudgeon", "hidalgo", "ictus", "jibbah",
        "kenosis", "logie", "maser", "nephology", "ozalid", "parallax", "reduit", "savate", "thyristor", "urate",
        "versicle", "wapentake", "xystus", "yogh", "zeugma",
    };
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeIter iter;
    static GQuark quark = 0;
    gchar *s;

    if (!quark)
        quark = g_quark_from_static_string(_("New item"));

    if (gwy_dict_contains(priv->meta, quark)) {
        if (find_key(browser, quark, &iter))
            focus_iter(browser, &iter);
    }
    else {
        if (g_random_int() % 4 == 0)
            s = g_strdup(whatever[g_random_int() % G_N_ELEMENTS(whatever)]);
        else
            s = g_strdup("");
        gwy_dict_pass_string(priv->meta, quark, s);
    }

    update_sensitivity(browser);
}

static void
delete_item(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeSelection *selection;
    GtkTreeModel *model;
    GtkTreeIter iter;
    GQuark quark;

    selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(priv->treeview));
    if (!gtk_tree_selection_get_selected(selection, &model, &iter))
        return;

    gtk_tree_model_get(model, &iter, META_KEY, &quark, -1);
    gwy_dict_remove(priv->meta, quark);
    update_sensitivity(browser);
}

static void
destroyed(GtkWidget *widget)
{
    GwyMetaBrowser *browser = GWY_META_BROWSER(widget);
    GwyMetaBrowserPrivate *priv = browser->priv;

    GWY_FREE(priv->filter_str);
    if (priv->meta) {
        g_clear_signal_handler(&priv->changed_id, priv->meta);
        g_object_weak_unref(G_OBJECT(priv->meta), data_finalized, browser);
        priv->meta = NULL;
    }

    GTK_WIDGET_CLASS(parent_class)->destroy(widget);
}

static void
data_finalized(gpointer user_data,
               G_GNUC_UNUSED GObject *where_the_object_was)
{
    GwyMetaBrowser *browser = (GwyMetaBrowser*)user_data;
    browser->priv->meta = NULL;
    gtk_widget_destroy(GTK_WIDGET(browser));
}

/* Takes backend store (NOT treeview model) iter! */
static void
focus_iter(GwyMetaBrowser *browser,
           GtkTreeIter *iter)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeView *treeview = GTK_TREE_VIEW(priv->treeview);
    GtkTreeViewColumn *column = gtk_tree_view_get_column(treeview, META_KEY);
    GtkTreeModel *model = gtk_tree_view_get_model(treeview);
    GtkTreeIter filter_iter;
    if (!gtk_tree_model_filter_convert_child_iter_to_iter(GTK_TREE_MODEL_FILTER(model), &filter_iter, iter))
        return;

    GtkTreePath *path = gtk_tree_model_get_path(gtk_tree_view_get_model(treeview), &filter_iter);
    gtk_tree_view_scroll_to_cell(treeview, path, NULL, FALSE, 0.0, 0.0);
    gtk_tree_view_set_cursor(treeview, path, column, FALSE);
    gtk_widget_grab_focus(priv->treeview);
    gtk_tree_path_free(path);
}

static gboolean
find_key(GwyMetaBrowser *browser,
         GQuark quark,
         GtkTreeIter *iter)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeModel *model = GTK_TREE_MODEL(priv->store);

    if (gtk_tree_model_get_iter_first(model, iter)) {
        do {
            GQuark q;
            gtk_tree_model_get(model, iter, META_KEY, &q, -1);
            if (q == quark)
                return TRUE;
        } while (gtk_tree_model_iter_next(model, iter));
    }

    return FALSE;
}

static void
filter_changed(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    const gchar *s = gtk_entry_get_text(GTK_ENTRY(priv->user_filter));

    if (*s) {
        gwy_assign_string(&priv->filter_str, s);
        gchar *filter_uncase = g_utf8_casefold(s, -1);
        priv->filter_is_casesens = !gwy_strequal(s, filter_uncase);
        g_free(filter_uncase);
    }
    else
        GWY_FREE(priv->filter_str);

    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(priv->treeview));
    gtk_tree_model_filter_refilter(GTK_TREE_MODEL_FILTER(model));
}

static void
save_items(GwyMetaBrowser *browser)
{
    GwyMetaBrowserPrivate *priv = browser->priv;
    GtkTreeModel *model = gtk_tree_view_get_model(GTK_TREE_VIEW(priv->treeview));
    GString *str_to_save = g_string_new(NULL);
    GtkTreeIter iter;

    if (gtk_tree_model_get_iter_first(model, &iter)) {
        do {
            GQuark name;
            gtk_tree_model_get(model, &iter, META_KEY, &name, -1);
            const gchar *value = gwy_dict_get_string(priv->meta, name);
            g_string_append_printf(str_to_save, "%s %s\n", g_quark_to_string(name), value);
        } while (gtk_tree_model_iter_next(model, &iter));
    }

    gwy_save_auxiliary_data(_("Save Metadata"), GTK_WINDOW(browser), str_to_save->str, str_to_save->len, FALSE);

    g_string_free(str_to_save, TRUE);
}

/**
 * SECTION: meta-browser
 * @title: Metadata browser
 * @short_description: Display and edit data object metadata
 **/

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