mirror of
https://gitlab.gnome.org/GNOME/gimp.git
synced 2025-07-03 17:33:25 +00:00

The GimpDial widget can now be snapped to the nearest fifteen degree interval by holding the shift key while adjusting. When adjusting both an alpha and beta value, both will be snapped according to the alpha, preserving the angle between them
604 lines
18 KiB
C
604 lines
18 KiB
C
/* GIMP - The GNU Image Manipulation Program
|
|
* Copyright (C) 1995 Spencer Kimball and Peter Mattis
|
|
*
|
|
* gimpdial.c
|
|
* Copyright (C) 2014 Michael Natterer <mitch@gimp.org>
|
|
*
|
|
* Based on code from the color-rotate plug-in
|
|
* Copyright (C) 1997-1999 Sven Anders (anderss@fmi.uni-passau.de)
|
|
* Based on code from Pavel Grinfeld (pavel@ml.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 "libgimpbase/gimpbase.h"
|
|
#include "libgimpmath/gimpmath.h"
|
|
#include "libgimpcolor/gimpcolor.h"
|
|
#include "libgimpwidgets/gimpwidgets.h"
|
|
|
|
#include "widgets-types.h"
|
|
|
|
#include "core/gimp-cairo.h"
|
|
|
|
#include "gimpdial.h"
|
|
|
|
|
|
#define SEGMENT_FRACTION 0.3
|
|
|
|
/* round n to the nearest multiple of m */
|
|
#define SNAP(n, m) (RINT((n) / (m)) * (m))
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_DRAW_BETA,
|
|
PROP_ALPHA,
|
|
PROP_BETA,
|
|
PROP_CLOCKWISE_ANGLES,
|
|
PROP_CLOCKWISE_DELTA
|
|
};
|
|
|
|
typedef enum
|
|
{
|
|
DIAL_TARGET_NONE = 0,
|
|
DIAL_TARGET_ALPHA = 1 << 0,
|
|
DIAL_TARGET_BETA = 1 << 1,
|
|
DIAL_TARGET_BOTH = DIAL_TARGET_ALPHA | DIAL_TARGET_BETA
|
|
} DialTarget;
|
|
|
|
|
|
struct _GimpDialPrivate
|
|
{
|
|
gdouble alpha;
|
|
gdouble beta;
|
|
gboolean clockwise_angles;
|
|
gboolean clockwise_delta;
|
|
gboolean draw_beta;
|
|
|
|
DialTarget target;
|
|
gdouble last_angle;
|
|
};
|
|
|
|
|
|
static void gimp_dial_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec);
|
|
static void gimp_dial_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec);
|
|
|
|
static gboolean gimp_dial_draw (GtkWidget *widget,
|
|
cairo_t *cr);
|
|
static gboolean gimp_dial_button_press_event (GtkWidget *widget,
|
|
GdkEventButton *bevent);
|
|
static gboolean gimp_dial_motion_notify_event (GtkWidget *widget,
|
|
GdkEventMotion *mevent);
|
|
|
|
static void gimp_dial_reset_target (GimpCircle *circle);
|
|
|
|
static void gimp_dial_set_target (GimpDial *dial,
|
|
DialTarget target);
|
|
|
|
static void gimp_dial_draw_arrows (cairo_t *cr,
|
|
gint size,
|
|
gdouble alpha,
|
|
gdouble beta,
|
|
gboolean clockwise_delta,
|
|
DialTarget highlight,
|
|
gboolean draw_beta);
|
|
|
|
static gdouble gimp_dial_normalize_angle (gdouble angle);
|
|
static gdouble gimp_dial_get_angle_distance (gdouble alpha,
|
|
gdouble beta);
|
|
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE (GimpDial, gimp_dial, GIMP_TYPE_CIRCLE)
|
|
|
|
#define parent_class gimp_dial_parent_class
|
|
|
|
|
|
static void
|
|
gimp_dial_class_init (GimpDialClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
GimpCircleClass *circle_class = GIMP_CIRCLE_CLASS (klass);
|
|
|
|
object_class->get_property = gimp_dial_get_property;
|
|
object_class->set_property = gimp_dial_set_property;
|
|
|
|
widget_class->draw = gimp_dial_draw;
|
|
widget_class->button_press_event = gimp_dial_button_press_event;
|
|
widget_class->motion_notify_event = gimp_dial_motion_notify_event;
|
|
|
|
circle_class->reset_target = gimp_dial_reset_target;
|
|
|
|
g_object_class_install_property (object_class, PROP_ALPHA,
|
|
g_param_spec_double ("alpha",
|
|
NULL, NULL,
|
|
0.0, 2 * G_PI, 0.0,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
|
|
g_object_class_install_property (object_class, PROP_BETA,
|
|
g_param_spec_double ("beta",
|
|
NULL, NULL,
|
|
0.0, 2 * G_PI, G_PI,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
|
|
g_object_class_install_property (object_class, PROP_CLOCKWISE_ANGLES,
|
|
g_param_spec_boolean ("clockwise-angles",
|
|
NULL, NULL,
|
|
FALSE,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
|
|
g_object_class_install_property (object_class, PROP_CLOCKWISE_DELTA,
|
|
g_param_spec_boolean ("clockwise-delta",
|
|
NULL, NULL,
|
|
FALSE,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
|
|
g_object_class_install_property (object_class, PROP_DRAW_BETA,
|
|
g_param_spec_boolean ("draw-beta",
|
|
NULL, NULL,
|
|
TRUE,
|
|
GIMP_PARAM_READWRITE |
|
|
G_PARAM_CONSTRUCT));
|
|
}
|
|
|
|
static void
|
|
gimp_dial_init (GimpDial *dial)
|
|
{
|
|
dial->priv = gimp_dial_get_instance_private (dial);
|
|
}
|
|
|
|
static void
|
|
gimp_dial_set_property (GObject *object,
|
|
guint property_id,
|
|
const GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GimpDial *dial = GIMP_DIAL (object);
|
|
|
|
switch (property_id)
|
|
{
|
|
case PROP_ALPHA:
|
|
dial->priv->alpha = g_value_get_double (value);
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
break;
|
|
|
|
case PROP_BETA:
|
|
dial->priv->beta = g_value_get_double (value);
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
break;
|
|
|
|
case PROP_CLOCKWISE_ANGLES:
|
|
dial->priv->clockwise_angles = g_value_get_boolean (value);
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
break;
|
|
|
|
case PROP_CLOCKWISE_DELTA:
|
|
dial->priv->clockwise_delta = g_value_get_boolean (value);
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
break;
|
|
|
|
case PROP_DRAW_BETA:
|
|
dial->priv->draw_beta = g_value_get_boolean (value);
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
gimp_dial_get_property (GObject *object,
|
|
guint property_id,
|
|
GValue *value,
|
|
GParamSpec *pspec)
|
|
{
|
|
GimpDial *dial = GIMP_DIAL (object);
|
|
|
|
switch (property_id)
|
|
{
|
|
case PROP_ALPHA:
|
|
g_value_set_double (value, dial->priv->alpha);
|
|
break;
|
|
|
|
case PROP_BETA:
|
|
g_value_set_double (value, dial->priv->beta);
|
|
break;
|
|
|
|
case PROP_CLOCKWISE_ANGLES:
|
|
g_value_set_boolean (value, dial->priv->clockwise_angles);
|
|
break;
|
|
|
|
case PROP_CLOCKWISE_DELTA:
|
|
g_value_set_boolean (value, dial->priv->clockwise_delta);
|
|
break;
|
|
|
|
case PROP_DRAW_BETA:
|
|
g_value_set_boolean (value, dial->priv->draw_beta);
|
|
break;
|
|
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gimp_dial_draw (GtkWidget *widget,
|
|
cairo_t *cr)
|
|
{
|
|
GimpDial *dial = GIMP_DIAL (widget);
|
|
GtkAllocation allocation;
|
|
gint size;
|
|
gdouble alpha = dial->priv->alpha;
|
|
gdouble beta = dial->priv->beta;
|
|
|
|
GTK_WIDGET_CLASS (parent_class)->draw (widget, cr);
|
|
|
|
g_object_get (widget,
|
|
"size", &size,
|
|
NULL);
|
|
|
|
gtk_widget_get_allocation (widget, &allocation);
|
|
|
|
if (dial->priv->clockwise_angles)
|
|
{
|
|
alpha = -alpha;
|
|
beta = -beta;
|
|
}
|
|
|
|
cairo_save (cr);
|
|
|
|
cairo_translate (cr,
|
|
(allocation.width - size) / 2.0,
|
|
(allocation.height - size) / 2.0);
|
|
|
|
gimp_dial_draw_arrows (cr, size,
|
|
alpha, beta,
|
|
dial->priv->clockwise_delta,
|
|
dial->priv->target,
|
|
dial->priv->draw_beta);
|
|
|
|
cairo_restore (cr);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
gimp_dial_button_press_event (GtkWidget *widget,
|
|
GdkEventButton *bevent)
|
|
{
|
|
GimpDial *dial = GIMP_DIAL (widget);
|
|
|
|
if (bevent->type == GDK_BUTTON_PRESS &&
|
|
bevent->button == 1 &&
|
|
dial->priv->target != DIAL_TARGET_NONE)
|
|
{
|
|
gdouble angle;
|
|
|
|
GTK_WIDGET_CLASS (parent_class)->button_press_event (widget, bevent);
|
|
|
|
angle = _gimp_circle_get_angle_and_distance (GIMP_CIRCLE (dial),
|
|
bevent->x, bevent->y,
|
|
NULL);
|
|
|
|
if (dial->priv->clockwise_angles && angle)
|
|
angle = 2.0 * G_PI - angle;
|
|
|
|
if (bevent->state & GDK_SHIFT_MASK)
|
|
angle = SNAP (angle, G_PI / 12.0);
|
|
|
|
dial->priv->last_angle = angle;
|
|
|
|
switch (dial->priv->target)
|
|
{
|
|
case DIAL_TARGET_ALPHA:
|
|
g_object_set (dial, "alpha", angle, NULL);
|
|
break;
|
|
|
|
case DIAL_TARGET_BETA:
|
|
g_object_set (dial, "beta", angle, NULL);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static gboolean
|
|
gimp_dial_motion_notify_event (GtkWidget *widget,
|
|
GdkEventMotion *mevent)
|
|
{
|
|
GimpDial *dial = GIMP_DIAL (widget);
|
|
gdouble angle;
|
|
gdouble distance;
|
|
|
|
angle = _gimp_circle_get_angle_and_distance (GIMP_CIRCLE (dial),
|
|
mevent->x, mevent->y,
|
|
&distance);
|
|
|
|
if (dial->priv->clockwise_angles && angle)
|
|
angle = 2.0 * G_PI - angle;
|
|
|
|
if (_gimp_circle_has_grab (GIMP_CIRCLE (dial)))
|
|
{
|
|
gdouble delta;
|
|
|
|
if (mevent->state & GDK_SHIFT_MASK)
|
|
angle = SNAP (angle, G_PI / 12.0);
|
|
|
|
delta = angle - dial->priv->last_angle;
|
|
dial->priv->last_angle = angle;
|
|
|
|
if (delta != 0.0)
|
|
{
|
|
gdouble alpha = dial->priv->alpha;
|
|
|
|
switch (dial->priv->target)
|
|
{
|
|
case DIAL_TARGET_ALPHA:
|
|
g_object_set (dial, "alpha", angle, NULL);
|
|
break;
|
|
|
|
case DIAL_TARGET_BETA:
|
|
g_object_set (dial, "beta", angle, NULL);
|
|
break;
|
|
|
|
case DIAL_TARGET_BOTH:
|
|
/* snap both by the alpha value */
|
|
if (mevent->state & GDK_SHIFT_MASK)
|
|
delta = SNAP (alpha + delta, G_PI / 12.0) - alpha;
|
|
|
|
g_object_set (dial,
|
|
"alpha", gimp_dial_normalize_angle (dial->priv->alpha + delta),
|
|
"beta", gimp_dial_normalize_angle (dial->priv->beta + delta),
|
|
NULL);
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DialTarget target;
|
|
gdouble dist_alpha;
|
|
gdouble dist_beta;
|
|
|
|
dist_alpha = gimp_dial_get_angle_distance (dial->priv->alpha, angle);
|
|
dist_beta = gimp_dial_get_angle_distance (dial->priv->beta, angle);
|
|
|
|
if (dial->priv->draw_beta &&
|
|
distance > SEGMENT_FRACTION &&
|
|
MIN (dist_alpha, dist_beta) < G_PI / 12)
|
|
{
|
|
if (dist_alpha < dist_beta)
|
|
{
|
|
target = DIAL_TARGET_ALPHA;
|
|
}
|
|
else
|
|
{
|
|
target = DIAL_TARGET_BETA;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
target = DIAL_TARGET_BOTH;
|
|
}
|
|
|
|
gimp_dial_set_target (dial, target);
|
|
}
|
|
|
|
gdk_event_request_motions (mevent);
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
gimp_dial_reset_target (GimpCircle *circle)
|
|
{
|
|
gimp_dial_set_target (GIMP_DIAL (circle), DIAL_TARGET_NONE);
|
|
}
|
|
|
|
|
|
/* public functions */
|
|
|
|
GtkWidget *
|
|
gimp_dial_new (void)
|
|
{
|
|
return g_object_new (GIMP_TYPE_DIAL, NULL);
|
|
}
|
|
|
|
|
|
/* private functions */
|
|
|
|
static void
|
|
gimp_dial_set_target (GimpDial *dial,
|
|
DialTarget target)
|
|
{
|
|
if (target != dial->priv->target)
|
|
{
|
|
dial->priv->target = target;
|
|
gtk_widget_queue_draw (GTK_WIDGET (dial));
|
|
}
|
|
}
|
|
|
|
static void
|
|
gimp_dial_draw_arrow (cairo_t *cr,
|
|
gdouble radius,
|
|
gdouble angle)
|
|
{
|
|
#define REL 0.8
|
|
#define DEL 0.1
|
|
|
|
cairo_move_to (cr, radius, radius);
|
|
cairo_line_to (cr,
|
|
radius + radius * cos (angle),
|
|
radius - radius * sin (angle));
|
|
|
|
cairo_move_to (cr,
|
|
radius + radius * cos (angle),
|
|
radius - radius * sin (angle));
|
|
cairo_line_to (cr,
|
|
radius + radius * REL * cos (angle - DEL),
|
|
radius - radius * REL * sin (angle - DEL));
|
|
|
|
cairo_move_to (cr,
|
|
radius + radius * cos (angle),
|
|
radius - radius * sin (angle));
|
|
cairo_line_to (cr,
|
|
radius + radius * REL * cos (angle + DEL),
|
|
radius - radius * REL * sin (angle + DEL));
|
|
}
|
|
|
|
static void
|
|
gimp_dial_draw_segment (cairo_t *cr,
|
|
gdouble radius,
|
|
gdouble alpha,
|
|
gdouble beta,
|
|
gboolean clockwise_delta)
|
|
{
|
|
gint direction = clockwise_delta ? -1 : 1;
|
|
gint segment_dist;
|
|
gint tick;
|
|
gdouble slice;
|
|
|
|
segment_dist = radius * SEGMENT_FRACTION;
|
|
tick = MIN (10, segment_dist);
|
|
|
|
cairo_move_to (cr,
|
|
radius + segment_dist * cos (beta),
|
|
radius - segment_dist * sin (beta));
|
|
cairo_line_to (cr,
|
|
radius + segment_dist * cos (beta) +
|
|
direction * tick * sin (beta),
|
|
radius - segment_dist * sin (beta) +
|
|
direction * tick * cos (beta));
|
|
|
|
cairo_new_sub_path (cr);
|
|
|
|
if (clockwise_delta)
|
|
slice = -gimp_dial_normalize_angle (alpha - beta);
|
|
else
|
|
slice = gimp_dial_normalize_angle (beta - alpha);
|
|
|
|
gimp_cairo_arc (cr, radius, radius, segment_dist,
|
|
alpha, slice);
|
|
}
|
|
|
|
static void
|
|
gimp_dial_draw_arrows (cairo_t *cr,
|
|
gint size,
|
|
gdouble alpha,
|
|
gdouble beta,
|
|
gboolean clockwise_delta,
|
|
DialTarget highlight,
|
|
gboolean draw_beta)
|
|
{
|
|
gdouble radius = size / 2.0 - 2.0; /* half the broad line with and half a px */
|
|
|
|
cairo_save (cr);
|
|
|
|
cairo_translate (cr, 2.0, 2.0); /* half the broad line width and half a px*/
|
|
|
|
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
|
|
cairo_set_line_join (cr, CAIRO_LINE_JOIN_ROUND);
|
|
|
|
if (highlight != DIAL_TARGET_BOTH)
|
|
{
|
|
if (! (highlight & DIAL_TARGET_ALPHA))
|
|
gimp_dial_draw_arrow (cr, radius, alpha);
|
|
|
|
if (draw_beta)
|
|
{
|
|
if (! (highlight & DIAL_TARGET_BETA))
|
|
gimp_dial_draw_arrow (cr, radius, beta);
|
|
|
|
if ((highlight & DIAL_TARGET_BOTH) != DIAL_TARGET_BOTH)
|
|
gimp_dial_draw_segment (cr, radius, alpha, beta, clockwise_delta);
|
|
}
|
|
|
|
cairo_set_line_width (cr, 3.0);
|
|
cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.6);
|
|
cairo_stroke_preserve (cr);
|
|
|
|
cairo_set_line_width (cr, 1.0);
|
|
cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.8);
|
|
cairo_stroke (cr);
|
|
}
|
|
|
|
if (highlight != DIAL_TARGET_NONE)
|
|
{
|
|
if (highlight & DIAL_TARGET_ALPHA)
|
|
gimp_dial_draw_arrow (cr, radius, alpha);
|
|
|
|
if (draw_beta)
|
|
{
|
|
if (highlight & DIAL_TARGET_BETA)
|
|
gimp_dial_draw_arrow (cr, radius, beta);
|
|
|
|
if ((highlight & DIAL_TARGET_BOTH) == DIAL_TARGET_BOTH)
|
|
gimp_dial_draw_segment (cr, radius, alpha, beta, clockwise_delta);
|
|
}
|
|
|
|
cairo_set_line_width (cr, 3.0);
|
|
cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.6);
|
|
cairo_stroke_preserve (cr);
|
|
|
|
cairo_set_line_width (cr, 1.0);
|
|
cairo_set_source_rgba (cr, 1.0, 1.0, 1.0, 0.8);
|
|
cairo_stroke (cr);
|
|
}
|
|
|
|
cairo_restore (cr);
|
|
}
|
|
|
|
static gdouble
|
|
gimp_dial_normalize_angle (gdouble angle)
|
|
{
|
|
if (angle < 0)
|
|
return angle + 2 * G_PI;
|
|
else if (angle > 2 * G_PI)
|
|
return angle - 2 * G_PI;
|
|
else
|
|
return angle;
|
|
}
|
|
|
|
static gdouble
|
|
gimp_dial_get_angle_distance (gdouble alpha,
|
|
gdouble beta)
|
|
{
|
|
return ABS (MIN (gimp_dial_normalize_angle (alpha - beta),
|
|
2 * G_PI - gimp_dial_normalize_angle (alpha - beta)));
|
|
}
|