/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimptoolline.c
 * Copyright (C) 2017 Michael Natterer <mitch@gimp.org>
 *
 * Major improvements for interactivity
 * Copyright (C) 2014 Michael Henning <drawoc@darkrefraction.com>
 *
 * 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 3 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, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <gegl.h>
#include <gtk/gtk.h>
#include <gdk/gdkkeysyms.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"

#include "display-types.h"

#include "core/gimp-utils.h"
#include "core/gimpmarshal.h"

#include "widgets/gimpwidgets-utils.h"

#include "gimpcanvasgroup.h"
#include "gimpcanvashandle.h"
#include "gimpcanvasline.h"
#include "gimpdisplayshell.h"
#include "gimpdisplayshell-cursor.h"
#include "gimpdisplayshell-utils.h"
#include "gimptoolline.h"

#include "gimp-intl.h"


#define SHOW_LINE            TRUE
#define GRAB_LINE_MASK       GDK_MOD1_MASK
#define ENDPOINT_HANDLE_TYPE GIMP_HANDLE_CROSS
#define ENDPOINT_HANDLE_SIZE GIMP_CANVAS_HANDLE_SIZE_CROSS
#define SLIDER_HANDLE_TYPE   GIMP_HANDLE_FILLED_DIAMOND
#define SLIDER_HANDLE_SIZE   (ENDPOINT_HANDLE_SIZE * 2 / 3)
#define HANDLE_CIRCLE_SCALE  1.8
#define LINE_VICINITY        ((gint) (SLIDER_HANDLE_SIZE * HANDLE_CIRCLE_SCALE) / 2)
#define SLIDER_TEAR_DISTANCE (5 * LINE_VICINITY)


/* hover-only "handles" */
#define HOVER_NEW_SLIDER     (GIMP_TOOL_LINE_HANDLE_NONE - 1)


typedef enum
{
  GRAB_NONE,
  GRAB_SELECTION,
  GRAB_LINE
} GimpToolLineGrab;

enum
{
  PROP_0,
  PROP_X1,
  PROP_Y1,
  PROP_X2,
  PROP_Y2,
  PROP_SLIDERS,
  PROP_SELECTION,
  PROP_STATUS_TITLE,
};

enum
{
  CAN_ADD_SLIDER,
  ADD_SLIDER,
  PREPARE_TO_REMOVE_SLIDER,
  REMOVE_SLIDER,
  SELECTION_CHANGED,
  HANDLE_CLICKED,
  LAST_SIGNAL
};

struct _GimpToolLinePrivate
{
  gdouble            x1;
  gdouble            y1;
  gdouble            x2;
  gdouble            y2;
  GArray            *sliders;
  gint               selection;
  gchar             *status_title;

  gdouble            saved_x1;
  gdouble            saved_y1;
  gdouble            saved_x2;
  gdouble            saved_y2;
  gdouble            saved_slider_value;

  gdouble            mouse_x;
  gdouble            mouse_y;
  gint               hover;
  gdouble            new_slider_value;
  gboolean           remove_slider;
  GimpToolLineGrab   grab;

  GimpCanvasItem    *line;
  GimpCanvasItem    *start_handle;
  GimpCanvasItem    *end_handle;
  GimpCanvasItem    *slider_group;
  GArray            *slider_handles;
  GimpCanvasItem    *handle_circle;
};


/*  local function prototypes  */

static void     gimp_tool_line_constructed     (GObject               *object);
static void     gimp_tool_line_finalize        (GObject               *object);
static void     gimp_tool_line_set_property    (GObject               *object,
                                                guint                  property_id,
                                                const GValue          *value,
                                                GParamSpec            *pspec);
static void     gimp_tool_line_get_property    (GObject               *object,
                                                guint                  property_id,
                                                GValue                *value,
                                                GParamSpec            *pspec);

static void     gimp_tool_line_changed         (GimpToolWidget        *widget);
static void     gimp_tool_line_focus_changed   (GimpToolWidget        *widget);
static gint     gimp_tool_line_button_press    (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                guint32                time,
                                                GdkModifierType        state,
                                                GimpButtonPressType    press_type);
static void     gimp_tool_line_button_release  (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                guint32                time,
                                                GdkModifierType        state,
                                                GimpButtonReleaseType  release_type);
static void     gimp_tool_line_motion          (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                guint32                time,
                                                GdkModifierType        state);
static GimpHit  gimp_tool_line_hit             (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                GdkModifierType        state,
                                                gboolean               proximity);
static void     gimp_tool_line_hover           (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                GdkModifierType        state,
                                                gboolean               proximity);
static void     gimp_tool_line_leave_notify    (GimpToolWidget        *widget);
static gboolean gimp_tool_line_key_press       (GimpToolWidget        *widget,
                                                GdkEventKey           *kevent);
static void     gimp_tool_line_motion_modifier (GimpToolWidget        *widget,
                                                GdkModifierType        key,
                                                gboolean               press,
                                                GdkModifierType        state);
static gboolean gimp_tool_line_get_cursor      (GimpToolWidget        *widget,
                                                const GimpCoords      *coords,
                                                GdkModifierType        state,
                                                GimpCursorType        *cursor,
                                                GimpToolCursorType    *tool_cursor,
                                                GimpCursorModifier    *modifier);

static gint     gimp_tool_line_get_hover       (GimpToolLine          *line,
                                                const GimpCoords      *coords,
                                                GdkModifierType        state);
static GimpControllerSlider *
                gimp_tool_line_get_slider      (GimpToolLine          *line,
                                                gint                   slider);
static GimpCanvasItem *
                gimp_tool_line_get_handle      (GimpToolLine          *line,
                                                gint                   handle);
static gdouble  gimp_tool_line_project_point   (GimpToolLine          *line,
                                                gdouble                x,
                                                gdouble                y,
                                                gboolean               constrain,
                                                gdouble               *dist);

static gboolean
               gimp_tool_line_selection_motion (GimpToolLine          *line,
                                                gboolean               constrain);

static void     gimp_tool_line_update_handles  (GimpToolLine          *line);
static void     gimp_tool_line_update_circle   (GimpToolLine          *line);
static void     gimp_tool_line_update_hilight  (GimpToolLine          *line);
static void     gimp_tool_line_update_status   (GimpToolLine          *line,
                                                GdkModifierType        state,
                                                gboolean               proximity);

