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