/*
 *  $Id: adjust-bar.c 30058 2026-06-12 17:38:28Z yeti-dn $
 *  Copyright (C) 2012-2024 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 <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>
#include <glib-object.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/field.h"
#include "libgwyddion/rgba.h"

#include "libgwyui/types.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/widget-impl-utils.h"
#include "libgwyui/adjust-bar.h"

enum {
    PROP_0,
    PROP_ADJUSTMENT,
    PROP_SNAP_TO_TICKS,
    PROP_MAPPING,
    PROP_HAS_CHECK_BUTTON,
    NUM_PROPERTIES,
};

enum {
    SGNL_CHANGE_VALUE,
    NUM_SIGNALS
};

typedef gdouble (*MappingFunc)(gdouble value);

struct _GwyAdjustBarPrivate {
    GdkWindow *event_window;
    const gchar *cursor_name;

    GtkAdjustment *adjustment;
    gulong adjustment_value_changed_id;
    gulong adjustment_changed_id;
    gdouble oldvalue; /* This is to avoid acting on no-change notifications. */
    gboolean snap_to_ticks;
    gboolean adjustment_ok;
    gboolean dragging;
    gboolean cursor_inside;

    GtkWidget *check;
    gboolean bar_sensitive;

    GwyScaleMappingType mapping;
    MappingFunc map_value;
    MappingFunc map_position;
    gint x;
    gint length;
    gdouble a;
    gdouble b;
};

static void     dispose                 (GObject *object);
static void     set_property            (GObject *object,
                                         guint prop_id,
                                         const GValue *value,
                                         GParamSpec *pspec);
static void     get_property            (GObject *object,
                                         guint prop_id,
                                         GValue *value,
                                         GParamSpec *pspec);
static void     forall                  (GtkContainer *container,
                                         gboolean include_internals,
                                         GtkCallback callback,
                                         gpointer cbdata);
static void     realize                 (GtkWidget *widget);
static void     unrealize               (GtkWidget *widget);
static void     map                     (GtkWidget *widget);
static void     unmap                   (GtkWidget *widget);
static void     get_preferred_width     (GtkWidget *widget,
                                         gint *minimum,
                                         gint *natural);
static void     get_preferred_height    (GtkWidget *widget,
                                         gint *minimum,
                                         gint *natural);
static void     size_allocate           (GtkWidget *widget,
                                         GdkRectangle *allocation);
static void     adjust_event_window     (GtkWidget *widget,
                                         GdkRectangle *allocation);
static gboolean draw                    (GtkWidget *widget,
                                         cairo_t *cr);
static gboolean enter_notify            (GtkWidget *widget,
                                         GdkEventCrossing *event);
static gboolean leave_notify            (GtkWidget *widget,
                                         GdkEventCrossing *event);
static gboolean scroll                  (GtkWidget *widget,
                                         GdkEventScroll *event);
static gboolean button_press            (GtkWidget *widget,
                                         GdkEventButton *event);
static gboolean button_release          (GtkWidget *widget,
                                         GdkEventButton *event);
static gboolean motion_notify           (GtkWidget *widget,
                                         GdkEventMotion *event);
static void     state_flags_changed     (GtkWidget *widget,
                                         GtkStateFlags previous_flags);
static GType    child_type              (GtkContainer *container);
static void     change_value            (GwyAdjustBar *adjbar,
                                         gdouble newvalue);
static gboolean set_adjustment          (GwyAdjustBar *adjbar,
                                         GtkAdjustment *adjustment);
static gboolean set_snap_to_ticks       (GwyAdjustBar *adjbar,
                                         gboolean setting);
static gboolean set_mapping             (GwyAdjustBar *adjbar,
                                         GwyScaleMappingType mapping);
static void     draw_bar                (GwyAdjustBar *adjbar,
                                         cairo_t *cr);
static void     adjustment_changed      (GwyAdjustBar *adjbar,
                                         GtkAdjustment *adjustment);
static void     adjustment_value_changed(GwyAdjustBar *adjbar,
                                         GtkAdjustment *adjustment);
static void     update_mapping          (GwyAdjustBar *adjbar);
static gdouble  map_value_to_position   (GwyAdjustBar *adjbar,
                                         gdouble value);
static gdouble  map_position_to_value   (GwyAdjustBar *adjbar,
                                         gdouble position);
static gdouble  map_both_linear         (gdouble value);
static void     update_value            (GtkWidget *widget,
                                         gdouble newposition);
static gdouble  snap_value              (GwyAdjustBar *adjbar,
                                         gdouble value);

/* FIXME GTK3 we use this in Gwyddion 2 version of the widget for some, probably better because field-tested,
 * size requisition. */
static const GtkBorder default_border = { 4, 4, 3, 3 };
static const gint indicator_spacing = 4;

static guint signals[NUM_SIGNALS];
static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyAdjustBar, gwy_adjust_bar, GTK_TYPE_BIN,
                        G_ADD_PRIVATE(GwyAdjustBar))