static gboolean gimp_tool_line_handle_hit      (GimpCanvasItem        *handle,
                                                gdouble                x,
                                                gdouble                y,
                                                gdouble               *min_dist);


G_DEFINE_TYPE_WITH_PRIVATE (GimpToolLine, gimp_tool_line, GIMP_TYPE_TOOL_WIDGET)

#define parent_class gimp_tool_line_parent_class

static guint line_signals[LAST_SIGNAL] = { 0, };


static void
gimp_tool_line_class_init (GimpToolLineClass *klass)
{
  GObjectClass        *object_class = G_OBJECT_CLASS (klass);
  GimpToolWidgetClass *widget_class = GIMP_TOOL_WIDGET_CLASS (klass);

  object_class->constructed     = gimp_tool_line_constructed;
  object_class->finalize        = gimp_tool_line_finalize;
  object_class->set_property    = gimp_tool_line_set_property;
  object_class->get_property    = gimp_tool_line_get_property;

  widget_class->changed         = gimp_tool_line_changed;
  widget_class->focus_changed   = gimp_tool_line_focus_changed;
  widget_class->button_press    = gimp_tool_line_button_press;
  widget_class->button_release  = gimp_tool_line_button_release;
  widget_class->motion          = gimp_tool_line_motion;
  widget_class->hit             = gimp_tool_line_hit;
  widget_class->hover           = gimp_tool_line_hover;
  widget_class->leave_notify    = gimp_tool_line_leave_notify;
  widget_class->key_press       = gimp_tool_line_key_press;
  widget_class->motion_modifier = gimp_tool_line_motion_modifier;
  widget_class->get_cursor      = gimp_tool_line_get_cursor;

  line_signals[CAN_ADD_SLIDER] =
    g_signal_new ("can-add-slider",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GimpToolLineClass, can_add_slider),
                  NULL, NULL,
                  gimp_marshal_BOOLEAN__DOUBLE,
                  G_TYPE_BOOLEAN, 1,
                  G_TYPE_DOUBLE);

  line_signals[ADD_SLIDER] =
    g_signal_new ("add-slider",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GimpToolLineClass, add_slider),
                  NULL, NULL,
                  gimp_marshal_INT__DOUBLE,
                  G_TYPE_INT, 1,
                  G_TYPE_DOUBLE);

  line_signals[PREPARE_TO_REMOVE_SLIDER] =
    g_signal_new ("prepare-to-remove-slider",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpToolLineClass, prepare_to_remove_slider),
                  NULL, NULL,
                  gimp_marshal_VOID__INT_BOOLEAN,
                  G_TYPE_NONE, 2,
                  G_TYPE_INT,
                  G_TYPE_BOOLEAN);

  line_signals[REMOVE_SLIDER] =
    g_signal_new ("remove-slider",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpToolLineClass, remove_slider),
                  NULL, NULL,
                  gimp_marshal_VOID__INT,
                  G_TYPE_NONE, 1,
                  G_TYPE_INT);

  line_signals[SELECTION_CHANGED] =
    g_signal_new ("selection-changed",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST,
                  G_STRUCT_OFFSET (GimpToolLineClass, selection_changed),
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);

  line_signals[HANDLE_CLICKED] =
    g_signal_new ("handle-clicked",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (GimpToolLineClass, handle_clicked),
                  NULL, NULL,
                  gimp_marshal_BOOLEAN__INT_UINT_ENUM,
                  G_TYPE_BOOLEAN, 3,
                  G_TYPE_INT,
                  G_TYPE_UINT,
                  GIMP_TYPE_BUTTON_PRESS_TYPE);

  g_object_class_install_property (object_class, PROP_X1,
                                   g_param_spec_double ("x1", NULL, NULL,
                                                        -GIMP_MAX_IMAGE_SIZE,
                                                        GIMP_MAX_IMAGE_SIZE, 0,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_Y1,
                                   g_param_spec_double ("y1", NULL, NULL,
                                                        -GIMP_MAX_IMAGE_SIZE,
                                                        GIMP_MAX_IMAGE_SIZE, 0,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_X2,
                                   g_param_spec_double ("x2", NULL, NULL,
                                                        -GIMP_MAX_IMAGE_SIZE,
                                                        GIMP_MAX_IMAGE_SIZE, 0,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_Y2,
                                   g_param_spec_double ("y2", NULL, NULL,
                                                        -GIMP_MAX_IMAGE_SIZE,
                                                        GIMP_MAX_IMAGE_SIZE, 0,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_SLIDERS,
                                   g_param_spec_boxed ("sliders", NULL, NULL,
                                                       G_TYPE_ARRAY,
                                                       GIMP_PARAM_READWRITE));

  g_object_class_install_property (object_class, PROP_SELECTION,
                                   g_param_spec_int ("selection", NULL, NULL,
                                                     GIMP_TOOL_LINE_HANDLE_NONE,
                                                     G_MAXINT,
                                                     GIMP_TOOL_LINE_HANDLE_NONE,
                                                     GIMP_PARAM_READWRITE |
                                                     G_PARAM_CONSTRUCT));

  g_object_class_install_property (object_class, PROP_STATUS_TITLE,
                                   g_param_spec_string ("status-title",
                                                        NULL, NULL,
                                                        _("Line: "),
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT));
}

static void
gimp_tool_line_init (GimpToolLine *line)
{
  GimpToolLinePrivate *private;

  private = line->private = gimp_tool_line_get_instance_private (line);

  private->sliders = g_array_new (FALSE, FALSE, sizeof (GimpControllerSlider));

  private->selection = GIMP_TOOL_LINE_HANDLE_NONE;
  private->hover     = GIMP_TOOL_LINE_HANDLE_NONE;
  private->grab      = GRAB_NONE;

  private->slider_handles = g_array_new (FALSE, TRUE,
                                         sizeof (GimpCanvasItem *));
}

static void
gimp_tool_line_constructed (GObject *object)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (object);
  GimpToolWidget      *widget  = GIMP_TOOL_WIDGET (object);
  GimpToolLinePrivate *private = line->private;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  private->line = gimp_tool_widget_add_line (widget,
                                             private->x1,
                                             private->y1,
                                             private->x2,
                                             private->y2);

  gimp_canvas_item_set_visible (private->line, SHOW_LINE);

  private->start_handle =
    gimp_tool_widget_add_handle (widget,
                                 ENDPOINT_HANDLE_TYPE,
                                 private->x1,
                                 private->y1,
                                 ENDPOINT_HANDLE_SIZE,
                                 ENDPOINT_HANDLE_SIZE,
                                 GIMP_HANDLE_ANCHOR_CENTER);

  private->end_handle =
    gimp_tool_widget_add_handle (widget,
                                 ENDPOINT_HANDLE_TYPE,
                                 private->x2,
                                 private->y2,
                                 ENDPOINT_HANDLE_SIZE,
                                 ENDPOINT_HANDLE_SIZE,
                                 GIMP_HANDLE_ANCHOR_CENTER);

  private->slider_group =
    gimp_canvas_group_new (gimp_tool_widget_get_shell (widget));
  gimp_tool_widget_add_item (widget, private->slider_group);
  g_object_unref (private->slider_group);

  private->handle_circle =
    gimp_tool_widget_add_handle (widget,
                                 GIMP_HANDLE_CIRCLE,
                                 private->x1,
                                 private->y1,
                                 ENDPOINT_HANDLE_SIZE * HANDLE_CIRCLE_SCALE,
                                 ENDPOINT_HANDLE_SIZE * HANDLE_CIRCLE_SCALE,
                                 GIMP_HANDLE_ANCHOR_CENTER);

  gimp_tool_line_changed (widget);
}

static void
gimp_tool_line_finalize (GObject *object)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (object);
  GimpToolLinePrivate *private = line->private;

  g_clear_pointer (&private->sliders, g_array_unref);
  g_clear_pointer (&private->status_title, g_free);
  g_clear_pointer (&private->slider_handles, g_array_unref);

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

static void
gimp_tool_line_set_property (GObject      *object,
                             guint         property_id,
                             const GValue *value,
                             GParamSpec   *pspec)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (object);
  GimpToolLinePrivate *private = line->private;

  switch (property_id)
    {
    case PROP_X1:
      private->x1 = g_value_get_double (value);
      break;
    case PROP_Y1:
      private->y1 = g_value_get_double (value);
      break;
    case PROP_X2:
      private->x2 = g_value_get_double (value);
      break;
    case PROP_Y2:
      private->y2 = g_value_get_double (value);
      break;

    case PROP_SLIDERS:
      {
        GArray   *sliders = g_value_dup_boxed (value);
        gboolean  deselect;

        g_return_if_fail (sliders != NULL);

        deselect =
          GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->selection) &&
          (sliders->len != private->sliders->len               ||
           ! gimp_tool_line_get_slider (line, private->selection)->selectable);

        g_array_unref (private->sliders);
        private->sliders = sliders;

        if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->hover))
          private->hover = GIMP_TOOL_LINE_HANDLE_NONE;

        if (deselect)
          gimp_tool_line_set_selection (line, GIMP_TOOL_LINE_HANDLE_NONE);
      }
      break;

    case PROP_SELECTION:
      {
        gint selection = g_value_get_int (value);

        g_return_if_fail (selection < (gint) private->sliders->len);
        g_return_if_fail (selection < 0 ||
                          gimp_tool_line_get_slider (line,
                                                     selection)->selectable);

        if (selection != private->selection)
          {
            private->selection = selection;

            if (private->grab == GRAB_SELECTION)
              private->grab = GRAB_NONE;

            g_signal_emit (line, line_signals[SELECTION_CHANGED], 0);
          }
      }
      break;

    case PROP_STATUS_TITLE:
      g_free (private->status_title);
      private->status_title = g_value_dup_string (value);
      if (! private->status_title)
        private->status_title = g_strdup (_("Line: "));
      break;

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

