/*
 *  $Id: vector-layer.c 30130 2026-06-25 07:46:27Z 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-object.h>
#include <gdk/gdk.h>
#include <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyui/vector-layer.h"
#include "libgwyui/data-view.h"

#define connect_swapped_after(obj, signal, cb, data) \
    g_signal_connect_object(obj, signal, G_CALLBACK(cb), data, G_CONNECT_SWAPPED | G_CONNECT_AFTER)

enum {
    SGNL_OBJECT_CHOSEN,
    NUM_SIGNALS
};

enum {
    PROP_0,
    PROP_SELECTION,
    PROP_EDITABLE,
    PROP_FOCUS,
    NUM_PROPERTIES,
};

typedef void (*InvalidateObjectFunc)(GwyVectorLayer *layer,
                                     cairo_region_t *region,
                                     const gdouble *xy);

/* TODO GTK3 we probably want to offer some more convenient PangoContext wrapping, but it should be based on
 * gtk_widget_get_pango_context() which only becomes invalid on "screen-changed" signal or when the widget is removed
 * from its parent. */

struct _GwyVectorLayerPrivate {
    GwySelection *selection;
    gulong selection_changed_id;
    gulong selection_notify_id;

    gulong enter_id;
    gulong leave_id;
    gulong motion_id;
    gulong key_pressed_id;
    gulong key_released_id;
    gulong button_pressed_id;
    gulong button_released_id;
    gulong hierarchy_changed_id;
    gulong screen_changed_id;

    PangoLayout *layout;

    gint button;

    gboolean editable;
    gint focus;

    gint selected;
};

struct _GwyVectorLayerClassPrivate {
    gint dummy;
};

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 gboolean set_focus_default(GwyVectorLayer *layer,
                                  gint i);
static gboolean set_selection    (GwyVectorLayer *layer,
                                  GwySelection *selection);
static void     plugged          (GwyDataViewLayer *layer);
static void     unplugged        (GwyDataViewLayer *layer);
static gboolean layer_entered    (GwyVectorLayer *layer,
                                  GdkEventCrossing *event);
static gboolean layer_left       (GwyVectorLayer *layer,
                                  GdkEventCrossing *event);
static gboolean button_pressed   (GwyVectorLayer *layer,
                                  GdkEventButton *event);
static gboolean button_released  (GwyVectorLayer *layer,
                                  GdkEventButton *event);
static gboolean pointer_moved    (GwyVectorLayer *layer,
                                  GdkEventMotion *event);
static gboolean key_pressed      (GwyVectorLayer *layer,
                                  GdkEventKey *event);
static gboolean key_released     (GwyVectorLayer *layer,
                                  GdkEventKey *event);
static void     screen_changed   (GwyVectorLayer *layer,
                                  GdkScreen *previous_screen);
static void     hierarchy_changed(GwyVectorLayer *layer,
                                  GtkWidget *previous_toplevel);
static void     selection_changed(GwySelection *selection,
                                  gint hint,
                                  GwyVectorLayer *layer);
static void     selection_notify (GwySelection *selection,
                                  const GParamSpec *pspec,
                                  GwyVectorLayer *layer);

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

G_DEFINE_ABSTRACT_TYPE_WITH_CODE(GwyVectorLayer, gwy_vector_layer, GWY_TYPE_DATA_VIEW_LAYER,
                                 G_ADD_PRIVATE(GwyVectorLayer))