static void
gwy_adjust_bar_class_init(GwyAdjustBarClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GtkContainerClass *container_class = GTK_CONTAINER_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_adjust_bar_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->size_allocate = size_allocate;
    widget_class->draw = draw;
    widget_class->enter_notify_event = enter_notify;
    widget_class->leave_notify_event = leave_notify;
    widget_class->scroll_event = scroll;
    widget_class->button_press_event = button_press;
    widget_class->button_release_event = button_release;
    widget_class->motion_notify_event = motion_notify;
    widget_class->state_flags_changed = state_flags_changed;

    container_class->forall = forall;
    container_class->child_type = child_type;

    klass->change_value = change_value;

    properties[PROP_ADJUSTMENT] = g_param_spec_object("adjustment", NULL,
                                                      "Adjustment representing the value.",
                                                      GTK_TYPE_ADJUSTMENT, GWY_GPARAM_RWE);
    properties[PROP_SNAP_TO_TICKS] = g_param_spec_boolean("snap-to-ticks", NULL,
                                                          "Whether only values that are multiples of step "
                                                          "size are allowed.",
                                                          FALSE, GWY_GPARAM_RWE);
    properties[PROP_MAPPING] = g_param_spec_enum("mapping", NULL,
                                                 "Mapping function between values and screen positions.",
                                                 GWY_TYPE_SCALE_MAPPING_TYPE, GWY_SCALE_MAPPING_SQRT, GWY_GPARAM_RWE);
    properties[PROP_HAS_CHECK_BUTTON] = g_param_spec_boolean("has-check-button", NULL,
                                                             "Whether the adjust bar has a check button.",
                                                             FALSE, GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);

    /**
     * GwyAdjustBar::change-value:
     * @gwyadjustbar: The #GwyAdjustBar which received the signal.
     * @arg1: New value for @gwyadjustbar.
     *
     * The ::change-value signal is emitted when the user interactively changes the value.
     *
     * It is an action signal.
     **/
    signals[SGNL_CHANGE_VALUE] = g_signal_new("change-value", type,
                                              G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                                              G_STRUCT_OFFSET(GwyAdjustBarClass, change_value),
                                              NULL, NULL,
                                              g_cclosure_marshal_VOID__DOUBLE,
                                              G_TYPE_NONE, 1, G_TYPE_DOUBLE);
    g_signal_set_va_marshaller(signals[SGNL_CHANGE_VALUE], type, g_cclosure_marshal_VOID__DOUBLEv);
}

static void
gwy_adjust_bar_init(GwyAdjustBar *adjbar)
{
    GwyAdjustBarPrivate *priv;

    priv = adjbar->priv = gwy_adjust_bar_get_instance_private(adjbar);
    priv->mapping = GWY_SCALE_MAPPING_SQRT;
    priv->bar_sensitive = TRUE;
    gtk_widget_set_has_window(GTK_WIDGET(adjbar), FALSE);

    GtkWidget *label = gtk_label_new(NULL);
    gtk_label_set_xalign(GTK_LABEL(label), 0.0);
    gtk_container_add(GTK_CONTAINER(adjbar), label);
}

static void
dispose(GObject *object)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(object);

    set_adjustment(adjbar, NULL);
    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(object);

    switch (prop_id) {
        case PROP_ADJUSTMENT:
        set_adjustment(adjbar, g_value_get_object(value));
        break;

        case PROP_SNAP_TO_TICKS:
        set_snap_to_ticks(adjbar, g_value_get_boolean(value));
        break;

        case PROP_MAPPING:
        set_mapping(adjbar, g_value_get_enum(value));
        break;

        case PROP_HAS_CHECK_BUTTON:
        gwy_adjust_bar_set_has_check_button(adjbar, g_value_get_boolean(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(object)->priv;

    switch (prop_id) {
        case PROP_ADJUSTMENT:
        g_value_set_object(value, priv->adjustment);
        break;

        case PROP_SNAP_TO_TICKS:
        g_value_set_boolean(value, priv->snap_to_ticks);
        break;

        case PROP_MAPPING:
        g_value_set_enum(value, priv->mapping);
        break;

        case PROP_HAS_CHECK_BUTTON:
        g_value_set_boolean(value, !!priv->check);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
forall(GtkContainer *container, gboolean include_internals, GtkCallback callback, gpointer cbdata)
{
    if (include_internals) {
        GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(container)->priv;
        if (priv->check)
            (*callback)(priv->check, cbdata);
    }
    GTK_CONTAINER_CLASS(parent_class)->forall(container, include_internals, callback, cbdata);
}

static void
realize(GtkWidget *widget)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;

    /* FIXME GTK3 widgets generally do not call parent's realize. Not sure why because it does a couple of things for
     * us like setting the widget realized. */
    parent_class->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
                                                        | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
                                                        | GDK_SCROLL_MASK
                                                        | GDK_POINTER_MOTION_MASK);
    if (priv->check) {
        // The above function creates the event window over the entire widget. But if we have a checkbox, that is not
        // correct. Fix the event window position and size.
        GdkRectangle allocation;
        gtk_widget_get_allocation(widget, &allocation);
        adjust_event_window(widget, &allocation);
    }
}

static void
unrealize(GtkWidget *widget)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;

    gwy_destroy_widget_input_window(widget, &priv->event_window);
    priv->adjustment_ok = FALSE;
    parent_class->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;
    GtkWidget *check = priv->check;

    parent_class->map(widget);
    gdk_window_show(priv->event_window);
    if (check && gtk_widget_get_visible(check) && gtk_widget_get_child_visible(check) && !gtk_widget_get_mapped(check))
        gtk_widget_map(check);
}

static void
unmap(GtkWidget *widget)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;
    GtkWidget *check = priv->check;

    gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, NULL);
    if (check)
        gtk_widget_unmap(check);
    gdk_window_hide(priv->event_window);
    parent_class->unmap(widget);
}

