ardour/gtk2_ardour/ardour_knob.cc
Paul Davis 9e3299f97d change Controllable::set_value() API to include grouped control consideration.
This also removes Route::group_gain_control() and associated machinery.
Not yet tested with Mackie or other surfaces. More work to done to
start using the group capabilities, and also potentially to add
or derive more controls as RouteAutomationControls
2016-01-02 04:58:30 -05:00

588 lines
15 KiB
C++

/*
Copyright (C) 2010 Paul Davis
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
#include <iostream>
#include <cmath>
#include <algorithm>
#include <pangomm/layout.h>
#include "pbd/compose.h"
#include "pbd/controllable.h"
#include "pbd/error.h"
#include "pbd/stacktrace.h"
#include "gtkmm2ext/utils.h"
#include "gtkmm2ext/rgb_macros.h"
#include "gtkmm2ext/gui_thread.h"
#include "gtkmm2ext/keyboard.h"
#include "ardour/rc_configuration.h" // for widget prelight preference
#include "ardour_knob.h"
#include "timers.h"
#include "ui_config.h"
#include "canvas/colors.h"
#include "canvas/utils.h"
#include "i18n.h"
using namespace Gtkmm2ext;
using namespace Gdk;
using namespace Gtk;
using namespace Glib;
using namespace PBD;
using std::max;
using std::min;
using namespace std;
ArdourKnob::Element ArdourKnob::default_elements = ArdourKnob::Element (ArdourKnob::Arc);
ArdourKnob::ArdourKnob (Element e, Flags flags)
: _elements (e)
, _hovering (false)
, _grabbed_x (0)
, _grabbed_y (0)
, _val (0)
, _normal (0)
, _dead_zone_delta (0)
, _flags (flags)
, _tooltip (this)
{
UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &ArdourKnob::color_handler));
// watch automation :(
Timers::rapid_connect (sigc::mem_fun (*this, &ArdourKnob::controllable_changed));
}
ArdourKnob::~ArdourKnob()
{
}
void
ArdourKnob::render (cairo_t* cr, cairo_rectangle_t *)
{
cairo_pattern_t* shade_pattern;
float width = get_width();
float height = get_height();
const float scale = min(width, height);
const float pointer_thickness = 3.0 * (scale/80); //(if the knob is 80 pixels wide, we want a 3-pix line on it)
const float start_angle = ((180 - 65) * G_PI) / 180;
const float end_angle = ((360 + 65) * G_PI) / 180;
float zero = 0;
if (_flags & ArcToZero) {
zero = _normal;
}
const float value_angle = start_angle + (_val * (end_angle - start_angle));
const float zero_angle = start_angle + (zero * (end_angle - start_angle));
float value_x = cos (value_angle);
float value_y = sin (value_angle);
float xc = 0.5 + width/ 2.0;
float yc = 0.5 + height/ 2.0;
cairo_translate (cr, xc, yc); //after this, everything is based on the center of the knob
//get the knob color from the theme
ArdourCanvas::Color knob_color = UIConfiguration::instance().color (string_compose ("%1", get_name()));
float center_radius = 0.48*scale;
float border_width = 0.8;
bool arc = (_elements & Arc)==Arc;
bool bevel = (_elements & Bevel)==Bevel;
bool flat = _flat_buttons;
if ( arc ) {
center_radius = scale*0.33;
float inner_progress_radius = scale*0.38;
float outer_progress_radius = scale*0.48;
float progress_width = (outer_progress_radius-inner_progress_radius);
float progress_radius = inner_progress_radius + progress_width/2.0;
//dark arc background
cairo_set_source_rgb (cr, 0.3, 0.3, 0.3 );
cairo_set_line_width (cr, progress_width);
cairo_arc (cr, 0, 0, progress_radius, start_angle, end_angle);
cairo_stroke (cr);
//look up the arc colors from the config
double red_start, green_start, blue_start, unused;
ArdourCanvas::Color arc_start_color = UIConfiguration::instance().color ( string_compose ("%1: arc start", get_name()));
ArdourCanvas::color_to_rgba( arc_start_color, red_start, green_start, blue_start, unused );
double red_end, green_end, blue_end;
ArdourCanvas::Color arc_end_color = UIConfiguration::instance().color ( string_compose ("%1: arc end", get_name()) );
ArdourCanvas::color_to_rgba( arc_end_color, red_end, green_end, blue_end, unused );
//vary the arc color over the travel of the knob
float intensity = fabsf (_val - zero) / std::max(zero, (1.f - zero));
const float intensity_inv = 1.0 - intensity;
float r = intensity_inv * red_end + intensity * red_start;
float g = intensity_inv * green_end + intensity * green_start;
float b = intensity_inv * blue_end + intensity * blue_start;
//draw the arc
cairo_set_source_rgb (cr, r,g,b);
cairo_set_line_width (cr, progress_width);
if (zero_angle > value_angle) {
cairo_arc (cr, 0, 0, progress_radius, value_angle, zero_angle);
} else {
cairo_arc (cr, 0, 0, progress_radius, zero_angle, value_angle);
}
cairo_stroke (cr);
//shade the arc
if (!flat) {
shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0, yc); //note we have to offset the pattern from our centerpoint
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.15);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.5, 1,1,1, 0.0);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 1,1,1, 0.0);
cairo_set_source (cr, shade_pattern);
cairo_arc (cr, 0, 0, outer_progress_radius-1, 0, 2.0*G_PI);
cairo_fill (cr);
cairo_pattern_destroy (shade_pattern);
}
#if 0 //black border
const float start_angle_x = cos (start_angle);
const float start_angle_y = sin (start_angle);
const float end_angle_x = cos (end_angle);
const float end_angle_y = sin (end_angle);
cairo_set_source_rgb (cr, 0, 0, 0 );
cairo_set_line_width (cr, border_width);
cairo_move_to (cr, (outer_progress_radius * start_angle_x), (outer_progress_radius * start_angle_y));
cairo_line_to (cr, (inner_progress_radius * start_angle_x), (inner_progress_radius * start_angle_y));
cairo_stroke (cr);
cairo_move_to (cr, (outer_progress_radius * end_angle_x), (outer_progress_radius * end_angle_y));
cairo_line_to (cr, (inner_progress_radius * end_angle_x), (inner_progress_radius * end_angle_y));
cairo_stroke (cr);
cairo_arc (cr, 0, 0, outer_progress_radius, start_angle, end_angle);
cairo_stroke (cr);
#endif
}
if (!flat) {
//knob shadow
cairo_save(cr);
cairo_translate(cr, pointer_thickness+1, pointer_thickness+1 );
cairo_set_source_rgba (cr, 0, 0, 0, 0.1 );
cairo_arc (cr, 0, 0, center_radius-1, 0, 2.0*G_PI);
cairo_fill (cr);
cairo_restore(cr);
//inner circle
ArdourCanvas::set_source_rgba(cr, knob_color);
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_fill (cr);
//gradient
if (bevel) {
//knob gradient
shade_pattern = cairo_pattern_create_linear (0.0, -yc, 0.0, yc); //note we have to offset the gradient from our centerpoint
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.2, 1,1,1, 0.2);
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.8, 0,0,0, 0.2);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.2);
cairo_set_source (cr, shade_pattern);
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_fill (cr);
cairo_pattern_destroy (shade_pattern);
//flat top over beveled edge
ArdourCanvas::set_source_rgb_a (cr, knob_color, 0.5 );
cairo_arc (cr, 0, 0, center_radius-pointer_thickness, 0, 2.0*G_PI);
cairo_fill (cr);
} else {
//radial gradient
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
cairo_pattern_add_color_stop_rgba (shade_pattern, 0.0, 1,1,1, 0.2);
cairo_pattern_add_color_stop_rgba (shade_pattern, 1.0, 0,0,0, 0.3);
cairo_set_source (cr, shade_pattern);
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_fill (cr);
cairo_pattern_destroy (shade_pattern);
}
} else {
//inner circle
ArdourCanvas::set_source_rgba(cr, knob_color);
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_fill (cr);
}
//black knob border
cairo_set_line_width (cr, border_width);
cairo_set_source_rgba (cr, 0,0,0, 1 );
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_stroke (cr);
//line shadow
if (!flat) {
cairo_save(cr);
cairo_translate(cr, 1, 1 );
cairo_set_source_rgba (cr, 0,0,0,0.3 );
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
cairo_set_line_width (cr, pointer_thickness);
cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
cairo_stroke (cr);
cairo_restore(cr);
}
//line
cairo_set_source_rgba (cr, 1,1,1, 1 );
cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND);
cairo_set_line_width (cr, pointer_thickness);
cairo_move_to (cr, (center_radius * value_x), (center_radius * value_y));
cairo_line_to (cr, ((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
cairo_stroke (cr);
//highlight if grabbed or if mouse is hovering over me
if (_tooltip.dragging() || (_hovering && UIConfiguration::instance().get_widget_prelight() ) ) {
cairo_set_source_rgba (cr, 1,1,1, 0.12 );
cairo_arc (cr, 0, 0, center_radius, 0, 2.0*G_PI);
cairo_fill (cr);
}
cairo_identity_matrix(cr);
}
void
ArdourKnob::on_size_request (Gtk::Requisition* req)
{
// see ardour-button VectorIcon size, use font scaling as default
CairoWidget::on_size_request (req); // allow to override
// we're square
if (req->width < req->height) {
req->width = req->height;
}
if (req->height < req->width) {
req->height = req->width;
}
}
bool
ArdourKnob::on_scroll_event (GdkEventScroll* ev)
{
/* mouse wheel */
float scale = 0.05; //by default, we step in 1/20ths of the knob travel
if (ev->state & Keyboard::GainFineScaleModifier) {
if (ev->state & Keyboard::GainExtraFineScaleModifier) {
scale *= 0.01;
} else {
scale *= 0.10;
}
}
boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
if (c) {
float val = c->get_interface();
if ( ev->direction == GDK_SCROLL_UP )
val += scale;
else
val -= scale;
c->set_interface(val);
}
return true;
}
bool
ArdourKnob::on_motion_notify_event (GdkEventMotion *ev)
{
if (!(ev->state & Gdk::BUTTON1_MASK)) {
return true;
}
boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
if (!c) {
return true;
}
//scale the adjustment based on keyboard modifiers & GUI size
const float ui_scale = max (1.f, UIConfiguration::instance().get_ui_scale());
float scale = 0.0025 / ui_scale;
if (ev->state & Keyboard::GainFineScaleModifier) {
if (ev->state & Keyboard::GainExtraFineScaleModifier) {
scale *= 0.01;
} else {
scale *= 0.10;
}
}
//calculate the travel of the mouse
int delta = (_grabbed_y - ev->y) - (_grabbed_x - ev->x);
if (delta == 0) {
return true;
}
_grabbed_x = ev->x;
_grabbed_y = ev->y;
float val = c->get_interface();
if (_flags & Detent) {
const float px_deadzone = 42.f * ui_scale;
if ((val - _normal) * (val - _normal + delta * scale) < 0) {
/* detent */
const int tozero = (_normal - val) * scale;
int remain = delta - tozero;
if (abs (remain) > px_deadzone) {
/* slow down passing the default value */
remain += (remain > 0) ? px_deadzone * -.5 : px_deadzone * .5;
delta = tozero + remain;
_dead_zone_delta = 0;
} else {
c->set_value (c->normal(), Controllable::NoGroup);
_dead_zone_delta = remain / px_deadzone;
return true;
}
}
if (fabsf (rintf((val - _normal) / scale) + _dead_zone_delta) < 1) {
c->set_value (c->normal(), Controllable::NoGroup);
_dead_zone_delta += delta / px_deadzone;
return true;
}
_dead_zone_delta = 0;
}
val += delta * scale;
c->set_interface(val);
return true;
}
bool
ArdourKnob::on_button_press_event (GdkEventButton *ev)
{
_grabbed_x = ev->x;
_grabbed_y = ev->y;
_dead_zone_delta = 0;
if (ev->type != GDK_BUTTON_PRESS) {
if (_grabbed) {
remove_modal_grab();
gdk_pointer_ungrab (GDK_CURRENT_TIME);
}
return true;
}
if (binding_proxy.button_press_handler (ev)) {
return true;
}
if (ev->button != 1 && ev->button != 2) {
return false;
}
set_active_state (Gtkmm2ext::ExplicitActive);
_tooltip.start_drag();
add_modal_grab();
_grabbed = true;
gdk_pointer_grab(ev->window,false,
GdkEventMask( Gdk::POINTER_MOTION_MASK | Gdk::BUTTON_PRESS_MASK |Gdk::BUTTON_RELEASE_MASK),
NULL,NULL,ev->time);
return true;
}
bool
ArdourKnob::on_button_release_event (GdkEventButton *ev)
{
_tooltip.stop_drag();
_grabbed = false;
remove_modal_grab();
gdk_pointer_ungrab (GDK_CURRENT_TIME);
if ( (_grabbed_y == ev->y && _grabbed_x == ev->x) && Keyboard::modifier_state_equals (ev->state, Keyboard::TertiaryModifier)) { //no move, shift-click sets to default
boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
if (!c) return false;
c->set_value (c->normal(), Controllable::NoGroup);
return true;
}
unset_active_state ();
return true;
}
void
ArdourKnob::color_handler ()
{
set_dirty ();
}
void
ArdourKnob::on_size_allocate (Allocation& alloc)
{
CairoWidget::on_size_allocate (alloc);
}
void
ArdourKnob::set_controllable (boost::shared_ptr<Controllable> c)
{
watch_connection.disconnect (); //stop watching the old controllable
if (!c) return;
binding_proxy.set_controllable (c);
c->Changed.connect (watch_connection, invalidator(*this), boost::bind (&ArdourKnob::controllable_changed, this), gui_context());
_normal = c->internal_to_interface(c->normal());
controllable_changed();
}
void
ArdourKnob::controllable_changed ()
{
boost::shared_ptr<PBD::Controllable> c = binding_proxy.get_controllable();
if (!c) return;
float val = c->get_interface();
val = min( max(0.0f, val), 1.0f); // clamp
if (val == _val) {
return;
}
_val = val;
if (!_tooltip_prefix.empty()) {
_tooltip.set_tip (_tooltip_prefix + c->get_user_string());
}
set_dirty();
}
void
ArdourKnob::on_style_changed (const RefPtr<Gtk::Style>&)
{
set_dirty ();
}
void
ArdourKnob::on_name_changed ()
{
set_dirty ();
}
void
ArdourKnob::set_active_state (Gtkmm2ext::ActiveState s)
{
if (_active_state != s)
CairoWidget::set_active_state (s);
}
void
ArdourKnob::set_visual_state (Gtkmm2ext::VisualState s)
{
if (_visual_state != s)
CairoWidget::set_visual_state (s);
}
bool
ArdourKnob::on_focus_in_event (GdkEventFocus* ev)
{
set_dirty ();
return CairoWidget::on_focus_in_event (ev);
}
bool
ArdourKnob::on_focus_out_event (GdkEventFocus* ev)
{
set_dirty ();
return CairoWidget::on_focus_out_event (ev);
}
bool
ArdourKnob::on_enter_notify_event (GdkEventCrossing* ev)
{
_hovering = true;
set_dirty ();
return CairoWidget::on_enter_notify_event (ev);
}
bool
ArdourKnob::on_leave_notify_event (GdkEventCrossing* ev)
{
_hovering = false;
set_dirty ();
return CairoWidget::on_leave_notify_event (ev);
}
void
ArdourKnob::set_elements (Element e)
{
_elements = e;
}
void
ArdourKnob::add_elements (Element e)
{
_elements = (ArdourKnob::Element) (_elements | e);
}
KnobPersistentTooltip::KnobPersistentTooltip (Gtk::Widget* w)
: PersistentTooltip (w, true, 3)
, _dragging (false)
{
}
void
KnobPersistentTooltip::start_drag ()
{
_dragging = true;
}
void
KnobPersistentTooltip::stop_drag ()
{
_dragging = false;
}
bool
KnobPersistentTooltip::dragging () const
{
return _dragging;
}