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

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

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/utils.h"

#include "libgwyui/adjust-bar.h"
#include "libgwyui/color-editor.h"
#include "libgwyui/color-dialog.h"
#include "libgwyui/utils.h"


#include "libgwyapp/sanity.h"

enum {
    GWY_HSCALE_WIDTH = 96
};

typedef struct {
    const gchar *label;
    const gchar *icon_name;
    gint dialog_response;
} StockItemInfo;

static void gwy_hscale_update_log (GtkAdjustment *adj,
                                   GtkAdjustment *slave);
static void gwy_hscale_update_exp (GtkAdjustment *adj,
                                   GtkAdjustment *slave);
static void gwy_hscale_update_sqrt(GtkAdjustment *adj,
                                   GtkAdjustment *slave);
static void gwy_hscale_update_sq  (GtkAdjustment *adj,
                                   GtkAdjustment *slave);
static void hscale_set_sensitive  (GObject *pivot,
                                   gboolean sensitive);
static void disconnect_slave      (GtkWidget *slave,
                                   GtkWidget *master);
static void disconnect_master     (GtkWidget *master,
                                   GtkWidget *slave);

// NB: Must keep these tables in sync with the enum. They are directly indexed by it!
static const StockItemInfo stock_items[] = {
    { NULL,                     NULL,                 GTK_RESPONSE_NONE          },
    { N_("_Apply"),             GWY_ICON_GTK_APPLY,   GTK_RESPONSE_APPLY,        },
    { N_("_Cancel"),            GWY_ICON_GTK_CANCEL,  GTK_RESPONSE_CANCEL,       },
    { N_("Clea_r"),             GWY_ICON_GTK_CLEAR,   GWY_RESPONSE_CLEAR,        },
    { N_("_Close"),             GWY_ICON_GTK_CLOSE,   GTK_RESPONSE_CLOSE,        },
    { NC_("verb", "_Copy"),     GWY_ICON_GTK_COPY,    GWY_RESPONSE_COPY,         },
    { N_("_Delete"),            GWY_ICON_GTK_DELETE,  GWY_RESPONSE_DELETE,       },
    { N_("_Edit"),              GWY_ICON_GTK_EDIT,    GWY_RESPONSE_EDIT,         },
    { NC_("verb", "_Estimate"), NULL,                 GWY_RESPONSE_EXECUTE,      },
    { N_("_Export"),            GWY_ICON_GTK_SAVE,    GWY_RESPONSE_SAVE,         },
    { N_("_Fit"),               NULL,                 GWY_RESPONSE_REFINE,       },
    { N_("_Help"),              GWY_ICON_GTK_HELP,    GWY_RESPONSE_HELP,         },
    { NC_("verb", "Hide"),      NULL,                 GTK_RESPONSE_DELETE_EVENT, },
    { N_("_Load"),              GWY_ICON_GTK_OPEN,    GWY_RESPONSE_OPEN,         },
    { N_("_New"),               GWY_ICON_GTK_NEW,     GWY_RESPONSE_NEW,          },
    { N_("_OK"),                GWY_ICON_GTK_OK,      GTK_RESPONSE_OK,           },
    { N_("_Open"),              GWY_ICON_GTK_OPEN,    GWY_RESPONSE_OPEN,         },
    { N_("_Refine"),            NULL,                 GWY_RESPONSE_REFINE,       },
    { N_("Re_move"),            GWY_ICON_GTK_REMOVE,  GWY_RESPONSE_DELETE,       },
    { N_("_Reset"),             GWY_ICON_GTK_RESET,   GWY_RESPONSE_RESET,        },
    { N_("_Save"),              GWY_ICON_GTK_SAVE,    GWY_RESPONSE_SAVE,         },
    { NC_("verb", "_Update"),   GWY_ICON_GTK_EXECUTE, GWY_RESPONSE_UPDATE,       },
};

G_STATIC_ASSERT(G_N_ELEMENTS(stock_items) == GWY_STOCK_UPDATE+1);

/************************** Scale attaching ****************************/

static gdouble
ssqrt(gdouble x)
{
    return (x < 0.0) ? -sqrt(fabs(x)) : sqrt(x);
}

static gdouble
ssqr(gdouble x)
{
    return x*fabs(x);
}

static void
gwy_hscale_update_log(GtkAdjustment *adj, GtkAdjustment *slave)
{
    gulong id;

    id = g_signal_handler_find(slave, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, 0, gwy_hscale_update_exp, adj);
    g_signal_handler_block(slave, id);
    gtk_adjustment_set_value(slave, log(gtk_adjustment_get_value(adj)));
    g_signal_handler_unblock(slave, id);
}

static void
gwy_hscale_update_exp(GtkAdjustment *adj, GtkAdjustment *slave)
{
    gulong id;

    id = g_signal_handler_find(slave, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, 0, gwy_hscale_update_log, adj);
    g_signal_handler_block(slave, id);
    gtk_adjustment_set_value(slave, exp(gtk_adjustment_get_value(adj)));
    g_signal_handler_unblock(slave, id);
}

static void
gwy_hscale_update_sqrt(GtkAdjustment *adj, GtkAdjustment *slave)
{
    gulong id;

    id = g_signal_handler_find(slave, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, 0, gwy_hscale_update_sq, adj);
    g_signal_handler_block(slave, id);
    gtk_adjustment_set_value(slave, ssqrt(gtk_adjustment_get_value(adj)));
    g_signal_handler_unblock(slave, id);
}

static void
gwy_hscale_update_sq(GtkAdjustment *adj, GtkAdjustment *slave)
{
    gulong id;

    id = g_signal_handler_find(slave, G_SIGNAL_MATCH_FUNC | G_SIGNAL_MATCH_DATA, 0, 0, 0, gwy_hscale_update_sqrt, adj);
    g_signal_handler_block(slave, id);
    gtk_adjustment_set_value(slave, ssqr(gtk_adjustment_get_value(adj)));
    g_signal_handler_unblock(slave, id);
}