static void
gimp_tool_line_get_property (GObject    *object,
                             guint       property_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (object);
  GimpToolLinePrivate *private = line->private;

  switch (property_id)
    {
    case PROP_X1:
      g_value_set_double (value, private->x1);
      break;
    case PROP_Y1:
      g_value_set_double (value, private->y1);
      break;
    case PROP_X2:
      g_value_set_double (value, private->x2);
      break;
    case PROP_Y2:
      g_value_set_double (value, private->y2);
      break;

    case PROP_SLIDERS:
      g_value_set_boxed (value, private->sliders);
      break;

    case PROP_SELECTION:
      g_value_set_int (value, private->selection);
      break;

    case PROP_STATUS_TITLE:
      g_value_set_string (value, private->status_title);
      break;

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

static void
gimp_tool_line_changed (GimpToolWidget *widget)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;
  gint                 i;

  gimp_canvas_line_set (private->line,
                        private->x1,
                        private->y1,
                        private->x2,
                        private->y2);

  gimp_canvas_handle_set_position (private->start_handle,
                                   private->x1,
                                   private->y1);

  gimp_canvas_handle_set_position (private->end_handle,
                                   private->x2,
                                   private->y2);

  /* remove excessive slider handles */
  for (i = private->sliders->len; i < private->slider_handles->len; i++)
    {
      gimp_canvas_group_remove_item (GIMP_CANVAS_GROUP (private->slider_group),
                                     gimp_tool_line_get_handle (line, i));
    }

  g_array_set_size (private->slider_handles, private->sliders->len);

  for (i = 0; i < private->sliders->len; i++)
    {
      gdouble          value;
      gdouble          x;
      gdouble          y;
      GimpCanvasItem **handle;

      value = gimp_tool_line_get_slider (line, i)->value;

      x = private->x1 + (private->x2 - private->x1) * value;
      y = private->y1 + (private->y2 - private->y1) * value;

      handle = &g_array_index (private->slider_handles, GimpCanvasItem *, i);

      if (*handle)
        {
          gimp_canvas_handle_set_position (*handle, x, y);
        }
      else
        {
          *handle = gimp_canvas_handle_new (gimp_tool_widget_get_shell (widget),
                                            SLIDER_HANDLE_TYPE,
                                            GIMP_HANDLE_ANCHOR_CENTER,
                                            x,
                                            y,
                                            SLIDER_HANDLE_SIZE,
                                            SLIDER_HANDLE_SIZE);

          gimp_canvas_group_add_item (GIMP_CANVAS_GROUP (private->slider_group),
                                      *handle);
          g_object_unref (*handle);
        }
    }

  gimp_tool_line_update_handles (line);
  gimp_tool_line_update_circle (line);
  gimp_tool_line_update_hilight (line);
}

static void
gimp_tool_line_focus_changed (GimpToolWidget *widget)
{
  GimpToolLine *line = GIMP_TOOL_LINE (widget);

  gimp_tool_line_update_hilight (line);
}

gboolean
gimp_tool_line_button_press (GimpToolWidget      *widget,
                             const GimpCoords    *coords,
                             guint32              time,
                             GdkModifierType      state,
                             GimpButtonPressType  press_type)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;
  gboolean             result  = FALSE;

  private->grab          = GRAB_NONE;
  private->remove_slider = FALSE;

  private->saved_x1 = private->x1;
  private->saved_y1 = private->y1;
  private->saved_x2 = private->x2;
  private->saved_y2 = private->y2;

  if (press_type         != GIMP_BUTTON_PRESS_NORMAL   &&
      private->hover      > GIMP_TOOL_LINE_HANDLE_NONE &&
      private->selection  > GIMP_TOOL_LINE_HANDLE_NONE)
    {
      g_signal_emit (line, line_signals[HANDLE_CLICKED], 0,
                     private->selection, state, press_type, &result);

      if (! result)
        gimp_tool_widget_hover (widget, coords, state, TRUE);
    }

  if (press_type == GIMP_BUTTON_PRESS_NORMAL || ! result)
    {
      private->saved_x1 = private->x1;
      private->saved_y1 = private->y1;
      private->saved_x2 = private->x2;
      private->saved_y2 = private->y2;

      if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->hover))
        {
          private->saved_slider_value =
            gimp_tool_line_get_slider (line, private->hover)->value;
        }

      if (private->hover > GIMP_TOOL_LINE_HANDLE_NONE)
        {
          gimp_tool_line_set_selection (line, private->hover);

          private->grab = GRAB_SELECTION;
        }
      else if (private->hover == HOVER_NEW_SLIDER)
        {
          gint slider;

          g_signal_emit (line, line_signals[ADD_SLIDER], 0,
                         private->new_slider_value, &slider);

          g_return_val_if_fail (slider < (gint) private->sliders->len, FALSE);

          if (slider >= 0)
            {
              gimp_tool_line_set_selection (line, slider);

              private->saved_slider_value =
                gimp_tool_line_get_slider (line, private->selection)->value;

              private->grab = GRAB_SELECTION;
            }
        }
      else if (state & GRAB_LINE_MASK)
        {
          private->grab = GRAB_LINE;
        }

      result = (private->grab != GRAB_NONE);
    }

  if (! result)
    {
      private->hover = GIMP_TOOL_LINE_HANDLE_NONE;

      gimp_tool_line_set_selection (line, GIMP_TOOL_LINE_HANDLE_NONE);
    }

  gimp_tool_line_update_handles (line);
  gimp_tool_line_update_circle (line);
  gimp_tool_line_update_status (line, state, TRUE);

  return result;
}