static void
gwy_vector_layer_class_init(GwyVectorLayerClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyDataViewLayerClass *layer_class = GWY_DATA_VIEW_LAYER_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_vector_layer_parent_class;

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

    layer_class->plugged = plugged;
    layer_class->unplugged = unplugged;

    klass->set_focus = set_focus_default;

    /**
     * GwyVectorLayer::object-chosen:
     * @gwyvectorlayer: The #GwyVectorLayer which received the signal.
     * @arg1: The number (index) of the chosen selection object.
     *
     * The ::object-chosen signal is emitted when user starts interacting with a selection object, even before
     * modifying it.  It is emitted even if the layer is not editable, but it is not emitted when focus is set and the
     * user attempts to choose a different object.
     **/
    signals[SGNL_OBJECT_CHOSEN] = g_signal_new("object-chosen", type,
                                               G_SIGNAL_RUN_FIRST,
                                               G_STRUCT_OFFSET(GwyVectorLayerClass, object_chosen),
                                               NULL, NULL,
                                               g_cclosure_marshal_VOID__INT,
                                               G_TYPE_NONE, 1, G_TYPE_INT);
    g_signal_set_va_marshaller(signals[SGNL_OBJECT_CHOSEN], type, g_cclosure_marshal_VOID__INTv);

    /* XXX: This is a lie. We have an abtract API working with selections – and the selection is held in the base
     * class. However, subclasses require selections of specific types. Subclasses can override the implementation of
     * properties. But it only means they do the getting and setting (instead of the base class); they cannot
     * specialise the type AFAICT. */
    properties[PROP_SELECTION] = g_param_spec_object("selection", NULL,
                                                     "Selection object representing the coordinates",
                                                     GWY_TYPE_SELECTION, GWY_GPARAM_RWE);
    properties[PROP_EDITABLE] = g_param_spec_boolean("editable", NULL,
                                                     "Whether selection objects are editable",
                                                     TRUE, GWY_GPARAM_RWE);
    properties[PROP_FOCUS] = g_param_spec_int("focus", NULL,
                                              "The id of focused object, or -1 if all objects are active",
                                              -1, G_MAXINT, -1, GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_vector_layer_init(GwyVectorLayer *layer)
{
    GwyVectorLayerPrivate *priv;

    layer->priv = priv = gwy_vector_layer_get_instance_private(layer);
    priv->editable = TRUE;
    priv->selected = -1;
    priv->focus = -1;
}

static void
dispose(GObject *object)
{
    GwyVectorLayer *layer = GWY_VECTOR_LAYER(object);
    set_selection(layer, NULL);
    g_clear_object(&layer->priv->layout);
    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyVectorLayer *vector_layer = GWY_VECTOR_LAYER(object);

    switch (prop_id) {
        case PROP_SELECTION:
        gwy_vector_layer_set_selection(vector_layer, g_value_get_object(value));
        break;

        case PROP_EDITABLE:
        gwy_vector_layer_set_editable(vector_layer, g_value_get_boolean(value));
        break;

        case PROP_FOCUS:
        gwy_vector_layer_set_focus(vector_layer, g_value_get_int(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)
{
    GwyVectorLayerPrivate *priv = GWY_VECTOR_LAYER(object)->priv;

    switch (prop_id) {
        case PROP_SELECTION:
        g_value_set_object(value, priv->selection);
        break;

        case PROP_EDITABLE:
        g_value_set_boolean(value, priv->editable);
        break;

        case PROP_FOCUS:
        g_value_set_int(value, priv->focus);
        break;

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

/* Most layers want this unchanged.  They should reset the method to NULL if they don't implement focus though */
static gboolean
set_focus_default(GwyVectorLayer *layer,
                  gint i)
{
    GwyVectorLayerPrivate *priv = layer->priv;

    /* Unfocus is always possible */
    if (i < 0) {
        if (priv->focus >= 0) {
            priv->focus = -1;
            g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_FOCUS]);
        }
        return TRUE;
    }
    /* Setting focus is possible only when user is selecting nothing or the same object */
    if (priv->selected < 0 || i == priv->selected) {
        if (i != priv->focus) {
            priv->focus = i;
            g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_FOCUS]);
        }
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_vector_layer_class_get_selection_type:
 * @klass: A vector layer class.
 *
 * Returns the selection type of a vector layer class.
 *
 * Note this is a class method, to get the type from an instance one can use gwy_vector_layer_get_selection_type().
 * To get the type from layer's #GType one has to use g_type_class_peek().
 *
 * Returns: The type of selection used by layers of class @klass.
 **/
GType
gwy_vector_layer_class_get_selection_type(GwyVectorLayerClass *klass)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER_CLASS(klass), 0);
    return klass->selection_type;
}

/**
 * gwy_vector_layer_set_focus:
 * @layer: A vector image view layer.
 * @focus: Index of object to focus on, use -1 to unfocus (allow interaction with any object).
 *
 * Focues on one selection object.
 *
 * When a selection object is focused, it becomes the only one user can interact with.  More precisely,
 * "object-chosen" signal is emitted only for this object, and if the layer is editable only this object can be
 * modified by the user.
 *
 * Returns: %TRUE if the object was focused, %FALSE on failure.  Failure can be caused by user currently moving
 *          another object, wrong object index, or the feature being unimplemented in @layer.
 **/
gboolean
gwy_vector_layer_set_focus(GwyVectorLayer *layer,
                           gint focus)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), FALSE);

    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    if (layer_class->set_focus)
        return layer_class->set_focus(layer, focus);
    return FALSE;
}