// FIXME: These method are *exact* reproductions of GtkBin's.  But if we do not override them the child is completely
// mis-allocated.
static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    *minimum = default_border.left + default_border.right;
    *natural = *minimum;

    gint border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
    *minimum += 2*border_width;
    *natural += 2*border_width;

    GtkWidget *label = gtk_bin_get_child(GTK_BIN(widget));
    if (label && gtk_widget_get_visible(label)) {
        gint child_min, child_nat;
        gtk_widget_get_preferred_width(label, &child_min, &child_nat);
        *minimum += child_min;
        *natural += child_nat;
    }

    if (priv->check) {
        gint child_min, child_nat;
        gtk_widget_get_preferred_width(priv->check, &child_min, &child_nat);
        /* Replace default border with two indicator spacings. */
        *minimum += child_min + 2*indicator_spacing - default_border.left;
        *natural += child_nat + 2*indicator_spacing - default_border.left;
    }
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    *minimum = default_border.top + default_border.bottom;
    *natural = *minimum;

    gint border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
    *minimum += 2*border_width;
    *natural += 2*border_width;

    GtkWidget *label = gtk_bin_get_child(GTK_BIN(widget));
    gint children_min = 0, children_nat = 0;
    if (label && gtk_widget_get_visible(label)) {
        gint child_min, child_nat;
        gtk_widget_get_preferred_height(label, &child_min, &child_nat);
        children_min = MAX(children_min, child_min);
        children_nat = MAX(children_nat, child_nat);
    }

    if (priv->check) {
        gint child_min, child_nat;
        gtk_widget_get_preferred_height(priv->check, &child_min, &child_nat);
        children_min = MAX(children_min, child_min);
        children_nat = MAX(children_nat, child_nat);
    }

    *minimum += children_min;
    *natural += children_nat;
}

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;
    GdkRectangle child_allocation;
    GtkRequisition child_req;

    parent_class->size_allocate(widget, allocation);

    gint border_width = gtk_container_get_border_width(GTK_CONTAINER(widget));
    gint width = allocation->width;

    priv->x = 0;
    if (priv->check) {
        gtk_widget_get_preferred_size(priv->check, NULL, &child_req);
        priv->x = child_req.width + 2*indicator_spacing - default_border.left;
        priv->x = MAX(priv->x, 0);
        width = MAX(width - priv->x, 1);

        child_allocation.x = allocation->x;
        child_allocation.y = allocation->y + (allocation->height - child_req.height)/2;
        child_allocation.width = child_req.width;
        child_allocation.height = child_req.height;
        gtk_widget_size_allocate(priv->check, &child_allocation);
    }

    gwy_debug("ALLOCATION %dx%d at (%d,%d)", allocation->width, allocation->height, allocation->x, allocation->y);
    adjust_event_window(widget, allocation);

    GtkWidget *label = gtk_bin_get_child(GTK_BIN(widget));
    if (label && gtk_widget_get_visible(GTK_WIDGET(label))) {
        gtk_widget_get_preferred_size(label, NULL, &child_req);
        gint h = child_req.height + (2*border_width + default_border.top + default_border.bottom);
        child_allocation.x = (allocation->x + priv->x + border_width + default_border.left);
        child_allocation.y = (allocation->y + MAX(allocation->height - h, 0)/2 + border_width + default_border.top);
        child_allocation.width = (width - 2*border_width - default_border.left - default_border.right);
        child_allocation.height = (allocation->height - 2*border_width - default_border.top - default_border.bottom);
        gtk_widget_size_allocate(label, &child_allocation);
    }

    update_mapping(GWY_ADJUST_BAR(widget));
}

static void
adjust_event_window(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    if (!priv->event_window)
        return;

    gint width = allocation->width;
    if (priv->check)
        width = MAX(width - priv->x, 1);
    gwy_debug("INPUT WINDOW %dx%d at (%d,%d)", width, allocation->height, allocation->x + priv->x, allocation->y);
    gdk_window_move_resize(priv->event_window, allocation->x + priv->x, allocation->y, width, allocation->height);
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    draw_bar(GWY_ADJUST_BAR(widget), cr);
    parent_class->draw(widget, cr);
    return FALSE;
}

