The Big Commit (tm): Remove Cairo.Color and most of Gdk.Color usage from libprolooks
[libprolooks.git] / src / Knob.vala
blob384121492abceed033af3aea6d31a7e6ca8cb276
1 /*
2 Copyright 2009 by Hans Baier, Krzysztof Foltman
3 License: LGPLv2+
4 */
6 using Gtk;
8 namespace Prolooks {
10 static const uint16 GDK_Home = 0xff50;
11 static const uint16 GDK_End = 0xff57;
12 static const uint16 GDK_Up = 0xff52;
13 static const uint16 GDK_Down = 0xff54;
14 static const uint16 GDK_Shift_L = 0xffe1;
15 static const uint16 GDK_Shift_R = 0xffe2;
17 public enum KnobMode {
18 POSITIVE,
19 BIPOLAR,
20 NEGATIVE,
21 ENDLESS
24 public class Knob : Range {
25 private static HashTable<string, Cairo.Surface> images = null;
28 private Cairo.Surface? cache_surface = null;
30 public void* user_data { get; set; }
31 private IKnobImageSource _image_source;
32 [Description(nick="image source", blurb="provides the routines for drawing the knob images")]
33 public IKnobImageSource image_source {
34 get {
35 if (_image_source == null) {
36 var isource = new SimpleKnobImageSource ();
37 isource.led_color = rgba_from_string ("#9fc717");
38 _image_source = isource;
39 set_size_request((int)image_source.get_knob_width (), (int)image_source.get_knob_height ());
41 return _image_source;
44 set {
45 if (value != null && value != _image_source) {
46 _image_source = value;
47 cache_surface = null;
48 set_size_request((int)image_source.get_knob_width (), (int)image_source.get_knob_height ());
49 queue_draw ();
54 private KnobMode _knob_mode;
55 [Description(nick="knob mode", blurb="determines, how the knob will work / look like")]
56 public KnobMode knob_mode {
57 get {
58 return _knob_mode;
60 set {
61 if (value != _knob_mode) {
62 _knob_mode = value;
63 cache_surface = null;
64 queue_draw ();
69 [Description(nick="decimal places", blurb="The value will be rounded to the given number of decimal places")]
70 public uint decimal_places { get; set; default = 2; }
72 double start_value;
73 int start_x;
74 int start_y;
75 int last_y;
77 static construct {
78 images = new HashTable<string, Cairo.Surface> (GLib.str_hash, GLib.str_equal);
81 construct {
82 Adjustment adj = new Adjustment(0, 0, 1, 0.01, 0.5, 0);
83 set_adjustment (adj);
84 set_can_focus(true);
85 add_events (Gdk.EventMask.KEY_PRESS_MASK | Gdk.EventMask.KEY_RELEASE_MASK );
86 draw.connect(on_draw);
87 motion_notify_event.connect(on_motion_notify_event);
90 public Knob.with_adjustment (Adjustment an_adjustment) {
91 set_adjustment (an_adjustment);
94 Cairo.Surface create_cache_surface_if_necessary (Cairo.Surface target) {
95 string image_state = "%s;knob_mode=%u".printf (image_source.to_string (), knob_mode);
97 debug ("looking up state: '%s', hash: '%u'\n", image_state, GLib.str_hash(image_state));
98 Cairo.Surface? cached_surface = images.lookup (image_state);
99 debug ("Result: %u\n", (uint)cached_surface);
100 if ( cached_surface != null) {
101 debug ("Cache hit!\n");
102 return cached_surface;
105 debug ("Cache miss!\n");
107 // looks like its either first call or the widget has been resized.
108 // create the cache_surface.
109 var cache_width = image_source.get_knob_width () * image_source.phases;
110 var cache_height = image_source.get_knob_height ();
112 Cairo.Surface surface = new Cairo.Surface.similar (target,
113 Cairo.Content.COLOR_ALPHA,
114 (int)cache_width,
115 (int)cache_height);
117 Cairo.Context cache_cr = new Cairo.Context ( surface );
118 cache_cr.set_source_rgba (0, 0, 0, 0);
119 cache_cr.rectangle (0, 0, cache_width, cache_height);
120 cache_cr.fill ();
122 image_source.paint_knobs (cache_cr,
123 knob_mode,
124 image_source.get_knob_width () / 2,
125 image_source.get_knob_height () / 2);
127 images.insert (image_state.dup(), surface);
128 debug ("HashTable size: %u\n", images.size());
129 images.foreach ((k, v) => { debug ("Key: '%s', Value: %x\n", (string)k, (uint)v); });
130 return surface;
133 public bool on_draw (Cairo.Context cr) {
134 Gtk.Allocation allocation;
135 get_allocation(out allocation);
136 Adjustment adj = get_adjustment ();
138 debug ("adjustment = %p value = %f\n", adj, adj.value);
139 int ox = allocation.x, oy = allocation.y;
141 int phase = (int)((adj.value - adj.lower) * 64 / (adj.upper - adj.lower));
142 // skip middle phase except for true middle value
143 if (knob_mode == KnobMode.BIPOLAR && phase == 32) {
144 double pt = (adj.value - adj.lower) * 2.0 / (adj.upper - adj.lower) - 1.0;
146 if (pt < 0)
147 phase = 31;
149 if (pt > 0)
150 phase = 33;
153 // endless knob: skip 90deg highlights unless the value is really a multiple of 90deg
154 if (knob_mode == KnobMode.ENDLESS && (phase % 16) == 0) {
155 if (phase == 64)
156 phase = 0;
158 double nom = adj.lower + phase * (adj.upper - adj.lower) / 64.0;
159 double diff = (adj.value - nom) / (adj.upper - adj.lower);
161 if (diff > 0.0001)
162 phase = (phase + 1) % 64;
164 if (diff < -0.0001)
165 phase = (phase + 63) % 64;
168 if (cache_surface == null) {
169 cache_surface = create_cache_surface_if_necessary (cr.get_target ());
172 assert (cache_surface != null);
173 cr.set_source_surface (cache_surface, - phase * image_source.get_knob_width (), oy);
174 cr.rectangle (0, 0, image_source.get_knob_width (), image_source.get_knob_height ());
175 cr.fill ();
177 debug ("exposed %p %d+%d", get_window(), allocation.x, allocation.y);
178 if (is_focus) {
179 get_style_context().render_focus (
180 cr,
181 0, 0,
182 (int)image_source.get_knob_width (), (int)image_source.get_knob_height ());
185 return true;
188 public new virtual void get_preferred_width (out int minimum_width, out int natural_width) {
189 minimum_width = (int)image_source.get_knob_width ();
190 natural_width = (int)image_source.get_knob_width ();
193 public new virtual void get_preferred_height_for_width (int width, out int minimum_height, out int natural_height) {
194 minimum_height = (int)image_source.get_knob_height ();
195 natural_height = (int)image_source.get_knob_height ();
198 public new virtual void get_preferred_height (out int minimum_height, out int natural_height) {
199 minimum_height = (int)image_source.get_knob_height ();
200 natural_height = (int)image_source.get_knob_height ();
203 public new virtual void get_preferred_width_for_height (int height, out int minimum_width, out int natural_width) {
204 minimum_width = (int)image_source.get_knob_width ();
205 natural_width = (int)image_source.get_knob_width ();
208 public new double get_value () {
209 debug ("Value: %1.30f\n", base.get_value());
210 var rounded = GLib.Math.rint (base.get_value() * Math.pow (10.0, decimal_places))
211 / Math.pow (10.0, decimal_places);
212 debug ("Rounded Value: %1.30f\n", rounded);
213 return rounded;
216 void knob_incr (int dir_down) {
217 Adjustment adj = get_adjustment ();
219 int oldstep = (int)(0.5 + (adj.value - adj.lower) / adj.step_increment);
220 int step;
221 int nsteps = (int)(0.5 + (adj.upper - adj.lower) / adj.step_increment); // less 1 actually
222 if (dir_down != 0)
223 step = oldstep - 1;
224 else
225 step = oldstep + 1;
226 if (knob_mode == KnobMode.ENDLESS && step >= nsteps)
227 step %= nsteps;
228 if (knob_mode == KnobMode.ENDLESS && step < 0)
229 step = nsteps - (nsteps - step) % nsteps;
231 // trying to reduce error cumulation here, by counting from lowest or from highest
232 double value = (adj.lower + step * (adj.upper - adj.lower) / nsteps);
233 set_value (value);
234 // debug ("step %d:%d nsteps %d value %f:%f\n", oldstep, step, nsteps, oldvalue, value);
238 public override bool key_press_event (Gdk.EventKey event) {
239 Adjustment adj = get_adjustment ();
241 switch (event.keyval)
243 case GDK_Home:
244 set_value (adj.lower);
245 return true;
247 case GDK_End:
248 set_value (adj.upper);
249 return true;
251 case GDK_Up:
252 knob_incr (0);
253 return true;
255 case GDK_Down:
256 knob_incr (1);
257 return true;
259 case GDK_Shift_L:
260 case GDK_Shift_R:
261 start_value = (int)get_value ();
262 start_y = last_y;
263 return true;
266 return false;
269 private Gdk.Cursor? cursor = null;
271 public override bool key_release_event (Gdk.EventKey event) {
272 if (event.keyval == GDK_Shift_L || event.keyval == GDK_Shift_R) {
273 start_value = (int)get_value ();
274 start_y = last_y;
275 return true;
278 return false;
281 public override bool button_press_event (Gdk.EventButton event)
283 grab_focus ();
284 grab_add (this);
285 start_x = (int)event.x;
286 start_y = (int)event.y;
287 start_value = get_value ();
288 debug ("PRESS: start_value: %f, event.y: %d, start_y: %d", start_value, (int)event.y, (int)start_y);
290 cursor = get_window().get_cursor ();
291 get_window().set_cursor (new Gdk.Cursor (Gdk.CursorType.SB_V_DOUBLE_ARROW));
292 return true;
295 public override bool button_release_event (Gdk.EventButton event)
297 if (has_grab ()) {
298 grab_remove (this);
300 if (cursor != null)
301 get_window().set_cursor (cursor);
302 else
303 get_window().set_cursor (new Gdk.Cursor (Gdk.CursorType.ARROW));
305 return false;
308 inline double endless (double value)
310 if (value >= 0.0)
311 return Math.fmod (value, 1.0);
312 else
313 return Math.fmod (1.0 - Math.fmod (1.0 - value, 1.0), 1.0);
316 inline double deadzone (double value, double incr, double scale)
318 double dzw = 10.0 / scale;
319 if (value >= 0.501)
320 value += dzw;
321 if (value < 0.499)
322 value -= dzw;
324 value += incr;
326 if (value >= (0.5 - dzw) && value <= (0.5 + dzw))
327 return 0.5;
328 if (value < 0.5)
329 return value + dzw;
330 return value - dzw;
333 public bool on_motion_notify_event (Gdk.EventMotion event) {
334 Adjustment adj = get_adjustment ();
335 double scale = (event.state == Gdk.ModifierType.SHIFT_MASK) ? 1000.0 : 100.0;
336 scale /= (adj.upper - adj.lower);
337 bool moved = false;
339 if (has_grab ())
341 //debug ("start_value: %d, event.y: %d, start_y: %d", (int)start_value, (int)event.y, (int)start_y);
342 if (knob_mode == KnobMode.ENDLESS) {
343 set_value (endless ((start_value - (event.y - start_y) / scale)));
344 } else if (knob_mode == KnobMode.BIPOLAR) {
345 set_value (deadzone (start_value, (-(event.y - start_y) / scale), scale));
346 } else {
347 set_value (start_value - (event.y - start_y) / scale);
349 moved = true;
351 last_y = (int)event.y;
352 return moved;
355 public override bool scroll_event (Gdk.EventScroll event) {
356 knob_incr (event.direction);
357 return true;
360 } // class Knob
362 } // namespace Prolooks