/**
 * gwy_vector_layer_get_focus:
 * @layer: A vector image view layer.
 *
 * Gets focused object index.
 *
 * Returns: Focued object index, or -1 if no object is focused.
 **/
gint
gwy_vector_layer_get_focus(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), -1);
    return layer->priv->focus;
}

/**
 * gwy_vector_layer_queue_draw_parent:
 * @layer: A vector image view layer.
 *
 * Queue redrawing of vector layer's parent image view.
 *
 * A conveninece function calling gtk_widget_queue_draw() on @layer's parent widget, provided it exists and is
 * drawable. If there is nothing to redraw the function does nothing.
 *
 * The return value can be used to decide whether to proceed with other actions which only make sense with a drawable
 * parent widget.
 *
 * Returns: Whether redrawing was queued.
 **/
gboolean
gwy_vector_layer_queue_draw_parent(GwyVectorLayer *layer)
{
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (parent && gtk_widget_is_drawable(parent)) {
        gtk_widget_queue_draw(parent);
        return TRUE;
    }
    return FALSE;
}

/**
 * gwy_vector_layer_draw:
 * @layer: A vector image view layer.
 * @cr: Cairo context for drawing.
 *
 * Draws @layer to a given Cairo context.
 **/
void
gwy_vector_layer_draw(GwyVectorLayer *layer,
                      cairo_t *cr)
{
    g_return_if_fail(GWY_IS_VECTOR_LAYER(layer));
    if (!layer->priv->selection)
        return;

    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    g_return_if_fail(layer_class->draw);

    layer_class->draw(layer, cr);
}

/**
 * gwy_vector_layer_ensure_layout:
 * @layer: A vector image view layer.
 *
 * Obtains a suitable Pango context for drawing text by a vector layer.
 *
 * This is a helper function for layer implementation. The layout is generally kept around, but in any case it is
 * created if it does not exist yet and the layer has a parent widget. It may be destroyed upon screen change, so
 * layers should not keep it around long-term themselves.
 *
 * Returns: (transfer none): A Pango context, or %NULL if the layer has no parent widget.
 **/
PangoLayout*
gwy_vector_layer_ensure_layout(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), NULL);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->layout) {
        GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
        if (parent)
            priv->layout = gtk_widget_create_pango_layout(parent, "");
    }
    return priv->layout;
}

static gboolean
layer_entered(GwyVectorLayer *layer, GdkEventCrossing *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    gwy_debug("x=%g, y=%g", event->x, event->y);
    layer_class->enter(layer, event->x, event->y);
    return FALSE;
}

static gboolean
layer_left(GwyVectorLayer *layer, G_GNUC_UNUSED GdkEventCrossing *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    gwy_debug("x=%g, y=%g", event->x, event->y);
    layer_class->leave(layer);
    return FALSE;
}

static gboolean
button_pressed(GwyVectorLayer *layer, GdkEventButton *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    /* Layers should only use the first button and we do not want to deal with nonsense such as left button being
     * doubleclicked while holding down the middle button anyway. Also ignore multipress events; we still get the
     * single press for each of them. */
    if (event->button != 1 || layer->priv->button || event->type != GDK_BUTTON_PRESS)
        return FALSE;

    layer->priv->button = event->button;
    layer_class->button_press(layer, event->x, event->y);
    return FALSE;
}