static gboolean
enter_notify(GtkWidget *widget,
             G_GNUC_UNUSED GdkEventCrossing *event)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    priv->cursor_inside = TRUE;
    if (gtk_widget_is_sensitive(widget) && priv->bar_sensitive) {
        gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, "col-resize");
        gtk_widget_set_state_flags(widget, GTK_STATE_FLAG_PRELIGHT, FALSE);
    }
    return FALSE;
}

static gboolean
leave_notify(GtkWidget *widget,
             G_GNUC_UNUSED GdkEventCrossing *event)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    priv->cursor_inside = FALSE;
    if (priv->bar_sensitive && !priv->dragging)
        gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);
    return FALSE;
}

static gboolean
scroll(GtkWidget *widget, GdkEventScroll *event)
{
    GdkScrollDirection dir = event->direction;
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;
    gdouble value, position, newposition, sposition;

    if (!priv->adjustment_ok)
        return TRUE;

    value = gtk_adjustment_get_value(priv->adjustment);
    position = map_value_to_position(adjbar, value);
    newposition = position;
    if (dir == GDK_SCROLL_UP || dir == GDK_SCROLL_RIGHT) {
        newposition += 1.0;
        if (priv->snap_to_ticks) {
            value += gtk_adjustment_get_step_increment(priv->adjustment);
            value = fmin(value, gtk_adjustment_get_upper(priv->adjustment));
            sposition = map_value_to_position(adjbar, value);
            newposition = fmax(newposition, sposition);
        }
    }
    else {
        newposition -= 1.0;
        if (priv->snap_to_ticks) {
            value -= gtk_adjustment_get_step_increment(priv->adjustment);
            value = fmax(value, gtk_adjustment_get_lower(priv->adjustment));
            sposition = map_value_to_position(adjbar, value);
            newposition = fmin(newposition, sposition);
        }
    }

    newposition = CLAMP(newposition, 0.0, priv->length);
    if (newposition != position)
        update_value(widget, newposition);
    return TRUE;
}

static gboolean
button_press(GtkWidget *widget, GdkEventButton *event)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;
    GtkWidget *child;

    if (!priv->bar_sensitive || event->button != 1)
        return FALSE;
    priv->dragging = TRUE;
    update_value(widget, event->x);
    child = gtk_bin_get_child(GTK_BIN(widget));
    if (child) {
        if (gtk_label_get_mnemonic_keyval(GTK_LABEL(child)) != GDK_KEY_VoidSymbol)
            gtk_widget_mnemonic_activate(child, FALSE);
        /* XXX: If the adjbar label does not have any mnemonic we do not know which widget we would like to give focus
         * to. */
    }
    return TRUE;
}

static gboolean
button_release(GtkWidget *widget, GdkEventButton *event)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    gboolean dragging = priv->dragging;
    priv->dragging = FALSE;
    if (!priv->bar_sensitive || event->button != 1 || !dragging)
        return FALSE;

    update_value(widget, event->x);
    if (!priv->cursor_inside)
        gtk_widget_unset_state_flags(widget, GTK_STATE_FLAG_PRELIGHT);

    return TRUE;
}

static gboolean
motion_notify(GtkWidget *widget, GdkEventMotion *event)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;

    if (!gtk_widget_is_sensitive(widget) || !priv->bar_sensitive || !(event->state & GDK_BUTTON1_MASK))
        return FALSE;
    update_value(widget, event->x);
    return TRUE;
}

static void
state_flags_changed(GtkWidget *widget,
                    GtkStateFlags previous_flags)
{
    GwyAdjustBarPrivate *priv = GWY_ADJUST_BAR(widget)->priv;
    if (!priv->event_window)
        return;

    GtkStateFlags new_flags = gtk_widget_get_state_flags(widget);
    if (!((previous_flags ^ new_flags) & GTK_STATE_FLAG_INSENSITIVE))
        return;

    if (new_flags & GTK_STATE_FLAG_INSENSITIVE)
        gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, NULL);
    else {
        if (priv->bar_sensitive && gtk_widget_is_sensitive(widget))
            gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, NULL);
    }
}

static GType
child_type(GtkContainer *container)
{
    return gtk_bin_get_child(GTK_BIN(container)) ? G_TYPE_NONE : GTK_TYPE_LABEL;
}

static void
change_value(GwyAdjustBar *adjbar, gdouble newvalue)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    gdouble value;

    g_return_if_fail(priv->adjustment);
    if (!priv->adjustment_ok)
        return;

    value = gtk_adjustment_get_value(priv->adjustment);
    newvalue = snap_value(adjbar, newvalue);
    if (fabs(newvalue - value) <= 1e-12*fmax(fabs(newvalue), fabs(value)))
        return;

    gtk_adjustment_set_value(priv->adjustment, newvalue);
}

/**
 * gwy_adjust_bar_new:
 * @adjustment: The adjustment the adjust bar should use, or %NULL.
 * @label: Text of the adjustment bar label, or %NULL.
 *
 * Creates a new adjustment bar.
 *
 * The label text, if any, is set with mnemonic enabled.  However, you still need to assign it to a widget (presumably
 * a #GtkSpinButton) using gtk_label_set_mnemonic_widget().
 *
 * Returns: A new adjustment bar.
 **/
