1 /*
2  * Copyright (C) 2016-2017 Paul Davis <paul@linuxaudiosystems.com>
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 along
15  * with this program; if not, write to the Free Software Foundation, Inc.,
16  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17  */
18 
19 #include <cmath>
20 
21 #include <cairomm/context.h>
22 #include <cairomm/pattern.h>
23 
24 #include "ardour/automation_control.h"
25 #include "ardour/dB.h"
26 #include "ardour/utils.h"
27 
28 #include "gtkmm2ext/gui_thread.h"
29 #include "gtkmm2ext/rgb_macros.h"
30 
31 #include "gtkmm2ext/colors.h"
32 #include "canvas/text.h"
33 
34 #include "knob.h"
35 #include "push2.h"
36 #include "utils.h"
37 
38 #include "pbd/i18n.h"
39 
40 #ifdef __APPLE__
41 #define Rect ArdourCanvas::Rect
42 #endif
43 
44 using namespace PBD;
45 using namespace ARDOUR;
46 using namespace ArdourSurface;
47 using namespace ArdourCanvas;
48 
49 Push2Knob::Element Push2Knob::default_elements = Push2Knob::Element (Push2Knob::Arc);
50 
Push2Knob(Push2 & p,Item * parent,Element e,Flags flags)51 Push2Knob::Push2Knob (Push2& p, Item* parent, Element e, Flags flags)
52 	: Container (parent)
53 	, p2 (p)
54 	, _elements (e)
55 	, _flags (flags)
56 	, _r (0)
57 	, _val (0)
58 	, _normal (0)
59 {
60 	Pango::FontDescription fd ("Sans 10");
61 
62 	text = new Text (this);
63 	text->set_font_description (fd);
64 	text->set_position (Duple (0, -20)); /* changed when radius changes */
65 
66 	/* typically over-ridden */
67 
68 	text_color = p2.get_color (Push2::ParameterName);
69 	arc_start_color = p2.get_color (Push2::KnobArcStart);
70 	arc_end_color = p2.get_color (Push2::KnobArcEnd);
71 }
72 
~Push2Knob()73 Push2Knob::~Push2Knob ()
74 {
75 }
76 
77 void
set_text_color(Gtkmm2ext::Color c)78 Push2Knob::set_text_color (Gtkmm2ext::Color c)
79 {
80 	text->set_color (c);
81 }
82 
83 void
set_radius(double r)84 Push2Knob::set_radius (double r)
85 {
86 	_r = r;
87 	text->set_position (Duple (-_r, -_r - 20));
88 	_bounding_box_dirty = true;
89 	redraw ();
90 }
91 
92 void
render(Rect const & area,Cairo::RefPtr<Cairo::Context> context) const93 Push2Knob::render (Rect const & area, Cairo::RefPtr<Cairo::Context> context) const
94 {
95 	if (!_controllable) {
96 		/* no controllable, nothing to draw */
97 		return;
98 	}
99 
100 	const float scale = 2.0 * _r;
101 	const float pointer_thickness = 3.0 * (scale/80);  //(if the knob is 80 pixels wide, we want a 3-pix line on it)
102 
103 	const float start_angle = ((180 - 65) * G_PI) / 180;
104 	const float end_angle = ((360 + 65) * G_PI) / 180;
105 
106 	float zero = 0;
107 
108 	if (_flags & ArcToZero) {
109 		zero = _normal;
110 	}
111 
112 	const float value_angle = start_angle + (_val * (end_angle - start_angle));
113 	const float zero_angle = start_angle + (zero * (end_angle - start_angle));
114 
115 	float value_x = cos (value_angle);
116 	float value_y = sin (value_angle);
117 
118 	/* translate so that all coordinates are based on the center of the
119 	 * knob (which is also its position()
120 	 */
121 	Duple origin = item_to_window (Duple (0, 0));
122 	context->translate (origin.x, origin.y);
123 	context->begin_new_path ();
124 
125 	float center_radius = 0.48*scale;
126 	float border_width = 0.8;
127 
128 	const bool arc = (_elements & Arc)==Arc;
129 	const bool flat = false;
130 
131 	if (arc) {
132 		center_radius = scale*0.33;
133 
134 		float inner_progress_radius = scale*0.38;
135 		float outer_progress_radius = scale*0.48;
136 		float progress_width = (outer_progress_radius-inner_progress_radius);
137 		float progress_radius = inner_progress_radius + progress_width/2.0;
138 
139 		//dark arc background
140 		set_source_rgb (context, p2.get_color (Push2::KnobArcBackground));
141 		context->set_line_width (progress_width);
142 		context->arc (0, 0, progress_radius, start_angle, end_angle);
143 		context->stroke ();
144 
145 		double red_start, green_start, blue_start, astart;
146 		double red_end, green_end, blue_end, aend;
147 
148 		Gtkmm2ext::color_to_rgba (arc_start_color, red_start, green_start, blue_start, astart);
149 		Gtkmm2ext::color_to_rgba (arc_end_color, red_end, green_end, blue_end, aend);
150 
151 		//vary the arc color over the travel of the knob
152 		float intensity = fabsf (_val - zero) / std::max(zero, (1.f - zero));
153 		const float intensity_inv = 1.0 - intensity;
154 		float r = intensity_inv * red_end   + intensity * red_start;
155 		float g = intensity_inv * green_end + intensity * green_start;
156 		float b = intensity_inv * blue_end  + intensity * blue_start;
157 
158 		//draw the arc
159 		context->set_source_rgb (r,g,b);
160 		context->set_line_width (progress_width);
161 		if (zero_angle > value_angle) {
162 			context->arc (0, 0, progress_radius, value_angle, zero_angle);
163 		} else {
164 			context->arc (0, 0, progress_radius, zero_angle, value_angle);
165 		}
166 		context->stroke ();
167 
168 		//shade the arc
169 		if (!flat) {
170 			//note we have to offset the pattern from our centerpoint
171 			Cairo::RefPtr<Cairo::LinearGradient> pattern = Cairo::LinearGradient::create (0.0, -_position.y, 0.0, _position.y);
172 			pattern->add_color_stop_rgba (0.0, 1,1,1, 0.15);
173 			pattern->add_color_stop_rgba (0.5, 1,1,1, 0.0);
174 			pattern->add_color_stop_rgba (1.0, 1,1,1, 0.0);
175 			context->set_source (pattern);
176 			context->arc (0, 0, outer_progress_radius-1, 0, 2.0*G_PI);
177 			context->fill ();
178 		}
179 	}
180 
181 	if (!flat) {
182 		//knob shadow
183 		context->save();
184 		context->translate(pointer_thickness+1, pointer_thickness+1 );
185 		set_source_rgba (context, p2.get_color (Push2::KnobShadow));
186 		context->arc (0, 0, center_radius-1, 0, 2.0*G_PI);
187 		context->fill ();
188 		context->restore();
189 
190 		//inner circle
191 		set_source_rgb (context, p2.get_color (Push2::KnobForeground));
192 		context->arc (0, 0, center_radius, 0, 2.0*G_PI);
193 		context->fill ();
194 
195 		//radial gradient as a lightness shade
196 		Cairo::RefPtr<Cairo::RadialGradient> pattern = Cairo::RadialGradient::create (-center_radius, -center_radius, 1, -center_radius, -center_radius, center_radius*2.5  );  //note we have to offset the gradient from our centerpoint
197 		pattern->add_color_stop_rgba (0.0, 0, 0, 0, 0.2);
198 		pattern->add_color_stop_rgba (1.0, 1, 1, 1, 0.3);
199 		context->set_source (pattern);
200 		context->arc (0, 0, center_radius, 0, 2.0*G_PI);
201 		context->fill ();
202 
203 	}
204 
205 	//black knob border
206 	context->set_line_width (border_width);
207 	set_source_rgba (context, p2.get_color (Push2::KnobBorder));
208 	context->set_source_rgba (0, 0, 0, 1 );
209 	context->arc (0, 0, center_radius, 0, 2.0*G_PI);
210 	context->stroke ();
211 
212 	//line shadow
213 	if (!flat) {
214 		context->save();
215 		context->translate(1, 1 );
216 		set_source_rgba (context, p2.get_color (Push2::KnobLineShadow));
217 		context->set_line_cap (Cairo::LINE_CAP_ROUND);
218 		context->set_line_width (pointer_thickness);
219 		context->move_to ((center_radius * value_x), (center_radius * value_y));
220 		context->line_to (((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
221 		context->stroke ();
222 		context->restore();
223 	}
224 
225 	//line
226 	set_source_rgba (context, p2.get_color (Push2::KnobLine));
227 	context->set_line_cap (Cairo::LINE_CAP_ROUND);
228 	context->set_line_width (pointer_thickness);
229 	context->move_to ((center_radius * value_x), (center_radius * value_y));
230 	context->line_to (((center_radius*0.4) * value_x), ((center_radius*0.4) * value_y));
231 	context->stroke ();
232 
233 	/* reset all translations, scaling etc. */
234 	context->set_identity_matrix();
235 
236 	render_children (area, context);
237 }
238 
239  void
compute_bounding_box() const240 Push2Knob::compute_bounding_box () const
241 {
242 	if (!_canvas || _r == 0) {
243 		_bounding_box = Rect ();
244 		_bounding_box_dirty = false;
245 		return;
246 	}
247 
248 	if (_bounding_box_dirty) {
249 		Rect r = Rect (_position.x - _r, _position.y - _r, _position.x + _r, _position.y + _r);
250 		_bounding_box = r;
251 		_bounding_box_dirty = false;
252 	}
253 
254 	add_child_bounding_boxes ();
255 }
256 
257 void
set_controllable(boost::shared_ptr<AutomationControl> c)258 Push2Knob::set_controllable (boost::shared_ptr<AutomationControl> c)
259 {
260 	watch_connection.disconnect ();  //stop watching the old controllable
261 
262 	if (!c) {
263 		_controllable.reset ();
264 		return;
265 	}
266 
267 	_controllable = c;
268 	_controllable->Changed.connect (watch_connection, invalidator(*this), boost::bind (&Push2Knob::controllable_changed, this), &p2);
269 
270 	controllable_changed ();
271 }
272 
273 void
set_pan_azimuth_text(double pos)274 Push2Knob::set_pan_azimuth_text (double pos)
275 {
276 	/* We show the position of the center of the image relative to the left & right.
277 	   This is expressed as a pair of percentage values that ranges from (100,0)
278 	   (hard left) through (50,50) (hard center) to (0,100) (hard right).
279 
280 	   This is pretty wierd, but its the way audio engineers expect it. Just remember that
281 	   the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
282 	*/
283 
284 	char buf[64];
285 	snprintf (buf, sizeof (buf), _("L:%3d R:%3d"), (int) rint (100.0 * (1.0 - pos)), (int) rint (100.0 * pos));
286 	text->set (buf);
287 }
288 
289 void
set_pan_width_text(double val)290 Push2Knob::set_pan_width_text (double val)
291 {
292 	char buf[16];
293 	snprintf (buf, sizeof (buf), "%d%%", (int) floor (val*100));
294 	text->set (buf);
295 }
296 
297 void
set_gain_text(double)298 Push2Knob::set_gain_text (double)
299 {
300 	char buf[16];
301 
302 	/* need to ignore argument, because it has already been converted into
303 	   the "interface" (0..1) range.
304 	*/
305 
306 	snprintf (buf, sizeof (buf), "%.1f dB", accurate_coefficient_to_dB (_controllable->get_value()));
307 	text->set (buf);
308 }
309 
310 void
controllable_changed()311 Push2Knob::controllable_changed ()
312 {
313 	if (_controllable) {
314 		_normal = _controllable->internal_to_interface (_controllable->normal());
315 		_val = _controllable->internal_to_interface (_controllable->get_value());
316 
317 		switch (_controllable->parameter().type()) {
318 		case ARDOUR::PanAzimuthAutomation:
319 			set_pan_azimuth_text (_val);
320 			break;
321 
322 		case ARDOUR::PanWidthAutomation:
323 			set_pan_width_text (_val);
324 			break;
325 
326 		case ARDOUR::GainAutomation:
327 		case ARDOUR::BusSendLevel:
328 		case ARDOUR::TrimAutomation:
329 			set_gain_text (_val);
330 			break;
331 
332 		default:
333 			text->set (std::string());
334 		}
335 	}
336 
337 	redraw ();
338 }
339 
340 void
add_flag(Flags f)341 Push2Knob::add_flag (Flags f)
342 {
343 	_flags = Flags (_flags | f);
344 	redraw ();
345 }
346 
347 void
remove_flag(Flags f)348 Push2Knob::remove_flag (Flags f)
349 {
350 	_flags = Flags (_flags & ~f);
351 	redraw ();
352 }
353 
354 void
set_arc_start_color(uint32_t c)355 Push2Knob::set_arc_start_color (uint32_t c)
356 {
357 	arc_start_color = c;
358 	redraw ();
359 }
360 
361 void
set_arc_end_color(uint32_t c)362 Push2Knob::set_arc_end_color (uint32_t c)
363 {
364 	arc_end_color = c;
365 	redraw ();
366 }
367