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