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