static gboolean
button_released(GwyVectorLayer *layer, GdkEventButton *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    if (event->button != 1 || !layer->priv->button)
        return FALSE;

    layer_class->button_release(layer, event->x, event->y);
    /* Do it after! The layer still wants to look at the button. */
    layer->priv->button = 0;
    return FALSE;
}

static gboolean
pointer_moved(GwyVectorLayer *layer, GdkEventMotion *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);

    gwy_debug("x=%g, y=%g", event->x, event->y);
    layer_class->motion(layer, event->x, event->y);
    gdk_event_request_motions(event);
    return FALSE;
}

static gboolean
key_pressed(GwyVectorLayer *layer, GdkEventKey *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    gwy_debug("x=%g, y=%g", event->x, event->y);
    layer_class->key_press(layer, event->keyval);
    return FALSE;
}

static gboolean
key_released(GwyVectorLayer *layer, GdkEventKey *event)
{
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwyVectorLayerPrivate *priv = layer->priv;
    if (!priv->selection)
        return FALSE;

    gwy_debug("x=%g, y=%g", event->x, event->y);
    layer_class->key_release(layer, event->keyval);
    return FALSE;
}

static void
screen_changed(GwyVectorLayer *layer,
               G_GNUC_UNUSED GdkScreen *previous_screen)
{
    g_clear_object(&layer->priv->layout);
}

static void
hierarchy_changed(GwyVectorLayer *layer,
                  G_GNUC_UNUSED GtkWidget *previous_toplevel)
{
    g_clear_object(&layer->priv->layout);
}

/**
 * gwy_vector_layer_ensure_selection:
 * @layer: A vector layer.
 *
 * Ensures a vector layer's selection exists.
 *
 * If the layer has a selection set the function is identical to gwy_vector_layer_get_selection().  Otherwise a
 * selection of appropriate type is created with default properties and returned.
 *
 * Returns: (transfer none):
 *          The layer's selection.
 **/
GwySelection*
gwy_vector_layer_ensure_selection(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), NULL);

    GwyVectorLayerPrivate *priv = layer->priv;
    if (priv->selection)
        return priv->selection;

    GwyVectorLayerClass *klass = GWY_VECTOR_LAYER_GET_CLASS(layer);
    GwySelection *selection = g_object_new(klass->selection_type, NULL);
    gwy_vector_layer_set_selection(layer, selection);
    /* set_selection() adds a new reference. */
    g_object_unref(selection);
    g_assert(priv->selection);

    return priv->selection;
}

/**
 * gwy_vector_layer_get_selection:
 * @layer: A vector layer.
 *
 * Returns vector layer's selection object.
 *
 * Returns: (nullable):
 *          The layer's selection (no reference is added), possibly %NULL if none exists yet.
 **/
GwySelection*
gwy_vector_layer_get_selection(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), NULL);
    return layer->priv->selection;
}

/**
 * gwy_vector_layer_set_selection:
 * @layer: A vector layer.
 * @selection: (nullable):
 *             Selection to use, possibly %NULL.
 *
 * Sets the vector layer's selection object.
 *
 * Vector layer subclasses use specific selection types, It is an error to set the wrong selection. See
 * gwy_vector_layer_class_get_selection_type() and gwy_vector_layer_get_selection_type() for obtaining the type
 * generically. In most cases, however, you work with a concrete layer type and thus know the selection type.
 **/
void
gwy_vector_layer_set_selection(GwyVectorLayer *layer,
                               GwySelection *selection)
{
    g_return_if_fail(GWY_IS_VECTOR_LAYER(layer));
    if (!set_selection(layer, selection))
        return;

    if (gwy_vector_layer_queue_draw_parent(layer)) {
        GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
        if (layer->priv->editable && layer_class->cancel_editing)
            layer_class->cancel_editing(layer);
    }
    gwy_vector_layer_set_current_object(layer, -1);
}

static gboolean
set_selection(GwyVectorLayer *layer, GwySelection *selection)
{
    GwyVectorLayerPrivate *priv = layer->priv;
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    return gwy_set_member_object(layer, selection, layer_class->selection_type, &priv->selection,
                                 "changed", selection_changed, &priv->selection_changed_id, 0,
                                 "notify", selection_notify, &priv->selection_notify_id, 0,
                                 NULL);
}