/**
 * gwy_gtkgrid_hscale_set_sensitive:
 * @pivot: The same object that was passed to gwy_gtkgrid_attach_hscale() as @pivot.
 * @sensitive: %TRUE to make the row sensitive, %FALSE to insensitive.
 *
 * Sets sensitivity of a group of controls created by gwy_gtkgrid_attach_adjbar().
 *
 * For controls without an enable/disable check button controls the sensitivity as expected.  If the hscale was
 * created with %GWY_HSCALE_CHECK you usually manage its sensitivity by setting state of the check button instead.
 * Only use this function when you want to enable/disable the entire group of controls, including the check button.
 **/
void
gwy_gtkgrid_hscale_set_sensitive(GObject *pivot, gboolean sensitive)
{
    GtkWidget *scale, *check;
    gboolean sens = sensitive;

    g_object_set_data(G_OBJECT(pivot), "gwy-explicit-disable", GINT_TO_POINTER(!sensitive));

    if ((check = gwy_gtkgrid_hscale_get_check(pivot))
        && GTK_IS_CHECK_BUTTON(check)) {
        sens = (sensitive && gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check)));
    }
    else
        check = NULL;

    hscale_set_sensitive(pivot, sens);

    if (check) {
        scale = gwy_gtkgrid_hscale_get_scale(pivot);
        if (scale && GWY_IS_ADJUST_BAR(scale))
            gtk_widget_set_sensitive(scale, sensitive);
        else
            gtk_widget_set_sensitive(check, sensitive);
    }
}

static void
hscale_checkbutton_toggled(GtkToggleButton *check, GObject *pivot)
{
    gboolean sens, active;

    active = gtk_toggle_button_get_active(check);
    sens = !GPOINTER_TO_INT(g_object_get_data(G_OBJECT(pivot), "gwy-explicit-disable"));
    hscale_set_sensitive(pivot, active && sens);
}

static void
hscale_set_sensitive(GObject *pivot, gboolean sensitive)
{
    GtkWidget *widget;
    gboolean is_adjbar = FALSE;

    widget = gwy_gtkgrid_hscale_get_scale(pivot);
    if (widget && GWY_IS_ADJUST_BAR(widget))
        is_adjbar = TRUE;

    if (is_adjbar)
        gwy_adjust_bar_set_bar_sensitive(GWY_ADJUST_BAR(widget), sensitive);
    else {
        if (widget)
            gtk_widget_set_sensitive(widget, sensitive);
        if ((widget = gwy_gtkgrid_hscale_get_label(pivot)))
            gtk_widget_set_sensitive(widget, sensitive);
    }

    if ((widget = gwy_gtkgrid_hscale_get_middle_widget(pivot)))
        gtk_widget_set_sensitive(widget, sensitive);
    if ((widget = gwy_gtkgrid_hscale_get_units(pivot)))
        gtk_widget_set_sensitive(widget, sensitive);
}

/**
 * gwy_gtkgrid_attach_adjbar:
 * @grid: A #GtkGrid.
 * @row: Row in @grid to attach stuff to.
 * @name: The label before @pivot widget.
 * @units: The label after @pivot widget.
 * @pivot: Either a #GtkAdjustment, or a widget to use instead of the spin button and scale widgets (if @style is
 *         %GWY_HSCALE_WIDGET).
 * @style: A mix of options an flags determining what and how will be attached to the grid.
 *
 * Attaches an adjustment bar with spinbutton and labels, or something else, to a grid row.
 *
 * The group of controls takes three grid columns: adjustment bar, spinbutton and units.
 *
 * You can use functions gwy_gtkgrid_hscale_get_scale(), gwy_gtkgrid_hscale_get_check(), etc. to get the various
 * widgets from pivot later.
 *
 * The function usually does the right thing but what exactly happens with various @style values is a bit convoluted.
 *
 * Returns: The middle widget.  If a spinbutton is attached, then this spinbutton is returned.  Otherwise (in
 *          %GWY_HSCALE_WIDGET case) @pivot itself.
 **/