void
gimp_tool_line_button_release (GimpToolWidget        *widget,
                               const GimpCoords      *coords,
                               guint32                time,
                               GdkModifierType        state,
                               GimpButtonReleaseType  release_type)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;
  GimpToolLineGrab     grab    = private->grab;

  private->grab = GRAB_NONE;

  if (release_type == GIMP_BUTTON_RELEASE_CANCEL)
    {
      if (grab != GRAB_NONE)
        {
          if (grab == GRAB_SELECTION &&
              GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->selection))
            {
              gimp_tool_line_get_slider (line, private->selection)->value =
                private->saved_slider_value;

              if (private->remove_slider)
                {
                  private->remove_slider = FALSE;

                  g_signal_emit (line, line_signals[PREPARE_TO_REMOVE_SLIDER], 0,
                                 private->selection, FALSE);
                }
            }

          g_object_set (line,
                        "x1", private->saved_x1,
                        "y1", private->saved_y1,
                        "x2", private->saved_x2,
                        "y2", private->saved_y2,
                        NULL);
        }
    }
  else if (grab == GRAB_SELECTION)
    {
      if (private->remove_slider)
        {
          private->remove_slider = FALSE;

          g_signal_emit (line, line_signals[REMOVE_SLIDER], 0,
                         private->selection);
        }
      else if (release_type == GIMP_BUTTON_RELEASE_CLICK)
        {
          gboolean result;

          g_signal_emit (line, line_signals[HANDLE_CLICKED], 0,
                         private->selection, state, GIMP_BUTTON_PRESS_NORMAL,
                         &result);
        }
    }
}

void
gimp_tool_line_motion (GimpToolWidget   *widget,
                       const GimpCoords *coords,
                       guint32           time,
                       GdkModifierType   state)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;
  gdouble              diff_x  = coords->x - private->mouse_x;
  gdouble              diff_y  = coords->y - private->mouse_y;

  private->mouse_x = coords->x;
  private->mouse_y = coords->y;

  if (private->grab == GRAB_LINE)
    {
      g_object_set (line,
                    "x1", private->x1 + diff_x,
                    "y1", private->y1 + diff_y,
                    "x2", private->x2 + diff_x,
                    "y2", private->y2 + diff_y,
                    NULL);
    }
  else
    {
      gboolean constrain = (state & gimp_get_constrain_behavior_mask ()) != 0;

      gimp_tool_line_selection_motion (line, constrain);
    }

  gimp_tool_line_update_status (line, state, TRUE);
}

GimpHit
gimp_tool_line_hit (GimpToolWidget   *widget,
                    const GimpCoords *coords,
                    GdkModifierType   state,
                    gboolean          proximity)
{
  GimpToolLine *line = GIMP_TOOL_LINE (widget);

  if (! (state & GRAB_LINE_MASK))
    {
      gint hover = gimp_tool_line_get_hover (line, coords, state);

      if (hover != GIMP_TOOL_LINE_HANDLE_NONE)
        return GIMP_HIT_DIRECT;
    }
  else
    {
      return GIMP_HIT_INDIRECT;
    }

  return GIMP_HIT_NONE;
}

void
gimp_tool_line_hover (GimpToolWidget   *widget,
                      const GimpCoords *coords,
                      GdkModifierType   state,
                      gboolean          proximity)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;

  private->mouse_x = coords->x;
  private->mouse_y = coords->y;

  if (! (state & GRAB_LINE_MASK))
    private->hover = gimp_tool_line_get_hover (line, coords, state);
  else
    private->hover = GIMP_TOOL_LINE_HANDLE_NONE;

  gimp_tool_line_update_handles (line);
  gimp_tool_line_update_circle (line);
  gimp_tool_line_update_status (line, state, proximity);
}