GtkWidget*
gwy_adjust_bar_new(GtkAdjustment *adjustment, const gchar *label)
{
    GwyAdjustBar *adjbar;

    if (adjustment)
        adjbar = g_object_new(GWY_TYPE_ADJUST_BAR, "adjustment", adjustment, NULL);
    else
        adjbar = g_object_new(GWY_TYPE_ADJUST_BAR, NULL);

    if (label) {
        GtkWidget *child = gtk_bin_get_child(GTK_BIN(adjbar));
        gtk_label_set_text_with_mnemonic(GTK_LABEL(child), label);
    }

    return GTK_WIDGET(adjbar);
}

/**
 * gwy_adjust_bar_set_adjustment:
 * @adjbar: An adjustment bar.
 * @adjustment: Adjustment to use for the value.
 *
 * Sets the adjustment that an adjustment bar visualises.
 **/
void
gwy_adjust_bar_set_adjustment(GwyAdjustBar *adjbar,
                              GtkAdjustment *adjustment)
{
    g_return_if_fail(GWY_IS_ADJUST_BAR(adjbar));
    g_return_if_fail(GTK_IS_ADJUSTMENT(adjustment));
    if (!set_adjustment(adjbar, adjustment))
        return;

    g_object_notify_by_pspec(G_OBJECT(adjbar), properties[PROP_ADJUSTMENT]);
}

/**
 * gwy_adjust_bar_get_adjustment:
 * @adjbar: An adjustment bar.
 *
 * Obtains the adjustment that an adjustment bar visualises.
 *
 * Returns: The adjustment used by @adjbar.  If no adjustment was set and the default one is used, function returns
 *          %NULL.
 **/
GtkAdjustment*
gwy_adjust_bar_get_adjustment(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), NULL);
    return adjbar->priv->adjustment;
}

/**
 * gwy_adjust_bar_set_snap_to_ticks:
 * @adjbar: An adjustment bar.
 * @setting: %TRUE to restrict values to multiples of step size, %FALSE to permit any values.
 *
 * Sets the snapping behaviour of an adjustment bar.
 *
 * Note the ‘multiples of step size’ condition in fact applies to the difference from the minimum value.  The maximum
 * adjustment value is always permissible, even if it does not satisfy this condition.  Values modified by the user
 * (i.e.  emission of signal "change-value") are snapped, however, values set explicitly gtk_adjustment_set_value()
 * are kept intact.
 *
 * Setting this option to %TRUE immediately causes an adjustment value change if it does not satisfy the condition.
 *
 * It is usually a poor idea to enable snapping for non-linear mappings.
 **/
void
gwy_adjust_bar_set_snap_to_ticks(GwyAdjustBar *adjbar,
                                 gboolean setting)
{
    g_return_if_fail(GWY_IS_ADJUST_BAR(adjbar));
    if (!set_snap_to_ticks(adjbar, setting))
        return;

    g_object_notify_by_pspec(G_OBJECT(adjbar), properties[PROP_SNAP_TO_TICKS]);
}

/**
 * gwy_adjust_bar_get_snap_to_ticks:
 * @adjbar: An adjustment bar.
 *
 * Sets the snapping behaviour of an adjustment bar.
 *
 * Returns: %TRUE if values are restricted to multiples of step size.
 **/
gboolean
gwy_adjust_bar_get_snap_to_ticks(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), FALSE);
    return !!adjbar->priv->snap_to_ticks;
}

/**
 * gwy_adjust_bar_set_mapping:
 * @adjbar: An adjustment bar.
 * @mapping: Mapping function type between values and screen positions in the adjustment bar.
 *
 * Sets the mapping function type for an adjustment bar.
 **/
void
gwy_adjust_bar_set_mapping(GwyAdjustBar *adjbar,
                           GwyScaleMappingType mapping)
{
    g_return_if_fail(GWY_IS_ADJUST_BAR(adjbar));
    if (!set_mapping(adjbar, mapping))
        return;

    g_object_notify_by_pspec(G_OBJECT(adjbar), properties[PROP_MAPPING]);
}

/**
 * gwy_adjust_bar_get_mapping:
 * @adjbar: An adjustment bar.
 *
 * Gets the mapping function type of an adjustment bar.
 *
 * Returns: The type of mapping function between values and screen positions.
 **/
GwyScaleMappingType
gwy_adjust_bar_get_mapping(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), GWY_SCALE_MAPPING_LINEAR);
    return adjbar->priv->mapping;
}

/**
 * gwy_adjust_bar_set_has_check_button:
 * @adjbar: An adjustment bar.
 * @setting: %TRUE to enable a check button; %FALSE to disable it.
 *
 * Sets whether an adjustment bar has a check button.
 **/
