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