static void
gimp_tool_line_leave_notify (GimpToolWidget *widget)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;

  private->hover = GIMP_TOOL_LINE_HANDLE_NONE;

  gimp_tool_line_update_handles (line);
  gimp_tool_line_update_circle (line);

  GIMP_TOOL_WIDGET_CLASS (parent_class)->leave_notify (widget);
}

static gboolean
gimp_tool_line_key_press (GimpToolWidget *widget,
                          GdkEventKey    *kevent)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;
  GimpDisplayShell    *shell;
  gdouble              pixels  = 1.0;
  gboolean             move_line;

  move_line = kevent->state & GRAB_LINE_MASK;

  if (private->selection == GIMP_TOOL_LINE_HANDLE_NONE && ! move_line)
    return GIMP_TOOL_WIDGET_CLASS (parent_class)->key_press (widget, kevent);

  shell = gimp_tool_widget_get_shell (widget);

  if (kevent->state & gimp_get_extend_selection_mask ())
    pixels = 10.0;

  if (kevent->state & gimp_get_toggle_behavior_mask ())
    pixels = 50.0;

  switch (kevent->keyval)
    {
    case GDK_KEY_Left:
    case GDK_KEY_Right:
    case GDK_KEY_Up:
    case GDK_KEY_Down:
      /* move an endpoint (or both endpoints) */
      if (private->selection < 0 || move_line)
        {
          gdouble xdist, ydist;
          gdouble dx,    dy;

          xdist = FUNSCALEX (shell, pixels);
          ydist = FUNSCALEY (shell, pixels);

          dx = 0.0;
          dy = 0.0;

          switch (kevent->keyval)
            {
            case GDK_KEY_Left:  dx = -xdist; break;
            case GDK_KEY_Right: dx = +xdist; break;
            case GDK_KEY_Up:    dy = -ydist; break;
            case GDK_KEY_Down:  dy = +ydist; break;
            }

          if (private->selection == GIMP_TOOL_LINE_HANDLE_START || move_line)
            {
              g_object_set (line,
                            "x1", private->x1 + dx,
                            "y1", private->y1 + dy,
                            NULL);
            }

          if (private->selection == GIMP_TOOL_LINE_HANDLE_END || move_line)
            {
              g_object_set (line,
                            "x2", private->x2 + dx,
                            "y2", private->y2 + dy,
                            NULL);
            }
        }
      /* move a slider */
      else
        {
          GimpControllerSlider *slider;
          gdouble               dist;
          gdouble               dvalue;

          slider = gimp_tool_line_get_slider (line, private->selection);

          if (! slider->movable)
            break;

          dist = gimp_canvas_item_transform_distance (private->line,
                                                      private->x1, private->y1,
                                                      private->x2, private->y2);

          if (dist > 0.0)
            dist = pixels / dist;

          dvalue = 0.0;

          switch (kevent->keyval)
            {
            case GDK_KEY_Left:
              if      (private->x1 < private->x2) dvalue = -dist;
              else if (private->x1 > private->x2) dvalue = +dist;
              break;

            case GDK_KEY_Right:
              if      (private->x1 < private->x2) dvalue = +dist;
              else if (private->x1 > private->x2) dvalue = -dist;
              break;

            case GDK_KEY_Up:
              if      (private->y1 < private->y2) dvalue = -dist;
              else if (private->y1 > private->y2) dvalue = +dist;
              break;

            case GDK_KEY_Down:
              if      (private->y1 < private->y2) dvalue = +dist;
              else if (private->y1 > private->y2) dvalue = -dist;
              break;
            }

          if (dvalue != 0.0)
            {
              slider->value += dvalue;
              slider->value  = CLAMP (slider->value, slider->min, slider->max);
              slider->value  = CLAMP (slider->value, 0.0, 1.0);

              g_object_set (line,
                            "sliders", private->sliders,
                            NULL);
            }
        }
      return TRUE;

    case GDK_KEY_BackSpace:
    case GDK_KEY_Delete:
      if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->selection))
        {
          if (gimp_tool_line_get_slider (line, private->selection)->removable)
            {
              g_signal_emit (line, line_signals[REMOVE_SLIDER], 0,
                             private->selection);
            }
        }
      return TRUE;
    }

  return GIMP_TOOL_WIDGET_CLASS (parent_class)->key_press (widget, kevent);
}

static void
gimp_tool_line_motion_modifier (GimpToolWidget  *widget,
                                GdkModifierType  key,
                                gboolean         press,
                                GdkModifierType  state)
{
  GimpToolLine *line = GIMP_TOOL_LINE (widget);

  if (key == gimp_get_constrain_behavior_mask ())
    {
      gimp_tool_line_selection_motion (line, press);

      gimp_tool_line_update_status (line, state, TRUE);
    }
}

static gboolean
gimp_tool_line_get_cursor (GimpToolWidget     *widget,
                           const GimpCoords   *coords,
                           GdkModifierType     state,
                           GimpCursorType     *cursor,
                           GimpToolCursorType *tool_cursor,
                           GimpCursorModifier *modifier)
{
  GimpToolLine        *line    = GIMP_TOOL_LINE (widget);
  GimpToolLinePrivate *private = line->private;

  if (private->grab ==GRAB_LINE || (state & GRAB_LINE_MASK))
    {
      *modifier = GIMP_CURSOR_MODIFIER_MOVE;

      return TRUE;
    }
  else if (private->grab  == GRAB_SELECTION ||
           private->hover >  GIMP_TOOL_LINE_HANDLE_NONE)
    {
      const GimpControllerSlider *slider = NULL;

      if (private->grab == GRAB_SELECTION)
        {
          if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->selection))
            slider = gimp_tool_line_get_slider (line, private->selection);
        }
      else if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->hover))
        {
          slider = gimp_tool_line_get_slider (line, private->hover);
        }

      if (private->grab == GRAB_SELECTION && slider && private->remove_slider)
        {
          *modifier = GIMP_CURSOR_MODIFIER_MINUS;

          return TRUE;
        }
      else if (! slider || slider->movable)
        {
          *modifier = GIMP_CURSOR_MODIFIER_MOVE;

          return TRUE;
        }
    }
  else if (private->hover == HOVER_NEW_SLIDER)
    {
      *modifier = GIMP_CURSOR_MODIFIER_PLUS;

      return TRUE;
    }

  return FALSE;
}

