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