void
gwy_adjust_bar_set_has_check_button(GwyAdjustBar *adjbar, gboolean setting)
{
    GwyAdjustBarPrivate *priv;

    g_return_if_fail(GWY_IS_ADJUST_BAR(adjbar));
    priv = adjbar->priv;
    if (!setting == !priv->check)
        return;

    if (setting) {
        priv->check = gtk_check_button_new();
        gtk_widget_set_parent(priv->check, GTK_WIDGET(adjbar));
        gtk_widget_set_name(priv->check, "gwyadjbarcheck");
        /* FIXME: Should do even if we are hidden? */
        gtk_widget_show(priv->check);
        gwy_debug("showing checkbutton %p", priv->check);
    }
    else {
        gtk_widget_destroy(priv->check);
        priv->check = NULL;
    }

    if (gtk_widget_get_visible(GTK_WIDGET(adjbar)))
        gtk_widget_queue_resize(GTK_WIDGET(adjbar));

    g_object_notify_by_pspec(G_OBJECT(adjbar), properties[PROP_HAS_CHECK_BUTTON]);
}

/**
 * gwy_adjust_bar_get_has_check_button:
 * @adjbar: An adjustment bar.
 *
 * Reports whether an adjustment bar has a check button.
 *
 * Returns: %TRUE if the adjustment bar has a check button.
 **/
gboolean
gwy_adjust_bar_get_has_check_button(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), FALSE);
    return !!adjbar->priv->check;
}

/**
 * gwy_adjust_bar_get_label:
 * @adjbar: An adjustment bar.
 *
 * Gets the label widget inside an adjustment bar.
 *
 * Use gtk_label_set_mnemonic_widget() to set the mnemonic widget for the label or change the label text.
 *
 * Returns: The label widget.
 **/
GtkWidget*
gwy_adjust_bar_get_label(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), NULL);
    return gtk_bin_get_child(GTK_BIN(adjbar));
}

/**
 * gwy_adjust_bar_get_check_button:
 * @adjbar: An adjustment bar.
 *
 * Gets the check button of an adjustment bar.
 *
 * Connect to the "toggled" signal of the check button.  Modifying it is not recommended.
 *
 * Returns: The check button widget, or %NULL if there is none.
 **/
GtkWidget*
gwy_adjust_bar_get_check_button(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), NULL);
    return adjbar->priv->check;
}

/**
 * gwy_adjust_bar_set_bar_sensitive:
 * @adjbar: An adjustment bar.
 * @sensitive: %TRUE to make the widget's bar sensitive.
 *
 * Sets the sensitivity of an adjustment bar.
 *
 * The bar's sensitivity can be controlled separately.  This is useful when @adjbar has a check button because
 * otherwise the bar is the entire widget and the function is not different from gtk_widget_set_sensitive(). However,
 * if you want to enable and disable the adjustment bar via the check button, use this function instead of
 * gtk_widget_set_sensitive() which would make insensitive also the check button.
 **/
void
gwy_adjust_bar_set_bar_sensitive(GwyAdjustBar *adjbar, gboolean sensitive)
{
    g_return_if_fail(GWY_IS_ADJUST_BAR(adjbar));

    GwyAdjustBarPrivate *priv = adjbar->priv;
    if (!priv->bar_sensitive == !sensitive)
        return;

    priv->bar_sensitive = sensitive;
    if (sensitive) {
        gtk_widget_set_sensitive(gtk_bin_get_child(GTK_BIN(adjbar)), TRUE);
        if (gtk_widget_is_sensitive(GTK_WIDGET(adjbar)))
            gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, "col-resize");
    }
    else {
        gwy_switch_window_cursor(priv->event_window, &priv->cursor_name, NULL);
        gtk_widget_set_sensitive(gtk_bin_get_child(GTK_BIN(adjbar)), FALSE);
    }

    GtkWidget *widget = GTK_WIDGET(adjbar);
    if (gtk_widget_is_drawable(widget))
        gtk_widget_queue_draw(widget);
}

/**
 * gwy_adjust_bar_get_bar_sensitive:
 * @adjbar: An adjustment bar.
 *
 * Reports whether an adjustment bar is sensitive.
 *
 * See gwy_adjust_bar_set_bar_sensitive() for discussion.
 *
 * Returns: %TRUE if the widget's bar is sensitive.
 **/
gboolean
gwy_adjust_bar_get_bar_sensitive(GwyAdjustBar *adjbar)
{
    g_return_val_if_fail(GWY_IS_ADJUST_BAR(adjbar), FALSE);
    return adjbar->priv->bar_sensitive;
}

static gboolean
set_adjustment(GwyAdjustBar *adjbar,
               GtkAdjustment *adjustment)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    if (!gwy_set_member_object(adjbar, adjustment, GTK_TYPE_ADJUSTMENT, &priv->adjustment,
                               "changed", &adjustment_changed,
                               &priv->adjustment_changed_id, G_CONNECT_SWAPPED,
                               "value-changed", &adjustment_value_changed,
                               &priv->adjustment_value_changed_id, G_CONNECT_SWAPPED,
                               NULL))
        return FALSE;

    priv->oldvalue = adjustment ? gtk_adjustment_get_value(adjustment) : 0.0;
    update_mapping(adjbar);
    gtk_widget_queue_draw(GTK_WIDGET(adjbar));
    return TRUE;
}