GtkWidget*
gwy_gtkgrid_attach_adjbar(GtkGrid *grid, gint row,
                          const gchar *name, const gchar *units,
                          GObject *pivot,
                          GwyHScaleStyle style)
{
    GtkWidget *spin, *label, *check = NULL, *middle_widget, *hbox;
    GtkAdjustment *adj = NULL;
    GwyHScaleStyle base_style;
    GwyAdjustBar *adjbar;
    gdouble u;
    gint digits, spacing;

    g_return_val_if_fail(GTK_IS_GRID(grid), NULL);

    base_style = style & ~(GWY_HSCALE_CHECK | GWY_HSCALE_SNAP);
    switch (base_style) {
        case GWY_HSCALE_DEFAULT:
        case GWY_HSCALE_NO_SCALE:
        case GWY_HSCALE_LOG:
        case GWY_HSCALE_SQRT:
        case GWY_HSCALE_LINEAR:
        if (pivot) {
            g_return_val_if_fail(GTK_IS_ADJUSTMENT(pivot), NULL);
            adj = GTK_ADJUSTMENT(pivot);
        }
        else {
            if (base_style == GWY_HSCALE_LOG || base_style == GWY_HSCALE_SQRT)
                g_warning("Nonlinear scale doesn't work with implicit adj.");
            adj = GTK_ADJUSTMENT(gtk_adjustment_new(0.01, 0.01, 1.00, 0.01, 0.1, 0));
        }
        break;

        case GWY_HSCALE_WIDGET:
        case GWY_HSCALE_WIDGET_NO_EXPAND:
        g_return_val_if_fail(GTK_IS_WIDGET(pivot), NULL);
        break;

        default:
        g_return_val_if_reached(NULL);
        break;
    }

    if ((style & GWY_HSCALE_SNAP)
        && (base_style == GWY_HSCALE_NO_SCALE
            || base_style == GWY_HSCALE_WIDGET
            || base_style == GWY_HSCALE_WIDGET_NO_EXPAND)) {
        g_warning("There is no adjust bar that could snap to ticks.");
    }

    if (base_style == GWY_HSCALE_WIDGET || base_style == GWY_HSCALE_WIDGET_NO_EXPAND) {
        middle_widget = GTK_WIDGET(pivot);

        g_object_get(grid, "column-spacing", &spacing, NULL);
        hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, spacing);
        gtk_grid_attach(grid, hbox, 0, row, 2, 1);

        if (base_style == GWY_HSCALE_WIDGET_NO_EXPAND)
            gtk_box_pack_end(GTK_BOX(hbox), middle_widget, FALSE, FALSE, 0);
        else
            gtk_box_pack_end(GTK_BOX(hbox), middle_widget, TRUE, TRUE, 0);

        if (style & GWY_HSCALE_CHECK) {
            check = gtk_check_button_new_with_mnemonic(name);
            gtk_box_pack_start(GTK_BOX(hbox), check, FALSE, FALSE, 0);
            g_signal_connect(check, "toggled", G_CALLBACK(hscale_checkbutton_toggled), pivot);
            g_object_set_data(G_OBJECT(pivot), "check", check);
        }
        else {
            label = gtk_label_new_with_mnemonic(name);
            gtk_label_set_xalign(GTK_LABEL(label), 0);
            gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
            gtk_label_set_mnemonic_widget(GTK_LABEL(label), middle_widget);
            g_object_set_data(G_OBJECT(pivot), "label", label);
        }
    }
    else {
        u = gtk_adjustment_get_step_increment(adj);
        digits = (u > 0.0) ? (gint)ceil(-log10(u)) : 0;
        spin = gtk_spin_button_new(adj, 0.1, MAX(digits, 0));
        gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(spin), TRUE);
        gtk_spin_button_set_snap_to_ticks(GTK_SPIN_BUTTON(spin), style & GWY_HSCALE_SNAP);
        gtk_grid_attach(grid, spin, 1, row, 1, 1);
        middle_widget = spin;

        if (base_style == GWY_HSCALE_NO_SCALE) {
            if (style & GWY_HSCALE_CHECK) {
                check = gtk_check_button_new_with_mnemonic(name);
                gtk_widget_set_hexpand(check, TRUE);
                gtk_grid_attach(grid, check, 0, row, 1, 1);
                g_signal_connect(check, "toggled", G_CALLBACK(hscale_checkbutton_toggled), pivot);
                g_object_set_data(G_OBJECT(pivot), "check", check);
            }
            else {
                label = gtk_label_new_with_mnemonic(name);
                gtk_widget_set_hexpand(label, TRUE);
                gtk_label_set_xalign(GTK_LABEL(label), 0.0);
                gtk_grid_attach(grid, check, 0, row, 1, 1);
                gtk_label_set_mnemonic_widget(GTK_LABEL(label), middle_widget);
                g_object_set_data(G_OBJECT(pivot), "label", label);
            }
        }
        else {
            adjbar = GWY_ADJUST_BAR(gwy_adjust_bar_new(adj, name));
            if (base_style == GWY_HSCALE_LINEAR)
                gwy_adjust_bar_set_mapping(adjbar, GWY_SCALE_MAPPING_LINEAR);
            else if (base_style == GWY_HSCALE_SQRT)
                gwy_adjust_bar_set_mapping(adjbar, GWY_SCALE_MAPPING_SQRT);
            else if (base_style == GWY_HSCALE_LOG)
                gwy_adjust_bar_set_mapping(adjbar, GWY_SCALE_MAPPING_LOG);

            gtk_widget_set_hexpand(GTK_WIDGET(adjbar), TRUE);
            gtk_grid_attach(grid, GTK_WIDGET(adjbar), 0, row, 1, 1);
            g_object_set_data(G_OBJECT(pivot), "scale", adjbar);

            label = gwy_adjust_bar_get_label(adjbar);
            g_object_set_data(G_OBJECT(pivot), "label", label);
            gtk_label_set_mnemonic_widget(GTK_LABEL(label), middle_widget);

            if (style & GWY_HSCALE_CHECK) {
                gwy_adjust_bar_set_has_check_button(adjbar, TRUE);
                check = gwy_adjust_bar_get_check_button(adjbar);
                g_signal_connect(check, "toggled", G_CALLBACK(hscale_checkbutton_toggled), pivot);
                g_object_set_data(G_OBJECT(pivot), "check", check);
            }
            if (style & GWY_HSCALE_SNAP)
                gwy_adjust_bar_set_snap_to_ticks(adjbar, TRUE);
        }
    }

    g_object_set_data(G_OBJECT(pivot), "middle_widget", middle_widget);

    if (units) {
        label = gtk_label_new(units);
        gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
        gtk_label_set_xalign(GTK_LABEL(label), 0.0);
        gtk_grid_attach(grid, label, 2, row, 1, 1);
        g_object_set_data(G_OBJECT(pivot), "units", label);
    }

    if (check)
        hscale_checkbutton_toggled(GTK_TOGGLE_BUTTON(check), pivot);

    return middle_widget;
}

/**
 * gwy_gtkgrid_hscale_get_scale:
 * @pivot: Pivot object passed to gwy_gtkgrid_attach_hscale().
 *
 * Gets the horizontal scale associated with a pivot object.
 *
 * May return %NULL if constructed with %GWY_HSCALE_NO_SCALE, %GWY_HSCALE_WIDGET, or %GWY_HSCALE_WIDGET_NO_EXPAND.
 **/
GtkWidget*
gwy_gtkgrid_hscale_get_scale(GObject *pivot)
{
    g_return_val_if_fail(pivot, NULL);
    return (GtkWidget*)g_object_get_data(G_OBJECT(pivot), "scale");
}

