005e9dcefdc47c3e0532ed9352ede68e2900a02b
[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
24 #include "gtkmm2ext/pixfader.h"
25 #include "gtkmm2ext/keyboard.h"
26 #include "gtkmm2ext/rgb_macros.h"
27 #include "gtkmm2ext/utils.h"
28
29 using namespace Gtkmm2ext;
30 using namespace Gtk;
31 using namespace std;
32
33 #define CORNER_RADIUS 4
34 #define FADER_RESERVE (2*CORNER_RADIUS)
35
36 std::list<PixFader::FaderImage*> PixFader::_patterns;
37
38 PixFader::PixFader (Gtk::Adjustment& adj, int orientation, int fader_length, int fader_girth)
39         : adjustment (adj)
40         , span (fader_length)
41         , girth (fader_girth)
42         , _orien (orientation)
43         , pattern (0)
44         , _hovering (false)
45         , last_drawn (-1)
46         , dragging (false)
47 {
48         default_value = adjustment.get_value();
49         update_unity_position ();
50
51         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);
52
53         adjustment.signal_value_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
54         adjustment.signal_changed().connect (mem_fun (*this, &PixFader::adjustment_changed));
55 }
56
57 PixFader::~PixFader ()
58 {
59 }
60
61 cairo_pattern_t*
62 PixFader::find_pattern (double afr, double afg, double afb, 
63                         double abr, double abg, double abb, 
64                         int w, int h)
65 {
66         for (list<FaderImage*>::iterator f = _patterns.begin(); f != _patterns.end(); ++f) {
67                 if ((*f)->matches (afr, afg, afb, abr, abg, abb, w, h)) {
68                         return (*f)->pattern;
69                 }
70         }
71         return 0;
72 }
73
74 void
75 PixFader::create_patterns ()
76 {
77         Gdk::Color c = get_style()->get_fg (get_state());
78         float fr, fg, fb;
79         float br, bg, bb;
80
81         fr = c.get_red_p ();
82         fg = c.get_green_p ();
83         fb = c.get_blue_p ();
84
85         c = get_style()->get_bg (get_state());
86
87         br = c.get_red_p ();
88         bg = c.get_green_p ();
89         bb = c.get_blue_p ();
90
91         if ( !_text.empty()) {
92                 _layout->get_pixel_size (_text_width, _text_height);
93         } else {
94                 _text_width = 0;
95                 _text_height = 0;
96         }
97
98         c = get_style()->get_text (get_state());
99
100         text_r = c.get_red_p ();
101         text_g = c.get_green_p ();
102         text_b = c.get_blue_p ();
103
104         cairo_surface_t* surface;
105         cairo_t* tc = 0;
106         float radius = CORNER_RADIUS;
107
108         double w = get_width();
109         
110         if (w <= 1 || get_height() <= 1) {
111                 return;
112         }
113
114         if ((pattern = find_pattern (fr, fg, fb, br, bg, bb, get_width(), get_height())) != 0) {
115                 /* found it - use it */
116                 return;
117         }
118
119         if (_orien == VERT) {
120                 
121                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, get_width(), get_height() * 2.0);
122                 tc = cairo_create (surface);
123
124                 /* paint background + border */
125
126                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, get_width(), 0);
127                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, br*0.8,bg*0.8,bb*0.8, 1.0);
128                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, br*0.6,bg*0.6,bb*0.6, 1.0);
129                 cairo_set_source (tc, shade_pattern);
130                 cairo_rectangle (tc, 0, 0, get_width(), get_height() * 2.0);
131                 cairo_fill (tc);
132
133                 cairo_pattern_destroy (shade_pattern);
134                 
135                 /* paint lower shade */
136                 
137                 w -= 2.0;
138
139                 shade_pattern = cairo_pattern_create_linear (0.0, 0.0, w, 0);
140                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, fr*0.8,fg*0.8,fb*0.8, 1.0);
141                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, fr*0.6,fg*0.6,fb*0.6, 1.0);
142                 cairo_set_source (tc, shade_pattern);
143                 Gtkmm2ext::rounded_top_half_rectangle (tc, 1.0, get_height(), w, get_height(), radius-1.5);
144                 cairo_fill (tc);
145
146                 cairo_pattern_destroy (shade_pattern);
147
148                 pattern = cairo_pattern_create_for_surface (surface);
149
150         } else {
151
152                 surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, get_width() * 2.0, get_height());
153                 tc = cairo_create (surface);
154
155                 /* paint right shade (background section)*/
156
157                 cairo_pattern_t* shade_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, get_height());
158                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, br*0.8,bg*0.8,bb*0.8, 1.0);
159                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, br*0.6,bg*0.6,bb*0.6, 1.0);
160                 cairo_set_source (tc, shade_pattern);
161                 cairo_rectangle (tc, 0, 0, get_width() * 2.0, get_height());
162                 cairo_fill (tc);
163
164                 /* paint left shade (active section/foreground) */
165                 
166                 shade_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, get_height());
167                 cairo_pattern_add_color_stop_rgba (shade_pattern, 0, fr*0.8,fg*0.8,fb*0.8, 1.0);
168                 cairo_pattern_add_color_stop_rgba (shade_pattern, 1, fr*0.6,fg*0.6,fb*0.6, 1.0);
169                 cairo_set_source (tc, shade_pattern);
170                 Gtkmm2ext::rounded_right_half_rectangle (tc, 0, 1, get_width(), get_height() - 2.0, radius-1.5);
171                 cairo_fill (tc);
172                 cairo_pattern_destroy (shade_pattern);
173                 
174                 pattern = cairo_pattern_create_for_surface (surface);
175         }
176
177         /* cache it for others to use */
178
179         _patterns.push_back (new FaderImage (pattern, fr, fg, fb, br, bg, bb, get_width(), get_height()));
180
181         cairo_destroy (tc);
182         cairo_surface_destroy (surface);
183 }
184
185 bool
186 PixFader::on_expose_event (GdkEventExpose* ev)
187 {
188         Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
189         cairo_t* cr = context->cobj();
190
191         if (!pattern) {
192                 create_patterns();
193         }
194
195         if (!pattern) {
196
197                 /* this isn't supposed to be happen, but some wackiness whereby
198                    the pixfader ends up with a 1xN or Nx1 size allocation
199                    leads to it. the basic wackiness needs fixing but we
200                    shouldn't crash. just fill in the expose area with 
201                    our bg color.
202                 */
203
204                 Gdk::Color c = get_style()->get_bg (get_state());
205                 float br, bg, bb;
206
207                 br = c.get_red_p ();
208                 bg = c.get_green_p ();
209                 bb = c.get_blue_p ();
210                 cairo_set_source_rgb (cr, br, bg, bb);
211                 cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
212                 cairo_fill (cr);
213
214                 return true;
215         }
216                    
217         cairo_rectangle (cr, ev->area.x, ev->area.y, ev->area.width, ev->area.height);
218         cairo_clip (cr);
219
220         int ds = display_span ();
221         float w = get_width();
222         float h = get_height();
223
224         cairo_matrix_t matrix;
225
226         if (_orien == VERT) {
227
228                 if (ds > h - FADER_RESERVE) {
229                         ds = h - FADER_RESERVE;
230                 }
231
232                 cairo_set_source (cr, pattern);
233                 cairo_matrix_init_translate (&matrix, 0, (h - ds));
234                 cairo_pattern_set_matrix (pattern, &matrix);
235                 Gtkmm2ext::rounded_rectangle (cr, 0, 0, w, h, CORNER_RADIUS-1.5);
236                 cairo_fill (cr);
237
238         } else {
239
240                 if (ds < FADER_RESERVE) {
241                         ds = FADER_RESERVE;
242                 }
243
244                 /*
245                   if ds == w, the pattern does not need to be translated
246                   if ds == 0 (or FADER_RESERVE), the pattern needs to be moved
247                       w to the left, which is -w in pattern space, and w in
248                       user space
249                   if ds == 10, then the pattern needs to be moved w - 10
250                       to the left, which is -(w-10) in pattern space, which 
251                       is (w - 10) in user space
252
253                   thus: translation = (w - ds)
254                  */
255
256                 cairo_set_source (cr, pattern);
257                 cairo_matrix_init_translate (&matrix, w - ds, 0);
258                 cairo_pattern_set_matrix (pattern, &matrix);
259                 Gtkmm2ext::rounded_rectangle (cr, 0, 0, w, h, CORNER_RADIUS-1.5);
260                 cairo_fill (cr);
261         }
262                 
263         /* draw the unity-position line if it's not at either end*/
264         if (unity_loc > 0) {
265                 if ( _orien == VERT) {
266                         if (unity_loc < h ) {
267                                         context->set_line_width (1); 
268                                         context->set_source_rgb (0.0, 1.0, 0.0);
269                                         context->move_to (1, unity_loc);
270                                         context->line_to (girth - 2.0, unity_loc);
271                                         context->stroke ();
272                         }
273                 } else {
274                         if ( unity_loc < w ){
275                                 context->set_line_width (1); 
276                                 context->set_source_rgb (0.0, 1.0, 0.0);
277                                 context->move_to (unity_loc, 1);
278                                 context->line_to (unity_loc, girth - 2.0);
279                                 context->stroke ();
280                         }
281                 }
282         }
283         
284         if ( !_text.empty() ) {
285
286                 cairo_new_path (cr);    
287
288                 /* center text */
289                 cairo_move_to (cr, (get_width() - _text_width)/2.0, get_height()/2.0 - _text_height/2.0);
290                 cairo_set_source_rgba (cr, text_r, text_g, text_b, 0.9);
291                 pango_cairo_show_layout (cr, _layout->gobj());
292         } 
293         
294 //      if (Config->get_widget_prelight()) {  //pixfader does not have access to config
295                 if (_hovering) {
296                         Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3);
297                         cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.1);
298                         cairo_fill (cr);
299                 }
300 //      }
301
302         last_drawn = ds;
303
304         return true;
305 }
306
307 void
308 PixFader::on_size_request (GtkRequisition* req)
309 {
310         if (_orien == VERT) {
311                 req->width = girth;
312                 req->height = span;
313         } else {
314                 req->height = girth;
315                 req->width = span;
316         }
317 }
318
319 void
320 PixFader::on_size_allocate (Gtk::Allocation& alloc)
321 {
322         DrawingArea::on_size_allocate(alloc);
323
324         if (_orien == VERT) {
325                 girth = alloc.get_width ();
326                 span = alloc.get_height ();
327         } else {
328                 girth = alloc.get_height ();
329                 span = alloc.get_width ();
330         }
331
332         update_unity_position ();
333 }
334
335 bool
336 PixFader::on_button_press_event (GdkEventButton* ev)
337 {
338         if (ev->type != GDK_BUTTON_PRESS) {
339                 return true;
340         }
341
342         if (ev->button != 1 && ev->button != 2) {
343                 return false;
344         }
345
346         add_modal_grab ();
347         grab_loc = (_orien == VERT) ? ev->y : ev->x;
348         grab_start = (_orien == VERT) ? ev->y : ev->x;
349         grab_window = ev->window;
350         dragging = true;
351         gdk_pointer_grab(ev->window,false,
352                         GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
353                         NULL,NULL,ev->time);
354
355         if (ev->button == 2) {
356                 set_adjustment_from_event (ev);
357         }
358         
359         return true;
360 }
361
362 bool
363 PixFader::on_button_release_event (GdkEventButton* ev)
364 {
365         double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
366         
367         switch (ev->button) {
368         case 1:
369                 if (dragging) {
370                         remove_modal_grab();
371                         dragging = false;
372                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
373
374                         if (!_hovering) {
375                                 Keyboard::magic_widget_drop_focus();
376                                 queue_draw ();
377                         }
378
379                         if (ev_pos == grab_start) {
380
381                                 /* no motion - just a click */
382
383                                 if (ev->state & Keyboard::TertiaryModifier) {
384                                         adjustment.set_value (default_value);
385                                 } else if (ev->state & Keyboard::GainFineScaleModifier) {
386                                         adjustment.set_value (adjustment.get_lower());
387                                 } else if ((_orien == VERT && ev_pos < display_span()) || (_orien == HORIZ && ev_pos > display_span())) {
388                                         /* above the current display height, remember X Window coords */
389                                         adjustment.set_value (adjustment.get_value() + adjustment.get_step_increment());
390                                 } else {
391                                         adjustment.set_value (adjustment.get_value() - adjustment.get_step_increment());
392                                 }
393                         }
394                         return true;
395                 } 
396                 break;
397                 
398         case 2:
399                 if (dragging) {
400                         remove_modal_grab();
401                         dragging = false;
402                         set_adjustment_from_event (ev);
403                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
404                         return true;
405                 }
406                 break;
407
408         default:
409                 break;
410         }
411
412         return false;
413 }
414
415 bool
416 PixFader::on_scroll_event (GdkEventScroll* ev)
417 {
418         double scale;
419         bool ret = false;
420
421         if (ev->state & Keyboard::GainFineScaleModifier) {
422                 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
423                         scale = 0.01;
424                 } else {
425                         scale = 0.05;
426                 }
427         } else {
428                 scale = 0.25;
429         }
430
431         if (_orien == VERT) {
432
433                 /* should left/right scroll affect vertical faders ? */
434
435                 switch (ev->direction) {
436
437                 case GDK_SCROLL_UP:
438                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
439                         ret = true;
440                         break;
441                 case GDK_SCROLL_DOWN:
442                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
443                         ret = true;
444                         break;
445                 default:
446                         break;
447                 }
448         } else {
449
450                 /* up/down scrolls should definitely affect horizontal faders
451                    because they are so much easier to use
452                 */
453
454                 switch (ev->direction) {
455
456                 case GDK_SCROLL_RIGHT:
457                 case GDK_SCROLL_UP:
458                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
459                         ret = true;
460                         break;
461                 case GDK_SCROLL_LEFT:
462                 case GDK_SCROLL_DOWN:
463                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
464                         ret = true;
465                         break;
466                 default:
467                         break;
468                 }
469         }
470         return ret;
471 }
472
473 bool
474 PixFader::on_motion_notify_event (GdkEventMotion* ev)
475 {
476         if (dragging) {
477                 double scale = 1.0;
478                 double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
479                 
480                 if (ev->window != grab_window) {
481                         grab_loc = ev_pos;
482                         grab_window = ev->window;
483                         return true;
484                 }
485                 
486                 if (ev->state & Keyboard::GainFineScaleModifier) {
487                         if (ev->state & Keyboard::GainExtraFineScaleModifier) {
488                                 scale = 0.05;
489                         } else {
490                                 scale = 0.1;
491                         }
492                 }
493
494                 double const delta = ev_pos - grab_loc;
495                 grab_loc = ev_pos;
496
497                 double fract = (delta / span);
498
499                 fract = min (1.0, fract);
500                 fract = max (-1.0, fract);
501
502                 // X Window is top->bottom for 0..Y
503                 
504                 if (_orien == VERT) {
505                         fract = -fract;
506                 }
507
508                 adjustment.set_value (adjustment.get_value() + scale * fract * (adjustment.get_upper() - adjustment.get_lower()));
509         }
510
511         return true;
512 }
513
514 void
515 PixFader::adjustment_changed ()
516 {
517         if (display_span() != last_drawn) {
518                 queue_draw ();
519         }
520 }
521
522 /** @return pixel offset of the current value from the right or bottom of the fader */
523 int
524 PixFader::display_span ()
525 {
526         float fract = (adjustment.get_value () - adjustment.get_lower()) / ((adjustment.get_upper() - adjustment.get_lower()));
527         int ds;
528         if (_orien == VERT) {
529                 ds = (int)floor ( span * (1.0 - fract));
530         } else {
531                 ds = (int)floor (span * fract);
532         }
533         
534         return ds;
535 }
536
537 void
538 PixFader::set_fader_length (int l)
539 {
540         span = l;
541         update_unity_position ();
542         queue_resize ();
543 }
544
545 void
546 PixFader::update_unity_position ()
547 {
548         if (_orien == VERT) {
549                 unity_loc = (int) rint (span * (1 - (default_value / (adjustment.get_upper() - adjustment.get_lower())))) - 1;
550         } else {
551                 unity_loc = (int) rint (default_value * span / (adjustment.get_upper() - adjustment.get_lower()));
552         }
553
554         queue_draw ();
555 }
556
557 bool
558 PixFader::on_enter_notify_event (GdkEventCrossing*)
559 {
560         _hovering = true;
561         Keyboard::magic_widget_grab_focus ();
562         queue_draw ();
563         return false;
564 }
565
566 bool
567 PixFader::on_leave_notify_event (GdkEventCrossing*)
568 {
569         if (!dragging) {
570                 _hovering = false;
571                 Keyboard::magic_widget_drop_focus();
572                 queue_draw ();
573         }
574         return false;
575 }
576
577 void
578 PixFader::set_adjustment_from_event (GdkEventButton* ev)
579 {
580         double fract = (_orien == VERT) ? (1.0 - (ev->y / span)) : (ev->x / span);
581
582         fract = min (1.0, fract);
583         fract = max (0.0, fract);
584
585         adjustment.set_value (fract * (adjustment.get_upper () - adjustment.get_lower ()));
586 }
587
588 void
589 PixFader::set_default_value (float d)
590 {
591         default_value = d;
592         update_unity_position ();
593 }
594
595 void
596 PixFader::set_text (const std::string& str)
597 {
598         _text = str;
599
600         if (!_layout && !_text.empty()) {
601                 _layout = Pango::Layout::create (get_pango_context());
602         } 
603
604         if (_layout) {
605                 _layout->set_text (str);
606                 _layout->get_pixel_size (_text_width, _text_height);
607         }
608
609         queue_resize ();
610 }
611
612 void
613 PixFader::on_state_changed (Gtk::StateType old_state)
614 {
615         Widget::on_state_changed (old_state);
616         create_patterns ();
617 }
618
619 void
620 PixFader::on_style_changed (const Glib::RefPtr<Gtk::Style>&)
621 {
622         if (_layout) {
623                 std::string txt = _layout->get_text();
624                 _layout.clear (); // drop reference to existing layout
625                 set_text (txt);
626         }
627
628         /* remember that all patterns are cached and not owned by an individual
629            pixfader. we will lazily create a new pattern when needed.
630         */
631
632         pattern = 0;
633 }