1 /*
2  * Copyright (C) 2011-2012 Carl Hetherington <carl@carlh.net>
3  * Copyright (C) 2011-2012 David Robillard <d@drobilla.net>
4  * Copyright (C) 2011-2016 Paul Davis <paul@linuxaudiosystems.com>
5  * Copyright (C) 2014-2017 Robin Gareus <robin@gareus.org>
6  * Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License along
19  * with this program; if not, write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21  */
22 
23 #include <iostream>
24 #include <iomanip>
25 #include <cstring>
26 #include <cmath>
27 
28 #include <gtkmm/window.h>
29 #include <pangomm/layout.h>
30 
31 #include "pbd/compose.h"
32 
33 #include "gtkmm2ext/gui_thread.h"
34 #include "gtkmm2ext/gtk_ui.h"
35 #include "gtkmm2ext/keyboard.h"
36 #include "gtkmm2ext/utils.h"
37 #include "gtkmm2ext/persistent_tooltip.h"
38 
39 #include "ardour/pannable.h"
40 #include "ardour/panner.h"
41 #include "ardour/panner_shell.h"
42 
43 #include "mono_panner.h"
44 #include "mono_panner_editor.h"
45 #include "rgb_macros.h"
46 #include "ui_config.h"
47 #include "utils.h"
48 
49 #include "pbd/i18n.h"
50 
51 using namespace std;
52 using namespace Gtk;
53 using namespace Gtkmm2ext;
54 using namespace ARDOUR_UI_UTILS;
55 
56 using PBD::Controllable;
57 
58 MonoPanner::ColorScheme MonoPanner::colors;
59 bool MonoPanner::have_colors = false;
60 
61 Pango::AttrList MonoPanner::panner_font_attributes;
62 bool            MonoPanner::have_font = false;
63 
MonoPanner(boost::shared_ptr<ARDOUR::PannerShell> p)64 MonoPanner::MonoPanner (boost::shared_ptr<ARDOUR::PannerShell> p)
65 	: PannerInterface (p->panner())
66 	, _panner_shell (p)
67 	, position_control (_panner->pannable()->pan_azimuth_control)
68 	, drag_start_x (0)
69 	, last_drag_x (0)
70 	, accumulated_delta (0)
71 	, detented (false)
72 	, position_binder (position_control)
73 	, _dragging (false)
74 {
75 	if (!have_colors) {
76 		set_colors ();
77 		have_colors = true;
78 	}
79 	if (!have_font) {
80 		Pango::FontDescription font;
81 		Pango::AttrFontDesc* font_attr;
82 		font = Pango::FontDescription (UIConfiguration::instance().get_SmallBoldMonospaceFont());
83 		font_attr = new Pango::AttrFontDesc (Pango::Attribute::create_attr_font_desc (font));
84 		panner_font_attributes.change(*font_attr);
85 		delete font_attr;
86 		have_font = true;
87 	}
88 
89 	position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&MonoPanner::value_change, this), gui_context());
90 
91 	_panner_shell->Changed.connect (panshell_connections, invalidator (*this), boost::bind (&MonoPanner::bypass_handler, this), gui_context());
92 	_panner_shell->PannableChanged.connect (panshell_connections, invalidator (*this), boost::bind (&MonoPanner::pannable_handler, this), gui_context());
93 	UIConfiguration::instance().ColorsChanged.connect (sigc::mem_fun (*this, &MonoPanner::color_handler));
94 
95 	set_tooltip ();
96 }
97 
~MonoPanner()98 MonoPanner::~MonoPanner ()
99 {
100 
101 }
102 
103 void
set_tooltip()104 MonoPanner::set_tooltip ()
105 {
106 	if (_panner_shell->bypassed()) {
107 		_tooltip.set_tip (_("bypassed"));
108 		return;
109 	}
110 	double pos = position_control->get_value(); // 0..1
111 
112 	/* We show the position of the center of the image relative to the left & right.
113 		 This is expressed as a pair of percentage values that ranges from (100,0)
114 		 (hard left) through (50,50) (hard center) to (0,100) (hard right).
115 
116 		 This is pretty wierd, but its the way audio engineers expect it. Just remember that
117 		 the center of the USA isn't Kansas, its (50LA, 50NY) and it will all make sense.
118 		 */
119 
120 	char buf[64];
121 	snprintf (buf, sizeof (buf), _("L:%3d R:%3d"),
122 			(int) rint (100.0 * (1.0 - pos)),
123 			(int) rint (100.0 * pos));
124 	_tooltip.set_tip (buf);
125 }
126 
127 bool
on_expose_event(GdkEventExpose *)128 MonoPanner::on_expose_event (GdkEventExpose*)
129 {
130 	Glib::RefPtr<Gdk::Window> win (get_window());
131 	Glib::RefPtr<Gdk::GC> gc (get_style()->get_base_gc (get_state()));
132 	Cairo::RefPtr<Cairo::Context> context = get_window()->create_cairo_context();
133 
134 	int width, height;
135 	double pos = position_control->get_value (); /* 0..1 */
136 	uint32_t o, f, t, b, pf, po;
137 
138 	width = get_width();
139 	height = get_height ();
140 
141 	const int step_down = rint(height / 3.5);
142 	const int lr_box_size = height - 2 * step_down;
143 	const int pos_box_size = (int)(rint(step_down * .8)) | 1;
144 	const int top_step = step_down - pos_box_size;
145 	const double corner_radius = 5 * UIConfiguration::instance().get_ui_scale();
146 
147 	o = colors.outline;
148 	f = colors.fill;
149 	t = colors.text;
150 	b = _send_mode ? colors.send_bg : colors.background;
151 	pf = (_send_mode && !_panner_shell->is_linked_to_route()) ? colors.send_pan : colors.pos_fill;
152 	po = colors.pos_outline;
153 
154 	if (_panner_shell->bypassed()) {
155 		b  = 0x20202040;
156 		f  = 0x404040ff;
157 		o  = 0x606060ff;
158 		po = 0x606060ff;
159 		pf = 0x404040ff;
160 		t  = 0x606060ff;
161 	}
162 
163 	/* background */
164 	context->set_source_rgba (UINT_RGBA_R_FLT(b), UINT_RGBA_G_FLT(b), UINT_RGBA_B_FLT(b), UINT_RGBA_A_FLT(b));
165 	context->rectangle (0, 0, width, height);
166 	context->fill ();
167 
168 	double usable_width = width - pos_box_size;
169 
170 	/* compute the centers of the L/R boxes based on the current stereo width */
171 	if (fmod (usable_width,2.0) == 0) {
172 		usable_width -= 1.0;
173 	}
174 	const double half_lr_box = lr_box_size/2.0;
175 	const double left = pos_box_size * .5 + half_lr_box; // center of left box
176 	const double right = width - pos_box_size * .5 - half_lr_box; // center of right box
177 
178 	/* center line */
179 	context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
180 	context->set_line_width (1.0);
181 	context->move_to ((pos_box_size/2.0) + (usable_width/2.0), 0);
182 	context->line_to ((pos_box_size/2.0) + (usable_width/2.0), height);
183 	context->stroke ();
184 
185 	context->set_line_width (1.0);
186 	/* left box */
187 
188 	rounded_left_half_rectangle (context,
189 			left - half_lr_box + .5,
190 			half_lr_box + step_down,
191 			lr_box_size, lr_box_size, corner_radius);
192 	context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
193 	context->fill_preserve ();
194 	context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
195 	context->stroke();
196 
197 	/* add text */
198 	int tw, th;
199 	Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create(get_pango_context());
200 	layout->set_attributes (panner_font_attributes);
201 
202 	layout->set_text (S_("Panner|L"));
203 	layout->get_pixel_size(tw, th);
204 	context->move_to (rint(left - tw/2), rint(lr_box_size + step_down - th/2));
205 	context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
206 	pango_cairo_show_layout (context->cobj(), layout->gobj());
207 
208 	/* right box */
209 	rounded_right_half_rectangle (context,
210 			right - half_lr_box - .5,
211 			half_lr_box + step_down,
212 			lr_box_size, lr_box_size, corner_radius);
213 	context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
214 	context->fill_preserve ();
215 	context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
216 	context->stroke();
217 
218 	/* add text */
219 	layout->set_text (S_("Panner|R"));
220 	layout->get_pixel_size(tw, th);
221 	context->move_to (rint(right - tw/2), rint(lr_box_size + step_down - th/2));
222 	context->set_source_rgba (UINT_RGBA_R_FLT(t), UINT_RGBA_G_FLT(t), UINT_RGBA_B_FLT(t), UINT_RGBA_A_FLT(t));
223 	pango_cairo_show_layout (context->cobj(), layout->gobj());
224 
225 	/* 2 lines that connect them both */
226 	context->set_line_width (1.0);
227 
228 	if (_panner_shell->panner_gui_uri() != "http://ardour.org/plugin/panner_balance#ui") {
229 		context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
230 		context->move_to (left  + half_lr_box, half_lr_box + step_down);
231 		context->line_to (right - half_lr_box, half_lr_box + step_down);
232 		context->stroke ();
233 
234 		context->move_to (left  + half_lr_box, half_lr_box+step_down+lr_box_size);
235 		context->line_to (right - half_lr_box, half_lr_box+step_down+lr_box_size);
236 		context->stroke ();
237 	} else {
238 		context->move_to (left  + half_lr_box, half_lr_box+step_down+lr_box_size);
239 		context->line_to (left  + half_lr_box, half_lr_box + step_down);
240 		context->line_to ((pos_box_size/2.0) + (usable_width/2.0), half_lr_box+step_down+lr_box_size);
241 		context->line_to (right - half_lr_box, half_lr_box + step_down);
242 		context->line_to (right - half_lr_box, half_lr_box+step_down+lr_box_size);
243 		context->close_path();
244 
245 		context->set_source_rgba (UINT_RGBA_R_FLT(f), UINT_RGBA_G_FLT(f), UINT_RGBA_B_FLT(f), UINT_RGBA_A_FLT(f));
246 		context->fill_preserve ();
247 		context->set_source_rgba (UINT_RGBA_R_FLT(o), UINT_RGBA_G_FLT(o), UINT_RGBA_B_FLT(o), UINT_RGBA_A_FLT(o));
248 		context->stroke ();
249 	}
250 
251 	/* draw the position indicator */
252 	double spos = (pos_box_size/2.0) + (usable_width * pos);
253 
254 	context->set_line_width (2.0);
255 	context->move_to (spos + (pos_box_size/2.0), top_step); /* top right */
256 	context->rel_line_to (0.0, pos_box_size); /* lower right */
257 	context->rel_line_to (-pos_box_size/2.0, 4.0 * UIConfiguration::instance().get_ui_scale()); /* bottom point */
258 	context->rel_line_to (-pos_box_size/2.0, -4.0 * UIConfiguration::instance().get_ui_scale()); /* lower left */
259 	context->rel_line_to (0.0, -pos_box_size); /* upper left */
260 	context->close_path ();
261 
262 
263 	context->set_source_rgba (UINT_RGBA_R_FLT(po), UINT_RGBA_G_FLT(po), UINT_RGBA_B_FLT(po), UINT_RGBA_A_FLT(po));
264 	context->stroke_preserve ();
265 	context->set_source_rgba (UINT_RGBA_R_FLT(pf), UINT_RGBA_G_FLT(pf), UINT_RGBA_B_FLT(pf), UINT_RGBA_A_FLT(pf));
266 	context->fill ();
267 
268 	/* marker line */
269 	context->set_line_width (1.0);
270 	context->move_to (spos, 1 + top_step + pos_box_size + 4.0 * UIConfiguration::instance().get_ui_scale());
271 	context->line_to (spos, half_lr_box + step_down + lr_box_size - 1);
272 	context->set_source_rgba (UINT_RGBA_R_FLT(po), UINT_RGBA_G_FLT(po), UINT_RGBA_B_FLT(po), UINT_RGBA_A_FLT(po));
273 	context->stroke ();
274 
275 	/* done */
276 
277 	return true;
278 }
279 
280 bool
on_button_press_event(GdkEventButton * ev)281 MonoPanner::on_button_press_event (GdkEventButton* ev)
282 {
283 	if (PannerInterface::on_button_press_event (ev)) {
284 		return true;
285 	}
286 	if (_panner_shell->bypassed()) {
287 		return false;
288 	}
289 
290 	drag_start_x = ev->x;
291 	last_drag_x = ev->x;
292 
293 	_dragging = false;
294 	_tooltip.target_stop_drag ();
295 	accumulated_delta = 0;
296 	detented = false;
297 
298 	/* Let the binding proxies get first crack at the press event
299 	*/
300 
301 	if (ev->y < 20) {
302 		if (position_binder.button_press_handler (ev)) {
303 			return true;
304 		}
305 	}
306 
307 	if (ev->button != 1) {
308 		return false;
309 	}
310 
311 	if (ev->type == GDK_2BUTTON_PRESS) {
312 		int width = get_width();
313 
314 		if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
315 			/* handled by button release */
316 			return true;
317 		}
318 
319 
320 		if (ev->x <= width/3) {
321 			/* left side dbl click */
322 			position_control->set_value (0, Controllable::NoGroup);
323 		} else if (ev->x > 2*width/3) {
324 			position_control->set_value (1.0, Controllable::NoGroup);
325 		} else {
326 			position_control->set_value (0.5, Controllable::NoGroup);
327 		}
328 
329 		_dragging = false;
330 		_tooltip.target_stop_drag ();
331 
332 	} else if (ev->type == GDK_BUTTON_PRESS) {
333 
334 		if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
335 			/* handled by button release */
336 			return true;
337 		}
338 
339 		_dragging = true;
340 		_tooltip.target_start_drag ();
341 		StartGesture ();
342 	}
343 
344 	return true;
345 }
346 
347 bool
on_button_release_event(GdkEventButton * ev)348 MonoPanner::on_button_release_event (GdkEventButton* ev)
349 {
350 	if (PannerInterface::on_button_release_event (ev)) {
351 		return true;
352 	}
353 
354 	if (ev->button != 1) {
355 		return false;
356 	}
357 
358 	if (_panner_shell->bypassed()) {
359 		return false;
360 	}
361 
362 	_dragging = false;
363 	_tooltip.target_stop_drag ();
364 	accumulated_delta = 0;
365 	detented = false;
366 
367 	if (Keyboard::modifier_state_contains (ev->state, Keyboard::TertiaryModifier)) {
368 		_panner->reset ();
369 	} else {
370 		StopGesture ();
371 	}
372 
373 	return true;
374 }
375 
376 bool
on_scroll_event(GdkEventScroll * ev)377 MonoPanner::on_scroll_event (GdkEventScroll* ev)
378 {
379 	double one_degree = 1.0/180.0; // one degree as a number from 0..1, since 180 degrees is the full L/R axis
380 	double pv = position_control->get_value(); // 0..1.0 ; 0 = left
381 	double step;
382 
383 	if (_panner_shell->bypassed()) {
384 		return false;
385 	}
386 
387 	if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
388 		step = one_degree;
389 	} else {
390 		step = one_degree * 5.0;
391 	}
392 
393 	switch (ev->direction) {
394 		case GDK_SCROLL_UP:
395 		case GDK_SCROLL_LEFT:
396 			pv -= step;
397 			position_control->set_value (pv, Controllable::NoGroup);
398 			break;
399 		case GDK_SCROLL_DOWN:
400 		case GDK_SCROLL_RIGHT:
401 			pv += step;
402 			position_control->set_value (pv, Controllable::NoGroup);
403 			break;
404 	}
405 
406 	return true;
407 }
408 
409 bool
on_motion_notify_event(GdkEventMotion * ev)410 MonoPanner::on_motion_notify_event (GdkEventMotion* ev)
411 {
412 	if (_panner_shell->bypassed()) {
413 		_dragging = false;
414 	}
415 	if (!_dragging) {
416 		return false;
417 	}
418 
419 	int w = get_width();
420 	double delta = (ev->x - last_drag_x) / (double) w;
421 
422 	/* create a detent close to the center, at approx 1/180 deg */
423 	if (!detented && fabsf (position_control->get_value() - .5f) < 0.006f) {
424 		detented = true;
425 		/* snap to center */
426 		position_control->set_value (0.5, Controllable::NoGroup);
427 	}
428 
429 	if (detented) {
430 		accumulated_delta += delta;
431 
432 		/* have we pulled far enough to escape ? */
433 
434 		if (fabs (accumulated_delta) >= 0.048) {
435 			position_control->set_value (position_control->get_value() + (accumulated_delta > 0 ? 0.006 : -0.006), Controllable::NoGroup);
436 			detented = false;
437 			accumulated_delta = 0;
438 		}
439 	} else {
440 		double pv = position_control->get_value(); // 0..1.0 ; 0 = left
441 		position_control->set_value (pv + delta, Controllable::NoGroup);
442 	}
443 
444 	last_drag_x = ev->x;
445 	return true;
446 }
447 
448 bool
on_key_press_event(GdkEventKey * ev)449 MonoPanner::on_key_press_event (GdkEventKey* ev)
450 {
451 	double one_degree = 1.0/180.0;
452 	double pv = position_control->get_value(); // 0..1.0 ; 0 = left
453 	double step;
454 
455 	if (_panner_shell->bypassed()) {
456 		return false;
457 	}
458 
459 	if (Keyboard::modifier_state_contains (ev->state, Keyboard::PrimaryModifier)) {
460 		step = one_degree;
461 	} else {
462 		step = one_degree * 5.0;
463 	}
464 
465 	switch (ev->keyval) {
466 		case GDK_Left:
467 			pv -= step;
468 			position_control->set_value (pv, Controllable::NoGroup);
469 			break;
470 		case GDK_Right:
471 			pv += step;
472 			position_control->set_value (pv, Controllable::NoGroup);
473 			break;
474 		case GDK_0:
475 		case GDK_KP_0:
476 			position_control->set_value (0.0, Controllable::NoGroup);
477 			break;
478 		default:
479 			return false;
480 	}
481 
482 	return true;
483 }
484 
485 void
set_colors()486 MonoPanner::set_colors ()
487 {
488 	colors.fill        = UIConfiguration::instance().color_mod ("mono panner fill", "panner fill");
489 	colors.outline     = UIConfiguration::instance().color ("mono panner outline");
490 	colors.text        = UIConfiguration::instance().color ("mono panner text");
491 	colors.background  = UIConfiguration::instance().color ("mono panner bg");
492 	colors.pos_outline = UIConfiguration::instance().color ("mono panner position outline");
493 	colors.pos_fill    = UIConfiguration::instance().color_mod ("mono panner position fill", "mono panner position fill");
494 	colors.send_bg     = UIConfiguration::instance().color ("send bg");
495 	colors.send_pan    = UIConfiguration::instance().color ("send pan");
496 }
497 
498 void
color_handler()499 MonoPanner::color_handler ()
500 {
501 	set_colors ();
502 	queue_draw ();
503 }
504 
505 void
bypass_handler()506 MonoPanner::bypass_handler ()
507 {
508 	queue_draw ();
509 }
510 
511 void
pannable_handler()512 MonoPanner::pannable_handler ()
513 {
514 	panvalue_connections.drop_connections();
515 	position_control = _panner->pannable()->pan_azimuth_control;
516 	position_binder.set_controllable(position_control);
517 	position_control->Changed.connect (panvalue_connections, invalidator(*this), boost::bind (&MonoPanner::value_change, this), gui_context());
518 	queue_draw ();
519 }
520 
521 PannerEditor*
editor()522 MonoPanner::editor ()
523 {
524 	return new MonoPannerEditor (this);
525 }
526