Task Force Viking: operation green line
[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 (girth <= 10) { // Line
269                         context->set_source_rgb (0.0, 1.0, 0.0);
270                         if ( _orien == VERT) {
271                                 if (unity_loc < h ) {
272                                         context->move_to (1, unity_loc + .5);
273                                         context->line_to (girth - 2.0, unity_loc + .5);
274                                         context->stroke ();
275                                 }
276                         } else {
277                                 if ( unity_loc < w ){
278                                         context->move_to (unity_loc + .5, 1);
279                                         context->line_to (unity_loc + .5, girth - 2.0);
280                                         context->stroke ();
281                                 }
282                         }
283                 } else { // triangle
284                         Gdk::Color c = get_style()->get_fg (Gtk::STATE_ACTIVE);
285                         context->set_source_rgba (c.get_red_p()*1.25, c.get_green_p()*1.25, c.get_blue_p()*1.25, 0.85);
286                         if ( _orien == VERT) {
287                                 if (unity_loc < h ) {
288                                         context->move_to (1, unity_loc - 1.0);
289                                         context->line_to (1, unity_loc + 2.0);
290                                         context->line_to (5.0, unity_loc + 0.5);
291                                         cairo_close_path(cr);
292                                         context->fill_preserve ();
293                                         context->stroke ();
294
295                                         context->move_to (girth - 1, unity_loc - 1.0);
296                                         context->line_to (girth - 1, unity_loc + 2.0);
297                                         context->line_to (girth - 5, unity_loc + 0.5);
298                                         cairo_close_path(cr);
299                                         context->fill_preserve ();
300                                         context->stroke ();
301                                 }
302                         } else {
303                                 if ( unity_loc < w ){
304                                         context->move_to (unity_loc + 1.0, 1);
305                                         context->line_to (unity_loc - 2.0, 1);
306                                         context->line_to (unity_loc - 0.5, 5);
307                                         cairo_close_path(cr);
308                                         context->fill_preserve ();
309                                         context->stroke ();
310
311                                         context->move_to (unity_loc + 1.0, girth - 1);
312                                         context->line_to (unity_loc - 2.0, girth - 1);
313                                         context->line_to (unity_loc - 0.5, girth - 5);
314                                         cairo_close_path(cr);
315                                         context->fill_preserve ();
316                                         context->stroke ();
317                                 }
318                         }
319                 }
320         }
321
322         if ( !_text.empty() ) {
323
324                 /* center text */
325                 cairo_new_path (cr);
326                 cairo_move_to (cr, (get_width() - _text_width)/2.0, get_height()/2.0 - _text_height/2.0);
327                 cairo_set_source_rgba (cr, text_r, text_g, text_b, 0.9);
328                 pango_cairo_show_layout (cr, _layout->gobj());
329         } 
330         
331 //      if (Config->get_widget_prelight()) {  //pixfader does not have access to config
332                 if (_hovering) {
333                         Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3);
334                         cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.1);
335                         cairo_fill (cr);
336                 }
337 //      }
338
339         last_drawn = ds;
340
341         return true;
342 }
343
344 void
345 PixFader::on_size_request (GtkRequisition* req)
346 {
347         if (_orien == VERT) {
348                 req->width = (girth ? girth : -1);
349                 req->height = (span ? span : -1);
350         } else {
351                 req->height = (girth ? girth : -1);
352                 req->width = (span ? span : -1);
353         }
354 }
355
356 void
357 PixFader::on_size_allocate (Gtk::Allocation& alloc)
358 {
359         DrawingArea::on_size_allocate(alloc);
360
361         if (_orien == VERT) {
362                 girth = alloc.get_width ();
363                 span = alloc.get_height ();
364         } else {
365                 girth = alloc.get_height ();
366                 span = alloc.get_width ();
367         }
368
369         if (is_realized()) {
370                 /* recreate patterns in case we've changed size */
371                 create_patterns ();
372         }
373
374         update_unity_position ();
375 }
376
377 bool
378 PixFader::on_button_press_event (GdkEventButton* ev)
379 {
380         if (ev->type != GDK_BUTTON_PRESS) {
381                 return true;
382         }
383
384         if (ev->button != 1 && ev->button != 2) {
385                 return false;
386         }
387
388         add_modal_grab ();
389         grab_loc = (_orien == VERT) ? ev->y : ev->x;
390         grab_start = (_orien == VERT) ? ev->y : ev->x;
391         grab_window = ev->window;
392         dragging = true;
393         gdk_pointer_grab(ev->window,false,
394                         GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
395                         NULL,NULL,ev->time);
396
397         if (ev->button == 2) {
398                 set_adjustment_from_event (ev);
399         }
400         
401         return true;
402 }
403
404 bool
405 PixFader::on_button_release_event (GdkEventButton* ev)
406 {
407         double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
408         
409         switch (ev->button) {
410         case 1:
411                 if (dragging) {
412                         remove_modal_grab();
413                         dragging = false;
414                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
415
416                         if (!_hovering) {
417                                 Keyboard::magic_widget_drop_focus();
418                                 queue_draw ();
419                         }
420
421                         if (ev_pos == grab_start) {
422
423                                 /* no motion - just a click */
424
425                                 if (ev->state & Keyboard::TertiaryModifier) {
426                                         adjustment.set_value (default_value);
427                                 } else if (ev->state & Keyboard::GainFineScaleModifier) {
428                                         adjustment.set_value (adjustment.get_lower());
429                                 } else if ((_orien == VERT && ev_pos < display_span()) || (_orien == HORIZ && ev_pos > display_span())) {
430                                         /* above the current display height, remember X Window coords */
431                                         adjustment.set_value (adjustment.get_value() + adjustment.get_step_increment());
432                                 } else {
433                                         adjustment.set_value (adjustment.get_value() - adjustment.get_step_increment());
434                                 }
435                         }
436                         return true;
437                 } 
438                 break;
439                 
440         case 2:
441                 if (dragging) {
442                         remove_modal_grab();
443                         dragging = false;
444                         set_adjustment_from_event (ev);
445                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
446                         return true;
447                 }
448                 break;
449
450         default:
451                 break;
452         }
453
454         return false;
455 }
456
457 bool
458 PixFader::on_scroll_event (GdkEventScroll* ev)
459 {
460         double scale;
461         bool ret = false;
462
463         if (ev->state & Keyboard::GainFineScaleModifier) {
464                 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
465                         scale = 0.01;
466                 } else {
467                         scale = 0.05;
468                 }
469         } else {
470                 scale = 0.25;
471         }
472
473         if (_orien == VERT) {
474
475                 /* should left/right scroll affect vertical faders ? */
476
477                 switch (ev->direction) {
478
479                 case GDK_SCROLL_UP:
480                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
481                         ret = true;
482                         break;
483                 case GDK_SCROLL_DOWN:
484                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
485                         ret = true;
486                         break;
487                 default:
488                         break;
489                 }
490         } else {
491
492                 /* up/down scrolls should definitely affect horizontal faders
493                    because they are so much easier to use
494                 */
495
496                 switch (ev->direction) {
497
498                 case GDK_SCROLL_RIGHT:
499                 case GDK_SCROLL_UP:
500                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
501                         ret = true;
502                         break;
503                 case GDK_SCROLL_LEFT:
504                 case GDK_SCROLL_DOWN:
505                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
506                         ret = true;
507                         break;
508                 default:
509                         break;
510                 }
511         }
512         return ret;
513 }
514
515 bool
516 PixFader::on_motion_notify_event (GdkEventMotion* ev)
517 {
518         if (dragging) {
519                 double scale = 1.0;
520                 double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
521                 
522                 if (ev->window != grab_window) {
523                         grab_loc = ev_pos;
524                         grab_window = ev->window;
525                         return true;
526                 }
527                 
528                 if (ev->state & Keyboard::GainFineScaleModifier) {
529                         if (ev->state & Keyboard::GainExtraFineScaleModifier) {
530                                 scale = 0.05;
531                         } else {
532                                 scale = 0.1;
533                         }
534                 }
535
536                 double const delta = ev_pos - grab_loc;
537                 grab_loc = ev_pos;
538
539                 double fract = (delta / span);
540
541                 fract = min (1.0, fract);
542                 fract = max (-1.0, fract);
543
544                 // X Window is top->bottom for 0..Y
545                 
546                 if (_orien == VERT) {
547                         fract = -fract;
548                 }
549
550                 adjustment.set_value (adjustment.get_value() + scale * fract * (adjustment.get_upper() - adjustment.get_lower()));
551         }
552
553         return true;
554 }
555
556 void
557 PixFader::adjustment_changed ()
558 {
559         if (display_span() != last_drawn) {
560                 queue_draw ();
561         }
562 }
563
564 /** @return pixel offset of the current value from the right or bottom of the fader */
565 int
566 PixFader::display_span ()
567 {
568         float fract = (adjustment.get_value () - adjustment.get_lower()) / ((adjustment.get_upper() - adjustment.get_lower()));
569         int ds;
570         if (_orien == VERT) {
571                 ds = (int)floor ( span * (1.0 - fract));
572         } else {
573                 ds = (int)floor (span * fract);
574         }
575         
576         return ds;
577 }
578
579 void
580 PixFader::update_unity_position ()
581 {
582         if (_orien == VERT) {
583                 unity_loc = (int) rint (span * (1 - (default_value / (adjustment.get_upper() - adjustment.get_lower())))) - 1;
584         } else {
585                 unity_loc = (int) rint (default_value * span / (adjustment.get_upper() - adjustment.get_lower()));
586         }
587
588         queue_draw ();
589 }
590
591 bool
592 PixFader::on_enter_notify_event (GdkEventCrossing*)
593 {
594         _hovering = true;
595         Keyboard::magic_widget_grab_focus ();
596         queue_draw ();
597         return false;
598 }
599
600 bool
601 PixFader::on_leave_notify_event (GdkEventCrossing*)
602 {
603         if (!dragging) {
604                 _hovering = false;
605                 Keyboard::magic_widget_drop_focus();
606                 queue_draw ();
607         }
608         return false;
609 }
610
611 void
612 PixFader::set_adjustment_from_event (GdkEventButton* ev)
613 {
614         double fract = (_orien == VERT) ? (1.0 - (ev->y / span)) : (ev->x / span);
615
616         fract = min (1.0, fract);
617         fract = max (0.0, fract);
618
619         adjustment.set_value (fract * (adjustment.get_upper () - adjustment.get_lower ()));
620 }
621
622 void
623 PixFader::set_default_value (float d)
624 {
625         default_value = d;
626         update_unity_position ();
627 }
628
629 void
630 PixFader::set_text (const std::string& str)
631 {
632         _text = str;
633
634         if (!_layout && !_text.empty()) {
635                 _layout = Pango::Layout::create (get_pango_context());
636         } 
637
638         if (_layout) {
639                 _layout->set_text (str);
640                 _layout->get_pixel_size (_text_width, _text_height);
641         }
642
643         queue_resize ();
644 }
645
646 void
647 PixFader::on_state_changed (Gtk::StateType old_state)
648 {
649         Widget::on_state_changed (old_state);
650         create_patterns ();
651 }
652
653 void
654 PixFader::on_style_changed (const Glib::RefPtr<Gtk::Style>&)
655 {
656         if (_layout) {
657                 std::string txt = _layout->get_text();
658                 _layout.clear (); // drop reference to existing layout
659                 set_text (txt);
660         }
661
662         /* remember that all patterns are cached and not owned by an individual
663            pixfader. we will lazily create a new pattern when needed.
664         */
665
666         pattern = 0;
667 }