static gboolean
set_snap_to_ticks(GwyAdjustBar *adjbar,
                  gboolean setting)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    if (!setting == !priv->snap_to_ticks)
        return FALSE;

    priv->snap_to_ticks = !!setting;
    if (setting && priv->adjustment) {
        gdouble value = gtk_adjustment_get_value(priv->adjustment);
        gdouble snapped = snap_value(adjbar, value);
        if (fabs(snapped - value) > 1e-12*fmax(fabs(snapped), fabs(value)))
            gtk_adjustment_set_value(priv->adjustment, snapped);
    }

    return TRUE;
}

static gboolean
set_mapping(GwyAdjustBar *adjbar,
            GwyScaleMappingType mapping)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    if (mapping == priv->mapping)
        return FALSE;

    if (mapping > GWY_SCALE_MAPPING_LOG1P) {
        g_warning("Wrong scale mapping %u.", mapping);
        return FALSE;
    }

    priv->mapping = mapping;
    update_mapping(adjbar);
    gtk_widget_queue_draw(GTK_WIDGET(adjbar));
    return TRUE;
}

static void
draw_bar(GwyAdjustBar *adjbar, cairo_t *cr)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    GtkWidget *widget = GTK_WIDGET(adjbar);
    GtkStateFlags state = gtk_widget_get_state_flags(widget);
    gdouble height = gtk_widget_get_allocated_height(widget);
    gdouble width = priv->length;
    gdouble val, barlength = 0.0;
    GwyRGBA base_color = { 0.3, 0.6, 1.0, 1.0 }, fcolor, bcolor;
    GwyRGBA bgcolor = { 1.0, 1.0, 1.0, 1.0 };

    cairo_save(cr);
    if ((state & GTK_STATE_FLAG_INSENSITIVE) || !priv->bar_sensitive) {
        base_color.a *= 0.4;
        bgcolor.a *= 0.4;
    }
    fcolor = bcolor = base_color;
    fcolor.a *= (state & GTK_STATE_FLAG_PRELIGHT) ? 0.5 : 0.333;
    bgcolor.a *= (state & GTK_STATE_FLAG_PRELIGHT) ? 0.5 : 0.333;

    if (priv->adjustment_ok) {
        val = gtk_adjustment_get_value(priv->adjustment);
        barlength = map_value_to_position(adjbar, val);
    }

    cairo_translate(cr, priv->x, 0);
    if (width > 2.0 && height > 2.0) {
        gwy_cairo_set_source_rgba(cr, &bgcolor);
        cairo_rectangle(cr, 1.0, 1.0, width-2.0, height-2.0);
        cairo_fill(cr);
    }

    if (barlength < 0.5)
        goto end;

    cairo_set_line_width(cr, 1.0);
    if (barlength > 2.0) {
        cairo_rectangle(cr, 0.0, 0.0, barlength, height);
        gwy_cairo_set_source_rgba(cr, &fcolor);
        cairo_fill(cr);

        cairo_rectangle(cr, 0.5, 0.5, barlength-1.0, height-1.0);
        gwy_cairo_set_source_rgba(cr, &bcolor);
        cairo_stroke(cr);
    }
    else {
        cairo_rectangle(cr, 0, 0, barlength, height);
        gwy_cairo_set_source_rgba(cr, &bcolor);
        cairo_fill(cr);
    }

end:
    cairo_restore(cr);
}

static void
adjustment_changed(GwyAdjustBar *adjbar,
                   G_GNUC_UNUSED GtkAdjustment *adjustment)
{
    update_mapping(adjbar);
    gtk_widget_queue_draw(GTK_WIDGET(adjbar));
}

static void
adjustment_value_changed(GwyAdjustBar *adjbar,
                         GtkAdjustment *adjustment)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    gdouble newvalue;

    if (!priv->adjustment_ok)
        return;

    newvalue = gtk_adjustment_get_value(adjustment);
    if (newvalue == priv->oldvalue)
        return;

    priv->oldvalue = newvalue;
    gtk_widget_queue_draw(GTK_WIDGET(adjbar));
}

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

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

#ifndef HAVE_LOG1P
static gdouble
log1p(gdouble x)
{
    return log(1.0 + x);
}
#endif

#ifndef HAVE_EXPM1
static gdouble
expm1(gdouble x)
{
    return exp(x) - 1.0;
}
#endif