static void
plugged(GwyDataViewLayer *layer)
{
    if (parent_class->plugged)
        parent_class->plugged(layer);

    /* Connect to widget events. */
    GwyVectorLayer *vector_layer = GWY_VECTOR_LAYER(layer);
    GtkWidget *parent = gwy_data_view_layer_get_parent(layer);
    g_return_if_fail(GTK_IS_WIDGET(parent));

    GwyVectorLayerPrivate *priv = vector_layer->priv;
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    /* Connect to the events the particular layer class actually wants to handle. */
    if (layer_class->enter) {
        priv->enter_id = g_signal_connect_swapped(parent, "enter-notify-event",
                                                  G_CALLBACK(layer_entered), vector_layer);
    }
    if (layer_class->leave) {
        priv->leave_id = g_signal_connect_swapped(parent, "leave-notify-event",
                                                  G_CALLBACK(layer_left), vector_layer);
    }
    if (layer_class->button_press) {
        priv->button_pressed_id = g_signal_connect_swapped(parent, "button-press-event",
                                                           G_CALLBACK(button_pressed), vector_layer);
    }
    if (layer_class->button_release) {
        priv->button_released_id = g_signal_connect_swapped(parent, "button-release-event",
                                                            G_CALLBACK(button_released), vector_layer);
    }
    if (layer_class->key_press) {
        priv->key_pressed_id = g_signal_connect_swapped(parent, "key-press-event",
                                                        G_CALLBACK(key_pressed), vector_layer);
    }
    if (layer_class->key_release) {
        priv->key_released_id = g_signal_connect_swapped(parent, "key-release-event",
                                                         G_CALLBACK(key_released), vector_layer);
    }
    /* FIXME: This is too simplistic. If someone zooms (scrolls mouse button) while making a selection, the cursor
     * might not move but we need to behave as if it did because the coordinates will change. Cache the last pointer
     * coordinates we get and synthetise a motion? */
    if (layer_class->motion) {
        priv->motion_id = g_signal_connect_swapped(parent, "motion-notify-event",
                                                   G_CALLBACK(pointer_moved), vector_layer);
    }
    priv->screen_changed_id = g_signal_connect_swapped(parent, "screen-changed",
                                                       G_CALLBACK(screen_changed), vector_layer);
    priv->hierarchy_changed_id = g_signal_connect_swapped(parent, "screen-changed",
                                                          G_CALLBACK(hierarchy_changed), vector_layer);
}

static void
unplugged(GwyDataViewLayer *layer)
{
    GwyVectorLayer *vector_layer = GWY_VECTOR_LAYER(layer);
    GwyVectorLayerPrivate *priv = vector_layer->priv;

    GtkWidget *parent = gwy_data_view_layer_get_parent(layer);
    g_assert(parent);
    g_clear_signal_handler(&priv->enter_id, parent);
    g_clear_signal_handler(&priv->leave_id, parent);
    g_clear_signal_handler(&priv->button_pressed_id, parent);
    g_clear_signal_handler(&priv->button_released_id, parent);
    g_clear_signal_handler(&priv->key_pressed_id, parent);
    g_clear_signal_handler(&priv->key_released_id, parent);
    g_clear_signal_handler(&priv->motion_id, parent);
    g_clear_signal_handler(&priv->screen_changed_id, parent);
    g_clear_signal_handler(&priv->hierarchy_changed_id, parent);

    if (parent_class->unplugged)
        parent_class->unplugged(layer);
}

