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