Standardize drawing of PixFader and BarController; implement flat_buttons and prelight.
[ardour.git] / libs / gtkmm2ext / barcontroller.cc
1 /*
2     Copyright (C) 2004 Paul Davis
3     This program is free software; you can redistribute it and/or modify
4     it under the terms of the GNU General Public License as published by
5     the Free Software Foundation; either version 2 of the License, or
6     (at your option) any later version.
7
8     This program is distributed in the hope that it will be useful,
9     but WITHOUT ANY WARRANTY; without even the implied warranty of
10     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11     GNU General Public License for more details.
12
13     You should have received a copy of the GNU General Public License
14     along with this program; if not, write to the Free Software
15     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
16
17     $Id$
18 */
19
20 #include <string>
21 #include <sstream>
22 #include <climits>
23 #include <cstdio>
24 #include <cmath>
25 #include <algorithm>
26
27 #include <pbd/controllable.h>
28 #include <pbd/locale_guard.h>
29
30 #include "gtkmm2ext/gtk_ui.h"
31 #include "gtkmm2ext/utils.h"
32 #include "gtkmm2ext/keyboard.h"
33 #include "gtkmm2ext/barcontroller.h"
34 #include "gtkmm2ext/cairo_widget.h"
35
36 #include "i18n.h"
37
38 using namespace std;
39 using namespace Gtk;
40 using namespace Gtkmm2ext;
41
42 BarController::BarController (Gtk::Adjustment& adj,
43                               boost::shared_ptr<PBD::Controllable> mc)
44
45         : adjustment (adj)
46           , binding_proxy (mc)
47           , _hovering (false)
48           , spinner (adjustment)
49 {                         
50         _style = LeftToRight;
51         grabbed = false;
52         switching = false;
53         switch_on_release = false;
54         use_parent = false;
55         logarithmic = false;
56
57         layout = darea.create_pango_layout("");
58
59         set_shadow_type (SHADOW_NONE);
60
61         initial_value = adjustment.get_value ();
62
63         adjustment.signal_value_changed().connect (mem_fun (*this, &Gtk::Widget::queue_draw));
64         adjustment.signal_changed().connect (mem_fun (*this, &Gtk::Widget::queue_draw));
65
66         darea.add_events (Gdk::BUTTON_RELEASE_MASK|
67                           Gdk::BUTTON_PRESS_MASK|
68                           Gdk::POINTER_MOTION_MASK|
69                           Gdk::ENTER_NOTIFY_MASK|
70                           Gdk::LEAVE_NOTIFY_MASK|
71                           Gdk::SCROLL_MASK);
72
73         darea.signal_expose_event().connect (mem_fun (*this, &BarController::expose));
74         darea.signal_motion_notify_event().connect (mem_fun (*this, &BarController::motion));
75         darea.signal_button_press_event().connect (mem_fun (*this, &BarController::button_press), false);
76         darea.signal_button_release_event().connect (mem_fun (*this, &BarController::button_release), false);
77         darea.signal_scroll_event().connect (mem_fun (*this, &BarController::scroll));
78         darea.signal_enter_notify_event().connect (mem_fun (*this, &BarController::on_enter_notify_event));
79         darea.signal_leave_notify_event().connect (mem_fun (*this, &BarController::on_leave_notify_event));
80
81         spinner.signal_activate().connect (mem_fun (*this, &BarController::entry_activated));
82         spinner.signal_focus_out_event().connect (mem_fun (*this, &BarController::entry_focus_out));
83         spinner.signal_input().connect (mem_fun (*this, &BarController::entry_input));
84         spinner.signal_output().connect (mem_fun (*this, &BarController::entry_output));
85         spinner.set_digits (9);
86         spinner.set_numeric (true);
87         
88         add (darea);
89
90         show_all ();
91 }
92
93 BarController::~BarController ()
94 {
95 }
96
97 bool
98 BarController::on_enter_notify_event (GdkEventCrossing*)
99 {
100         _hovering = true;
101         Keyboard::magic_widget_grab_focus ();
102         queue_draw ();
103         return false;
104 }
105
106 bool
107 BarController::on_leave_notify_event (GdkEventCrossing*)
108 {
109         _hovering = false;
110         Keyboard::magic_widget_drop_focus();
111         queue_draw ();
112         return false;
113 }
114
115 void
116 BarController::drop_grab ()
117 {
118         if (grabbed) {
119                 grabbed = false;
120                 darea.remove_modal_grab();
121                 StopGesture ();
122         }
123 }
124
125 bool
126 BarController::button_press (GdkEventButton* ev)
127 {
128         double fract;
129
130         if (binding_proxy.button_press_handler (ev)) {
131                 return true;
132         }
133
134         switch (ev->button) {
135         case 1:
136                 if (ev->type == GDK_2BUTTON_PRESS) {
137                         switch_on_release = true;
138                         drop_grab ();
139                 } else {
140                         switch_on_release = false;
141                         darea.add_modal_grab();
142                         grabbed = true;
143                         grab_x = ev->x;
144                         grab_window = ev->window;
145                         StartGesture ();
146                 }
147                 return true;
148                 break;
149
150         case 2:
151                 fract = ev->x / (darea.get_width() - 2.0);
152                 adjustment.set_value (adjustment.get_lower() + fract * (adjustment.get_upper() - adjustment.get_lower()));
153
154         case 3:
155                 break;
156
157         case 4:
158         case 5:
159                 break;
160         }
161
162         return false;
163 }
164
165 bool
166 BarController::button_release (GdkEventButton* ev)
167 {
168         drop_grab ();
169         
170         switch (ev->button) {
171         case 1:
172                 if (switch_on_release) {
173                         Glib::signal_idle().connect (mem_fun (*this, &BarController::switch_to_spinner));
174                         return true;
175                 }
176
177                 if ((ev->state & (Keyboard::TertiaryModifier|Keyboard::PrimaryModifier)) == Keyboard::TertiaryModifier) {
178                         adjustment.set_value (initial_value);
179                 } else {
180                         double scale;
181
182                         if ((ev->state & (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) == (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) {
183                                 scale = 0.01;
184                         } else if (ev->state & Keyboard::PrimaryModifier) {
185                                 scale = 0.1;
186                         } else {
187                                 scale = 1.0;
188                         }
189
190                         mouse_control (ev->x, ev->window, scale);
191                 }
192                 break;
193
194         case 2:
195                 break;
196                 
197         case 3:
198                 return false;
199                 
200         default:
201                 break;
202         }
203
204         return true;
205 }
206
207 bool
208 BarController::scroll (GdkEventScroll* ev)
209 {
210         double scale;
211
212         if ((ev->state & (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) == (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) {
213                 scale = 0.01;
214         } else if (ev->state & Keyboard::PrimaryModifier) {
215                 scale = 0.1;
216         } else {
217                 scale = 1.0;
218         }
219
220         switch (ev->direction) {
221         case GDK_SCROLL_UP:
222         case GDK_SCROLL_RIGHT:
223                 adjustment.set_value (adjustment.get_value() + (scale * adjustment.get_step_increment()));
224                 break;
225
226         case GDK_SCROLL_DOWN:
227         case GDK_SCROLL_LEFT:
228                 adjustment.set_value (adjustment.get_value() - (scale * adjustment.get_step_increment()));
229                 break;
230         }
231
232         return true;
233 }
234
235 bool
236 BarController::motion (GdkEventMotion* ev)
237 {
238         double scale;
239         
240         if (!grabbed) {
241                 return true;
242         }
243
244         if ((ev->state & (Keyboard::TertiaryModifier|Keyboard::PrimaryModifier)) == Keyboard::TertiaryModifier) {
245                 return TRUE;
246         }
247
248         if ((ev->state & (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) == (Keyboard::PrimaryModifier|Keyboard::TertiaryModifier)) {
249                 scale = 0.01;
250         } else if (ev->state & Keyboard::PrimaryModifier) {
251                 scale = 0.1;
252         } else {
253                 scale = 1.0;
254         }
255
256         return mouse_control (ev->x, ev->window, scale);
257 }
258
259 gint
260 BarController::mouse_control (double x, GdkWindow* window, double scaling)
261 {
262         double fract = 0.0;
263         double delta;
264
265         if (window != grab_window) {
266                 grab_x = x;
267                 grab_window = window;
268                 return TRUE;
269         }
270
271         delta = x - grab_x;
272         grab_x = x;
273         
274         switch (_style) {
275         case Line:
276         case Blob:
277         case LeftToRight:
278         case CenterOut:
279                 fract = scaling * (delta / (darea.get_width() - 2));
280                 fract = min (1.0, fract);
281                 fract = max (-1.0, fract);
282                 adjustment.set_value (adjustment.get_value() + fract * (adjustment.get_upper() - adjustment.get_lower()));
283                 break;
284         default:
285                 fract = 0.0;
286         }
287         
288         
289         return TRUE;
290 }
291
292 Gdk::Color
293 BarController::get_parent_bg ()
294 {
295         Widget* parent;
296
297         parent = get_parent ();
298
299         while (parent) {
300                 static const char* has_cairo_widget_background_info = "has_cairo_widget_background_info";
301                 void* p = g_object_get_data (G_OBJECT(parent->gobj()), has_cairo_widget_background_info);
302
303                 if (p) {
304                         Glib::RefPtr<Gtk::Style> style = parent->get_style();
305                         return style->get_bg (get_state());
306                 }
307                 
308                 if (!parent->get_has_window()) {
309                         parent = parent->get_parent();
310                 } else {
311                         break;
312                 }
313         }
314
315         if (parent && parent->get_has_window()) {
316                 return parent->get_style ()->get_bg (parent->get_state());
317         } 
318
319         return get_style ()->get_bg (get_state());
320 }
321
322 bool
323 BarController::expose (GdkEventExpose* /*event*/)
324 {
325         Glib::RefPtr<Gdk::Window> win (darea.get_window());
326         Cairo::RefPtr<Cairo::Context> context = win->create_cairo_context();
327         cairo_t* cr = context->cobj();
328
329         Gdk::Color fg_col = get_style()->get_fg (get_state());
330
331         double fract = ((adjustment.get_value() - adjustment.get_lower()) /
332                  (adjustment.get_upper() - adjustment.get_lower()));
333         
334         gint w = darea.get_width() ;
335         gint h = darea.get_height();
336         gint bar_start, bar_width;
337         double radius = 4;
338
339         switch (_style) {
340         case Line:
341                 bar_start = (gint) floor ((w-1) * fract);
342                 bar_width = 1;
343                 break;
344
345         case Blob:
346                 // ????
347                 break;
348
349         case CenterOut:
350         bar_width = (w*fract);
351         bar_start = (w/2) - bar_width/2; // center, back up half the bar width
352                 break;
353
354         case LeftToRight:
355                 bar_start = 1;
356                 bar_width = floor((w-2)*fract);
357
358                 break;
359
360         case RightToLeft:
361                 break;
362         case TopToBottom:
363                 break;
364         case BottomToTop:
365                 break;
366         }
367
368         //fill in the bg rect ... 
369         Gdk::Color c = get_parent_bg(); //get_style()->get_bg (Gtk::STATE_PRELIGHT);  //why prelight?  Shouldn't we be using the parent's color?  maybe   get_parent_bg  ?
370         CairoWidget::set_source_rgb_a (cr, c);
371         cairo_rectangle (cr, 0, 0, w, h);
372         cairo_fill(cr);
373
374         //"slot"
375         cairo_set_source_rgba (cr, 0.17, 0.17, 0.17, 1.0);
376         Gtkmm2ext::rounded_rectangle (cr, 1, 1, w-2, h-2, radius-0.5);
377         cairo_fill(cr);
378
379         //mask off the corners
380         Gtkmm2ext::rounded_rectangle (cr, 1, 1, w-2, h-2, radius-0.5);
381         cairo_clip(cr);
382         
383                 //background gradient
384                 if ( !CairoWidget::flat_buttons() ) {
385                         cairo_pattern_t *bg_gradient = cairo_pattern_create_linear (0.0, 0.0, 0, h);
386                         cairo_pattern_add_color_stop_rgba (bg_gradient, 0, 0, 0, 0, 0.4);
387                         cairo_pattern_add_color_stop_rgba (bg_gradient, 0.2, 0, 0, 0, 0.2);
388                         cairo_pattern_add_color_stop_rgba (bg_gradient, 1, 0, 0, 0, 0.0);
389                         cairo_set_source (cr, bg_gradient);
390                         Gtkmm2ext::rounded_rectangle (cr, 1, 1, w-2, h-2, radius-1.5);
391                         cairo_fill (cr);
392                         cairo_pattern_destroy(bg_gradient);
393                 }
394                 
395                 //fg color
396                 CairoWidget::set_source_rgb_a (cr, fg_col, 1.0);
397                 Gtkmm2ext::rounded_rectangle (cr, bar_start, 1, bar_width, h-2, radius - 1.5);
398                 cairo_fill(cr);
399
400                 //fg gradient
401                 if (!CairoWidget::flat_buttons() ) {
402                         cairo_pattern_t * fg_gradient = cairo_pattern_create_linear (0.0, 0.0, 0, h);
403                         cairo_pattern_add_color_stop_rgba (fg_gradient, 0, 0, 0, 0, 0.0);
404                         cairo_pattern_add_color_stop_rgba (fg_gradient, 0.1, 0, 0, 0, 0.0);
405                         cairo_pattern_add_color_stop_rgba (fg_gradient, 1, 0, 0, 0, 0.3);
406                         cairo_set_source (cr, fg_gradient);
407                         Gtkmm2ext::rounded_rectangle (cr, bar_start, 1, bar_width, h-2, radius - 1.5);
408                         cairo_fill (cr);
409                         cairo_pattern_destroy(fg_gradient);
410                 }
411                 
412         cairo_reset_clip(cr);
413
414         //black border
415         cairo_set_line_width (cr, 1.0);
416         cairo_set_source_rgba (cr, 0, 0, 0, 1.0);
417         Gtkmm2ext::rounded_rectangle (cr, 0.5, 0.5, w-1, h-1, radius);
418         cairo_stroke(cr);
419
420         /* draw the unity-position line if it's not at either end*/
421 /*      if (unity_loc > 0) {
422                 context->set_line_width (1);
423                 cairo_set_source_rgba (cr, 1,1,1, 1.0);
424                 if ( _orien == VERT) {
425                         if (unity_loc < h ) {
426                                 context->move_to (2.5, unity_loc + radius + .5);
427                                 context->line_to (girth-2.5, unity_loc + radius + .5);
428                                 context->stroke ();
429                         }
430                 } else {
431                         if ( unity_loc < w ){
432                                 context->move_to (unity_loc - radius + .5, 3.5);
433                                 context->line_to (unity_loc - radius + .5, girth-3.5);
434                                 context->stroke ();
435                         }
436                 }
437         }*/
438         
439         if (!darea.get_sensitive()) {
440                 rounded_rectangle (context, 0, 0, darea.get_width(), darea.get_height(), 3);
441                 context->set_source_rgba (0.505, 0.517, 0.525, 0.6);
442                 context->fill ();
443         } else if (_hovering) {
444                 Gtkmm2ext::rounded_rectangle (cr, 1, 1, w-2, h-2, radius);
445                 cairo_set_source_rgba (cr, 0.905, 0.917, 0.925, 0.1);
446                 cairo_fill (cr);
447         }
448
449         /* draw label */
450
451         double xpos = -1;
452         std::string const label = get_label (xpos);
453
454 /*      if (!label.empty()) {
455                 
456                 layout->set_text (label);
457                 
458                 int width, height, x;
459                 layout->get_pixel_size (width, height);
460
461                 if (xpos == -1) {
462                         x = max (3, 1 + (x2 - (width/2)));
463                         x = min (darea.get_width() - width - 3, (int) lrint (xpos));
464                 } else {
465                         x = lrint (darea.get_width() * xpos);
466                 }
467
468                 c = get_style()->get_text (get_state());
469                 r = c.get_red_p ();
470                 g = c.get_green_p ();
471                 b = c.get_blue_p ();
472                 context->set_source_rgb (r, g, b);
473                 context->move_to (x, (darea.get_height()/2) - (height/2));
474                 layout->show_in_cairo_context (context);
475         }*/
476         
477         return true;
478 }
479
480 void
481 BarController::set_style (barStyle s)
482 {
483         _style = s;
484         darea.queue_draw ();
485 }
486
487 gint
488 BarController::switch_to_bar ()
489 {
490         if (switching) {
491                 return FALSE;
492         }
493
494         switching = true;
495
496         if (get_child() == &darea) {
497                 return FALSE;
498         }
499
500         remove ();
501         add (darea);
502         darea.show ();
503
504         switching = false;
505
506         SpinnerActive (false); /* EMIT SIGNAL */
507         
508         return FALSE;
509 }
510
511 gint
512 BarController::switch_to_spinner ()
513 {
514         if (switching) {
515                 return FALSE;
516         }
517
518         switching = true;
519
520         if (get_child() == &spinner) {
521                 return FALSE;
522         }
523
524         remove ();
525         add (spinner);
526         spinner.show ();
527         spinner.select_region (0, spinner.get_text_length());
528         spinner.grab_focus ();
529
530         switching = false;
531
532         SpinnerActive (true); /* EMIT SIGNAL */
533
534         return FALSE;
535 }
536
537 void
538 BarController::entry_activated ()
539 {
540         switch_to_bar ();
541 }
542
543 bool
544 BarController::entry_focus_out (GdkEventFocus* /*ev*/)
545 {
546         entry_activated ();
547         return true;
548 }
549
550 void
551 BarController::set_use_parent (bool yn)
552 {
553         use_parent = yn;
554         queue_draw ();
555 }
556
557 void
558 BarController::set_sensitive (bool yn)
559 {
560         Frame::set_sensitive (yn);
561         darea.set_sensitive (yn);
562 }
563
564 /* 
565     This is called when we need to update the adjustment with the value
566     from the spinner's text entry.
567     
568     We need to use Gtk::Entry::get_text to avoid recursive nastiness :)
569     
570     If we're not in logarithmic mode we can return false to use the 
571     default conversion.
572     
573     In theory we should check for conversion errors but set numeric
574     mode to true on the spinner prevents invalid input.
575 */
576 int
577 BarController::entry_input (double* new_value)
578 {
579         if (!logarithmic) {
580                 return false;
581         }
582
583         // extract a double from the string and take its log
584         Entry *entry = dynamic_cast<Entry *>(&spinner);
585         double value;
586
587         {
588                 // Switch to user's preferred locale so that
589                 // if they use different LC_NUMERIC conventions,
590                 // we will honor them.
591
592                 PBD::LocaleGuard lg ("");
593                 sscanf (entry->get_text().c_str(), "%lf", &value);
594         }
595
596         *new_value = log(value);
597
598         return true;
599 }
600
601 /* 
602     This is called when we need to update the spinner's text entry 
603     with the value of the adjustment.
604     
605     We need to use Gtk::Entry::set_text to avoid recursive nastiness :)
606     
607     If we're not in logarithmic mode we can return false to use the 
608     default conversion.
609 */
610 bool
611 BarController::entry_output ()
612 {
613         if (!logarithmic) {
614                 return false;
615         }
616
617         // generate the exponential and turn it into a string
618         // convert to correct locale. 
619         
620         stringstream stream;
621         string str;
622
623         char buf[128];
624
625         {
626                 // Switch to user's preferred locale so that
627                 // if they use different LC_NUMERIC conventions,
628                 // we will honor them.
629                 
630                 PBD::LocaleGuard lg ("");
631                 snprintf (buf, sizeof (buf), "%g", exp (spinner.get_adjustment()->get_value()));
632         }
633
634         Entry *entry = dynamic_cast<Entry *>(&spinner);
635         entry->set_text(buf);
636         
637         return true;
638 }
639
640
641