static void
selection_changed(GwySelection *selection, gint hint, GwyVectorLayer *layer)
{
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (!parent || !gtk_widget_is_drawable(parent))
        return;

    /* We used to change layer->selected here, but I think it was misguided. If someone wants to choose a specific
     * object, they should do it explicitly, not by fiddling with selection data. */

    InvalidateObjectFunc invalidate_object = GWY_VECTOR_LAYER_GET_CLASS(layer)->invalidate_object;
    /* If the layer does not support single-object redrawing just redraw the entire thing. */
    if (!invalidate_object) {
        gtk_widget_queue_draw(parent);
        return;
    }

    guint object_size = gwy_selection_get_object_size(selection);
    gdouble xy[object_size];
    gboolean have_previous = gwy_selection_get_changed_object(selection, xy);
    /* If the change is not single-object just redraw the entire thing. */
    if (hint < 0 && !have_previous) {
        gtk_widget_queue_draw(parent);
        return;
    }

    cairo_region_t *redraw_region = cairo_region_create();
    if (have_previous)
        invalidate_object(layer, redraw_region, xy);
    if (hint >= 0) {
        gwy_selection_get_object(selection, hint, xy);
        invalidate_object(layer, redraw_region, xy);
    }

    /* NB: GTK+ queue draw coordinates differ from drawing coodinates by the widget origin (see
     * gtk_widget_queue_draw_area() documentation). */
    GdkRectangle allocation;
    gtk_widget_get_allocation(parent, &allocation);
    cairo_region_translate(redraw_region, allocation.x, allocation.y);

    gtk_widget_queue_draw_region(parent, redraw_region);
    cairo_region_destroy(redraw_region);
}

static void
selection_notify(G_GNUC_UNUSED GwySelection *selection,
                 const GParamSpec *pspec,
                 GwyVectorLayer *layer)
{
    /* FIXME GTK3: Subclasses may need to respond to this. When path closeness or slackness is changed the layer needs
     * to be redrawn entirely and when axis orientation changes the entire interpretation changes (we may need to
     * cancel editing in such case). */
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
    GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
    if (layer_class->selection_notify)
        layer_class->selection_notify(layer, pspec);
}

/**
 * gwy_vector_layer_get_editable:
 * @layer: A vector image view layer.
 *
 * Gets editability of a vector layer.
 *
 * Returns: %TRUE if layer is edtiable, %FALSE if it is not editable.
 **/
gboolean
gwy_vector_layer_get_editable(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), FALSE);
    return layer->priv->editable;
}

/**
 * gwy_vector_layer_set_editable:
 * @layer: A vector image view layer.
 * @editable: %TRUE to set layer editable, %FALSE to set it noneditable.
 *
 * Sets a vector layer editability.
 *
 * It is an error to attempt to set a layer non-editabile while it is being edited.
 *
 * When a layer is set noneditable, the user cannot change the selection. However, "object-chosen" signal is still
 * emitted.
 **/
void
gwy_vector_layer_set_editable(GwyVectorLayer *layer,
                              gboolean editable)
{
    g_return_if_fail(GWY_IS_VECTOR_LAYER(layer));

    GwyVectorLayerPrivate *priv = layer->priv;
    if (!editable == !priv->editable)
        return;

    if (priv->selected >= 0) {
        GwyVectorLayerClass *layer_class = GWY_VECTOR_LAYER_GET_CLASS(layer);
        if (layer_class->cancel_editing)
            layer_class->cancel_editing(layer);
        gwy_vector_layer_set_current_object(layer, -1);
    }

    priv->editable = !!editable;
    /* Reset cursor for the case it's currently non-default.
     * GTK3 FIXME: How to properly handle the reverse? Although it is less serious as it gets fixed automatically once
     * user moves the cursor -- unlike this case. We need to get the layer implementation to update its cursor.
     *
     * This is bizarre. There should be an API instead of doing something with dv_layer->parent! */
    if (!priv->editable) {
        GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
        if (parent)
            gwy_data_view_set_named_cursor(GWY_DATA_VIEW(parent), NULL);
    }

    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_EDITABLE]);
}

/**
 * gwy_vector_layer_get_modifiers:
 * @layer: A vector image view layer.
 *
 * Gets the current modifier state of a vector layer.
 *
 * This is a helper function for implementations of simple layers which then do not need to query the state
 * themselves.
 *
 * Returns: The current modifier state.
 **/
GdkModifierType
gwy_vector_layer_get_modifiers(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), 0);
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_val_if_fail(parent, 0);
    GdkKeymap *keymap = gdk_keymap_get_for_display(gtk_widget_get_display(parent));
    return gdk_keymap_get_modifier_state(keymap);
}

/**
 * gwy_vector_layer_get_current_button:
 * @layer: A vector image view layer.
 *
 * Gets the current pointer button interacting with a vector layer.
 *
 * This is a helper function for implementations of layers.
 *
 * Returns: The current button (or 0 for none).
 **/