static gint
gimp_tool_line_get_hover (GimpToolLine     *line,
                          const GimpCoords *coords,
                          GdkModifierType   state)
{
  GimpToolLinePrivate *private = line->private;
  gint                 hover   = GIMP_TOOL_LINE_HANDLE_NONE;
  gdouble              min_dist;
  gint                 first_handle;
  gint                 i;

  /* find the closest handle to the cursor */
  min_dist     = G_MAXDOUBLE;
  first_handle = private->sliders->len - 1;

  /* skip the sliders if the two endpoints are the same, in particular so
   * that if the line is created during a button-press event (as in the
   * blend tool), the end endpoint is dragged, instead of a slider.
   */
  if (private->x1 == private->x2 && private->y1 == private->y2)
    first_handle = -1;

  for (i = first_handle; i > GIMP_TOOL_LINE_HANDLE_NONE; i--)
    {
      GimpCanvasItem *handle;

      if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (i))
        {
          const GimpControllerSlider *slider;

          slider = gimp_tool_line_get_slider (line, i);

          if (! slider->visible || ! slider->selectable)
            continue;
        }

      handle = gimp_tool_line_get_handle (line, i);

      if (gimp_tool_line_handle_hit (handle,
                                     private->mouse_x,
                                     private->mouse_y,
                                     &min_dist))
        {
          hover = i;
        }
    }

  if (hover == GIMP_TOOL_LINE_HANDLE_NONE)
    {
      gboolean constrain;
      gdouble  value;
      gdouble  dist;

      constrain = (state & gimp_get_constrain_behavior_mask ()) != 0;

      value = gimp_tool_line_project_point (line,
                                            private->mouse_x,
                                            private->mouse_y,
                                            constrain,
                                            &dist);

      if (value >= 0.0 && value <= 1.0 && dist <= LINE_VICINITY)
        {
          gboolean can_add;

          g_signal_emit (line, line_signals[CAN_ADD_SLIDER], 0,
                         value, &can_add);

          if (can_add)
            {
              hover                     = HOVER_NEW_SLIDER;
              private->new_slider_value = value;
            }
        }
    }

  return hover;
}

static GimpControllerSlider *
gimp_tool_line_get_slider (GimpToolLine *line,
                           gint          slider)
{
  GimpToolLinePrivate *private = line->private;

  gimp_assert (slider >= 0 && slider < private->sliders->len);

  return &g_array_index (private->sliders, GimpControllerSlider, slider);
}

static GimpCanvasItem *
gimp_tool_line_get_handle (GimpToolLine *line,
                           gint          handle)
{
  GimpToolLinePrivate *private = line->private;

  switch (handle)
    {
    case GIMP_TOOL_LINE_HANDLE_NONE:
      return NULL;

    case GIMP_TOOL_LINE_HANDLE_START:
      return private->start_handle;

    case GIMP_TOOL_LINE_HANDLE_END:
      return private->end_handle;

    default:
      gimp_assert (handle >= 0 &&
                   handle <  (gint) private->slider_handles->len);

      return g_array_index (private->slider_handles,
                            GimpCanvasItem *, handle);
    }
}

static gdouble
gimp_tool_line_project_point (GimpToolLine *line,
                              gdouble       x,
                              gdouble       y,
                              gboolean      constrain,
                              gdouble      *dist)
{
  GimpToolLinePrivate *private = line->private;
  gdouble              length_sqr;
  gdouble              value   = 0.0;

  length_sqr = SQR (private->x2 - private->x1) +
               SQR (private->y2 - private->y1);

  /* don't calculate the projection for 0-length lines, since we'll just get
   * NaN.
   */
  if (length_sqr > 0.0)
    {
      value  = (private->x2 - private->x1) * (x - private->x1) +
               (private->y2 - private->y1) * (y - private->y1);
      value /= length_sqr;

      if (dist)
        {
          gdouble px;
          gdouble py;

          px = private->x1 + (private->x2 - private->x1) * value;
          py = private->y1 + (private->y2 - private->y1) * value;

          *dist = gimp_canvas_item_transform_distance (private->line,
                                                       x,  y,
                                                       px, py);
        }

      if (constrain)
        value = RINT (12.0 * value) / 12.0;
    }
  else
    {
      if (dist)
        {
          *dist = gimp_canvas_item_transform_distance (private->line,
                                                       x,           y,
                                                       private->x1, private->y1);
        }
    }

  return value;
}

static gboolean
gimp_tool_line_selection_motion (GimpToolLine *line,
                                 gboolean      constrain)
{
  GimpToolLinePrivate *private = line->private;
  gdouble              x       = private->mouse_x;
  gdouble              y       = private->mouse_y;

  if (private->grab != GRAB_SELECTION)
    return FALSE;

  switch (private->selection)
    {
    case GIMP_TOOL_LINE_HANDLE_NONE:
      gimp_assert_not_reached ();

    case GIMP_TOOL_LINE_HANDLE_START:
      if (constrain)
        {
          gimp_display_shell_constrain_line (
            gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (line)),
            private->x2, private->y2,
            &x, &y,
            GIMP_CONSTRAIN_LINE_15_DEGREES);
        }

      g_object_set (line,
                    "x1", x,
                    "y1", y,
                    NULL);
      return TRUE;

    case GIMP_TOOL_LINE_HANDLE_END:
      if (constrain)
        {
          gimp_display_shell_constrain_line (
            gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (line)),
            private->x1, private->y1,
            &x, &y,
            GIMP_CONSTRAIN_LINE_15_DEGREES);
        }

      g_object_set (line,
                    "x2", x,
                    "y2", y,
                    NULL);
      return TRUE;

    default:
      {
        GimpDisplayShell     *shell;
        GimpControllerSlider *slider;
        gdouble               value;
        gdouble               dist;
        gboolean              remove_slider;

        shell = gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (line));

        slider = gimp_tool_line_get_slider (line, private->selection);

        /* project the cursor position onto the line */
        value = gimp_tool_line_project_point (line, x, y, constrain, &dist);

        /* slider dragging */
        if (slider->movable)
          {
            value = CLAMP (value, slider->min, slider->max);
            value = CLAMP (value, 0.0,         1.0);

            value = fabs (value); /* avoid negative zero */

            slider->value = value;

            g_object_set (line,
                          "sliders", private->sliders,
                          NULL);
          }

        /* slider tearing */
        remove_slider = slider->removable && dist > SLIDER_TEAR_DISTANCE;

        if (remove_slider != private->remove_slider)
          {
            private->remove_slider = remove_slider;

            g_signal_emit (line, line_signals[PREPARE_TO_REMOVE_SLIDER], 0,
                           private->selection, remove_slider);

            /* set the cursor modifier to a minus by talking to the shell
             * directly -- eek!
             */
            {
              GimpCursorType     cursor;
              GimpToolCursorType tool_cursor;
              GimpCursorModifier modifier;

              cursor      = shell->current_cursor;
              tool_cursor = shell->tool_cursor;
              modifier    = GIMP_CURSOR_MODIFIER_NONE;

              gimp_tool_line_get_cursor (GIMP_TOOL_WIDGET (line), NULL, 0,
                                         &cursor, &tool_cursor, &modifier);

              gimp_display_shell_set_cursor (shell, cursor, tool_cursor, modifier);
            }

            gimp_tool_line_update_handles (line);
            gimp_tool_line_update_circle (line);
            gimp_tool_line_update_status (line,
                                          constrain ?
                                            gimp_get_constrain_behavior_mask () :
                                            0,
                                          TRUE);
          }

        return TRUE;
      }
    }
}