/**
 * gwy_gtkgrid_hscale_get_check:
 * @pivot: Pivot object passed to gwy_gtkgrid_attach_hscale().
 *
 * Gets the check button associated with a pivot object.
 *
 * May return %NULL if not constructed with %GWY_HSCALE_CHECK.
 **/
GtkWidget*
gwy_gtkgrid_hscale_get_check(GObject *pivot)
{
    g_return_val_if_fail(pivot, NULL);
    return (GtkWidget*)g_object_get_data(G_OBJECT(pivot), "check");
}

/**
 * gwy_gtkgrid_hscale_get_label:
 * @pivot: Pivot object passed to gwy_gtkgrid_attach_hscale().
 *
 * Gets the (left) label associated with a pivot object.
 *
 * May return %NULL if constructed with %GWY_HSCALE_CHECK.
 **/
GtkWidget*
gwy_gtkgrid_hscale_get_label(GObject *pivot)
{
    g_return_val_if_fail(pivot, NULL);
    return (GtkWidget*)g_object_get_data(G_OBJECT(pivot), "label");
}

/**
 * gwy_gtkgrid_hscale_get_units:
 * @pivot: Pivot object passed to gwy_gtkgrid_attach_hscale().
 *
 * Gets the units label associated with a pivot object.
 *
 * May return %NULL if constructed without units.
 **/
GtkWidget*
gwy_gtkgrid_hscale_get_units(GObject *pivot)
{
    g_return_val_if_fail(pivot, NULL);
    return (GtkWidget*)g_object_get_data(G_OBJECT(pivot), "units");
}

/**
 * gwy_gtkgrid_hscale_get_middle_widget:
 * @pivot: Pivot object passed to gwy_gtkgrid_attach_hscale().
 *
 * Gets the middle widget associated with a pivot object.
 **/
GtkWidget*
gwy_gtkgrid_hscale_get_middle_widget(GObject *pivot)
{
    g_return_val_if_fail(pivot, NULL);
    return (GtkWidget*)g_object_get_data(G_OBJECT(pivot), "middle_widget");
}

/************************** Mask colors ****************************/

typedef struct {
    GwyColorButton *color_button;
    GwyDict *container;
    GQuark quark;
} MaskColorSelectorData;

static void
mask_color_updated(GwyColorEditor *editor, MaskColorSelectorData *mcsdata)
{
    gwy_debug("mcsdata = %p", mcsdata);

    GwyRGBA color;
    gwy_color_editor_get_color(editor, &color);
    gwy_dict_set_boxed(mcsdata->container, GWY_TYPE_RGBA, mcsdata->quark, &color);

    if (mcsdata->color_button)
        gwy_color_button_set_color(mcsdata->color_button, &color);
}

/**
 * gwy_mask_color_selector_run:
 * @dialog_title: (nullable): Title of the color selection dialog (%NULL to use default).
 * @parent: (nullable) (transfer none):
 *          Dialog parent window.  The color selector dialog will be made transient for this window.
 * @color_button: (nullable) (transfer none): Color button to update on color change (or %NULL).
 * @container: (transfer none): Container to initialize the color from and save it to.
 * @quark: Quark key of the color in @container.
 *
 * Creates and runs a color selector dialog for a mask.
 *
 * Note this function does not return anything, it runs the color selection dialog modally and returns when it is
 * finished.
 **/
void
gwy_mask_color_selector_run(const gchar *dialog_title,
                            GtkWindow *parent,
                            GwyColorButton *color_button,
                            GwyDict *container,
                            GQuark quark)
{
    g_return_if_fail(quark);

    MaskColorSelectorData mcsdata = {
        .color_button = color_button,
        .container = container,
        .quark = quark,
    };

    GwyRGBA color = { 1.0, 0.0, 0.0, 0.5 };
    if (!gwy_dict_gis_boxed(container, GWY_TYPE_RGBA, quark, &color))
        g_warning("No current color to modify.");

    GtkWidget *dialog = gwy_color_dialog_new(parent, TRUE);
    gtk_window_set_title(GTK_WINDOW(dialog), dialog_title ? dialog_title : _("Change Mask Color"));
    GtkWidget *editor = gwy_color_dialog_get_editor(GWY_COLOR_DIALOG(dialog));
    gwy_color_editor_set_color(GWY_COLOR_EDITOR(editor), &color);
    gwy_color_editor_set_use_alpha(GWY_COLOR_EDITOR(editor), TRUE);
    g_signal_connect(editor, "color-changed", G_CALLBACK(mask_color_updated), &mcsdata);

    gtk_dialog_run(GTK_DIALOG(dialog));
}

/************************** ListStore ****************************/

/**
 * gwy_list_store_row_changed:
 * @store: A list store.
 * @iter: A tree model iterator in @store, or %NULL for none.
 * @path: A tree model path in @store, or %NULL for none.
 * @row: A row number in @store, or -1 for none.
 *
 * Convenience function to emit "GtkTreeModel::row-changed" signal on a tree store.
 *
 * At least one of @iter, @path, @row must be set to identify the row to emit "row-changed" on, and usually exactly
 * one should be set.  The remaining information necessary to call gtk_tree_model_row_changed() is inferred
 * automatically.
 *
 * The behaviour of this function is undefined for specified, but inconsistent @iter, @path, and @row.
 **/
void
gwy_list_store_row_changed(GtkListStore *store,
                           GtkTreeIter *iter,
                           GtkTreePath *path,
                           gint row)
{
    GtkTreeIter myiter;
    GtkTreeModel *model;
    gboolean iter_ok;

    g_return_if_fail(GTK_IS_LIST_STORE(store));
    g_return_if_fail(iter || path || row >= 0);

    model = GTK_TREE_MODEL(store);
    if (iter && path) {
        gtk_tree_model_row_changed(model, path, iter);
        return;
    }

    if (!iter && row >= 0) {
        iter_ok = gtk_tree_model_iter_nth_child(model, &myiter, NULL, row);
        g_return_if_fail(iter_ok);
        iter = &myiter;
    }

    if (!iter) {
        iter_ok = gtk_tree_model_get_iter(model, &myiter, path);
        g_return_if_fail(iter_ok);
        iter = &myiter;
    }

    if (path) {
        gtk_tree_model_row_changed(model, path, iter);
        return;
    }

    path = gtk_tree_model_get_path(model, iter);
    gtk_tree_model_row_changed(model, path, iter);
    gtk_tree_path_free(path);
}

