1 /*
2  * Copyright (C) 2017 Robin Gareus <robin@gareus.org>
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 <gtkmm/sizegroup.h>
20 
21 #include "ardour/dB.h"
22 #include "ardour/profile.h"
23 #include "widgets/tooltips.h"
24 #include "gtkmm2ext/gui_thread.h"
25 
26 #include "actions.h"
27 #include "ardour_ui.h"
28 #include "timers.h"
29 #include "transport_control_ui.h"
30 
31 #include "pbd/i18n.h"
32 
33 using namespace Glib;
34 using namespace Gtk;
35 using namespace ARDOUR;
36 using namespace ArdourWidgets;
37 
TransportControlUI()38 TransportControlUI::TransportControlUI ()
39 {
40 	Config->ParameterChanged.connect (config_connection, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::parameter_changed, this, _1), gui_context());
41 }
42 
43 void
map_actions()44 TransportControlUI::map_actions ()
45 {
46 	/* setup actions */
47 	RefPtr<Action> act;
48 
49 	act = ActionManager::get_action (X_("Transport"), X_("ToggleClick"));
50 	click_button.set_related_action (act);
51 	act = ActionManager::get_action (X_("Transport"), X_("Stop"));
52 	stop_button.set_related_action (act);
53 	act = ActionManager::get_action (X_("Transport"), X_("Roll"));
54 	roll_button.set_related_action (act);
55 	act = ActionManager::get_action (X_("Transport"), X_("Record"));
56 	rec_button.set_related_action (act);
57 	act = ActionManager::get_action (X_("Transport"), X_("GotoStart"));
58 	goto_start_button.set_related_action (act);
59 	act = ActionManager::get_action (X_("Transport"), X_("GotoEnd"));
60 	goto_end_button.set_related_action (act);
61 	act = ActionManager::get_action (X_("Transport"), X_("Loop"));
62 	auto_loop_button.set_related_action (act);
63 	act = ActionManager::get_action (X_("Transport"), X_("PlaySelection"));
64 	play_selection_button.set_related_action (act);
65 
66 	act = ActionManager::get_action (X_("MIDI"), X_("panic"));
67 	midi_panic_button.set_related_action (act);
68 
69 	/* tooltips depend on actions */
70 	set_tooltip (roll_button, _("Play from playhead"));
71 	set_tooltip (stop_button, _("Stop playback"));
72 	set_tooltip (rec_button, _("Toggle record"));
73 	set_tooltip (play_selection_button, _("Play range/selection"));
74 	set_tooltip (goto_start_button, _("Go to start of session"));
75 	set_tooltip (goto_end_button, _("Go to end of session"));
76 	set_tooltip (auto_loop_button, _("Play loop range"));
77 	set_tooltip (midi_panic_button, _("MIDI Panic\nSend note off and reset controller messages on all MIDI channels"));
78 
79 	/* set click_button tooltip */
80 	parameter_changed ("click-gain");
81 }
82 
83 void
setup(TransportControlProvider * ui)84 TransportControlUI::setup (TransportControlProvider* ui)
85 {
86 	click_button.signal_button_press_event().connect (sigc::mem_fun (*ui, &TransportControlProvider::click_button_clicked), false);
87 	click_button.signal_scroll_event().connect (sigc::mem_fun (*this, &TransportControlUI::click_button_scroll), false);
88 
89 	/* setup icons */
90 
91 	click_button.set_icon (ArdourIcon::TransportMetronom);
92 	goto_start_button.set_icon (ArdourIcon::TransportStart);
93 	goto_end_button.set_icon (ArdourIcon::TransportEnd);
94 	roll_button.set_icon (ArdourIcon::TransportPlay);
95 	stop_button.set_icon (ArdourIcon::TransportStop);
96 	play_selection_button.set_icon (ArdourIcon::TransportRange);
97 	auto_loop_button.set_icon (ArdourIcon::TransportLoop);
98 	rec_button.set_icon (ArdourIcon::RecButton);
99 	midi_panic_button.set_icon (ArdourIcon::TransportPanic);
100 
101 	/* transport control size-group */
102 
103 	Glib::RefPtr<SizeGroup> transport_button_size_group = SizeGroup::create (SIZE_GROUP_BOTH);
104 	transport_button_size_group->add_widget (goto_start_button);
105 	transport_button_size_group->add_widget (goto_end_button);
106 	transport_button_size_group->add_widget (auto_loop_button);
107 	transport_button_size_group->add_widget (rec_button);
108 	if (!ARDOUR::Profile->get_mixbus()) {
109 		/*note: since we aren't showing this button, it doesn't get allocated
110 		 * and therefore blows-up the size-group.  so remove it.
111 		 */
112 		transport_button_size_group->add_widget (play_selection_button);
113 	}
114 	transport_button_size_group->add_widget (roll_button);
115 	transport_button_size_group->add_widget (stop_button);
116 
117 	transport_button_size_group->add_widget (midi_panic_button);
118 	transport_button_size_group->add_widget (click_button);
119 
120 #define PX_SCALE(px) std::max((float)px, rintf((float)px * UIConfiguration::instance().get_ui_scale()))
121 
122 	click_button.set_size_request (PX_SCALE(20), PX_SCALE(20));
123 	set_spacing (PX_SCALE(2));
124 
125 #undef PX_SCALE
126 
127 	if (!ARDOUR::Profile->get_mixbus()) {
128 		pack_start (midi_panic_button, true, true, 0);
129 	} else {
130 		pack_start (midi_panic_button, true, true, 3);
131 	}
132 	pack_start (click_button, true, true, 0);
133 	pack_start (goto_start_button, true, true);
134 	pack_start (goto_end_button, true, true);
135 	pack_start (auto_loop_button, true, true);
136 	if (!ARDOUR::Profile->get_mixbus()) {
137 		pack_start (play_selection_button, true, true);
138 	}
139 	pack_start (roll_button, true, true);
140 	pack_start (stop_button, true, true);
141 	pack_start (rec_button, true, true, 3);
142 
143 	roll_button.set_name ("transport button");
144 	stop_button.set_name ("transport button");
145 	goto_start_button.set_name ("transport button");
146 	goto_end_button.set_name ("transport button");
147 	auto_loop_button.set_name ("transport button");
148 	play_selection_button.set_name ("transport button");
149 	rec_button.set_name ("transport recenable button");
150 	midi_panic_button.set_name ("transport button"); // XXX ???
151 	click_button.set_name ("transport button");
152 
153 	roll_button.set_controllable (ui->roll_controllable);
154 	stop_button.set_controllable (ui->stop_controllable);
155 	goto_start_button.set_controllable (ui->goto_start_controllable);
156 	goto_end_button.set_controllable (ui->goto_end_controllable);
157 	auto_loop_button.set_controllable (ui->auto_loop_controllable);
158 	play_selection_button.set_controllable (ui->play_selection_controllable);
159 	rec_button.set_controllable (ui->rec_controllable);
160 
161 	stop_button.set_active (true);
162 
163 	show_all ();
164 
165 	Timers::blink_connect (sigc::mem_fun (*this, &TransportControlUI::blink_rec_enable));
166 }
167 
168 void
set_session(ARDOUR::Session * s)169 TransportControlUI::set_session (ARDOUR::Session *s)
170 {
171 	SessionHandlePtr::set_session (s);
172 	set_loop_sensitivity ();
173 	map_transport_state ();
174 
175 	if (!_session) {
176 		rec_button.set_sensitive (false);
177 		return;
178 	}
179 
180 	_session->config.ParameterChanged.connect (_session_connections, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::parameter_changed, this, _1), gui_context());
181 	_session->StepEditStatusChange.connect (_session_connections, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::step_edit_status_change, this, _1), gui_context());
182 	_session->TransportStateChange.connect (_session_connections, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::map_transport_state, this), gui_context());
183 	_session->auto_loop_location_changed.connect (_session_connections, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::set_loop_sensitivity, this), gui_context ());
184 	_session->PunchLoopConstraintChange.connect (_session_connections, MISSING_INVALIDATOR, boost::bind (&TransportControlUI::set_loop_sensitivity, this), gui_context ());
185 
186 	rec_button.set_sensitive (true);
187 }
188 
189 void
parameter_changed(std::string p)190 TransportControlUI::parameter_changed (std::string p)
191 {
192 	if (p == "external-sync") {
193 		set_loop_sensitivity ();
194 	} else if (p == "click-record-only") {
195 		// TODO set a flag, blink or gray-out metronome button while rolling, only
196 		if (Config->get_click_record_only()) {
197 			click_button.set_name ("generic button"); // XXX
198 		} else {
199 			click_button.set_name ("transport button");
200 		}
201 	} else if (p == "click-gain") {
202 		float gain_db = accurate_coefficient_to_dB (Config->get_click_gain());
203 		char tmp[32];
204 		snprintf(tmp, 31, "%+.1f", gain_db);
205 		set_tooltip (click_button, string_compose (_("Enable/Disable metronome\n\nRight-click to access preferences\nMouse-wheel to modify level\nSignal Level: %1 dBFS"), tmp));
206 	}
207 }
208 
209 void
map_transport_state()210 TransportControlUI::map_transport_state ()
211 {
212 	if (!_session) {
213 		auto_loop_button.unset_active_state ();
214 		play_selection_button.unset_active_state ();
215 		roll_button.unset_active_state ();
216 		stop_button.set_active_state (Gtkmm2ext::ExplicitActive);
217 		return;
218 	}
219 
220 	float sp = _session->transport_speed();
221 
222 	if (sp != 0.0f) {
223 
224 		/* we're rolling */
225 
226 		if (_session->get_play_range()) {
227 
228 			play_selection_button.set_active_state (Gtkmm2ext::ExplicitActive);
229 			roll_button.unset_active_state ();
230 			auto_loop_button.unset_active_state ();
231 
232 		} else if (_session->get_play_loop ()) {
233 
234 			auto_loop_button.set_active (true);
235 			play_selection_button.set_active (false);
236 
237 			if (Config->get_loop_is_mode()) {
238 				roll_button.set_active (true);
239 			} else {
240 				roll_button.set_active (false);
241 			}
242 
243 		} else {
244 
245 			roll_button.set_active (true);
246 			play_selection_button.set_active (false);
247 			auto_loop_button.set_active (false);
248 
249 		}
250 
251 		if (UIConfiguration::instance().get_follow_edits() && !_session->config.get_external_sync()) {
252 			/* light up both roll and play-selection if they are joined */
253 			roll_button.set_active (true);
254 			play_selection_button.set_active (true);
255 		}
256 
257 		stop_button.set_active (false);
258 
259 	} else {
260 
261 		stop_button.set_active (true);
262 		roll_button.set_active (false);
263 		play_selection_button.set_active (false);
264 		if (Config->get_loop_is_mode ()) {
265 			auto_loop_button.set_active (_session->get_play_loop());
266 		} else {
267 			auto_loop_button.set_active (false);
268 		}
269 	}
270 }
271 
272 void
step_edit_status_change(bool yn)273 TransportControlUI::step_edit_status_change (bool yn)
274 {
275 	// XXX should really store pre-step edit status of things
276 	// we make insensitive
277 
278 	if (yn) {
279 		rec_button.set_active_state (Gtkmm2ext::ImplicitActive);
280 		rec_button.set_sensitive (false);
281 	} else {
282 		rec_button.unset_active_state ();;
283 		rec_button.set_sensitive (true);
284 	}
285 }
286 
287 void
set_loop_sensitivity()288 TransportControlUI::set_loop_sensitivity ()
289 {
290 	if (!_session || _session->config.get_external_sync()) {
291 		auto_loop_button.set_sensitive (false);
292 	} else {
293 		auto_loop_button.set_sensitive (_session && _session->loop_is_possible() && _session->locations()->auto_loop_location());
294 	}
295 }
296 
297 void
blink_rec_enable(bool onoff)298 TransportControlUI::blink_rec_enable (bool onoff)
299 {
300 	if (_session == 0) {
301 		return;
302 	}
303 
304 	if (_session->step_editing()) {
305 		return;
306 	}
307 
308 	Session::RecordState const r = _session->record_status ();
309 	bool const h = _session->have_rec_enabled_track ();
310 
311 	if (r == Session::Enabled || (r == Session::Recording && !h)) {
312 		if (onoff) {
313 			rec_button.set_active_state (Gtkmm2ext::ExplicitActive);
314 		} else {
315 			rec_button.set_active_state (Gtkmm2ext::Off);
316 		}
317 	} else if (r == Session::Recording && h) {
318 		rec_button.set_active_state (Gtkmm2ext::ExplicitActive);
319 	} else {
320 		rec_button.unset_active_state ();
321 	}
322 }
323 
324 bool
click_button_scroll(GdkEventScroll * ev)325 TransportControlUI::click_button_scroll (GdkEventScroll* ev)
326 {
327 	gain_t gain = Config->get_click_gain();
328 	float gain_db = accurate_coefficient_to_dB (gain);
329 
330 	switch (ev->direction) {
331 		case GDK_SCROLL_UP:
332 		case GDK_SCROLL_LEFT:
333 			gain_db += 1;
334 			break;
335 		case GDK_SCROLL_DOWN:
336 		case GDK_SCROLL_RIGHT:
337 			gain_db -= 1;
338 			break;
339 	}
340 	gain_db = std::max (-60.f, gain_db);
341 	gain = dB_to_coefficient (gain_db);
342 	gain = std::min (gain, Config->get_max_gain());
343 	Config->set_click_gain (gain);
344 	return true;
345 }
346