static void
gimp_tool_line_update_handles (GimpToolLine *line)
{
  GimpToolLinePrivate *private = line->private;
  gdouble              value;
  gdouble              dist;
  gint                 i;

  value = gimp_tool_line_project_point (line,
                                        private->mouse_x,
                                        private->mouse_y,
                                        FALSE,
                                        &dist);

  for (i = 0; i < private->sliders->len; i++)
    {
      const GimpControllerSlider *slider;
      GimpCanvasItem             *handle;
      gint                        size;
      gint                        hit_radius;
      gboolean                    show_autohidden;
      gboolean                    visible;

      slider = gimp_tool_line_get_slider (line, i);
      handle = gimp_tool_line_get_handle (line, i);

      size = slider->size * SLIDER_HANDLE_SIZE;
      size = MAX (size, 1);

      hit_radius = (MAX (size, SLIDER_HANDLE_SIZE) * HANDLE_CIRCLE_SCALE) / 2;

      /* show a autohidden slider if it's selected, or if no other handle is
       * grabbed or hovered-over, and the cursor is close enough to the line,
       * between the slider's min and max values.
       */
      show_autohidden = private->selection == i                        ||
                        (private->grab == GRAB_NONE                    &&
                         (private->hover <= GIMP_TOOL_LINE_HANDLE_NONE ||
                          private->hover == i)                         &&
                         dist <= hit_radius                            &&
                         value >= slider->min                          &&
                         value <= slider->max);

      visible = slider->visible                         &&
                (! slider->autohide || show_autohidden) &&
                ! (private->selection == i && private->remove_slider);

      handle = gimp_tool_line_get_handle (line, i);

      if (visible)
        {
          g_object_set (handle,
                        "type",   slider->type,
                        "width",  size,
                        "height", size,
                        NULL);
        }

      gimp_canvas_item_set_visible (handle, visible);
    }
}

static void
gimp_tool_line_update_circle (GimpToolLine *line)
{
  GimpToolLinePrivate *private = line->private;
  gboolean             visible;

  visible = (private->grab == GRAB_NONE                    &&
             private->hover != GIMP_TOOL_LINE_HANDLE_NONE) ||
            (private->grab == GRAB_SELECTION               &&
             private->remove_slider);

  if (visible)
    {
      gdouble  x;
      gdouble  y;
      gint     width;
      gint     height;
      gboolean dashed;

      if (private->grab == GRAB_NONE && private->hover == HOVER_NEW_SLIDER)
        {
          /* new slider */
          x = private->x1 +
              (private->x2 - private->x1) * private->new_slider_value;
          y = private->y1 +
              (private->y2 - private->y1) * private->new_slider_value;

          width = height = SLIDER_HANDLE_SIZE;

          dashed = TRUE;
        }
      else
        {
          GimpCanvasItem *handle;

          if (private->grab == GRAB_SELECTION)
            {
              /* tear slider */
              handle = gimp_tool_line_get_handle (line, private->selection);
              dashed = TRUE;
            }
          else
            {
              /* hover over handle */
              handle = gimp_tool_line_get_handle (line, private->hover);
              dashed = FALSE;
            }

          gimp_canvas_handle_get_position (handle, &x,     &y);
          gimp_canvas_handle_get_size     (handle, &width, &height);
        }

      width   = MAX (width,  SLIDER_HANDLE_SIZE);
      height  = MAX (height, SLIDER_HANDLE_SIZE);

      width  *= HANDLE_CIRCLE_SCALE;
      height *= HANDLE_CIRCLE_SCALE;

      gimp_canvas_handle_set_position (private->handle_circle, x,     y);
      gimp_canvas_handle_set_size     (private->handle_circle, width, height);

      g_object_set (private->handle_circle,
                    "type", dashed ? GIMP_HANDLE_DASHED_CIRCLE :
                                     GIMP_HANDLE_CIRCLE,
                    NULL);
    }

  gimp_canvas_item_set_visible (private->handle_circle, visible);
}

static void
gimp_tool_line_update_hilight (GimpToolLine *line)
{
  GimpToolLinePrivate *private = line->private;
  gboolean             focus;
  gint                 i;

  focus = gimp_tool_widget_get_focus (GIMP_TOOL_WIDGET (line));

  for (i = GIMP_TOOL_LINE_HANDLE_NONE + 1;
       i < (gint) private->sliders->len;
       i++)
    {
      GimpCanvasItem *handle;

      handle = gimp_tool_line_get_handle (line, i);

      gimp_canvas_item_set_highlight (handle, focus && i == private->selection);
    }
}