/************************** Activate on Unfocus ****************************/

static gboolean
activate_on_unfocus(GtkWidget *widget)
{
    gtk_widget_activate(widget);
    return FALSE;
}

/**
 * gwy_widget_get_activate_on_unfocus:
 * @widget: A widget.
 *
 * Obtains the activate-on-unfocus state of a widget.
 *
 * Returns: %TRUE if signal "GtkWidget::activate" is emitted when focus leaves the widget.
 **/
gboolean
gwy_widget_get_activate_on_unfocus(GtkWidget *widget)
{
    g_return_val_if_fail(GTK_IS_WIDGET(widget), FALSE);
    return !!g_signal_handler_find(widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, activate_on_unfocus, NULL);
}

/**
 * gwy_widget_set_activate_on_unfocus:
 * @widget: A widget.
 * @activate: %TRUE to enable activate-on-unfocus, %FALSE disable it.
 *
 * Sets the activate-on-unfocus state of a widget.
 *
 * When it is enabled, signal "GtkWidget::activate" is emited whenever focus leaves the widget.
 **/
void
gwy_widget_set_activate_on_unfocus(GtkWidget *widget,
                                   gboolean activate)
{
    gulong id;

    g_return_if_fail(GTK_IS_WIDGET(widget));
    g_return_if_fail(GTK_WIDGET_GET_CLASS(widget)->activate_signal);
    id = g_signal_handler_find(widget, G_SIGNAL_MATCH_FUNC, 0, 0, NULL, activate_on_unfocus, NULL);
    if (id && !activate)
        g_signal_handler_disconnect(widget, id);
    if (!id && activate)
        g_signal_connect(widget, "focus-out-event", G_CALLBACK(activate_on_unfocus), NULL);
}

/************************** Utils ****************************/

/**
 * gwy_label_new_header:
 * @text: Text to put into the label.  It must be a valid markup and it will be made bold by adding appropriate markup
 *        around it.
 *
 * Creates a bold, left aligned label.
 *
 * The purpose of this function is to avoid propagation of too much markup to translations (and to reduce code clutter
 * by avoiding dummy constructor and left-aligning automatically).
 *
 * Returns: A newly created #GtkLabel.
 **/
GtkWidget*
gwy_label_new_header(const gchar *text)
{
    GtkWidget *label = gtk_label_new(NULL);
    gchar *s = g_strconcat("<b>", text, "</b>", NULL);

    gtk_label_set_markup(GTK_LABEL(label), s);
    g_free(s);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);

    return label;
}

/**
 * gwy_create_stock_like_button:
 * @label_text: Button label text (with mnemonic).
 * @icon_name: Name of icon from the default icon theme.
 *
 * Creates a button that looks like a stock button, but can have different label text.
 *
 * The button created by this function always have an icon if it exists, regardless of user preferences.
 *
 * Returns: (transfer full): The newly created button as #GtkWidget.
 **/
GtkWidget*
gwy_create_stock_like_button(const gchar *label_text,
                             const gchar *icon_name)
{
    GtkWidget *button = g_object_new(GTK_TYPE_BUTTON,
                                     "label", label_text,
                                     "use-underline", TRUE,
                                     "always-show-image", TRUE,
                                     NULL);
    if (gtk_icon_theme_has_icon(gtk_icon_theme_get_default(), icon_name)) {
        gtk_button_set_image(GTK_BUTTON(button),
                             gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON));
    }
    return button;
}

/**
 * gwy_create_image_menu_item:
 * @label_text: (nullable): Menu item label text (with mnemonic).
 * @icon_name: (nullable): Name of icon from the default icon theme.
 * @always_show: %TRUE to always show the image; %FALSE to show it according to GTK+ settings.
 *
 * Creates a menu item with image.
 *
 * This function either wraps the deprecated #GtkImageMenuItem and contains the deprecation in one place, or it
 * reimplements menu items with images by other means. The returned widget is a subclass of #GtkMenuItem.
 *
 * The icon can later be changed by gwy_set_image_menu_item_icon().
 *
 * Returns: (transfer full): The newly created menu item as #GtkWidget.
 **/
GtkWidget*
gwy_create_image_menu_item(const gchar *label_text,
                           const gchar *icon_name,
                           gboolean always_show)
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    GtkWidget *item = gtk_image_menu_item_new_with_mnemonic(label_text);
#pragma GCC diagnostic pop
    if (icon_name)
        gwy_set_image_menu_item_icon(GTK_MENU_ITEM(item), icon_name);
    if (always_show)
        g_object_set(item, "always-show-image", TRUE, NULL);
    return item;
}

/**
 * gwy_set_image_menu_item_icon:
 * @item: A menu item with image created with gwy_create_image_menu_item().
 * @icon_name: (nullable): Name of icon from the default icon theme.
 *
 * Sets the icon of a menu item with image.
 **/
void
gwy_set_image_menu_item_icon(GtkMenuItem *item,
                             const gchar *icon_name)
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    g_return_if_fail(GTK_IS_IMAGE_MENU_ITEM(item));
    /* If images in menus are disabled by the "gtk-menu-images" setting, Gtk+ will not show them anyway. */
    GtkWidget *image = gtk_image_menu_item_get_image(GTK_IMAGE_MENU_ITEM(item));
    if (icon_name && image)
        gtk_image_set_from_icon_name(GTK_IMAGE(image), icon_name, GTK_ICON_SIZE_MENU);
    else if (image)
        gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), NULL);
    else {
        image = gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
        gtk_widget_show_all(image);
        gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image);
    }
