1 /*
2 * Copyright (C) 2010 Paul Davis <paul@linuxaudiosystems.com>
3 * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include <iostream>
21 #include <cmath>
22 #include <algorithm>
23
24 #include <pangomm/layout.h>
25
26 #include "pbd/compose.h"
27 #include "pbd/controllable.h"
28 #include "pbd/error.h"
29
30 #include "gtkmm2ext/colors.h"
31 #include "gtkmm2ext/gui_thread.h"
32 #include "gtkmm2ext/keyboard.h"
33 #include "gtkmm2ext/rgb_macros.h"
34 #include "gtkmm2ext/utils.h"
35
36 #include "widgets/ardour_knob.h"
37 #include "widgets/ui_config.h"
38
39 #include "pbd/i18n.h"
40
41 using namespace Gtkmm2ext;
42 using namespace ArdourWidgets;
43 using namespace Gtk;
44 using namespace Glib;
45 using namespace PBD;
46 using std::max;
47 using std::min;
48 using namespace std;
49
50 ArdourKnob::Element ArdourKnob::default_elements = ArdourKnob::Element (ArdourKnob::Arc);
51
ArdourKnob(Element e,Flags flags)52 ArdourKnob::ArdourKnob (Element e, Flags flags)
53 : _elements (e)
54 , _hovering (false)
55 , _grabbed_x (0)
56 , _grabbed_y (0)
57 , _val (0)
58 , _normal (0)
59 , _dead_zone_delta (0)
60 , _flags (flags)
61 , _tooltip (this)
62 {
63 UIConfigurationBase::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ArdourKnob::color_handler));
64
65 // watch automation :(
66 // TODO only use for GainAutomation
67 //Timers::rapid_connect (sigc::bind (sigc::mem_fun (*this, &ArdourKnob::controllable_changed), false));
68 }
69
~ArdourKnob()70 ArdourKnob::~ArdourKnob()
71 {
72 }
73
74 void
render(Cairo::RefPtr<Cairo::Context> const & ctx,cairo_rectangle_t *)75 ArdourKnob::render (Cairo::RefPtr<Cairo::Context> const& ctx, cairo_rectangle_t*)
76 {
77 cairo_t* cr = ctx->cobj();
78 cairo_pattern_t* shade_pattern;
79
80 float width = get_width();
81 float height = get_height();
82
83 const float scale = min(width, height);
84 const float pointer_thickness = 3.0 * (scale/80); //(if the knob is 80 pixels wide, we want a 3-pix line on it)
85
86 const float start_angle = ((180 - 65) * G_PI) / 180;
87 const float end_angle = ((360 + 65) * G_PI) / 180;
88
89 float zero = 0;
90 if (_flags & ArcToZero) {
91 zero = _normal;
92 }
93
94 const float value_angle = start_angle + (_val * (end_angle - start_angle));
95 const float zero_angle = start_angle + (zero * (end_angle - start_angle));
96
97 float value_x = cos (value_angle);
98 float value_y = sin (value_angle);
99
100 float xc = 0.5 + width/ 2.0;
101 float yc = 0.5 + height/ 2.0;
102
103 cairo_translate (cr, xc, yc); //after this, everything is based on the center of the knob
104
105 //get the knob color from the theme
106 Gtkmm2ext::Color knob_color = UIConfigurationBase::instance().color (string_compose ("%1", get_name()));
107
108 float center_radius = 0.48*scale;
109 float border_width = 0.8;
110
111 bool arc = (_elements & Arc)==Arc;
112 bool bevel = (_elements & Bevel)==Bevel;
113 bool flat = flat_buttons ();
114
115 if ( arc ) {
116 center_radius = scale*0.33;
117
118 float inner_progress_radius = scale*0.38;
119 float outer_progress_radius = scale*0.48;
120 float progress_width = (outer_progress_radius-inner_progress_radius);
121 float progress_radius = inner_progress_radius + progress_width/2.0;
122
123 //dark arc background
124 cairo_set_source_rgb (cr, 0.3, 0.3, 0.3 );
125 cairo_set_line_width (cr, progress_width);
126 cairo_arc (cr, 0, 0, progress_radius, start_angle, end_angle);
127 cairo_stroke (cr);
128
129 //look up the arc colors from the config
130 double red_start, green_start, blue_start, unused;
131 Gtkmm2ext::Color arc_start_color = UIConfigurationBase::instance().color ( string_compose ("%1: arc start", get_name()));
132 Gtkmm2ext::color_to_rgba( arc_start_color, red_start, green_start, blue_start, unused );
133 double red_end, green_end, blue_end;
134 Gtkmm2ext::Color arc_end_color = UIConfigurationBase::instance().color ( string_compose ("%1: arc end", get_name()) );
135 Gtkmm2ext::color_to_rgba( arc_end_color, red_end, green_end, blue_end, unused );
136
137 //vary the arc color over the travel of the knob
138 float intensity = fabsf (_val - zero) / std::max(zero, (1.f - zero));
139 const float intensity_inv = 1.0 - intensity;
140 float r = intensity_inv * red_end + intensity * red_start;
141 float g = intensity_inv * green_end + intensity * green_start;
142 float b = intensity_inv * blue_end + intensity * blue_start;
143
144 //draw the arc
145 cairo_set_source_rgb (cr, r,g,b);
146 cairo_set_line_width (cr, progress_width);
147 if (zero_angle > value_angle) {
148 cairo_arc (cr, 0, 0, progress_radius, value_angle, zero_angle);
149 } else {
150 cairo_arc (cr, 0, 0, progress_radius, zero_angle, value_angle);
151 }
152 cairo_stroke (cr);
153
154 //shade the arc
155 if (!flat) {
156 shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0, yc); //note we have to offset the pattern from our centerpoint
157 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.15);
158 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.5, 1,1,1, 0.0);
159 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1,1,1, 0.0);
160 cairo_set_source (cr, shade_pattern);
161 cairo_arc (cr, 0, 0, outer_progress_radius-1, 0, 2.0*G_PI);
162 cairo_fill (cr);
163 cairo_pattern_destroy (shade_pattern);
164 }
165
166 #if 0 //black border
167 const float start_angle_x = cos (start_angle);
168 const float start_angle_y = sin (start_angle);
169 const float end_angle_x = cos (end_angle);
170 const float end_angle_y = sin (end_angle);
171
172 cairo_set_source_rgb (cr, 0, 0, 0 );
173 cairo_set_line_width (cr, border_width);
174 cairo_move_to (cr, (outer_progress_radius * start_angle_x), (outer_progress_radius * start_angle_y));
175 cairo_line_to (cr, (inner_progress_radius * start_angle_x), (inner_progress_radius * start_angle_y));
176 cairo_stroke (cr);
177 cairo_move_to (cr, (outer_progress_radius * end_angle_x), (outer_progress_radius * end_angle_y));
178 cairo_line_to (cr, (inner_progress_radius * end_angle_x), (inner_progress_radius * end_angle_y));
179 cairo_stroke (cr);
180 cairo_arc (cr, 0, 0, outer_progress_radius, start_angle, end_angle);
181 cairo_stroke (cr);
182 #endif
183 }
184
185 if (!flat) {
186 //knob shadow
187 cairo_save(cr);
188 cairo_translate(cr, pointer_thickness+1, pointer_thickness+1 );
189 cairo_set_source_rgba (cr, 0, 0, 0, 0.1 );
190 cairo_arc (cr, 0, 0, center_radius-1, 0, 2.0*G_PI);
191 cairo_fill (cr);
192 cairo_restore(cr);
193
194 //inner circle
195 Gtkmm2ext::set_source_rgba(cr, knob_color);
196 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
197 cairo_fill (cr);
198
199 //gradient
200 if (bevel) {
201 //knob gradient
202 shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0, yc); //note we have to offset the gradient from our centerpoint
203 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
204 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.2, 1,1,1, 0.2);
205 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.8, 0,0,0, 0.2);
206 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.2);
207 cairo_set_source (cr, shade_pattern);
208 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
209 cairo_fill (cr);
210 cairo_pattern_destroy (shade_pattern);
211
212 //flat top over beveled edge
213 Gtkmm2ext::set_source_rgb_a (cr, knob_color, 0.5 );
214 cairo_arc (cr, 0, 0, center_radius-pointer_thickness, 0, 2.0*G_PI);
215 cairo_fill (cr);
216 } else {
217 //radial gradient
218 shade_pattern = cairo_pattern_create_radial ( -center_radius, -center_radius, 1, -center_radius, -center_radius, center_radius*2.5 ); //note we have to offset the gradient from our centerpoint
219 cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
220 cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.3);
221 cairo_set_source (cr, shade_pattern);
222 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
223 cairo_fill (cr);
224 cairo_pattern_destroy (shade_pattern);
225 }
226
227 } else {
228 //inner circle
229 Gtkmm2ext::set_source_rgba(cr, knob_color);
230 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
231 cairo_fill (cr);
232 }
233
234
235 //black knob border
236 cairo_set_line_width (cr, border_width);
237 cairo_set_source_rgba (cr, 0,0,0, 1 );
238 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
239 cairo_stroke (cr);
240
241 //line shadow
242 if (!flat) {
243 cairo_save(cr);
244 cairo_translate(cr, 1, 1 );
245 cairo_set_source_rgba (cr, 0,0,0,0.3 );
246 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
247 cairo_set_line_width (cr, pointer_thickness);
248 cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
249 cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
250 cairo_stroke (cr);
251 cairo_restore(cr);
252 }
253
254 //line
255 cairo_set_source_rgba (cr, 1,1,1, 1 );
256 cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
257 cairo_set_line_width (cr, pointer_thickness);
258 cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
259 cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
260 cairo_stroke (cr);
261
262 // a transparent overlay to indicate insensitivity
263 if (!sensitive ()) {
264 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
265 uint32_t ins_color = UIConfigurationBase::instance().color ("gtk_background");
266 Gtkmm2ext::set_source_rgb_a (cr, ins_color, 0.6);
267 cairo_fill (cr);
268 }
269
270 //highlight if grabbed or if mouse is hovering over me
271 if (_tooltip.dragging() || (_hovering && UIConfigurationBase::instance().get_widget_prelight() ) ) {
272 cairo_set_source_rgba (cr, 1,1,1, 0.12);
273 cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
274 cairo_fill (cr);
275 }
276
277 cairo_identity_matrix(cr);
278 }
279
280 void
on_size_request(Gtk::Requisition * req)281 ArdourKnob::on_size_request (Gtk::Requisition* req)
282 {
283 // see ardour-button VectorIcon size, use font scaling as default
284 CairoWidget::on_size_request (req); // allow to override
285
286 // we're square
287 if (req->width < req->height) {
288 req->width = req->height;
289 }
290 if (req->height < req->width) {
291 req->height = req->width;
292 }
293 }
294
295 bool
on_scroll_event(GdkEventScroll * ev)296 ArdourKnob::on_scroll_event (GdkEventScroll* ev)
297 {
298 /* mouse wheel */
299
300 float scale = 0.05; //by default, we step in 1/20ths of the knob travel
301 if (ev->state & Keyboard::GainFineScaleModifier) {
302 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
303 scale *= 0.01;
304 } else {
305 scale *= 0.10;
306 }
307 }
308
309 boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
310 if (c) {
311 float val = c->get_interface (true);
312
313 if ( ev->direction == GDK_SCROLL_UP )
314 val += scale;
315 else
316 val -= scale;
317
318 c->set_interface (val, true);
319 }
320
321 return true;
322 }
323
324 bool
on_motion_notify_event(GdkEventMotion * ev)325 ArdourKnob::on_motion_notify_event (GdkEventMotion *ev)
326 {
327 if (!(ev->state & Gdk::BUTTON1_MASK)) {
328 return true;
329 }
330
331 boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
332 if (!c) {
333 return true;
334 }
335
336
337 //scale the adjustment based on keyboard modifiers & GUI size
338 const float ui_scale = max (1.f, UIConfigurationBase::instance().get_ui_scale());
339 float scale = 0.0025 / ui_scale;
340
341 if (ev->state & Keyboard::GainFineScaleModifier) {
342 if (ev->state & Keyboard::GainExtraFineScaleModifier) {
343 scale *= 0.01;
344 } else {
345 scale *= 0.10;
346 }
347 }
348
349 //calculate the travel of the mouse
350 int delta = (_grabbed_y - ev->y) - (_grabbed_x - ev->x);
351 if (delta == 0) {
352 return true;
353 }
354
355 _grabbed_x = ev->x;
356 _grabbed_y = ev->y;
357 float val = c->get_interface (true);
358
359 if (_flags & Detent) {
360 const float px_deadzone = 42.f * ui_scale;
361
362 if ((val - _normal) * (val - _normal + delta * scale) < 0) {
363 /* detent */
364 const int tozero = (_normal - val) * scale;
365 int remain = delta - tozero;
366 if (abs (remain) > px_deadzone) {
367 /* slow down passing the default value */
368 remain += (remain > 0) ? px_deadzone * -.5 : px_deadzone * .5;
369 delta = tozero + remain;
370 _dead_zone_delta = 0;
371 } else {
372 c->set_value (c->normal(), Controllable::NoGroup);
373 _dead_zone_delta = remain / px_deadzone;
374 return true;
375 }
376 }
377
378 if (fabsf (rintf((val - _normal) / scale) + _dead_zone_delta) < 1) {
379 c->set_value (c->normal(), Controllable::NoGroup);
380 _dead_zone_delta += delta / px_deadzone;
381 return true;
382 }
383
384 _dead_zone_delta = 0;
385 }
386
387 val += delta * scale;
388 c->set_interface (val, true);
389
390 return true;
391 }
392
393 bool
on_button_press_event(GdkEventButton * ev)394 ArdourKnob::on_button_press_event (GdkEventButton *ev)
395 {
396 _grabbed_x = ev->x;
397 _grabbed_y = ev->y;
398 _dead_zone_delta = 0;
399
400 if (ev->type != GDK_BUTTON_PRESS) {
401 if (_grabbed) {
402 remove_modal_grab();
403 _grabbed = false;
404 StopGesture ();
405 gdk_pointer_ungrab (GDK_CURRENT_TIME);
406 }
407 return true;
408 }
409
410 if (binding_proxy.button_press_handler (ev)) {
411 return true;
412 }
413
414 if (ev->button != 1 && ev->button != 2) {
415 return false;
416 }
417
418 set_active_state (Gtkmm2ext::ExplicitActive);
419 _tooltip.start_drag();
420 add_modal_grab();
421 _grabbed = true;
422 StartGesture ();
423 gdk_pointer_grab(ev->window,false,
424 GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
425 NULL,NULL,ev->time);
426 return true;
427 }
428
429 bool
on_button_release_event(GdkEventButton * ev)430 ArdourKnob::on_button_release_event (GdkEventButton *ev)
431 {
432 _tooltip.stop_drag();
433 _grabbed = false;
434 StopGesture ();
435 remove_modal_grab();
436 gdk_pointer_ungrab (GDK_CURRENT_TIME);
437
438 if ( (_grabbed_y == ev->y && _grabbed_x == ev->x) && Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) { //no move, shift-click sets to default
439 boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
440 if (!c) return false;
441 c->set_value (c->normal(), Controllable::NoGroup);
442 return true;
443 }
444
445 unset_active_state ();
446
447 return true;
448 }
449
450 void
color_handler()451 ArdourKnob::color_handler ()
452 {
453 set_dirty ();
454 }
455
456 void
on_size_allocate(Allocation & alloc)457 ArdourKnob::on_size_allocate (Allocation& alloc)
458 {
459 CairoWidget::on_size_allocate (alloc);
460 }
461
462 void
set_controllable(boost::shared_ptr<Controllable> c)463 ArdourKnob::set_controllable (boost::shared_ptr<Controllable> c)
464 {
465 watch_connection.disconnect (); //stop watching the old controllable
466
467 if (!c) return;
468
469 binding_proxy.set_controllable (c);
470
471 c->Changed.connect (watch_connection, invalidator(*this), boost::bind (&ArdourKnob::controllable_changed, this, false), gui_context());
472
473 _normal = c->internal_to_interface(c->normal());
474
475 controllable_changed();
476 }
477
478 void
controllable_changed(bool force_update)479 ArdourKnob::controllable_changed (bool force_update)
480 {
481 boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
482 if (!c) return;
483
484 float val = c->get_interface (true);
485 val = min( max(0.0f, val), 1.0f); // clamp
486
487 if (val == _val && !force_update) {
488 return;
489 }
490
491 _val = val;
492 if (!_tooltip_prefix.empty()) {
493 _tooltip.set_tip (_tooltip_prefix + c->get_user_string());
494 }
495 set_dirty();
496 }
497
498 void
on_style_changed(const RefPtr<Gtk::Style> &)499 ArdourKnob::on_style_changed (const RefPtr<Gtk::Style>&)
500 {
501 set_dirty ();
502 }
503
504 void
on_name_changed()505 ArdourKnob::on_name_changed ()
506 {
507 set_dirty ();
508 }
509
510
511 void
set_active_state(Gtkmm2ext::ActiveState s)512 ArdourKnob::set_active_state (Gtkmm2ext::ActiveState s)
513 {
514 if (_active_state != s)
515 CairoWidget::set_active_state (s);
516 }
517
518 void
set_visual_state(Gtkmm2ext::VisualState s)519 ArdourKnob::set_visual_state (Gtkmm2ext::VisualState s)
520 {
521 if (_visual_state != s)
522 CairoWidget::set_visual_state (s);
523 }
524
525
526 bool
on_focus_in_event(GdkEventFocus * ev)527 ArdourKnob::on_focus_in_event (GdkEventFocus* ev)
528 {
529 set_dirty ();
530 return CairoWidget::on_focus_in_event (ev);
531 }
532
533 bool
on_focus_out_event(GdkEventFocus * ev)534 ArdourKnob::on_focus_out_event (GdkEventFocus* ev)
535 {
536 set_dirty ();
537 return CairoWidget::on_focus_out_event (ev);
538 }
539
540 bool
on_enter_notify_event(GdkEventCrossing * ev)541 ArdourKnob::on_enter_notify_event (GdkEventCrossing* ev)
542 {
543 _hovering = true;
544
545 set_dirty ();
546
547 boost::shared_ptr<PBD::Controllable> c (binding_proxy.get_controllable ());
548 if (c) {
549 PBD::Controllable::GUIFocusChanged (boost::weak_ptr<PBD::Controllable> (c));
550 }
551
552 return CairoWidget::on_enter_notify_event (ev);
553 }
554
555 bool
on_leave_notify_event(GdkEventCrossing * ev)556 ArdourKnob::on_leave_notify_event (GdkEventCrossing* ev)
557 {
558 _hovering = false;
559
560 set_dirty ();
561
562 if (binding_proxy.get_controllable()) {
563 PBD::Controllable::GUIFocusChanged (boost::weak_ptr<PBD::Controllable> ());
564 }
565
566 return CairoWidget::on_leave_notify_event (ev);
567 }
568
569 void
set_elements(Element e)570 ArdourKnob::set_elements (Element e)
571 {
572 _elements = e;
573 }
574
575 void
add_elements(Element e)576 ArdourKnob::add_elements (Element e)
577 {
578 _elements = (ArdourKnob::Element) (_elements | e);
579 }
580
581
KnobPersistentTooltip(Gtk::Widget * w)582 KnobPersistentTooltip::KnobPersistentTooltip (Gtk::Widget* w)
583 : PersistentTooltip (w, true, 3)
584 , _dragging (false)
585 {
586 }
587
588 void
start_drag()589 KnobPersistentTooltip::start_drag ()
590 {
591 _dragging = true;
592 }
593
594 void
stop_drag()595 KnobPersistentTooltip::stop_drag ()
596 {
597 _dragging = false;
598 }
599
600 bool
dragging() const601 KnobPersistentTooltip::dragging () const
602 {
603 return _dragging;
604 }
605