Merge branch 'master' into windows
[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                 context->set_line_cap (Cairo::LINE_CAP_ROUND);
269                 Gdk::Color c = get_style()->get_fg (Gtk::STATE_ACTIVE);
270                 context->set_source_rgba (c.get_red_p()*1.5, c.get_green_p()*1.5, c.get_blue_p()*1.5, 0.85);
271                 if ( _orien == VERT) {
272                         if (unity_loc < h ) {
273                                 context->move_to (1.5, unity_loc + .5);
274                                 context->line_to (girth - 1.5, unity_loc + .5);
275                                 context->stroke ();
276                         }
277                 } else {
278                         if ( unity_loc < w ){
279                                 context->move_to (unity_loc + .5, 1.5);
280                                 context->line_to (unity_loc + .5, girth - 1.5);
281                                 context->stroke ();
282                         }
283                 }
284         }
285
286         if ( !_text.empty() ) {
287
288                 /* center text */
289                 cairo_new_path (cr);
290                 cairo_move_to (cr, (get_width() - _text_width)/2.0, get_height()/2.0 - _text_height/2.0);
291                 cairo_set_source_rgba (cr, text_r, text_g, text_b, 0.9);
292                 pango_cairo_show_layout (cr, _layout->gobj());
293         } 
294         
295         if (!get_sensitive()) {
296                 Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3);
297                 cairo_set_source_rgba (cr, 0.505, 0.517, 0.525, 0.4);
298                 cairo_fill (cr);
299         } else if (_hovering) {
300                 Gtkmm2ext::rounded_rectangle (cr, 0, 0, get_width(), get_height(), 3);
301                 cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.1);
302                 cairo_fill (cr);
303         }
304
305         last_drawn = ds;
306
307         return true;
308 }
309
310 void
311 PixFader::on_size_request (GtkRequisition* req)
312 {
313         if (_orien == VERT) {
314                 req->width = (girth ? girth : -1);
315                 req->height = (span ? span : -1);
316         } else {
317                 req->height = (girth ? girth : -1);
318                 req->width = (span ? span : -1);
319         }
320 }
321
322 void
323 PixFader::on_size_allocate (Gtk::Allocation& alloc)
324 {
325         DrawingArea::on_size_allocate(alloc);
326
327         if (_orien == VERT) {
328                 girth = alloc.get_width ();
329                 span = alloc.get_height ();
330         } else {
331                 girth = alloc.get_height ();
332                 span = alloc.get_width ();
333         }
334
335         if (is_realized()) {
336                 /* recreate patterns in case we've changed size */
337                 create_patterns ();
338         }
339
340         update_unity_position ();
341 }
342
343 bool
344 PixFader::on_button_press_event (GdkEventButton* ev)
345 {
346         if (ev->type != GDK_BUTTON_PRESS) {
347                 return true;
348         }
349
350         if (ev->button != 1 && ev->button != 2) {
351                 return false;
352         }
353
354         add_modal_grab ();
355         grab_loc = (_orien == VERT) ? ev->y : ev->x;
356         grab_start = (_orien == VERT) ? ev->y : ev->x;
357         grab_window = ev->window;
358         dragging = true;
359         gdk_pointer_grab(ev->window,false,
360                         GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
361                         NULL,NULL,ev->time);
362
363         if (ev->button == 2) {
364                 set_adjustment_from_event (ev);
365         }
366         
367         return true;
368 }
369
370 bool
371 PixFader::on_button_release_event (GdkEventButton* ev)
372 {
373         double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
374         
375         switch (ev->button) {
376         case 1:
377                 if (dragging) {
378                         remove_modal_grab();
379                         dragging = false;
380                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
381
382                         if (!_hovering) {
383                                 Keyboard::magic_widget_drop_focus();
384                                 queue_draw ();
385                         }
386
387                         if (ev_pos == grab_start) {
388
389                                 /* no motion - just a click */
390
391                                 if (ev->state & Keyboard::TertiaryModifier) {
392                                         adjustment.set_value (default_value);
393                                 } else if (ev->state & Keyboard::GainFineScaleModifier) {
394                                         adjustment.set_value (adjustment.get_lower());
395                                 } else if ((_orien == VERT && ev_pos < display_span()) || (_orien == HORIZ && ev_pos > display_span())) {
396                                         /* above the current display height, remember X Window coords */
397                                         adjustment.set_value (adjustment.get_value() + adjustment.get_step_increment());
398                                 } else {
399                                         adjustment.set_value (adjustment.get_value() - adjustment.get_step_increment());
400                                 }
401                         }
402                         return true;
403                 } 
404                 break;
405                 
406         case 2:
407                 if (dragging) {
408                         remove_modal_grab();
409                         dragging = false;
410                         set_adjustment_from_event (ev);
411                         gdk_pointer_ungrab (GDK_CURRENT_TIME);
412                         return true;
413                 }
414                 break;
415
416         default:
417                 break;
418         }
419
420         return false;
421 }
422
423 bool
424 PixFader::on_scroll_event (GdkEventScroll* ev)
425 {
426         double scale;
427         bool ret = false;
428
429         if (ev->state & Keyboard::GainFineScaleModifier) {
430                 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
431                         scale = 0.01;
432                 } else {
433                         scale = 0.05;
434                 }
435         } else {
436                 scale = 0.25;
437         }
438
439         if (_orien == VERT) {
440
441                 /* should left/right scroll affect vertical faders ? */
442
443                 switch (ev->direction) {
444
445                 case GDK_SCROLL_UP:
446                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
447                         ret = true;
448                         break;
449                 case GDK_SCROLL_DOWN:
450                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
451                         ret = true;
452                         break;
453                 default:
454                         break;
455                 }
456         } else {
457
458                 /* up/down scrolls should definitely affect horizontal faders
459                    because they are so much easier to use
460                 */
461
462                 switch (ev->direction) {
463
464                 case GDK_SCROLL_RIGHT:
465                 case GDK_SCROLL_UP:
466                         adjustment.set_value (adjustment.get_value() + (adjustment.get_page_increment() * scale));
467                         ret = true;
468                         break;
469                 case GDK_SCROLL_LEFT:
470                 case GDK_SCROLL_DOWN:
471                         adjustment.set_value (adjustment.get_value() - (adjustment.get_page_increment() * scale));
472                         ret = true;
473                         break;
474                 default:
475                         break;
476                 }
477         }
478         return ret;
479 }
480
481 bool
482 PixFader::on_motion_notify_event (GdkEventMotion* ev)
483 {
484         if (dragging) {
485                 double scale = 1.0;
486                 double const ev_pos = (_orien == VERT) ? ev->y : ev->x;
487                 
488                 if (ev->window != grab_window) {
489                         grab_loc = ev_pos;
490                         grab_window = ev->window;
491                         return true;
492                 }
493                 
494                 if (ev->state & Keyboard::GainFineScaleModifier) {
495                         if (ev->state & Keyboard::GainExtraFineScaleModifier) {
496                                 scale = 0.05;
497                         } else {
498                                 scale = 0.1;
499                         }
500                 }
501
502                 double const delta = ev_pos - grab_loc;
503                 grab_loc = ev_pos;
504
505                 double fract = (delta / span);
506
507                 fract = min (1.0, fract);
508                 fract = max (-1.0, fract);
509
510                 // X Window is top->bottom for 0..Y
511                 
512                 if (_orien == VERT) {
513                         fract = -fract;
514                 }
515
516                 adjustment.set_value (adjustment.get_value() + scale * fract * (adjustment.get_upper() - adjustment.get_lower()));
517         }
518
519         return true;
520 }
521
522 void
523 PixFader::adjustment_changed ()
524 {
525         if (display_span() != last_drawn) {
526                 queue_draw ();
527         }
528 }
529
530 /** @return pixel offset of the current value from the right or bottom of the fader */
531 int
532 PixFader::display_span ()
533 {
534         float fract = (adjustment.get_value () - adjustment.get_lower()) / ((adjustment.get_upper() - adjustment.get_lower()));
535         int ds;
536         if (_orien == VERT) {
537                 ds = (int)floor ( span * (1.0 - fract));
538         } else {
539                 ds = (int)floor (span * fract);
540         }
541         
542         return ds;
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 }