#pragma GCC diagnostic pop
}

/**
 * gwy_set_image_menu_item_pixbuf:
 * @item: A menu item with image created with gwy_create_image_menu_item().
 * @pixbuf: (nullable): Pixbuf to use as the icon.
 *
 * Sets the image of a menu item with image to a pixbuf.
 **/
void
gwy_set_image_menu_item_pixbuf(GtkMenuItem *item,
                               GdkPixbuf *pixbuf)
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
    g_return_if_fail(GTK_IS_IMAGE_MENU_ITEM(item));
    /* If images in menus are disabled by the "gtk-menu-images" setting, Gtk+ will not show them anyway. */
    GtkWidget *image = gtk_image_menu_item_get_image(GTK_IMAGE_MENU_ITEM(item));
    if (pixbuf && image)
        gtk_image_set_from_pixbuf(GTK_IMAGE(image), pixbuf);
    else if (image)
        gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), NULL);
    else {
        image = gtk_image_new_from_pixbuf(pixbuf);
        gtk_widget_show_all(image);
        gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image);
    }
#pragma GCC diagnostic pop
}

/**
 * gwy_add_button_to_dialog:
 * @dialog: A dialog.
 * @label_text: Button label text (with mnemonic).
 * @icon_name: Name of icon from the default icon theme.
 * @response_id: Dialog response id.
 *
 * Adds a button with an icon to a dialog.
 *
 * The button may actually have an icon or it may not, depending on user preferences. Create a button directly with
 * gwy_create_stock_like_button() if the button must have an icon.
 *
 * Returns: The button widget, in case you need it.
 **/
GtkWidget*
gwy_add_button_to_dialog(GtkDialog *dialog,
                         const gchar *label_text,
                         const gchar *icon_name,
                         gint response_id)
{
    // GTK+ distinguishes NULL and empty labels for whether an icon should be shown. But that is silly. Make sure they
    // behave the same.
    if (label_text && !strlen(label_text))
        label_text = NULL;

    GtkWidget *button = gtk_dialog_add_button(dialog, label_text, response_id);
    if (icon_name && gtk_icon_theme_has_icon(gtk_icon_theme_get_default(), icon_name)) {
        gtk_button_set_image(GTK_BUTTON(button),
                             gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_BUTTON));
    }
    return button;
}

/**
 * gwy_stock_label:
 * @item: A stock item identifier.
 *
 * Obtains a predefined common label.
 *
 * The returned string is already translated.
 *
 * The label goes together with the icon obtained with gwy_stock_icon_name() and response code obtained with
 * gwy_stock_response().
 *
 * Returns: The predefined label.
 **/
const gchar*
gwy_stock_label(GwyStockItem item)
{
    g_return_val_if_fail((guint)item < G_N_ELEMENTS(stock_items), NULL);
    return gwy_C(stock_items[item].label);
}

/**
 * gwy_stock_icon_name:
 * @item: A stock item identifier.
 *
 * Obtains a predefined common icon name.
 *
 * Not all items have an associated icon name. The function may return %NULL.
 *
 * The icon goes together with the label obtained with gwy_stock_label() and response code obtained with
 * gwy_stock_response().
 *
 * Returns: (nullable): The predefined icon name.
 **/
const gchar*
gwy_stock_icon_name(GwyStockItem item)
{
    g_return_val_if_fail((guint)item < G_N_ELEMENTS(stock_items), NULL);
    return stock_items[item].icon_name;
}

/**
 * gwy_stock_response:
 * @item: A stock item identifier.
 *
 * Obtains a predefined dialog response.
 *
 * The response is from #GwyResponseType enum and goes together with the label obtained with gwy_stock_label() and
 * icon obtained with gwy_stock_icon_name().
 *
 * Returns: The predefined respose.
 **/
gint
gwy_stock_response(GwyStockItem item)
{
    g_return_val_if_fail((guint)item < G_N_ELEMENTS(stock_items), 0);
    return stock_items[item].dialog_response;
}

/**
 * gwy_stock_add_button_to_dialog:
 * @dialog: A dialog window.
 * @item: A stock item identifier.
 *
 * Adds a standard button to a dialog.
 *
 * The button may actually have an icon or it may not, depending on user preferences. Create a button directly with
 * gwy_create_stock_like_button() if the button must have an icon.
 *
 * Returns: (transfer none): The button widget in case it is needed.
 **/
GtkWidget*
gwy_stock_add_button_to_dialog(GtkDialog *dialog,
                               GwyStockItem item)
{
    g_return_val_if_fail((guint)item < G_N_ELEMENTS(stock_items), NULL);
    return gwy_add_button_to_dialog(dialog,
                                    gwy_C(stock_items[item].label),
                                    stock_items[item].icon_name,
                                    stock_items[item].dialog_response);
}

/**
 * gwy_create_stock_button:
 * @item: A stock item identifier.
 *
 * Creates a standard button.
 *
 * See gwy_stock_add_button_to_dialog() if you want to add a standard button to a dialog. The button created by this
 * function always has an icon if the stock item has one associtated, regardless of user preferences.
 *
 * Returns: (transfer full): The newly created button.
 **/
GtkWidget*
gwy_create_stock_button(GwyStockItem item)
{
    g_return_val_if_fail((guint)item < G_N_ELEMENTS(stock_items), NULL);
    return gwy_create_stock_like_button(gwy_C(stock_items[item].label),
                                        stock_items[item].icon_name);
}

static void
sync_sensitivity(GtkWidget *master,
                 G_GNUC_UNUSED GtkStateType state,
                 GtkWidget *slave)
{
    gtk_widget_set_sensitive(slave, gtk_widget_get_sensitive(GTK_WIDGET(master)));
}

static void
disconnect_slave(GtkWidget *slave,
                 GtkWidget *master)
{
    g_signal_handlers_disconnect_by_func(master, sync_sensitivity, slave);
    g_signal_handlers_disconnect_by_func(master, disconnect_master, slave);
}