static void
gimp_tool_line_update_status (GimpToolLine    *line,
                              GdkModifierType  state,
                              gboolean         proximity)
{
  GimpToolLinePrivate *private = line->private;

  if (proximity)
    {
      GimpDisplayShell *shell;
      const gchar      *toggle_behavior_format = NULL;
      const gchar      *message                = NULL;
      gchar            *line_status            = NULL;
      gchar            *status;
      gint              handle;

      shell = gimp_tool_widget_get_shell (GIMP_TOOL_WIDGET (line));

      if (private->grab == GRAB_SELECTION)
        handle = private->selection;
      else
        handle = private->hover;

      if (handle == GIMP_TOOL_LINE_HANDLE_START ||
          handle == GIMP_TOOL_LINE_HANDLE_END)
        {
          line_status = gimp_display_shell_get_line_status (shell,
                                                            _("Click-Drag to move the endpoint"),
                                                            ". ",
                                                            private->x1,
                                                            private->y1,
                                                            private->x2,
                                                            private->y2);
          toggle_behavior_format = _("%s for constrained angles");
        }
      else if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (handle) ||
               handle == HOVER_NEW_SLIDER)
        {
          if (private->grab == GRAB_SELECTION && private->remove_slider)
            {
              message = _("Release to remove the slider");
            }
          else
            {
              toggle_behavior_format = _("%s for constrained values");

              if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (handle))
                {
                  if (gimp_tool_line_get_slider (line, handle)->movable)
                    {
                      if (gimp_tool_line_get_slider (line, handle)->removable)
                        {
                          if (private->grab == GRAB_SELECTION)
                            {
                              message = _("Click-Drag to move the slider; "
                                          "drag away to remove the slider");
                            }
                          else
                            {
                              message = _("Click-Drag to move or remove the slider");
                            }
                        }
                      else
                        {
                          message = _("Click-Drag to move the slider");
                        }
                    }
                  else
                    {
                      toggle_behavior_format = NULL;

                      if (gimp_tool_line_get_slider (line, handle)->removable)
                        {
                          if (private->grab == GRAB_SELECTION)
                            {
                              message = _("Click-Drag away to remove the slider");
                            }
                          else
                            {
                              message = _("Click-Drag to remove the slider");
                            }
                        }
                      else
                        {
                          message = NULL;
                        }
                    }
                }
              else
                {
                  message = _("Click or Click-Drag to add a new slider");
                }
            }
        }
      else if (state & GDK_MOD1_MASK)
        {
          message = _("Click-Drag to move the line");
        }

      status =
        gimp_suggest_modifiers (message ? message : (line_status ? line_status : ""),
                                ((toggle_behavior_format ?
                                   gimp_get_constrain_behavior_mask () : 0) |
                                 (private->grab == GRAB_NONE ?
                                   GDK_MOD1_MASK : 0)) &
                                ~state,
                                NULL,
                                toggle_behavior_format,
                                _("%s to move the whole line"));

      if (message || line_status)
        {
          gimp_tool_widget_set_status (GIMP_TOOL_WIDGET (line), status);
        }
      else
        {
          line_status = gimp_display_shell_get_line_status (shell,
                                                            private->status_title,
                                                            ". ",
                                                            private->x1,
                                                            private->y1,
                                                            private->x2,
                                                            private->y2);
          gimp_tool_widget_set_status_coords (GIMP_TOOL_WIDGET (line),
                                              line_status,
                                              private->x2 - private->x1,
                                              ", ",
                                              private->y2 - private->y1,
                                              status);
        }

      g_free (status);
      if (line_status)
        g_free (line_status);
    }
  else
    {
      gimp_tool_widget_set_status (GIMP_TOOL_WIDGET (line), NULL);
    }
}

static gboolean
gimp_tool_line_handle_hit (GimpCanvasItem *handle,
                           gdouble         x,
                           gdouble         y,
                           gdouble        *min_dist)
{
  gdouble handle_x;
  gdouble handle_y;
  gint    handle_width;
  gint    handle_height;
  gint    radius;
  gdouble dist;

  gimp_canvas_handle_get_position (handle, &handle_x,     &handle_y);
  gimp_canvas_handle_get_size     (handle, &handle_width, &handle_height);

  handle_width  = MAX (handle_width,  SLIDER_HANDLE_SIZE);
  handle_height = MAX (handle_height, SLIDER_HANDLE_SIZE);

  radius = ((gint) (handle_width * HANDLE_CIRCLE_SCALE)) / 2;
  radius = MAX (radius, LINE_VICINITY);

  dist = gimp_canvas_item_transform_distance (handle,
                                              x, y, handle_x, handle_y);

  if (dist <= radius && dist < *min_dist)
    {
      *min_dist = dist;

      return TRUE;
    }
  else
    {
      return FALSE;
    }
}


/*  public functions  */

GimpToolWidget *
gimp_tool_line_new (GimpDisplayShell *shell,
                    gdouble           x1,
                    gdouble           y1,
                    gdouble           x2,
                    gdouble           y2)
{
  g_return_val_if_fail (GIMP_IS_DISPLAY_SHELL (shell), NULL);

  return g_object_new (GIMP_TYPE_TOOL_LINE,
                       "shell", shell,
                       "x1",    x1,
                       "y1",    y1,
                       "x2",    x2,
                       "y2",    y2,
                       NULL);
}

void
gimp_tool_line_set_sliders (GimpToolLine               *line,
                            const GimpControllerSlider *sliders,
                            gint                        n_sliders)
{
  GimpToolLinePrivate *private;

  g_return_if_fail (GIMP_IS_TOOL_LINE (line));
  g_return_if_fail (n_sliders == 0 || (n_sliders > 0 && sliders != NULL));

  private = line->private;

  if (GIMP_TOOL_LINE_HANDLE_IS_SLIDER (private->selection) &&
      private->sliders->len != n_sliders)
    {
      gimp_tool_line_set_selection (line, GIMP_TOOL_LINE_HANDLE_NONE);
    }

  g_array_set_size (private->sliders, n_sliders);

  memcpy (private->sliders->data, sliders,
          n_sliders * sizeof (GimpControllerSlider));

  g_object_set (line,
                "sliders", private->sliders,
                NULL);
}

const GimpControllerSlider *
gimp_tool_line_get_sliders (GimpToolLine *line,
                            gint         *n_sliders)
{
  GimpToolLinePrivate *private;

  g_return_val_if_fail (GIMP_IS_TOOL_LINE (line), NULL);

  private = line->private;

  if (n_sliders) *n_sliders = private->sliders->len;

  return (const GimpControllerSlider *) private->sliders->data;
}

void
gimp_tool_line_set_selection (GimpToolLine *line,
                              gint          handle)
{
  GimpToolLinePrivate *private;

  g_return_if_fail (GIMP_IS_TOOL_LINE (line));

  private = line->private;

  g_return_if_fail (handle >= GIMP_TOOL_LINE_HANDLE_NONE &&
                    handle <  (gint) private->sliders->len);

  g_object_set (line,
                "selection", handle,
                NULL);
}

gint
gimp_tool_line_get_selection (GimpToolLine *line)
{
  GimpToolLinePrivate *private;

  g_return_val_if_fail (GIMP_IS_TOOL_LINE (line), GIMP_TOOL_LINE_HANDLE_NONE);

  private = line->private;

  return private->selection;
}
