substantive changes to pixfaders and derived classes. now fully dynamically sizable...
[ardour.git] / libs / gtkmm2ext / pixfader.cc
1 /*
2     Copyright (C) 2006 Paul Davis 
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18     $Id: fastmeter.h 570 2006-06-07 21:21:21Z sampo $
19 */
20
21
22 #include <iostream>
23 #include "gtkmm2ext/pixfader.h"
24 #include "gtkmm2ext/keyboard.h"
25 #include "gtkmm2ext/rgb_macros.h"
26 #include "gtkmm2ext/utils.h"
27
28 using namespace Gtkmm2ext;
29 using namespace Gtk;
30 using namespace std;
31
32 #define CORNER_RADIUS 4
33 #define FADER_RESERVE (2*CORNER_RADIUS)
34
35 PixFader::PixFader (Gtk::Adjustment& adj, int orientation, int fader_length, int fader_girth)
36         : adjustment (adj)
37         , span (fader_length)
38         , girth (fader_girth)
39         , _orien (orientation)
40         , pattern (0)
41         , texture_pattern (0)
42         , _hovering (false)
43         , last_drawn (-1)
44         , dragging (false)
45 {
46         default_value = adjustment.get_value();
47         update_unity_position ();
48
49         add_events (Gdk::BUTTON_PRESS_MASK|Gdk::BUTTON_RELEASE_MASK|Gdk::POINTER_MOTION_MASK|Gdk::SCROLL_MASK|Gdk::ENTER_NOTIFY_MASK|Gdk::LEAVE_NOTIFY_MASK);
50
51         adjustment.signal_value_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
52         adjustment.signal_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
53 }
54
55 PixFader::~PixFader ()
56 {
57         free_patterns ();
58 }
59
60 void
61 PixFader::free_patterns ()
62 {
63         if (pattern) {
64                 cairo_pattern_destroy (pattern);
65                 pattern = 0;
66         }
67         if (texture_pattern) {
68                 cairo_pattern_destroy (texture_pattern);
69                 texture_pattern = 0;
70         }
71 }
72
73 void
74 PixFader::create_patterns ()
75 {
76         Gdk::Color c = get_style()->get_fg (get_state());
77         float r, g, b;
78
79         free_patterns ();
80
81         r = c.get_red_p ();
82         g = c.get_green_p ();
83         b = c.get_blue_p ();
84
85         cairo_surface_t* texture_surface;
86         cairo_t* tc = 0;
87         const double texture_margin = 3.0;
88
89         if (_orien == VERT) {
90
91                 pattern = cairo_pattern_create_linear (0.0, 0.0, get_width(), 0);
92                 cairo_pattern_add_color_stop_rgba (pattern, 0, r*0.8,g*0.8,b*0.8, 1.0);
93                 cairo_pattern_add_color_stop_rgba (pattern, 1, r*0.6,g*0.6,b*0.6, 1.0);
94
95                 if (girth > 10) {
96                         texture_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, girth, 6);
97                         tc = cairo_create (texture_surface);
98                         
99                         for (double x = texture_margin; x < girth - texture_margin; x += 3.0) {
100                                 cairo_set_source_rgba (tc, 0.533, 0.533, 0.580, 1.0);
101                                 cairo_rectangle (tc, x, 2, 2, 2);
102                                 cairo_fill (tc);
103                                 cairo_set_source_rgba (tc, 0.337, 0.345, 0.349, 1.0);
104                                 cairo_rectangle (tc, x, 2, 1, 1);
105                                 cairo_fill (tc);
106                         }
107                 }
108
109         } else {
110
111                 texture_surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, 6, girth);
112                 tc = cairo_create (texture_surface);
113
114                 pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, get_height());
115                 cairo_pattern_add_color_stop_rgba (pattern, 0, r*0.8,g*0.8,b*0.8, 1.0);
116                 cairo_pattern_add_color_stop_rgba (pattern, 1, r*0.6,g*0.6,b*0.6, 1.0);
117
118                 if (girth > 10) {
119                         for (double y = texture_margin; y < girth - texture_margin; y += 3) {
120                                 cairo_set_source_rgba (tc, 0.533, 0.533, 0.580, 1.0);
121                                 cairo_rectangle (tc, 0, y, 2, 2);
122                                 cairo_fill (tc);
123                                 cairo_set_source_rgba (tc, 0.337, 0.345, 0.349, 1.0);
124                                 cairo_rectangle (tc, 0, y, 1, 1);
125                                 cairo_fill (tc);
126                         }
127                 }
128         }
129
130         if (texture_surface) {
131                 texture_pattern = cairo_pattern_create_for_surface (texture_surface);
132                 cairo_pattern_set_extend (texture_pattern, CAIRO_EXTEND_REPEAT);
133                 
134                 cairo_destroy (tc);
135                 cairo_surface_destroy (texture_surface);
136         }
137
138         if ( !_text.empty()) {
139                 _layout->get_pixel_size (_text_width, _text_height);
140         } else {
141                 _text_width = 0;
142                 _text_height = 0;
143         }
144
145         c = get_style()->get_text (get_state());
146
147         text_r = c.get_red_p ();
148         text_g = c.get_green_p ();
149         text_b = c.get_blue_p ();
150 }
151
152 bool
153 PixFader::on_expose_event (GdkEventExpose* ev)
154 {
155         Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
156         cairo_t* cr = context->cobj();
157
158         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
159         cairo_clip (cr);
160
161         if (!pattern) {
162                 create_patterns();
163         }
164         
165 //      int const pi = get_sensitive() ? NORMAL : DESENSITISED;
166         
167         int ds = display_span ();
168
169         float w = get_width();
170         float h = get_height();
171         float radius = CORNER_RADIUS;
172
173         /* background/ border */
174         cairo_set_source_rgb (cr, 0.290, 0.286, 0.337);
175         cairo_rectangle (cr, 0, 0, w, h);
176         cairo_fill (cr);
177
178         /* draw active box */
179         
180         cairo_matrix_t matrix;
181
182         if (_orien == VERT) {
183
184                 if (ds > h - FADER_RESERVE) {
185                         ds = h - FADER_RESERVE;
186                 }
187
188                 cairo_set_source (cr, pattern);
189                 Gtkmm2ext::rounded_top_half_rectangle (cr, 1, 1+ds, w-1, h-(1+ds)-1, radius-1.5);
190                 cairo_fill (cr);
191                 
192                 if (texture_pattern) {
193                         cairo_save (cr);
194                         cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
195                         cairo_set_source (cr, texture_pattern);
196                         cairo_matrix_init_translate (&matrix, -1, -(1+ds));
197                         cairo_pattern_set_matrix (texture_pattern, &matrix);
198                         cairo_rectangle (cr, 1, 1+ds, w-1, h-(1+ds)-1);
199                         cairo_fill (cr);
200                         cairo_restore (cr);
201                 }
202
203         } else {
204
205                 if (ds < FADER_RESERVE) {
206                         ds = FADER_RESERVE;
207                 }
208
209                 cairo_set_source (cr, pattern);
210                 Gtkmm2ext::rounded_right_half_rectangle (cr, 1, 1, ds-1, h-1, radius-1.5);
211                 cairo_fill (cr);
212                 
213                 if (texture_pattern) {
214                         cairo_save (cr);
215                         cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
216                         cairo_set_source (cr, texture_pattern);
217                         cairo_matrix_init_translate (&matrix, -1, -1);
218                         cairo_pattern_set_matrix (texture_pattern, &matrix);
219                         cairo_rectangle (cr, 1, 1, ds-1, h-1);
220                         cairo_fill (cr);
221                         cairo_restore (cr);
222                 }
223         }
224                 
225         /* draw the unity-position line if it's not at either end*/
226         if (unity_loc > 0) {
227                 if ( _orien == VERT) {
228                         if (unity_loc < h ) {
229                                         context->set_line_width (1); 
230                                         context->set_source_rgb (0.0, 1.0, 0.0);
231                                         context->move_to (1, unity_loc);
232                                         context->line_to (girth, unity_loc);
233                                         context->stroke ();
234                         }
235                 } else {
236                         if ( unity_loc < w ){
237                                 context->set_line_width (1); 
238                                 context->set_source_rgb (0.0, 1.0, 0.0);
239                                 context->move_to (unity_loc, 1);
240                                 context->line_to (unity_loc, girth);
241                                 context->stroke ();
242                         }
243                 }
244         }
245         
246         if ( !_text.empty() ) {
247
248                 cairo_new_path (cr);    
249
250                 /* center text */
251                 cairo_move_to (cr, (get_width() - _text_width)/2.0, get_height()/2.0 - _text_height/2.0);
252
253                 cairo_set_source_rgba (cr, text_r, text_g, text_b, 0.9);
254                 pango_cairo_show_layout (cr, _layout->gobj());
255         } 
256         
257 //      if (Config->get_widget_prelight()) {  //pixfader does not have access to config
258                 if (_hovering) {
259                         Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3);
260                         cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.2);
261                         cairo_fill (cr);
262                 }
263 //      }
264
265         last_drawn = ds;
266
267         return true;
268 }
269
270 void
271 PixFader::on_size_request (GtkRequisition* req)
272 {
273         if (_orien == VERT) {
274                 req->width = girth;
275                 req->height = span;
276         } else {
277                 req->height = girth;
278                 req->width = span;
279         }
280 }
281
282 void
283 PixFader::on_size_allocate (Gtk::Allocation& alloc)
284 {
285         DrawingArea::on_size_allocate(alloc);
286
287         if (_orien == VERT) {
288                 span = alloc.get_height();
289                 girth = alloc.get_width ();
290         } else {
291                 span = alloc.get_width();
292                 girth = alloc.get_height ();
293         }
294
295         update_unity_position ();
296
297         if (is_realized()) {
298                 create_patterns();
299                 queue_draw ();
300         }
301 }
302
303 bool
304 PixFader::on_button_press_event (GdkEventButton* ev)
305 {
306         if (ev->type != GDK_BUTTON_PRESS) {
307                 return true;
308         }
309
310         if (ev->button != 1 && ev->button != 2) {
311                 return false;
312         }
313
314         add_modal_grab ();
315         grab_loc = (_orien == VERT) ? ev->y : ev->x;
316         grab_start = (_orien == VERT) ? ev->y : ev->x;
317         grab_window = ev->window;
318         dragging = true;
319         gdk_pointer_grab(ev->window,false,
320                         GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
321                         NULL,NULL,ev->time);
322
323         if (ev->button == 2) {
324                 set_adjustment_from_event (ev);
325         }
326         
327         return true;
328 }
329
330 bool
331 PixFader::on_button_release_event (GdkEventButton* ev)
332 {
333         double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
334         
335         switch (ev->button) {
336         case 1:
337                 if (dragging) {
338                         remove_modal_grab();
339                         dragging = false;
340                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
341
342                         if (!_hovering) {
343                                 Keyboard::magic_widget_drop_focus();
344                                 queue_draw ();
345                         }
346
347                         if (ev_pos == grab_start) {
348
349                                 /* no motion - just a click */
350
351                                 if (ev->state & Keyboard::TertiaryModifier) {
352                                         adjustment.set_value (default_value);
353                                 } else if (ev->state & Keyboard::GainFineScaleModifier) {
354                                         adjustment.set_value (adjustment.get_lower());
355                                 } else if ((_orien == VERT && ev_pos < display_span()) || (_orien == HORIZ && ev_pos > display_span())) {
356                                         /* above the current display height, remember X Window coords */
357                                         adjustment.set_value (adjustment.get_value() + adjustment.get_step_increment());
358                                 } else {
359                                         adjustment.set_value (adjustment.get_value() - adjustment.get_step_increment());
360                                 }
361                         }
362
363                 } 
364                 break;
365                 
366         case 2:
367                 if (dragging) {
368                         remove_modal_grab();
369                         dragging = false;
370                         set_adjustment_from_event (ev);
371                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
372                 }
373                 break;
374
375         default:
376                 break;
377         }
378
379         return false;
380 }
381
382 bool
383 PixFader::on_scroll_event (GdkEventScroll* ev)
384 {
385         double scale;
386         bool ret = false;
387
388         if (ev->state & Keyboard::GainFineScaleModifier) {
389                 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
390                         scale = 0.01;
391                 } else {
392                         scale = 0.05;
393                 }
394         } else {
395                 scale = 0.25;
396         }
397
398         if (_orien == VERT) {
399
400                 /* should left/right scroll affect vertical faders ? */
401
402                 switch (ev->direction) {
403
404                 case GDK_SCROLL_UP:
405                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
406                         ret = true;
407                         break;
408                 case GDK_SCROLL_DOWN:
409                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
410                         ret = true;
411                         break;
412                 default:
413                         break;
414                 }
415         } else {
416
417                 /* up/down scrolls should definitely affect horizontal faders
418                    because they are so much easier to use
419                 */
420
421                 switch (ev->direction) {
422
423                 case GDK_SCROLL_RIGHT:
424                 case GDK_SCROLL_UP:
425                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
426                         ret = true;
427                         break;
428                 case GDK_SCROLL_LEFT:
429                 case GDK_SCROLL_DOWN:
430                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
431                         ret = true;
432                         break;
433                 default:
434                         break;
435                 }
436         }
437         return ret;
438 }
439
440 bool
441 PixFader::on_motion_notify_event (GdkEventMotion* ev)
442 {
443         if (dragging) {
444                 double scale = 1.0;
445                 double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
446                 
447                 if (ev->window != grab_window) {
448                         grab_loc = ev_pos;
449                         grab_window = ev->window;
450                         return true;
451                 }
452                 
453                 if (ev->state & Keyboard::GainFineScaleModifier) {
454                         if (ev->state & Keyboard::GainExtraFineScaleModifier) {
455                                 scale = 0.05;
456                         } else {
457                                 scale = 0.1;
458                         }
459                 }
460
461                 double const delta = ev_pos - grab_loc;
462                 grab_loc = ev_pos;
463
464                 double fract = (delta / span);
465
466                 fract = min (1.0, fract);
467                 fract = max (-1.0, fract);
468
469                 // X Window is top->bottom for 0..Y
470                 
471                 if (_orien == VERT) {
472                         fract = -fract;
473                 }
474
475                 adjustment.set_value (adjustment.get_value() + scale * fract * (adjustment.get_upper() - adjustment.get_lower()));
476         }
477
478         return true;
479 }
480
481 void
482 PixFader::adjustment_changed ()
483 {
484         if (display_span() != last_drawn) {
485                 queue_draw ();
486         }
487 }
488
489 /** @return pixel offset of the current value from the right or bottom of the fader */
490 int
491 PixFader::display_span ()
492 {
493         float fract = (adjustment.get_value () - adjustment.get_lower()) / ((adjustment.get_upper() - adjustment.get_lower()));
494         int ds;
495         if (_orien == VERT) {
496                 ds = (int)floor ( span * (1.0 - fract));
497         } else {
498                 ds = (int)floor (span * fract);
499         }
500         
501         return ds;
502 }
503
504 void
505 PixFader::set_fader_length (int l)
506 {
507         span = l;
508         update_unity_position ();
509         queue_draw ();
510 }
511
512 void
513 PixFader::update_unity_position ()
514 {
515         if (_orien == VERT) {
516                 unity_loc = (int) rint (span * (1 - (default_value / (adjustment.get_upper() - adjustment.get_lower())))) - 1;
517         } else {
518                 unity_loc = (int) rint (default_value * span);
519         }
520
521         queue_draw ();
522 }
523
524 bool
525 PixFader::on_enter_notify_event (GdkEventCrossing*)
526 {
527         _hovering = true;
528         Keyboard::magic_widget_grab_focus ();
529         queue_draw ();
530         return false;
531 }
532
533 bool
534 PixFader::on_leave_notify_event (GdkEventCrossing*)
535 {
536         if (!dragging) {
537                 _hovering = false;
538                 Keyboard::magic_widget_drop_focus();
539                 queue_draw ();
540         }
541         return false;
542 }
543
544 void
545 PixFader::set_adjustment_from_event (GdkEventButton* ev)
546 {
547         double fract = (_orien == VERT) ? (1.0 - (ev->y / span)) : (ev->x / span);
548
549         fract = min (1.0, fract);
550         fract = max (0.0, fract);
551
552         adjustment.set_value (fract * (adjustment.get_upper () - adjustment.get_lower ()));
553 }
554
555 void
556 PixFader::set_default_value (float d)
557 {
558         default_value = d;
559         update_unity_position ();
560 }
561
562 void
563 PixFader::set_text (const std::string& str)
564 {
565         _text = str;
566
567         if (!_layout && !_text.empty()) {
568                 _layout = Pango::Layout::create (get_pango_context());
569         } 
570
571         if (_layout) {
572                 _layout->set_text (str);
573         }
574
575         queue_resize ();
576 }
577
578 void
579 PixFader::on_state_changed (Gtk::StateType old_state)
580 {
581         Widget::on_state_changed (old_state);
582         create_patterns ();
583 }