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