static void
disconnect_master(GtkWidget *master,
                  GtkWidget *slave)
{
    g_signal_handlers_disconnect_by_func(master, disconnect_slave, slave);
}

/**
 * gwy_widget_sync_sensitivity:
 * @master: Master widget.
 * @slave: Slave widget.
 *
 * Make widget's sensitivity follow the sensitivity of another widget.
 *
 * The sensitivity of @slave is set according to @master's effective sensitivity (as returned by
 * GTK_WIDGET_IS_SENSITIVE()), i.e. it does not just synchronize GtkWidget:sensitive property.
 **/
void
gwy_widget_sync_sensitivity(GtkWidget *master,
                            GtkWidget *slave)
{
    g_signal_connect(master, "state-changed", G_CALLBACK(sync_sensitivity), slave);
    g_signal_connect(slave, "destroy", G_CALLBACK(disconnect_slave), master);
    g_signal_connect(master, "destroy", G_CALLBACK(disconnect_master), slave);
}

/**
 * gwy_widget_is_hbox:
 * @widget: A widget.
 *
 * Checks if a widget is a GTK+ horizontal box.
 *
 * The function checks whether the widget is a #GtkBox and also whether it is horizontal. The latter can change in
 * principle, so the check should to be used when it makes sense.
 *
 * Returns: %TRUE if widget is a horizontal #GtkBox.
 **/
gboolean
gwy_widget_is_hbox(GtkWidget *widget)
{
    g_return_val_if_fail(GTK_IS_WIDGET(widget), FALSE);
    if (!GTK_IS_BOX(widget))
        return FALSE;
    return gtk_orientable_get_orientation(GTK_ORIENTABLE(widget)) == GTK_ORIENTATION_HORIZONTAL;
}

/**
 * gwy_set_message_label:
 * @label: A label widget.
 * @text: New label text.
 * @type: Message type.
 * @is_markup: %TRUE if @text is Pango markup, %FALSE if it is raw text.
 *
 * Sets the text of a label according to message type style.
 **/
void
gwy_set_message_label(GtkLabel *label,
                      const gchar *text,
                      GtkMessageType type,
                      gboolean is_markup)
{
    if (!text || !*text) {
        gtk_label_set_text(label, NULL);
        return;
    }
    if (type != GTK_MESSAGE_ERROR && type != GTK_MESSAGE_WARNING) {
        if (is_markup)
            gtk_label_set_markup(label, text);
        else
            gtk_label_set_text(label, text);
        return;
    }

    gchar *freeme = NULL;
    if (!is_markup && (strchr(text, '<') || strchr(text, '>') || strchr(text, '&')))
        text = freeme = g_markup_escape_text(text, -1);

    gchar *markup = NULL;
    if (type == GTK_MESSAGE_ERROR)
        markup = g_strconcat("<span foreground='#c80000'>", text, "</span>", NULL);
    else if (type == GTK_MESSAGE_WARNING)
        markup = g_strconcat("<span foreground='#b05000'>", text, "</span>", NULL);
    else {
        g_assert_not_reached();
    }

    gtk_label_set_markup(label, markup);
    g_free(markup);
    g_free(freeme);
}

/**
 * gwy_set_widget_margins:
 * @widget: A widget.
 * @start: Start (left) margin, or -1 to keep unchanged.
 * @end: End (right) margin, or -1 to keep unchanged.
 * @top: Top margin, or -1 to keep unchanged.
 * @bottom: Bottom margin, or -1 to keep unchanged.
 *
 * Set multiple widget margins.
 *
 * This is a convenience wrapper for multiple calls to gtk_widget_set_margin_start() and similar functions.
 **/
void
gwy_set_widget_margins(GtkWidget *widget,
                       gint start, gint end, gint top, gint bottom)
{
    g_return_if_fail(GTK_IS_WIDGET(widget));

    if (start >= 0)
        gtk_widget_set_margin_start(widget, start);
    if (end >= 0)
        gtk_widget_set_margin_end(widget, end);
    if (top >= 0)
        gtk_widget_set_margin_top(widget, top);
    if (bottom >= 0)
        gtk_widget_set_margin_bottom(widget, bottom);
}

/**
 * gwy_get_screen_size:
 * @widget: A widget, possibly %NULL (although not advisable).
 * @width: (out) (nullable): Location where to store the screen width.
 * @height: (out) (nullable): Location where to store the screen height.
 *
 * Gets the working area size of widget's screen.
 *
 * If @widget is NULL, the default screen is substituted. The function can be used to check whether we might be going
 * to to create an unreasonably large window. If, for whatever reason, we cannot obtain the screen size, the function
 * still sets the width and height to reasonable values.
 *
 * Returns: %TRUE if the result should be real; %FALSE when some default was substituted.
 **/
gboolean
gwy_get_screen_size(GtkWidget *widget,
                    gint *pwidth,
                    gint *pheight)
{
    GdkDisplay *display = NULL;
    GdkWindow *window = NULL;
    GdkMonitor *monitor = NULL;

    if (widget) {
        display = gtk_widget_get_display(widget);
        window = gtk_widget_get_window(widget);
    }
    else {
        display = gdk_display_get_default();
    }

    if (display && window)
        monitor = gdk_display_get_monitor_at_window(display, window);
    else if (display)
        monitor = gdk_display_get_primary_monitor(display);

    gint width = -1, height = -1;
    if (monitor) {
        GdkRectangle workarea;
        gdk_monitor_get_workarea(monitor, &workarea);
        width = workarea.width;
        height = workarea.height;
    }

    gboolean retval = TRUE;
    if (width < 16) {
        width = 1025;
        retval = FALSE;
    }
    if (height < 16) {
        height = 768;
        retval = FALSE;
    }

    if (pwidth)
        *pwidth = width;
    if (pheight)
        *pheight = height;

    return retval;
}

