d4fee88fee1983e3052d06e6060eb6ed790893c8
[ardour.git] / gtk2_ardour / ardour_button.cc
1 /*
2     Copyright (C) 2010 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 */
19
20 #include <iostream>
21 #include <cmath>
22 #include <algorithm>
23
24 #include <pangomm/layout.h>
25
26 #include "pbd/compose.h"
27 #include "pbd/error.h"
28 #include "pbd/stacktrace.h"
29
30 #include "gtkmm2ext/utils.h"
31 #include "gtkmm2ext/rgb_macros.h"
32 #include "gtkmm2ext/gui_thread.h"
33
34 #include "ardour/rc_configuration.h" // for widget prelight preference
35
36 #include "canvas/utils.h"
37 #include "canvas/colors.h"
38
39 #include "ardour_button.h"
40 #include "ardour_ui.h"
41 #include "global_signals.h"
42
43 #include "i18n.h"
44
45 #define BASELINESTRETCH (1.25)
46 #define TRACKHEADERBTNW (3.10)
47
48 using namespace Gdk;
49 using namespace Gtk;
50 using namespace Glib;
51 using namespace PBD;
52 using std::max;
53 using std::min;
54 using namespace std;
55
56 ArdourButton::Element ArdourButton::default_elements = ArdourButton::Element (ArdourButton::Edge|ArdourButton::Body|ArdourButton::Text);
57 ArdourButton::Element ArdourButton::led_default_elements = ArdourButton::Element (ArdourButton::default_elements|ArdourButton::Indicator);
58 ArdourButton::Element ArdourButton::just_led_default_elements = ArdourButton::Element (ArdourButton::Edge|ArdourButton::Body|ArdourButton::Indicator);
59
60 ArdourButton::ArdourButton (Element e)
61         : _elements (e)
62         , _icon (ArdourButton::NoIcon)
63         , _tweaks (Tweaks (0))
64         , _char_pixel_width (0)
65         , _char_pixel_height (0)
66         , _char_avg_pixel_width (0)
67         , _text_width (0)
68         , _text_height (0)
69         , _diameter (0)
70         , _corner_radius (2.5)
71         , _corner_mask (0xf)
72         , _angle(0)
73         , _xalign(.5)
74         , _yalign(.5)
75         , fill_inactive_color (0)
76         , fill_active_color (0)
77         , text_active_color(0)
78         , text_inactive_color(0)
79         , led_active_color(0)
80         , led_inactive_color(0)
81         , led_custom_color (0)
82         , use_custom_led_color (false)
83         , convex_pattern (0)
84         , concave_pattern (0)
85         , led_inset_pattern (0)
86         , _led_rect (0)
87         , _act_on_release (true)
88         , _led_left (false)
89         , _distinct_led_click (false)
90         , _hovering (false)
91         , _focused (false)
92         , _fixed_colors_set (false)
93         , _fallthrough_to_parent (false)
94         , _layout_ellipsize_width (-1)
95         , _ellipsis (Pango::ELLIPSIZE_NONE)
96         , _update_colors (true)
97         , _pattern_height (0)
98 {
99         ARDOUR_UI_UTILS::ColorsChanged.connect (sigc::mem_fun (*this, &ArdourButton::color_handler));
100 }
101
102 ArdourButton::ArdourButton (const std::string& str, Element e)
103         : _elements (e)
104         , _tweaks (Tweaks (0))
105         , _text_width (0)
106         , _text_height (0)
107         , _diameter (0)
108         , _corner_radius (2.5)
109         , _corner_mask (0xf)
110         , _angle(0)
111         , _xalign(.5)
112         , _yalign(.5)
113         , fill_inactive_color (0)
114         , fill_active_color (0)
115         , text_active_color(0)
116         , text_inactive_color(0)
117         , led_active_color(0)
118         , led_inactive_color(0)
119         , led_custom_color (0)
120         , use_custom_led_color (false)
121         , convex_pattern (0)
122         , concave_pattern (0)
123         , led_inset_pattern (0)
124         , _led_rect (0)
125         , _act_on_release (true)
126         , _led_left (false)
127         , _distinct_led_click (false)
128         , _hovering (false)
129         , _focused (false)
130         , _fixed_colors_set (false)
131         , _fallthrough_to_parent (false)
132         , _layout_ellipsize_width (-1)
133         , _ellipsis (Pango::ELLIPSIZE_NONE)
134         , _update_colors (true)
135         , _pattern_height (0)
136 {
137         set_text (str);
138         ARDOUR_UI_UTILS::ColorsChanged.connect (sigc::mem_fun (*this, &ArdourButton::color_handler));
139         ARDOUR_UI_UTILS::DPIReset.connect (sigc::mem_fun (*this, &ArdourButton::on_name_changed));
140 }
141
142 ArdourButton::~ArdourButton()
143 {
144         delete _led_rect;
145
146         if (convex_pattern) {
147                 cairo_pattern_destroy (convex_pattern);
148         }
149
150         if (concave_pattern) {
151                 cairo_pattern_destroy (concave_pattern);
152         }
153
154         if (led_inset_pattern) {
155                 cairo_pattern_destroy (led_inset_pattern);
156         }
157 }
158
159 void
160 ArdourButton::set_layout_font (const Pango::FontDescription& fd)
161 {
162         ensure_layout ();
163         if (_layout) {
164                 _layout->set_font_description (fd);
165                 queue_resize ();
166         }
167 }
168
169 void
170 ArdourButton::set_text (const std::string& str)
171 {
172         _text = str;
173         if (!is_realized()) {
174                 return;
175         }
176         ensure_layout ();
177         if (_layout && _layout->get_text() != _text) {
178                 _layout->set_text (_text);
179                 queue_resize ();
180         }
181 }
182
183 void
184 ArdourButton::set_angle (const double angle)
185 {
186         _angle = angle;
187 }
188
189 void
190 ArdourButton::set_alignment (const float xa, const float ya)
191 {
192         _xalign = xa;
193         _yalign = ya;
194 }
195
196 void
197 ArdourButton::render (cairo_t* cr, cairo_rectangle_t *)
198 {
199         uint32_t text_color;
200         uint32_t led_color;
201
202         if (_update_colors) {
203                 set_colors ();
204         }
205         if (get_height() != _pattern_height) {
206                 build_patterns ();
207         }
208
209         if ( active_state() == Gtkmm2ext::ExplicitActive ) {
210                 text_color = text_active_color;
211                 led_color = led_active_color;
212         } else {
213                 text_color = text_inactive_color;
214                 led_color = led_inactive_color;
215         }
216
217         if (use_custom_led_color) {
218                 led_color = led_custom_color;
219         }
220
221         void (*rounded_function)(cairo_t*, double, double, double, double, double);
222
223         switch (_corner_mask) {
224         case 0x1: /* upper left only */
225                 rounded_function = Gtkmm2ext::rounded_top_left_rectangle;
226                 break;
227         case 0x2: /* upper right only */
228                 rounded_function = Gtkmm2ext::rounded_top_right_rectangle;
229                 break;
230         case 0x3: /* upper only */
231                 rounded_function = Gtkmm2ext::rounded_top_rectangle;
232                 break;
233                 /* should really have functions for lower right, lower left,
234                    lower only, but for now, we don't
235                 */
236         default:
237                 rounded_function = Gtkmm2ext::rounded_rectangle;
238         }
239
240         // draw edge (filling a rect underneath, rather than stroking a border on top, allows the corners to be lighter-weight.
241         if ((_elements & (Body|Edge)) == (Body|Edge)) {
242                 rounded_function (cr, 0, 0, get_width(), get_height(), _corner_radius + 1.5);
243                 cairo_set_source_rgba (cr, 0, 0, 0, 1);
244                 cairo_fill(cr);
245         }
246
247         // background fill
248         if ((_elements & Body)==Body) {
249                 rounded_function (cr, 1, 1, get_width() - 2, get_height() - 2, _corner_radius);
250                 if (active_state() == Gtkmm2ext::ImplicitActive && !((_elements & Indicator)==Indicator)) {
251                         ArdourCanvas::set_source_rgba (cr, fill_inactive_color);
252                         cairo_fill (cr);
253                 } else if ( (active_state() == Gtkmm2ext::ExplicitActive) && !((_elements & Indicator)==Indicator) ) {
254                         //background color
255                         ArdourCanvas::set_source_rgba (cr, fill_active_color);
256                         cairo_fill (cr);
257                 } else {  //inactive, or it has an indicator
258                         //background color
259                         ArdourCanvas::set_source_rgba (cr, fill_inactive_color);
260                 }
261                 cairo_fill (cr);
262         }
263
264         // IMPLICIT ACTIVE: draw a border of the active color
265         if ((_elements & Body)==Body) {
266                 if (active_state() == Gtkmm2ext::ImplicitActive && !((_elements & Indicator)==Indicator)) {
267                         cairo_set_line_width (cr, 2.0);
268                         rounded_function (cr, 2, 2, get_width() - 4, get_height() - 4, _corner_radius-0.5);
269                         ArdourCanvas::set_source_rgba (cr, fill_active_color);
270                         cairo_stroke (cr);
271                 }
272         }
273
274         //show the "convex" or "concave" gradient
275         if (!_flat_buttons) {
276                 if ( active_state() == Gtkmm2ext::ExplicitActive && ( !((_elements & Indicator)==Indicator) || use_custom_led_color) ) {
277                         //concave
278                         cairo_set_source (cr, concave_pattern);
279                         Gtkmm2ext::rounded_rectangle (cr, 1, 1, get_width() - 2, get_height() - 2, _corner_radius);
280                         cairo_fill (cr);
281                 } else {
282                         cairo_set_source (cr, convex_pattern);
283                         Gtkmm2ext::rounded_rectangle (cr, 1, 1, get_width() - 2, get_height() - 2, _corner_radius);
284                         cairo_fill (cr);
285                 }
286         }
287
288         //Pixbuf, if any
289         if (_pixbuf) {
290                 double x = rint((get_width() - _pixbuf->get_width()) * .5);
291                 const double y = rint((get_height() - _pixbuf->get_height()) * .5);
292 #if 0 // DEBUG style (print on hover)
293                 if (_hovering || (_elements & Inactive)) {
294                         printf("%s: p:%dx%d (%dx%d)\n",
295                                         get_name().c_str(),
296                                         _pixbuf->get_width(), _pixbuf->get_height(),
297                                         get_width(), get_height());
298                 }
299 #endif
300                 if (_elements & Menu) {
301                         //if this is a DropDown with an icon, then we need to
302                         //move the icon left slightly to accomomodate the arrow
303                         x -= _diameter - 2;
304                 }
305                 cairo_rectangle (cr, x, y, _pixbuf->get_width(), _pixbuf->get_height());
306                 gdk_cairo_set_source_pixbuf (cr, _pixbuf->gobj(), x, y);
307                 cairo_fill (cr);
308         }
309         else // rec-en is exclusive to pixbuf (tape machine mode, rec-en)
310         if ((_elements & VectorIcon) && _icon == RecTapeMode) {
311                 const double x = get_width() * .5;
312                 const double y = get_height() * .5;
313                 const double r = std::min(10., std::min(x, y) * .6); // TODO we need a better way to limit max. radius.
314                 const double slit = .11 * M_PI;
315                 cairo_save(cr);
316                 cairo_translate(cr, x, y);
317
318                 cairo_arc (cr, 0, 0, r, 0, 2 * M_PI);
319                 if (active_state() == Gtkmm2ext::ExplicitActive)
320                         cairo_set_source_rgba (cr, .95, .1, .1, 1.);
321                 else
322                         cairo_set_source_rgba (cr, .95, .44, .44, 1.); // #f46f6f
323                 cairo_fill_preserve(cr);
324                 cairo_set_source_rgba (cr, .0, .0, .0, .5);
325                 cairo_set_line_width(cr, 1);
326                 cairo_stroke(cr);
327
328                 cairo_save(cr);
329                 cairo_set_source_rgba (cr, .15, .07, .07, 1.0);
330
331                 cairo_rotate (cr, -.5 * M_PI);
332                 cairo_move_to(cr, 0, 0);
333                 cairo_arc (cr, 0, 0, r *.85, -slit, slit);
334                 cairo_line_to(cr, 0, 0);
335                 cairo_close_path(cr);
336
337                 cairo_fill(cr);
338                 cairo_rotate (cr, 2. * M_PI / 3.);
339
340                 cairo_move_to(cr, 0, 0);
341                 cairo_arc (cr, 0, 0, r *.85, -slit, slit);
342                 cairo_line_to(cr, 0, 0);
343                 cairo_close_path(cr);
344                 cairo_fill(cr);
345
346                 cairo_rotate (cr, 2. * M_PI / 3.);
347                 cairo_move_to(cr, 0, 0);
348                 cairo_arc (cr, 0, 0, r *.85, -slit, slit);
349                 cairo_line_to(cr, 0, 0);
350                 cairo_close_path(cr);
351                 cairo_fill(cr);
352
353                 cairo_restore(cr);
354
355                 cairo_arc (cr, 0, 0, r * .3, 0, 2 * M_PI);
356                 if (active_state() == Gtkmm2ext::ExplicitActive)
357                         cairo_set_source_rgba (cr, .95, .1, .1, 1.);
358                 else
359                         cairo_set_source_rgba (cr, .95, .44, .44, 1.); // #f46f6f
360                 cairo_fill(cr);
361                 cairo_set_source_rgba (cr, .0, .0, .0, 1.0);
362                 cairo_arc (cr, 0, 0, r *.15, 0, 2 * M_PI); // hole in the middle
363                 cairo_fill(cr);
364
365                 cairo_restore(cr);
366         }
367         else if ((_elements & VectorIcon) && _icon == RecButton) {
368                 const double x = get_width() * .5;
369                 const double y = get_height() * .5;
370                 const double r = std::min(10., std::min(x, y) * .55); // TODO we need a better way to limit max. radius.
371                 cairo_arc (cr, x, y, r, 0, 2 * M_PI);
372                 if (active_state() == Gtkmm2ext::ExplicitActive)
373                         cairo_set_source_rgba (cr, .95, .1, .1, 1.);
374                 else
375                         cairo_set_source_rgba (cr, .95, .44, .44, 1.); // #f46f6f
376                 cairo_fill_preserve(cr);
377                 cairo_set_source_rgba (cr, .0, .0, .0, .8);
378                 cairo_set_line_width(cr, 1);
379                 cairo_stroke(cr);
380         }
381         else if ((_elements & VectorIcon) && _icon == CloseCross) {
382                 const double x = get_width() * .5;
383                 const double y = get_height() * .5;
384                 const double o = .5 + std::min(x, y) * .4;
385                 ArdourCanvas::set_source_rgba (cr, text_color);
386                 cairo_set_line_width(cr, 1);
387                 cairo_move_to(cr, x-o, y-o);
388                 cairo_line_to(cr, x+o, y+o);
389                 cairo_move_to(cr, x+o, y-o);
390                 cairo_line_to(cr, x-o, y+o);
391                 cairo_stroke(cr);
392         }
393         else if ((_elements & VectorIcon) && _icon == StripWidth) {
394                 const double x0 = get_width()  * .2;
395                 const double x1 = get_width()  * .8;
396
397                 const double y0 = get_height() * .25;
398                 const double y1= get_height()  * .75;
399
400                 const double ym= get_height()  * .5;
401
402                 // arrow
403                 const double xa0= get_height()  * .39;
404                 const double xa1= get_height()  * .61;
405                 const double ya0= get_height()  * .35;
406                 const double ya1= get_height()  * .65;
407
408                 ArdourCanvas::set_source_rgba (cr, text_color);
409                 cairo_set_line_width(cr, 1);
410
411                 // left + right
412                 cairo_move_to(cr, x0, y0);
413                 cairo_line_to(cr, x0, y1);
414                 cairo_move_to(cr, x1, y0);
415                 cairo_line_to(cr, x1, y1);
416
417                 // horiz center line
418                 cairo_move_to(cr, x0, ym);
419                 cairo_line_to(cr, x1, ym);
420
421                 // arrow left
422                 cairo_move_to(cr,  x0, ym);
423                 cairo_line_to(cr, xa0, ya0);
424                 cairo_move_to(cr,  x0, ym);
425                 cairo_line_to(cr, xa0, ya1);
426
427                 // arrow right
428                 cairo_move_to(cr,  x1,  ym);
429                 cairo_line_to(cr, xa1, ya0);
430                 cairo_move_to(cr,  x1,  ym);
431                 cairo_line_to(cr, xa1, ya1);
432                 cairo_stroke(cr);
433         }
434         else if ((_elements & VectorIcon) && _icon == DinMidi) {
435                 const double x = get_width() * .5;
436                 const double y = get_height() * .5;
437                 const double r = std::min(x, y) * .75;
438                 ArdourCanvas::set_source_rgba (cr, text_color);
439                 cairo_set_line_width(cr, 1);
440                 cairo_arc (cr, x, y, r, 0, 2 * M_PI);
441                 cairo_stroke(cr);
442
443                 // pins equally spaced 45deg
444                 cairo_arc (cr, x, y * 0.5, r * .15, 0, 2 * M_PI);
445                 cairo_fill(cr);
446                 cairo_arc (cr, x * 0.5, y, r * .15, 0, 2 * M_PI);
447                 cairo_fill(cr);
448                 cairo_arc (cr, x * 1.5, y, r * .15, 0, 2 * M_PI);
449                 cairo_fill(cr);
450                 //  .5 + .5 * .5 * sin(45deg),  1.5 - .5 * .5 * cos(45deg)
451                 cairo_arc (cr, x * 0.677, y * .677, r * .15, 0, 2 * M_PI);
452                 cairo_fill(cr);
453                 cairo_arc (cr, x * 1.323, y * .677, r * .15, 0, 2 * M_PI);
454                 cairo_fill(cr);
455
456                 // bottom notch
457                 cairo_arc (cr, x, y+r, r * .28, 1.05 * M_PI, 1.95 * M_PI);
458                 cairo_stroke(cr);
459         }
460
461         const int text_margin = char_pixel_width();
462         // Text, if any
463         if (!_pixbuf && ((_elements & Text)==Text) && !_text.empty()) {
464                 assert(_layout);
465 #if 0 // DEBUG style (print on hover)
466                 if (_hovering || (_elements & Inactive)) {
467                         bool layout_font = true;
468                         Pango::FontDescription fd = _layout->get_font_description();
469                         if (fd.gobj() == NULL) {
470                                 layout_font = false;
471                                 fd = get_pango_context()->get_font_description();
472                         }
473                         printf("%s: f:%dx%d aw:%.3f bh:%.0f t:%dx%d (%dx%d) %s\"%s\"\n",
474                                         get_name().c_str(),
475                                         char_pixel_width(), char_pixel_height(), char_avg_pixel_width(),
476                                         ceil(char_pixel_height() * BASELINESTRETCH),
477                                         _text_width, _text_height,
478                                         get_width(), get_height(),
479                                         layout_font ? "L:" : "W:",
480                                         fd.to_string().c_str());
481                 }
482 #endif
483
484                 cairo_save (cr);
485                 cairo_rectangle (cr, 2, 1, get_width() - 4, get_height() - 2);
486                 cairo_clip(cr);
487
488                 cairo_new_path (cr);
489                 ArdourCanvas::set_source_rgba (cr, text_color);
490                 const double text_ypos = (get_height() - _text_height) * .5;
491
492                 if (_elements & Menu) {
493                         // always left align (dropdown)
494                         cairo_move_to (cr, text_margin, text_ypos);
495                         pango_cairo_show_layout (cr, _layout->gobj());
496                 } else if ( (_elements & Indicator)  == Indicator) {
497                         // left/right align depending on LED position
498                         if (_led_left) {
499                                 cairo_move_to (cr, text_margin + _diameter + .5 * char_pixel_width(), text_ypos);
500                         } else {
501                                 cairo_move_to (cr, text_margin, text_ypos);
502                         }
503                         pango_cairo_show_layout (cr, _layout->gobj());
504                 } else {
505                         /* centered text otherwise */
506                         double ww, wh;
507                         double xa, ya;
508                         ww = get_width();
509                         wh = get_height();
510
511                         cairo_save (cr);
512                         cairo_rotate(cr, _angle * M_PI / 180.0);
513                         cairo_device_to_user(cr, &ww, &wh);
514                         xa = (ww - _text_width) * _xalign;
515                         ya = (wh - _text_height) * _yalign;
516
517                         /* quick hack for left/bottom alignment at -90deg
518                          * TODO this should be generalized incl rotation.
519                          * currently only 'user' of this API is meter_strip.cc
520                          */
521                         if (_xalign < 0) xa = ceil(.5 + (ww * fabs(_xalign) + text_margin));
522
523                         cairo_move_to (cr, xa, ya);
524                         pango_cairo_update_layout(cr, _layout->gobj());
525                         pango_cairo_show_layout (cr, _layout->gobj());
526                         cairo_restore (cr);
527                 }
528                 cairo_restore (cr);
529         }
530
531         //Menu "triangle"
532         if (_elements & Menu) {
533                 const float trih = ceil(_diameter * .5);
534                 const float triw2 = ceil(.577 * _diameter * .5); // 1/sqrt(3) Equilateral triangle
535                 //menu arrow
536                 cairo_set_source_rgba (cr, 1, 1, 1, 0.4);
537                 cairo_move_to(cr, get_width() - triw2 - 3. , rint((get_height() + trih) * .5));
538                 cairo_rel_line_to(cr, -triw2, -trih);
539                 cairo_rel_line_to(cr, 2. * triw2, 0);
540                 cairo_close_path(cr);
541
542                 cairo_set_source_rgba (cr, 1, 1, 1, 0.4);
543                 cairo_fill(cr);
544
545                 cairo_move_to(cr, get_width() - triw2 - 3 , rint((get_height() + trih) * .5));
546                 cairo_rel_line_to(cr, .5 - triw2, .5 - trih);
547                 cairo_rel_line_to(cr, 2. * triw2 - 1, 0);
548                 cairo_close_path(cr);
549                 cairo_set_source_rgba (cr, 0, 0, 0, 0.8);
550                 cairo_set_line_width(cr, 1);
551                 cairo_stroke(cr);
552         }
553
554         //Indicator LED
555         if (_elements & Indicator) {
556                 cairo_save (cr);
557
558                 /* move to the center of the indicator/led */
559                 if (_elements & Text) {
560                         int led_xoff = ceil(char_pixel_width() + _diameter * .5);
561                         if (_led_left) {
562                                 cairo_translate (cr, led_xoff, get_height() * .5);
563                         } else {
564                                 cairo_translate (cr, get_width() - led_xoff, get_height() * .5);
565                         }
566                 } else {
567                         cairo_translate (cr, get_width() * .5, get_height() * .5);
568                 }
569
570                 //inset
571                 if (!_flat_buttons) {
572                         cairo_arc (cr, 0, 0, _diameter * .5, 0, 2 * M_PI);
573                         cairo_set_source (cr, led_inset_pattern);
574                         cairo_fill (cr);
575                 }
576
577                 //black ring
578                 cairo_set_source_rgb (cr, 0, 0, 0);
579                 cairo_arc (cr, 0, 0, _diameter * .5 - 1, 0, 2 * M_PI);
580                 cairo_fill(cr);
581
582                 //led color
583                 ArdourCanvas::set_source_rgba (cr, led_color);
584                 cairo_arc (cr, 0, 0, _diameter * .5 - 3, 0, 2 * M_PI);
585                 cairo_fill(cr);
586
587                 cairo_restore (cr);
588         }
589
590         // a transparent overlay to indicate insensitivity
591         if ((visual_state() & Gtkmm2ext::Insensitive)) {
592                 rounded_function (cr, 0, 0, get_width(), get_height(), _corner_radius);
593                 uint32_t ins_color = ARDOUR_UI::config()->color ("gtk_background");
594                 ArdourCanvas::set_source_rgb_a (cr, ins_color, 0.6);
595                 cairo_fill (cr);
596         }
597
598         // if requested, show hovering
599         if (ARDOUR_UI::config()->get_widget_prelight()
600                         && !((visual_state() & Gtkmm2ext::Insensitive))) {
601                 if (_hovering) {
602                         rounded_function (cr, 1, 1, get_width() - 2, get_height() - 2, _corner_radius);
603                         cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.2);
604                         cairo_fill (cr);
605                 }
606         }
607
608         //user is currently pressing the button. dark outline helps to indicate this
609         if (_grabbed && !(_elements & (Inactive|Menu))) {
610                 rounded_function (cr, 1, 1, get_width() - 2, get_height() - 2, _corner_radius);
611                 cairo_set_line_width(cr, 2);
612                 cairo_set_source_rgba (cr, 0.1, 0.1, 0.1, .5);
613                 cairo_stroke (cr);
614         }
615
616         //some buttons (like processor boxes) can be selected  (so they can be deleted).  Draw a selection indicator
617         if (visual_state() & Gtkmm2ext::Selected) {
618                 cairo_set_line_width(cr, 1);
619                 cairo_set_source_rgba (cr, 1, 0, 0, 0.8);
620                 rounded_function (cr, 0.5, 0.5, get_width() - 1, get_height() - 1, _corner_radius);
621                 cairo_stroke (cr);
622         }
623
624         //I guess this means we have keyboard focus.  I don't think this works currently
625         //
626         //A: yes, it's keyboard focus and it does work when there's no editor window
627         //   (the editor is always the first receiver for KeyDown).
628         //   It's needed for eg. the engine-dialog at startup or after closing a sesion.
629         if (_focused) {
630                 rounded_function (cr, 1.5, 1.5, get_width() - 3, get_height() - 3, _corner_radius);
631                 cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.8);
632                 double dashes = 1;
633                 cairo_set_dash (cr, &dashes, 1, 0);
634                 cairo_set_line_cap (cr, CAIRO_LINE_CAP_BUTT);
635                 cairo_set_line_width (cr, 1.0);
636                 cairo_stroke (cr);
637                 cairo_set_dash (cr, 0, 0, 0);
638         }
639 }
640
641 void
642 ArdourButton::set_corner_radius (float r)
643 {
644         _corner_radius = r;
645         CairoWidget::set_dirty ();
646 }
647
648 void
649 ArdourButton::on_realize()
650 {
651         CairoWidget::on_realize ();
652         ensure_layout ();
653         if (_layout && _layout->get_text() != _text) {
654                 _layout->set_text (_text);
655                 queue_resize ();
656         }
657 }
658
659 void
660 ArdourButton::on_size_request (Gtk::Requisition* req)
661 {
662         req->width = req->height = 0;
663         CairoWidget::on_size_request (req);
664
665         if (_diameter == 0) {
666                 const float newdia = rint (ARDOUR_UI::config()->get_font_scale () / 9600.0); // 11px with 100% font-scaling
667                 if (_diameter != newdia) {
668                         _pattern_height = 0;
669                         _diameter = newdia;
670                 }
671         }
672
673         if ((_elements & Text) && !_text.empty()) {
674                 // if _layout does not exist, char_pixel_height() creates it,
675                 req->height = std::max(req->height, (int) ceil(char_pixel_height() * BASELINESTRETCH + 1.0));
676                 _layout->get_pixel_size (_text_width, _text_height);
677                 req->width += rint(1.75 * char_pixel_width()); // padding
678                 req->width += _text_width;
679         } else {
680                 _text_width = 0;
681                 _text_height = 0;
682         }
683
684         if (_pixbuf) {
685                 req->width += _pixbuf->get_width() + char_pixel_width();
686                 req->height = std::max(req->height, _pixbuf->get_height() + 4);
687         }
688
689         if (_elements & Indicator) {
690                 req->width += lrint (_diameter) + char_pixel_width();
691                 req->height = std::max (req->height, (int) lrint (_diameter) + 4);
692         }
693
694         if ((_elements & Menu)) {
695                 req->width += _diameter + 4;
696         }
697
698         if (_elements & VectorIcon) {
699                 assert(!(_elements & Text));
700                 const int wh = std::max (rint (TRACKHEADERBTNW * char_avg_pixel_width()), ceil (char_pixel_height() * BASELINESTRETCH + 1.));
701                 req->width += wh;
702                 req->height = std::max(req->height, wh);
703         }
704
705         /* Tweaks to mess the nice stuff above up again. */
706         if (_tweaks & TrackHeader) {
707                 // forget everything above and just use a fixed square [em] size
708                 // "TrackHeader Buttons" are single letter (usually uppercase)
709                 // a SizeGroup is much less efficient (lots of gtk work under the hood for each track)
710                 const int wh = std::max (rint (TRACKHEADERBTNW * char_avg_pixel_width()), ceil (char_pixel_height() * BASELINESTRETCH + 1.));
711                 req->width  = wh;
712                 req->height = wh;
713         }
714         else if (_tweaks & Square) {
715                 // currerntly unused (again)
716                 if (req->width < req->height)
717                         req->width = req->height;
718                 if (req->height < req->width)
719                         req->height = req->width;
720         } else if (_text_width > 0 && !(_elements & (Menu | Indicator))) {
721                 // properly centered text for those elements that are centered
722                 // (no sub-pixel offset)
723                 if ((req->width - _text_width) & 1) { ++req->width; }
724                 if ((req->height - _text_height) & 1) { ++req->height; }
725         }
726 #if 0
727                 printf("REQ: %s: %dx%d\n", get_name().c_str(), req->width, req->height);
728 #endif
729 }
730
731 /**
732  * This sets the colors used for rendering based on the name of the button, and
733  * thus uses information from the GUI config data.
734  */
735 void
736 ArdourButton::set_colors ()
737 {
738         _update_colors = false;
739         if (_fixed_colors_set) {
740                 return;
741         }
742         std::string name = get_name();
743         bool failed = false;
744
745         fill_active_color = ARDOUR_UI::config()->color (string_compose ("%1: fill active", name), &failed);
746         if (failed) {
747                 fill_active_color = ARDOUR_UI::config()->color ("generic button: fill active");
748         }
749         fill_inactive_color = ARDOUR_UI::config()->color (string_compose ("%1: fill", name), &failed);
750         if (failed) {
751                 fill_inactive_color = ARDOUR_UI::config()->color ("generic button: fill");
752         }
753
754         text_active_color = ArdourCanvas::contrasting_text_color (fill_active_color);
755         text_inactive_color = ArdourCanvas::contrasting_text_color (fill_inactive_color);
756
757         led_active_color = ARDOUR_UI::config()->color (string_compose ("%1: led active", name), &failed);
758         if (failed) {
759                 led_active_color = ARDOUR_UI::config()->color ("generic button: led active");
760         }
761
762         /* The inactive color for the LED is just a fairly dark version of the
763          * active color.
764          */
765         
766         ArdourCanvas::HSV inactive (led_active_color);
767         inactive.v = 0.35;
768
769         led_inactive_color = inactive.color ();
770 }
771
772 /**
773  * This sets the colors used for rendering based on two fixed values, rather
774  * than basing them on the button name, and thus information in the GUI config
775  * data.
776  */
777 void ArdourButton::set_fixed_colors (const uint32_t color_active, const uint32_t color_inactive)
778 {
779         _fixed_colors_set = true;
780
781         fill_active_color = color_active;
782         fill_inactive_color = color_inactive;
783
784         unsigned char r, g, b, a;
785         UINT_TO_RGBA(color_active, &r, &g, &b, &a);
786
787         double white_contrast = (max (double(r), 255.) - min (double(r), 255.)) +
788                 (max (double(g), 255.) - min (double(g), 255.)) +
789                 (max (double(b), 255.) - min (double(b), 255.));
790
791         double black_contrast = (max (double(r), 0.) - min (double(r), 0.)) +
792                 (max (double(g), 0.) - min (double(g), 0.)) +
793                 (max (double(b), 0.) - min (double(b), 0.));
794
795         text_active_color = (white_contrast > black_contrast) ?
796                 RGBA_TO_UINT(255, 255, 255, 255) : /* use white */
797                 RGBA_TO_UINT(  0,   0,   0,   255);  /* use black */
798
799
800         UINT_TO_RGBA(color_inactive, &r, &g, &b, &a);
801
802         white_contrast = (max (double(r), 255.) - min (double(r), 255.)) +
803                 (max (double(g), 255.) - min (double(g), 255.)) +
804                 (max (double(b), 255.) - min (double(b), 255.));
805
806         black_contrast = (max (double(r), 0.) - min (double(r), 0.)) +
807                 (max (double(g), 0.) - min (double(g), 0.)) +
808                 (max (double(b), 0.) - min (double(b), 0.));
809
810         text_inactive_color = (white_contrast > black_contrast) ?
811                 RGBA_TO_UINT(255, 255, 255, 255) : /* use white */
812                 RGBA_TO_UINT(  0,   0,   0,   255);  /* use black */
813
814         /* XXX what about led colors ? */
815         CairoWidget::set_dirty ();
816 }
817
818 void
819 ArdourButton::build_patterns ()
820 {
821         if (convex_pattern) {
822                 cairo_pattern_destroy (convex_pattern);
823                 convex_pattern = 0;
824         }
825
826         if (concave_pattern) {
827                 cairo_pattern_destroy (concave_pattern);
828                 concave_pattern = 0;
829         }
830
831         if (led_inset_pattern) {
832                 cairo_pattern_destroy (led_inset_pattern);
833                 led_inset_pattern = 0;
834         }
835
836         //convex gradient
837         convex_pattern = cairo_pattern_create_linear (0.0, 0, 0.0,  get_height());
838         cairo_pattern_add_color_stop_rgba (convex_pattern, 0.0, 0,0,0, 0.0);
839         cairo_pattern_add_color_stop_rgba (convex_pattern, 1.0, 0,0,0, 0.35);
840
841         //concave gradient
842         concave_pattern = cairo_pattern_create_linear (0.0, 0, 0.0,  get_height());
843         cairo_pattern_add_color_stop_rgba (concave_pattern, 0.0, 0,0,0, 0.5);
844         cairo_pattern_add_color_stop_rgba (concave_pattern, 0.7, 0,0,0, 0.0);
845
846         led_inset_pattern = cairo_pattern_create_linear (0.0, 0.0, 0.0, _diameter);
847         cairo_pattern_add_color_stop_rgba (led_inset_pattern, 0, 0,0,0, 0.4);
848         cairo_pattern_add_color_stop_rgba (led_inset_pattern, 1, 1,1,1, 0.7);
849
850         _pattern_height = get_height() ;
851 }
852
853 void
854 ArdourButton::set_led_left (bool yn)
855 {
856         _led_left = yn;
857 }
858
859 bool
860 ArdourButton::on_button_press_event (GdkEventButton *ev)
861 {
862         focus_handler ();
863
864         if (ev->button == 1 && (_elements & Indicator) && _led_rect && _distinct_led_click) {
865                 if (ev->x >= _led_rect->x && ev->x < _led_rect->x + _led_rect->width &&
866                     ev->y >= _led_rect->y && ev->y < _led_rect->y + _led_rect->height) {
867                         return true;
868                 }
869         }
870
871         if (binding_proxy.button_press_handler (ev)) {
872                 return true;
873         }
874
875         _grabbed = true;
876         CairoWidget::set_dirty ();
877
878         if (ev->button == 1 && !_act_on_release) {
879                 if (_action) {
880                         _action->activate ();
881                         return true;
882                 }
883         }
884
885         if (_fallthrough_to_parent)
886                 return false;
887
888         return true;
889 }
890
891 bool
892 ArdourButton::on_button_release_event (GdkEventButton *ev)
893 {
894         if (ev->button == 1 && _hovering && (_elements & Indicator) && _led_rect && _distinct_led_click) {
895                 if (ev->x >= _led_rect->x && ev->x < _led_rect->x + _led_rect->width &&
896                     ev->y >= _led_rect->y && ev->y < _led_rect->y + _led_rect->height) {
897                         signal_led_clicked(); /* EMIT SIGNAL */
898                         return true;
899                 }
900         }
901
902         _grabbed = false;
903         CairoWidget::set_dirty ();
904
905         if (ev->button == 1 && _hovering) {
906                 signal_clicked ();
907                 if (_act_on_release) {
908                         if (_action) {
909                                 _action->activate ();
910                                 return true;
911                         }
912                 }
913         }
914
915         if (_fallthrough_to_parent)
916                 return false;
917
918         return true;
919 }
920
921 void
922 ArdourButton::set_distinct_led_click (bool yn)
923 {
924         _distinct_led_click = yn;
925         setup_led_rect ();
926 }
927
928 void
929 ArdourButton::color_handler ()
930 {
931         _update_colors = true;
932         CairoWidget::set_dirty ();
933 }
934
935 void
936 ArdourButton::on_size_allocate (Allocation& alloc)
937 {
938         CairoWidget::on_size_allocate (alloc);
939         setup_led_rect ();
940 }
941
942 void
943 ArdourButton::set_controllable (boost::shared_ptr<Controllable> c)
944 {
945         watch_connection.disconnect ();
946         binding_proxy.set_controllable (c);
947 }
948
949 void
950 ArdourButton::watch ()
951 {
952         boost::shared_ptr<Controllable> c (binding_proxy.get_controllable ());
953
954         if (!c) {
955                 warning << _("button cannot watch state of non-existing Controllable\n") << endmsg;
956                 return;
957         }
958         c->Changed.connect (watch_connection, invalidator(*this), boost::bind (&ArdourButton::controllable_changed, this), gui_context());
959 }
960
961 void
962 ArdourButton::controllable_changed ()
963 {
964         float val = binding_proxy.get_controllable()->get_value();
965
966         if (fabs (val) >= 0.5f) {
967                 set_active_state (Gtkmm2ext::ExplicitActive);
968         } else {
969                 unset_active_state ();
970         }
971         CairoWidget::set_dirty ();
972 }
973
974 void
975 ArdourButton::set_related_action (RefPtr<Action> act)
976 {
977         Gtkmm2ext::Activatable::set_related_action (act);
978
979         if (_action) {
980
981                 action_tooltip_changed ();
982
983                 Glib::RefPtr<ToggleAction> tact = Glib::RefPtr<ToggleAction>::cast_dynamic (_action);
984                 if (tact) {
985                         action_toggled ();
986                         tact->signal_toggled().connect (sigc::mem_fun (*this, &ArdourButton::action_toggled));
987                 }
988
989                 _action->connect_property_changed ("sensitive", sigc::mem_fun (*this, &ArdourButton::action_sensitivity_changed));
990                 _action->connect_property_changed ("visible", sigc::mem_fun (*this, &ArdourButton::action_visibility_changed));
991                 _action->connect_property_changed ("tooltip", sigc::mem_fun (*this, &ArdourButton::action_tooltip_changed));
992         }
993 }
994
995 void
996 ArdourButton::action_toggled ()
997 {
998         Glib::RefPtr<ToggleAction> tact = Glib::RefPtr<ToggleAction>::cast_dynamic (_action);
999
1000         if (tact) {
1001                 if (tact->get_active()) {
1002                         set_active_state (Gtkmm2ext::ExplicitActive);
1003                 } else {
1004                         unset_active_state ();
1005                 }
1006         }
1007 }
1008
1009 void
1010 ArdourButton::on_style_changed (const RefPtr<Gtk::Style>&)
1011 {
1012         _update_colors = true;
1013         CairoWidget::set_dirty ();
1014 }
1015
1016 void
1017 ArdourButton::on_name_changed ()
1018 {
1019         _char_pixel_width = 0;
1020         _char_pixel_height = 0;
1021         _diameter = 0;
1022         _update_colors = true;
1023         if (is_realized()) {
1024                 queue_resize ();
1025         }
1026 }
1027
1028 void
1029 ArdourButton::setup_led_rect ()
1030 {
1031         if (!(_elements & Indicator)) {
1032                 delete _led_rect;
1033                 _led_rect = 0;
1034                 return;
1035         }
1036
1037         if (!_led_rect) {
1038                 _led_rect = new cairo_rectangle_t;
1039         }
1040
1041         if (_elements & Text) {
1042                 if (_led_left) {
1043                         _led_rect->x = char_pixel_width();
1044                 } else {
1045                         _led_rect->x = get_width() - char_pixel_width() + _diameter;
1046                 }
1047         } else {
1048                 /* centered */
1049                 _led_rect->x = .5 * get_width() - _diameter;
1050         }
1051
1052         _led_rect->y = .5 * (get_height() - _diameter);
1053         _led_rect->width = _diameter;
1054         _led_rect->height = _diameter;
1055 }
1056
1057 void
1058 ArdourButton::set_image (const RefPtr<Gdk::Pixbuf>& img)
1059 {
1060         _pixbuf = img;
1061         if (is_realized()) {
1062                 queue_resize ();
1063         }
1064 }
1065
1066 void
1067 ArdourButton::set_active_state (Gtkmm2ext::ActiveState s)
1068 {
1069         bool changed = (_active_state != s);
1070         CairoWidget::set_active_state (s);
1071         if (changed) {
1072                 _update_colors = true;
1073                 CairoWidget::set_dirty ();
1074         }
1075 }
1076
1077 void
1078 ArdourButton::set_visual_state (Gtkmm2ext::VisualState s)
1079 {
1080         bool changed = (_visual_state != s);
1081         CairoWidget::set_visual_state (s);
1082         if (changed) {
1083                 _update_colors = true;
1084                 CairoWidget::set_dirty ();
1085         }
1086 }
1087
1088 bool
1089 ArdourButton::on_focus_in_event (GdkEventFocus* ev)
1090 {
1091         _focused = true;
1092         CairoWidget::set_dirty ();
1093         return CairoWidget::on_focus_in_event (ev);
1094 }
1095
1096 bool
1097 ArdourButton::on_focus_out_event (GdkEventFocus* ev)
1098 {
1099         _focused = false;
1100         CairoWidget::set_dirty ();
1101         return CairoWidget::on_focus_out_event (ev);
1102 }
1103
1104 bool
1105 ArdourButton::on_key_release_event (GdkEventKey *ev) {
1106         if (_focused &&
1107                         (ev->keyval == GDK_space || ev->keyval == GDK_Return))
1108         {
1109                 signal_clicked();
1110                 if (_action) {
1111                         _action->activate ();
1112                 }
1113                 return true;
1114         }
1115         return CairoWidget::on_key_release_event (ev);
1116 }
1117
1118 bool
1119 ArdourButton::on_enter_notify_event (GdkEventCrossing* ev)
1120 {
1121         _hovering = (_elements & Inactive) ? false : true;
1122
1123         if (ARDOUR_UI::config()->get_widget_prelight()) {
1124                 CairoWidget::set_dirty ();
1125         }
1126
1127         return CairoWidget::on_enter_notify_event (ev);
1128 }
1129
1130 bool
1131 ArdourButton::on_leave_notify_event (GdkEventCrossing* ev)
1132 {
1133         _hovering = false;
1134
1135         if (ARDOUR_UI::config()->get_widget_prelight()) {
1136                 CairoWidget::set_dirty ();
1137         }
1138
1139         return CairoWidget::on_leave_notify_event (ev);
1140 }
1141
1142 void
1143 ArdourButton::set_tweaks (Tweaks t)
1144 {
1145         if (_tweaks != t) {
1146                 _tweaks = t;
1147                 if (is_realized()) {
1148                         queue_resize ();
1149                 }
1150         }
1151 }
1152
1153 void
1154 ArdourButton::action_sensitivity_changed ()
1155 {
1156         if (_action->property_sensitive ()) {
1157                 set_visual_state (Gtkmm2ext::VisualState (visual_state() & ~Gtkmm2ext::Insensitive));
1158         } else {
1159                 set_visual_state (Gtkmm2ext::VisualState (visual_state() | Gtkmm2ext::Insensitive));
1160         }
1161 }
1162
1163 void
1164 ArdourButton::set_layout_ellipsize_width (int w)
1165 {
1166         if (_layout_ellipsize_width == w) {
1167                 return;
1168         }
1169         _layout_ellipsize_width = w;
1170         if (!_layout) {
1171                 return;
1172         }
1173         if (_layout_ellipsize_width > 3 * PANGO_SCALE) {
1174                 _layout->set_width (_layout_ellipsize_width - 3 * PANGO_SCALE);
1175         }
1176         if (is_realized ()) {
1177                 queue_resize ();
1178         }
1179 }
1180
1181 void
1182 ArdourButton::set_text_ellipsize (Pango::EllipsizeMode e)
1183 {
1184         if (_ellipsis == e) {
1185                 return;
1186         }
1187         _ellipsis = e;
1188         if (!_layout) {
1189                 return;
1190         }
1191         _layout->set_ellipsize(_ellipsis);
1192         if (_layout_ellipsize_width > 3 * PANGO_SCALE) {
1193                 _layout->set_width (_layout_ellipsize_width - 3 * PANGO_SCALE);
1194         }
1195         if (is_realized ()) {
1196                 queue_resize ();
1197         }
1198 }
1199
1200 void
1201 ArdourButton::ensure_layout ()
1202 {
1203         if (!_layout) {
1204                 ensure_style ();
1205                 _layout = Pango::Layout::create (get_pango_context());
1206                 _layout->set_ellipsize(_ellipsis);
1207                 if (_layout_ellipsize_width > 3 * PANGO_SCALE) {
1208                         _layout->set_width (_layout_ellipsize_width - 3* PANGO_SCALE);
1209                 }
1210         }
1211 }
1212
1213 void
1214 ArdourButton::recalc_char_pixel_geometry ()
1215 {
1216         if (_char_pixel_height > 0 && _char_pixel_width > 0) {
1217                 return;
1218         }
1219         ensure_layout();
1220         // NB. this is not static, since the geometry is different
1221         // depending on the font used.
1222         int w, h;
1223         std::string x = _("ABCDEFGHIJLKMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
1224         _layout->set_text (x);
1225         _layout->get_pixel_size (w, h);
1226         _char_pixel_height = std::max(4, h);
1227         // number of actual chars in the string (not bytes)
1228         // Glib to the rescue.
1229         Glib::ustring gx(x);
1230         _char_avg_pixel_width = w / (float)gx.size();
1231         _char_pixel_width = std::max(4, (int) ceil (_char_avg_pixel_width));
1232         _layout->set_text (_text);
1233 }
1234
1235 void
1236 ArdourButton::action_visibility_changed ()
1237 {
1238         if (_action->property_visible ()) {
1239                 show ();
1240         } else {
1241                 hide ();
1242         }
1243 }
1244
1245 void
1246 ArdourButton::action_tooltip_changed ()
1247 {
1248         string str = _action->property_tooltip().get_value();
1249         ARDOUR_UI::instance()->set_tip (*this, str);
1250 }
1251
1252 void
1253 ArdourButton::set_elements (Element e)
1254 {
1255         _elements = e;
1256         CairoWidget::set_dirty ();
1257 }
1258
1259 void
1260 ArdourButton::add_elements (Element e)
1261 {
1262         _elements = (ArdourButton::Element) (_elements | e);
1263         CairoWidget::set_dirty ();
1264 }
1265
1266 void
1267 ArdourButton::set_icon (Icon i)
1268 {
1269         _icon = i;
1270         CairoWidget::set_dirty ();
1271 }
1272
1273 void
1274 ArdourButton::set_custom_led_color (uint32_t c, bool useit)
1275 {
1276         if (led_custom_color == c && use_custom_led_color == useit) {
1277                 return;
1278         }
1279
1280         led_custom_color = c;
1281         use_custom_led_color = useit;
1282         CairoWidget::set_dirty ();
1283 }