gint
gwy_vector_layer_get_current_button(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), 0);
    return layer->priv->button;
}

/**
 * gwy_vector_layer_get_current_object:
 * @layer: A vector image view layer.
 *
 * Gets the index of the currently edited selection object.
 *
 * Returns: Index of the selection object, if any. Otherwise -1.
 **/
gint
gwy_vector_layer_get_current_object(GwyVectorLayer *layer)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), -1);
    return layer->priv->selected;
}

/**
 * gwy_vector_layer_set_current_object:
 * @layer: A vector image view layer.
 * @i: Index of the new current object (or -1 for none).
 *
 * Sets the index of the currently selected selection object.
 *
 * It also emits the "object-chosen" signal. Usually, the selected object is the one being edited. However, objects
 * can also be selected then the selection is not editable.
 **/
void
gwy_vector_layer_set_current_object(GwyVectorLayer *layer,
                                    gint i)
{
    g_return_if_fail(GWY_IS_VECTOR_LAYER(layer));
    GwyVectorLayerPrivate *priv = layer->priv;
    if (i == priv->selected)
        return;
    priv->selected = i;
    g_signal_emit(layer, signals[SGNL_OBJECT_CHOSEN], 0, i);
}

/**
 * gwy_vector_layer_update_object:
 * @layer: A vector image view layer.
 * @i: Index of the object to update (or -1).
 * @xy: New coordinates, according to the layer's selection.
 *
 * Updates or adds one selection object of a vector layer.
 *
 * This is mainly a helper function for layer implementation. Index @i may be negative for appending, as usual.
 * Appending can also be done by passing index equal to the current number of objects. If the maximum number of
 * objects is 1, passing negative @i is the same as passing 0 (replacing the single object, if it exists). Do not pass
 * negative @i if such behaviour is not wanted.
 *
 * Returns: Index of modified object, usually equal to @i. Negative value is returned if no object was modified.
 **/
gint
gwy_vector_layer_update_object(GwyVectorLayer *layer,
                               gint i,
                               const gdouble *xy)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    g_return_val_if_fail(GWY_IS_SELECTION(selection), -1);
    gint maxobj = gwy_selection_get_max_objects(selection);

    if (i < 0) {
        if (gwy_selection_is_full(selection)) {
            if (maxobj > 1)
                return -1;
            i = 0;
        }
    }

    return gwy_selection_set_object(selection, i, xy);
}

/**
 * gwy_vector_layer_find_nearest:
 * @layer: A vector image view layer.
 * @search: Function to which calculates the distance to the closest feature.
 * @subdivision: How many features each selection object is considered to consist of.
 * @x: X-coodinate of the point to search.
 * @y: Y-coodinate of the point to search.
 * @mindistance: Location where to store the minimum distance, or %NULL.
 *
 * Finds the closest vector layer feature using a search function.
 *
 * What type of feature is sought depends on the function @calc_distance and there can be multiple features on each
 * selection object. The function is usually something like gwy_math_find_nearest_line() and works with squared
 * distances. However, it can compute any type of distance as needed.
 *
 * The search function must accept the coordinate array exactly in the same form as they are in stored the selection
 * data. But, for instance, the selection data of #GwySelectionLine are compatible with both
 * gwy_math_find_nearest_line() and gwy_math_find_nearest_point().
 *
 * Focus is taken into account. Meaning, if focus is set only the single focused selection object is considered.
 * Otherwise all objects are considered.
 *
 * Returns: Index of the closest feature, or -1 if not found.
 **/