/**
 * gwy_strip_menu_item_label:
 * @label: Menu item label to modify in place.
 *
 * Removes mnemonics and ellipses from a label string.
 *
 * The label should not contain XML. Pango markup is currently preserved, but it may be stripped in future, so the
 * behaviour is undefined.
 *
 * The label should not contain arbitrary content like file names. Odd labels can arise.
 *
 * Returns: The label again, for easier chaining.
 **/
gchar*
gwy_strip_menu_item_label(gchar *label)
{
    if (!label || !*label)
        return label;

    guint i, j;
    for (i = j = 0; label[i]; i++) {
        label[j] = label[i];
        if (label[i] != '_' || label[i+1] == '_')
            j++;
    }
    /* If the label *ends* with an underscore, just kill it */
    label[j] = '\0';
    if (j >= 3 && label[j-3] == '.' && label[j-2] == '.' && label[j-1] == '.')
        label[j-3] = '\0';
    return label;
}

/**
 * SECTION: utils
 * @title: Widget utilities
 * @short_description: Miscellaneous widget utilities
 *
 * For the consistency of labelling and mnemonic letters (and general convenience), several common labels are
 * predefined in #GwyStockItem enum. Use gwy_stock_label() to obtain a predefined label. A couple of helper functions
 * directly creating widgets are also provided.
 **/

/**
 * gwy_adjustment_get_int:
 * @adj: A #GtkAdjustment to get value of.
 *
 * Gets a properly rounded integer value from an adjustment, cast to #gint.
 **/

/**
 * GwyHScaleStyle:
 * @GWY_HSCALE_DEFAULT: Default label, hscale, spinbutton, and units widget row.  Note that the default mapping is
 *                      linear for hscales but signed square root for adjust bars.
 * @GWY_HSCALE_LOG: The scale mapping is logarithmic.
 * @GWY_HSCALE_SQRT: The scale mapping is signed square root.
 * @GWY_HSCALE_LINEAR: The scale mapping is linear.
 * @GWY_HSCALE_NO_SCALE: There is no hscale/adjust bar.
 * @GWY_HSCALE_WIDGET: An user-specified widget is used in place of the adjustment control(s).
 * @GWY_HSCALE_WIDGET_NO_EXPAND: An user-specified widget is used in place of hscale and spinbutton, and it is
 *                               left-aligned instead of taking all the alloted space.
 * @GWY_HSCALE_CHECK: The label is actually a check button that controls sensitivity of the row.  This is a flag, to
 *                    be bitwise or-ed with other values.
 * @GWY_HSCALE_SNAP: The adjust bar snaps to ticks (hscales cannot snap). This is a flag, to be bitwise or-ed with
 *                   other values.
 *
 * Options controlling gwy_table_attach_adjbar() and gwy_table_attach_hscale() behaviour.
 **/

/**
 * GwyResponseType:
 * @GWY_RESPONSE_RESET: Reset of all parameters (that do not have the no-reset flag set).  Adding it to a #GwyDialog
 *                      creates a Reset button which is normally handled fully by the dialog itself.
 * @GWY_RESPONSE_UPDATE: Update of the preview.  Adding it to a #GwyDialog creates an Update button which is normally
 *                       handled fully by the dialog itself (including sensitivity tied to an instant updates
 *                       parameter).
 * @GWY_RESPONSE_CLEAR: Clearing/resetting of selection.  Adding it to a #GwyDialog creates a Clear button.  You need
 *                      to connect to #GwyDialog::response and handle the response yourself.
 * @GWY_RESPONSE_COPY: Predefined response for a Copy button.
 * @GWY_RESPONSE_DELETE: Predefined response for a Delete or Remove button.
 * @GWY_RESPONSE_SAVE: Predefined response for a Save or Export button.
 * @GWY_RESPONSE_EXECUTE: Predefined response for an Excecute or Estimate button.
 * @GWY_RESPONSE_OPEN: Predefined response for an Open or Load button.
 * @GWY_RESPONSE_HELP: Predefined response for a Help button (usually handled by the libraries).
 * @GWY_RESPONSE_NEW: Predefined response for a New button.
 * @GWY_RESPONSE_EDIT: Predefined response for an Edit button.
 * @GWY_RESPONSE_REFINE: Predefined response for a Refine or Fit button.
 * @GWY_RESPONSE_USER: The smallest value to use for non-standard response codes. It is not used by anything in
 *                     the library (and neither is any larger response code).
 *
 * Type of predefined dialog response types.
 **/

/**
 * GwyStockItem:
 * @GWY_STOCK_NONE: No/invalid stock item.
 * @GWY_STOCK_APPLY: Apply stock item.
 * @GWY_STOCK_CANCEL: Apply stock item.
 * @GWY_STOCK_CLEAR: Apply stock item.
 * @GWY_STOCK_CLOSE: Close stock item.
 * @GWY_STOCK_COPY: Copy stock item.
 * @GWY_STOCK_DELETE: Delete stock item.
 * @GWY_STOCK_EDIT: Edit stock item.
 * @GWY_STOCK_ESTIMATE: Estimate stock item.
 * @GWY_STOCK_EXPORT: Export stock item.
 * @GWY_STOCK_FIT: Fit stock item.
 * @GWY_STOCK_HELP: Help stock item.
 * @GWY_STOCK_HIDE: Hide stock item.
 * @GWY_STOCK_LOAD: Load stock item.
 * @GWY_STOCK_NEW: New stock item.
 * @GWY_STOCK_OK: OK stock item.
 * @GWY_STOCK_OPEN: Open stock item.
 * @GWY_STOCK_REFINE: Refine stock item.
 * @GWY_STOCK_REMOVE: Remove stock item.
 * @GWY_STOCK_RESET: Reset stock item.
 * @GWY_STOCK_SAVE: Save stock item.
 * @GWY_STOCK_UPDATE: Update stock item.
 *
 * Type of predefined labels.
 **/

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