static void
update_mapping(GwyAdjustBar *adjbar)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;

    priv->adjustment_ok = FALSE;
    if (!priv->adjustment)
        return;

    gdouble lower = gtk_adjustment_get_lower(priv->adjustment);
    gdouble upper = gtk_adjustment_get_upper(priv->adjustment);

    if (gwy_isinf(lower) || gwy_isnan(lower)
        || gwy_isinf(upper) || gwy_isnan(upper))
        return;

    if (priv->mapping == GWY_SCALE_MAPPING_LOG) {
        if (lower <= 0.0 || upper <= 0.0)
            return;
    }
    if (priv->mapping == GWY_SCALE_MAPPING_LOG1P) {
        if (lower < 0.0 || upper < 0.0)
            return;
    }

    priv->length = gtk_widget_get_allocated_width(GTK_WIDGET(adjbar)) - priv->x;
    if (priv->length < 2)
        return;

    if (priv->mapping == GWY_SCALE_MAPPING_LINEAR)
        priv->map_value = priv->map_position = map_both_linear;
    else if (priv->mapping == GWY_SCALE_MAPPING_SQRT) {
        priv->map_value = ssqrt;
        priv->map_position = ssqr;
    }
    else if (priv->mapping == GWY_SCALE_MAPPING_LOG) {
        priv->map_value = log;
        priv->map_position = exp;
    }
    else if (priv->mapping == GWY_SCALE_MAPPING_LOG1P) {
        priv->map_value = log1p;
        priv->map_position = expm1;
    }
    priv->b = priv->map_value(lower);
    priv->a = (priv->map_value(upper) - priv->b)/priv->length;
    if (gwy_isinf(priv->a) || gwy_isnan(priv->a) || !priv->a
        || gwy_isinf(priv->b) || gwy_isnan(priv->b))
        return;

    priv->adjustment_ok = TRUE;
}

static gdouble
map_value_to_position(GwyAdjustBar *adjbar, gdouble value)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;

    return (priv->map_value(value) - priv->b)/priv->a;
}

static gdouble
map_position_to_value(GwyAdjustBar *adjbar, gdouble position)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;

    return priv->map_position(priv->a*position + priv->b);
}

static gdouble
map_both_linear(gdouble value)
{
    return value;
}

static void
update_value(GtkWidget *widget, gdouble newposition)
{
    GwyAdjustBar *adjbar = GWY_ADJUST_BAR(widget);
    GwyAdjustBarPrivate *priv = adjbar->priv;
    gdouble value, newvalue;

    if (!priv->adjustment_ok)
        return;

    value = gtk_adjustment_get_value(priv->adjustment);
    newposition = CLAMP(newposition, 0.0, priv->length);
    newvalue = map_position_to_value(adjbar, newposition);
    if (newvalue != value)
        g_signal_emit(adjbar, signals[SGNL_CHANGE_VALUE], 0, newvalue);
}

static gdouble
snap_value(GwyAdjustBar *adjbar, gdouble value)
{
    GwyAdjustBarPrivate *priv = adjbar->priv;
    gdouble step, lower, upper, m;

    if (!priv->adjustment || !priv->snap_to_ticks)
        return value;

    step = gtk_adjustment_get_step_increment(priv->adjustment);
    if (!step)
        return value;

    lower = gtk_adjustment_get_lower(priv->adjustment);
    upper = gtk_adjustment_get_upper(priv->adjustment);
    m = 0.5*fmin(step, upper - lower);
    if (value >= upper - m)
        return upper;

    value = GWY_ROUND((value - lower)/step)*step + lower;
    if (value > upper)
        value -= step;
    if (value < lower)
        value = lower;

    return value;
}

/**
 * SECTION: adjust-bar
 * @title: GwyAdjustBar
 * @short_description: Compact adjustment visualisation and modification
 *
 * #GwyAdjustBar is a compact widget for visualisation and modification of the value of an #GtkAdjustment.  It can
 * contain a label with an overlaid bar that can be clicked, dragged or modified by the scroll-wheel by the user.
 * Since the widget does not take keyboard focus and does not display a numerical value, it should be usually paired
 * with a #GtkSpinButton sharing the same adjustment. This spin button would also be the typical mnemonic widget for
 * the adjustment bar.
 *
 * #GwyAdjustBar supports several different types of mapping between screen positions and values of the underlying
 * adjustment.  Nevertheless, the default mapping (signed square root, %GWY_SCALE_MAPPING_SQRT) should fit most
 * situations.
 **/

/**
 * GwyAdjustBar:
 *
 * Adjustment bar widget visualising an adjustment.
 *
 * The #GwyAdjustBar struct contains private data only and should be accessed
 * using the functions below.
 **/

/**
 * GwyAdjustBarClass:
 *
 * Class of adjustment bars visualising adjustments.
 **/

/**
 * GwyScaleMappingType:
 * @GWY_SCALE_MAPPING_LINEAR: Linear mapping between values and screen positions.  This recommended for signed
 *                            additive quantities of a limited range.
 * @GWY_SCALE_MAPPING_SQRT: Screen positions correspond to ‘signed square roots’ of the value.  This is the
 *                          recommended general-purpose default mapping type as it works with both signed and usigned
 *                          quantities and offers good sensitivity for both large and small values.
 * @GWY_SCALE_MAPPING_LOG: Screen positions correspond to logarithms of values. The adjustment range must contain only
 *                         positive values.  For quantities of extreme ranges this mapping may be preferred to
 *                         %GWY_SCALE_MAPPING_SQRT.
 * @GWY_SCALE_MAPPING_LOG1P: Screen positions correspond to logarithms of values with 1 added fist. The adjustment
 *                           range must contain only non-negative values.  This mapping may be preferred for integer
 *                           quantities with extreme ranges which also include zero. (Since 2.62)
 *
 * Type of adjustment bar mapping functions.
 **/

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