gint
gwy_vector_layer_find_nearest(GwyVectorLayer *layer,
                              GwyVectorLayerSearchFunc search,
                              gint subdivision,
                              gdouble x,
                              gdouble y,
                              gdouble *mindistance)
{
    g_return_val_if_fail(GWY_IS_VECTOR_LAYER(layer), -1);
    g_return_val_if_fail(search, -1);
    g_return_val_if_fail(subdivision > 0, -1);

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    gint i, n = gwy_selection_get_n_objects(selection);
    gint focus = gwy_vector_layer_get_focus(layer);
    if (mindistance)
        *mindistance = G_MAXDOUBLE;

    if (!n || focus >= n)
        return -1;

    gdouble metric[4], *pmetric = metric;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (parent)
        gwy_data_view_get_metric(GWY_DATA_VIEW(parent), metric);
    else
        pmetric = NULL;

    if (focus >= 0) {
        guint object_size = gwy_selection_get_object_size(selection);
        gdouble xy[object_size];
        gwy_selection_get_object(selection, focus, xy);
        i = search(x, y, mindistance, xy, subdivision, pmetric);
        if (i >= 0)
            i += subdivision*focus;
    }
    else {
        const gdouble *coords = gwy_selection_get_data_array(selection);
        i = search(x, y, mindistance, coords, n*subdivision, pmetric);
    }
    return i;
}

/**
 * SECTION: vector-layer
 * @title: GwyVectorLayer
 * @short_description: Base class for GwyDataView vector (interactive) layers
 *
 * #GwyVectorLayer is a base class for #GwyDataViewLayer<!-- -->s displaying selections and handling user input.  It
 * is a #GwyDataView component and it is not normally usable outside of it.
 *
 * #GwyVectorLayer provides two independent means to restrict user interaction with displayed selection: editability
 * and focus.  When a layer is set noneditable (see gwy_vector_layer_set_editable()), the user cannot modify displayed
 * objects, however "object-chosen" is emitted normally.  Focus (see gwy_vector_layer_set_focus()) on the other hand
 * limits all possible interactions to a single selection object.  It is possible although not very useful to combine
 * both restrictions.
 *
 * The other methods are rarely useful outside #GwyDataView and/or layer implementation.
 **/

/**
 * gwy_vector_layer_get_selection_type:
 * @layer: A vector layer.
 *
 * Gets the selection type of a vector layer.
 *
 * This is a convenience wrapper around gwy_vector_layer_class_get_selection_type().
 **/

/**
 * GwyVectorLayerClass:
 * @button_press: Invoked on pressing the first mouse button if a selection exists.
 * @button_release: Invoked on releasing the first mouse button if a selection exists.
 * @key_press: Invoked on pressing a key if a selection exists.
 * @key_release: Invoked on releasing a key if a selection exists.
 * @motion: Invoked on pointer motion if a selection exists.
 * @enter: Invoked on pointer entering the layer if a selection exists.
 * @leave: Invoked on pointer leaving the layer if a selection exists.
 * @cancel_editing: Invoked after a subtantial change such as replacing the selection object entirely. The selection
 *                  may be %NULL. The current object is unset by the base class itself.
 * @draw: Draw the selection on the parent widget, invoked if selection and parent exist. FIXME: But in future drawing
 *        on other Cairo surfaces may be added.
 *
 * Class of vector layers drawn on image views.
 *
 * Specific vector layers need to implement the methods to provide useful functionality.
 **/

/**
 * GwyVectorLayerSearchFunc:
 * @x: X-coodinate of the point to search.
 * @y: Y-coodinate of the point to search.
 * @mindistance: Location holding the current minimum distance, to be updated.
 * @coords: Coordinates defining the selection geometrical shapes, according to the selection type.
 * @n: The number of features in the @coords array.
 * @metric: A 2×2 metric matrix, possibly %NULL to use the standard Euclidean metric.
 *
 * Type of coodinate searching function.
 *
 * This is the type of function passed to gwy_vector_layer_find_nearest().
 *
 * The number of features is often the same as the number of object in the #GwySelection. It does not have to be,
 * however. For example, lines are defined by four coordinates representing the two endpoints. We can have a function
 * looking for the closest line, but we can also have a function looking for the closest endpoint – and for such
 * function the number of features doubles.
 *
 * The returned value pintpoints a specific feature. Continuing with the lines example, when looking for the closest
 * endpoint, the function must return the index of a specific endpoint (not just the index of the line). In some
 * cases, there may not be a closest feature even if the selection is nonempty because coordinates are considered
 * close only if they are in a specific position with respect to the geometrical shape.
 *
 * Returns: The index of the closest feature, or -1 if none can be considered close.
 **/

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