/* * phatsvgknob.c * * A GTK+ widget that implements a knob face * * (C) 2007, Ulrich Lorenz Schlueter * * derived from: * http://www.gnomejournal.org/article/34/writing-a-widget-using-cairo-and-gtk28 * http://gnomejournal.org/article/36/writing-a-widget-using-cairo-and-gtk28-part-2 * * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Authors: * Ulrich Lorenz Schlueter */ #include #include #include #include "phatsvgknob.h" #include "phatsvgknob-marshallers.h" #define PHAT_SVG_KNOB_FACE_GET_PRIVATE(obj) (G_TYPE_INSTANCE_GET_PRIVATE ((obj), PHAT_TYPE_SVG_KNOB_FACE, PhatSvgKnobFacePrivate)) G_DEFINE_TYPE (PhatSvgKnobFace, phat_svg_knob_face, GTK_TYPE_DRAWING_AREA); static gboolean phat_svg_knob_face_expose (GtkWidget *knob, GdkEventExpose *event); static gboolean phat_svg_knob_face_button_press (GtkWidget *knob, GdkEventButton *event); static gboolean phat_svg_knob_face_button_release (GtkWidget *knob, GdkEventButton *event); static gboolean phat_svg_knob_face_motion_notify (GtkWidget *knob, GdkEventMotion *event); static gboolean phat_svg_knob_face_scroll (GtkWidget *knob, GdkEventScroll *event); static gboolean phat_svg_knob_face_update (gpointer data); typedef struct _PhatSvgKnobFacePrivate PhatSvgKnobFacePrivate; struct _PhatSvgKnobFacePrivate { double decibel; /* the decibels */ double decibel_offset; /* the offset of the decibels */ gboolean dragging; /* true if the interface is being dragged */ }; enum { VALUE_CHANGED, LAST_SIGNAL, }; static guint phat_svg_knob_face_signals[LAST_SIGNAL] = { 0 }; static void phat_svg_knob_face_class_init (PhatSvgKnobFaceClass *class) { GObjectClass *obj_class; GtkWidgetClass *widget_class; obj_class = G_OBJECT_CLASS (class); widget_class = GTK_WIDGET_CLASS (class); /* GtkWidget signals */ widget_class->expose_event = phat_svg_knob_face_expose; widget_class->button_press_event = phat_svg_knob_face_button_press; widget_class->button_release_event = phat_svg_knob_face_button_release; widget_class->motion_notify_event = phat_svg_knob_face_motion_notify; widget_class->scroll_event = phat_svg_knob_face_scroll; /* PhatSvgKnobFace signals */ phat_svg_knob_face_signals[VALUE_CHANGED] = g_signal_new ( "value-changed", G_OBJECT_CLASS_TYPE (obj_class), G_SIGNAL_RUN_FIRST, G_STRUCT_OFFSET (PhatSvgKnobFaceClass, value_changed), NULL, NULL, _phatsvgknob_marshal_VOID__INT_INT, G_TYPE_NONE, 2, G_TYPE_INT, G_TYPE_INT); g_type_class_add_private (obj_class, sizeof (PhatSvgKnobFacePrivate)); } static void phat_svg_knob_face_init (PhatSvgKnobFace *knob) { gtk_widget_add_events (GTK_WIDGET (knob), GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_SCROLL_MASK |GDK_POINTER_MOTION_MASK); phat_svg_knob_face_update (knob); } static void draw (GtkWidget *knob, cairo_t *cr) { PhatSvgKnobFacePrivate *priv; double x, y; double radius; int i; double decibels; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); x = knob->allocation.width / 2; y = knob->allocation.height / 2; radius = MIN (knob->allocation.width / 2, knob->allocation.height / 2) - 10; /* knob back */ cairo_arc (cr, x, y, radius, 0, 2 * M_PI); cairo_set_source_rgb (cr, 0, 0, 0); cairo_fill_preserve(cr); cairo_set_source_rgb (cr, 1, 0.5, 0); cairo_stroke (cr); cairo_save (cr); /* knob ticks */ for (i = 6; i < 25; i++) { int inset; cairo_save (cr); /* stack-pen-size */ if ( i % 3 == 0 ) { inset = 0.075 * radius; cairo_set_line_width (cr, 1.5 * cairo_get_line_width (cr)); } else { inset = 0.05 * radius; cairo_set_line_width (cr, 1 * cairo_get_line_width (cr)); } cairo_move_to (cr, x + (radius + inset) * cos (i * M_PI / 10), y + (radius + inset) * sin (i * M_PI / 10)); cairo_line_to (cr, x + radius * cos (i * M_PI / 10), y + radius * sin (i * M_PI / 10)); cairo_stroke (cr); cairo_restore (cr); /* stack-pen-size */ } /* knob hands */ decibels = priv->decibel + priv->decibel_offset; /* decibel hand: * the decibel hand is rotated 6 degrees (pi/45 r) per decibel */ cairo_move_to (cr, x, y); cairo_set_line_width (cr, 2.3 * cairo_get_line_width (cr)); cairo_line_to (cr, x + radius * sin (M_PI / PHAT_SVG_KNOB_DEFAULT_ADJUST * decibels), y + radius * -cos (M_PI / PHAT_SVG_KNOB_DEFAULT_ADJUST * decibels)); cairo_stroke (cr); cairo_restore (cr); } static gboolean phat_svg_knob_face_expose (GtkWidget *knob, GdkEventExpose *event) { cairo_t *cr; /* get a cairo_t */ cr = gdk_cairo_create(knob->window); cairo_rectangle (cr, event->area.x, event->area.y, event->area.width, event->area.height); cairo_clip (cr); draw (knob, cr); cairo_destroy (cr); return FALSE; } static gboolean phat_svg_knob_face_button_press (GtkWidget *knob, GdkEventButton *event) { PhatSvgKnobFacePrivate *priv; double decibels; double lx, ly; double px, py; double u, d2; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); decibels = priv->decibel + priv->decibel_offset; /* From * http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html */ px = event->x - knob->allocation.width / 2; py = knob->allocation.height / 2 - event->y; lx = sin (M_PI / PHAT_SVG_KNOB_DEFAULT_ADJUST * decibels); ly = cos (M_PI / PHAT_SVG_KNOB_DEFAULT_ADJUST * decibels); u = lx * px + ly * py; /* on opposite side of origin */ if (u < 0) return FALSE; d2 = pow (px - u * lx, 2) + pow (py - u * ly, 2); if (d2 < 25) /* 5 pixels away from the line */ { priv->dragging = TRUE; } return FALSE; } static void phat_svg_knob_face_redraw_canvas (PhatSvgKnobFace *knob) { GtkWidget *widget; GdkRegion *region; widget = GTK_WIDGET (knob); if (!widget->window) return; region = gdk_drawable_get_clip_region (widget->window); /* redraw the cairo canvas completely by exposing it */ gdk_window_invalidate_region (widget->window, region, TRUE); gdk_window_process_updates (widget->window, TRUE); gdk_region_destroy (region); } static emit_value_changed_signal (PhatSvgKnobFace *knob, int x, int y) { PhatSvgKnobFacePrivate *priv; double phi; double decibels; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); /* decode the decibel hand */ /* normalise the coordinates around the origin */ x -= GTK_WIDGET (knob)->allocation.width / 2; y -= GTK_WIDGET (knob)->allocation.height / 2; /* phi is a bearing from north clockwise, use the same geometry as we * did to position the decibel hand originally */ phi = atan2 (x, -y); if (phi < 0) phi += M_PI * 2; decibels = phi * PHAT_SVG_KNOB_DEFAULT_ADJUST / M_PI; if(decibels<=5){ /* Stop drawing on min */ priv->decibel=5; priv->decibel_offset = 0; decibels=5; } else if(decibels>=85){ /* Stop drawing on max */ priv->decibel=85; priv->decibel_offset = 0; decibels=85; } else if(decibels>=43 && decibels<=47){ /* Magnetic on 45 */ priv->decibel=45; priv->decibel_offset = 0; decibels=45; } else { /* update the offset */ priv->decibel_offset = decibels - priv->decibel; priv->decibel= decibels; } phat_svg_knob_face_redraw_canvas (knob); g_signal_emit (knob, phat_svg_knob_face_signals[VALUE_CHANGED], 0, decibels); } static emit_scroll_event_signal (PhatSvgKnobFace *knob, int direction) { PhatSvgKnobFacePrivate *priv; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); if(direction==0){ priv->decibel = priv->decibel + 1; } else if(direction==1){ priv->decibel = priv->decibel -1; } else if(direction==2){ priv->decibel = priv->decibel + 5; } else { priv->decibel = priv->decibel -5; } /* Stop drawing on min */ if(priv->decibel<=5){ priv->decibel=5; } /* Stop drawing on max */ if (priv->decibel>=85){ priv->decibel=85; } phat_svg_knob_face_redraw_canvas(knob); g_signal_emit (knob, phat_svg_knob_face_signals[VALUE_CHANGED], 0, priv->decibel); } static gboolean phat_svg_knob_face_scroll (GtkWidget *knob, GdkEventScroll *event) { emit_scroll_event_signal (PHAT_SVG_KNOB_FACE (knob), event->direction); return FALSE; } static gboolean phat_svg_knob_face_motion_notify (GtkWidget *knob, GdkEventMotion *event) { PhatSvgKnobFacePrivate *priv; int x, y; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); if (priv->dragging) { emit_value_changed_signal (PHAT_SVG_KNOB_FACE (knob), event->x, event->y); } return FALSE; } static gboolean phat_svg_knob_face_button_release (GtkWidget *knob, GdkEventButton *event) { PhatSvgKnobFacePrivate *priv; priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); if (priv->dragging) { priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); priv->dragging = FALSE; emit_value_changed_signal (PHAT_SVG_KNOB_FACE (knob), event->x, event->y); } return FALSE; } static gboolean phat_svg_knob_face_update (gpointer data) { PhatSvgKnobFace *knob; PhatSvgKnobFacePrivate *priv; knob = PHAT_SVG_KNOB_FACE (data); priv = PHAT_SVG_KNOB_FACE_GET_PRIVATE (knob); phat_svg_knob_face_redraw_canvas (knob); return TRUE; /* keep running this event */ } GtkWidget * phat_svg_knob_face_new (void) { return g_object_new (PHAT_TYPE_SVG_KNOB_FACE, NULL); }