1 /*
2  * Copyright (C) 2018-2019 Paul Davis <paul@linuxaudiosystems.com>
3  * Copyright (C) 2018-2019 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 "pbd/enumwriter.h"
21 #include "pbd/unwind.h"
22 
23 #include "temporal/time.h"
24 
25 #include "ardour/audioengine.h"
26 #include "ardour/session.h"
27 #include "ardour/transport_master.h"
28 #include "ardour/transport_master_manager.h"
29 
30 #include "widgets/tooltips.h"
31 #include "widgets/ardour_icon.h"
32 
33 #include "gtkmm2ext/utils.h"
34 #include "gtkmm2ext/gui_thread.h"
35 
36 #include "ardour_ui.h"
37 #include "floating_text_entry.h"
38 #include "transport_masters_dialog.h"
39 #include "ui_config.h"
40 #include "utils.h"
41 
42 #include "pbd/i18n.h"
43 
44 using namespace std;
45 using namespace Gtk;
46 using namespace Gtkmm2ext;
47 using namespace ARDOUR;
48 using namespace PBD;
49 using namespace ArdourWidgets;
50 
TransportMastersWidget()51 TransportMastersWidget::TransportMastersWidget ()
52 	: table (4, 13)
53 	, add_master_button (_("Add a new Transport Master"))
54 	, lost_sync_button (_("Keep rolling if sync is lost"))
55 	, ignore_active_change (false)
56 {
57 	midi_port_store = ListStore::create (port_columns);
58 	audio_port_store = ListStore::create (port_columns);
59 
60 	AudioEngine::instance()->PortRegisteredOrUnregistered.connect (port_reg_connection, invalidator (*this),  boost::bind (&TransportMastersWidget::update_ports, this), gui_context());
61 	AudioEngine::instance()->PortPrettyNameChanged.connect (port_reg_connection, invalidator (*this),  boost::bind (&TransportMastersWidget::update_ports, this), gui_context());
62 	update_ports ();
63 
64 	Gtk::Table *add_table = manage(new Gtk::Table(1,2));
65 	add_table->attach(add_master_button, 0,1, 0,1, Gtk::SHRINK);
66 
67 	pack_start (table, FALSE, FALSE, 12);
68 	pack_start (*add_table, FALSE, FALSE);
69 	pack_start (lost_sync_button, FALSE, FALSE, 12);
70 
71 	Config->ParameterChanged.connect (config_connection, invalidator (*this), boost::bind (&TransportMastersWidget::param_changed, this, _1), gui_context());
72 	lost_sync_button.signal_toggled().connect (sigc::mem_fun (*this, &TransportMastersWidget::lost_sync_button_toggled));
73 	lost_sync_button.set_active (Config->get_transport_masters_just_roll_when_sync_lost());
74 	set_tooltip (lost_sync_button, string_compose (_("<b>When enabled</b>, if the signal from a transport master is lost, %1 will keep rolling at its current speed.\n"
75 	                                                 "<b>When disabled</b>, loss of transport master sync causes %1 to stop"), PROGRAM_NAME));
76 
77 	add_master_button.signal_clicked.connect (sigc::mem_fun (*this, &TransportMastersWidget::add_master));
78 
79 	col_title[0].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Select")));               align[0]=0.0;
80 	col_title[1].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Name")));                 align[1]=0.5;
81 	col_title[2].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Type")));                 align[2]=0.5;
82 	col_title[3].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Source")));               align[3]=0.5;
83 	col_title[4].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Format")));               align[4]=0.5;
84 	col_title[5].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Sync Position + Delta")));align[5]=0.5;
85 	col_title[6].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Last Message + Age")));   align[6]=0.5;
86 	col_title[7].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Active\nCommands")));     align[7]=0.5;
87 	col_title[8].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Clock\nSynced")));        align[9]=0.0;
88 	col_title[9].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("29.97/\n30")));          align[10]=0.0;
89 	col_title[10].set_markup (string_compose ("<span weight=\"bold\">%1</span>", _("Remove")));              align[11]=0.5;
90 
91 	set_tooltip (col_title[7], _("Controls whether or not certain transport-related commands can be sent from the GUI or control "
92 	                              "surfaces when this transport master is in use. The default is not to allow any such commands "
93 	                              "when the master is in use."));
94 
95 	set_tooltip (col_title[9], _("<b>When enabled</b> the external timecode source is assumed to use 29.97 fps instead of 30000/1001.\n"
96 	                              "SMPTE 12M-1999 specifies 29.97df as 30000/1001. The spec further mentions that "
97 	                              "drop-sample timecode has an accumulated error of -86ms over a 24-hour period.\n"
98 	                              "Drop-sample timecode would compensate exactly for a NTSC color frame rate of 30 * 0.9990 (ie 29.970000). "
99 	                              "That is not the actual rate. However, some vendors use that rate - despite it being against the specs - "
100 	                              "because the variant of using exactly 29.97 fps has zero timecode drift.\n"
101 		             ));
102 
103 	set_tooltip (col_title[8], string_compose (_("<b>When enabled</b> the external timecode source is assumed to be sample-clock synced to the audio interface\n"
104 	                                              "being used by %1."), PROGRAM_NAME));
105 
106 	table.set_col_spacings (12);
107 	table.set_row_spacings (6);
108 
109 	TransportMasterManager::instance().CurrentChanged.connect (current_connection, invalidator (*this), boost::bind (&TransportMastersWidget::current_changed, this, _1, _2), gui_context());
110 	TransportMasterManager::instance().Added.connect (add_connection, invalidator (*this), boost::bind (&TransportMastersWidget::rebuild, this), gui_context());
111 	TransportMasterManager::instance().Removed.connect (remove_connection, invalidator (*this), boost::bind (&TransportMastersWidget::rebuild, this), gui_context());
112 
113 	AudioEngine::instance()->Running.connect (engine_running_connection, invalidator (*this), boost::bind (&TransportMastersWidget::update_usability, this), gui_context());
114 
115 	rebuild ();
116 }
117 
~TransportMastersWidget()118 TransportMastersWidget::~TransportMastersWidget ()
119 {
120 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
121 		delete *r;
122 	}
123 }
124 
125 void
set_transport_master(boost::shared_ptr<TransportMaster> tm)126 TransportMastersWidget::set_transport_master (boost::shared_ptr<TransportMaster> tm)
127 {
128 	_session->request_sync_source (tm);
129 }
130 
131 void
current_changed(boost::shared_ptr<TransportMaster> old_master,boost::shared_ptr<TransportMaster> new_master)132 TransportMastersWidget::current_changed (boost::shared_ptr<TransportMaster> old_master, boost::shared_ptr<TransportMaster> new_master)
133 {
134 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
135 		if ((*r)->tm == new_master) {
136 			(*r)->use_button.set_active (true);
137 			break; /* there can only be one */
138 		}
139 	}
140 }
141 
142 void
add_master()143 TransportMastersWidget::add_master ()
144 {
145 	printf ("TransportMastersWidget::add_master\n");
146 	AddTransportMasterDialog d;
147 
148 	d.present ();
149 	string name;
150 
151 	while (name.empty()) {
152 
153 		int r = d.run ();
154 
155 		switch (r) {
156 		case RESPONSE_ACCEPT:
157 			name = d.get_name();
158 			break;
159 		default:
160 			return;
161 		}
162 	}
163 
164 	d.hide ();
165 
166 	if (TransportMasterManager::instance().add (d.get_type(), name)) {
167 		MessageDialog msg (_("New transport master not added - check error log for details"));
168 		msg.run ();
169 	}
170 }
171 
172 void
clear()173 TransportMastersWidget::clear ()
174 {
175 	container_clear (table);
176 
177 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
178 		delete *r;
179 	}
180 
181 	rows.clear ();
182 }
183 
184 void
rebuild()185 TransportMastersWidget::rebuild ()
186 {
187 	TransportMasterManager::TransportMasters const & masters (TransportMasterManager::instance().transport_masters());
188 
189 	clear ();
190 	table.resize (masters.size()+1, 14);
191 
192 	for (size_t col = 0; col < sizeof (col_title) / sizeof (col_title[0]); ++col) {
193 		table.attach (col_title[col], col, col+1, 0, 1);
194 		col_title[col].set_alignment( align[col], 0.5);
195 	}
196 
197 	uint32_t n = 1;
198 
199 	Gtk::RadioButtonGroup use_button_group;
200 
201 	for (TransportMasterManager::TransportMasters::const_iterator m = masters.begin(); m != masters.end(); ++m, ++n) {
202 
203 		Row* r = new Row (*this);
204 		rows.push_back (r);
205 
206 		r->tm = *m;
207 		r->label.set_text ((*m)->name());
208 		r->type.set_text (enum_2_string  ((*m)->type()));
209 
210 		r->use_button.set_group (use_button_group);
211 
212 		if (TransportMasterManager::instance().current() == r->tm) {
213 			r->use_button.set_active (true);
214 		}
215 
216 		int col = 0;
217 
218 		r->label_box.add (r->label);
219 		r->current_box.add (r->current);
220 		r->last_box.add (r->last);
221 
222 		table.attach (r->use_button,      col, col+1, n, n+1, FILL, SHRINK); ++col;
223 		table.attach (r->label_box,       col, col+1, n, n+1, FILL, SHRINK); ++col;
224 		table.attach (r->type,            col, col+1, n, n+1, FILL, SHRINK); ++col;
225 		table.attach (r->port_combo,      col, col+1, n, n+1, FILL, SHRINK); ++col;
226 		table.attach (r->format,          col, col+1, n, n+1, FILL, SHRINK); ++col;
227 		table.attach (r->current_box,     col, col+1, n, n+1, FILL, SHRINK); ++col;
228 		table.attach (r->last_box,        col, col+1, n, n+1, FILL, SHRINK); ++col;
229 		table.attach (r->request_options, col, col+1, n, n+1, FILL, SHRINK); ++col;
230 
231 		boost::shared_ptr<TimecodeTransportMaster> ttm (boost::dynamic_pointer_cast<TimecodeTransportMaster> (r->tm));
232 
233 		if (ttm) {
234 			table.attach (r->sclock_synced_button, col, col+1, n, n+1, FILL, SHRINK); ++col;
235 			table.attach (r->fr2997_button,        col, col+1, n, n+1, FILL, SHRINK); ++col;
236 			r->fr2997_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::fr2997_button_toggled));
237 		} else {
238 			col += 2;
239 		}
240 
241 		if (r->tm->removeable()) {
242 			table.attach (r->remove_button, col, col+1, n, n+1, SHRINK, EXPAND|FILL);
243 		}
244 
245 		table.show_all ();
246 
247 		r->label_box.signal_button_press_event().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::name_press));
248 		r->port_combo.signal_changed().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::port_choice_changed));
249 		r->use_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::use_button_toggled));
250 		r->request_options.signal_button_press_event().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::request_option_press), false);
251 		r->remove_button.signal_clicked.connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::remove_clicked));
252 
253 		if (ttm) {
254 			r->sclock_synced_button.signal_toggled().connect (sigc::mem_fun (*r, &TransportMastersWidget::Row::sync_button_toggled));
255 		}
256 
257 		r->tm->PropertyChanged.connect (r->property_change_connection, invalidator (*this), boost::bind (&TransportMastersWidget::Row::prop_change, r, _1), gui_context());
258 
259 		PropertyChange all_change;
260 		all_change.add (Properties::locked);
261 		all_change.add (Properties::collect);
262 		all_change.add (Properties::connected);
263 		all_change.add (Properties::allowed_transport_requests);
264 
265 		if (ttm) {
266 			all_change.add (Properties::fr2997);
267 			all_change.add (Properties::sclock_synced);
268 		}
269 
270 		r->prop_change (all_change);
271 	}
272 
273 	update_usability ();
274 }
275 
276 bool
idle_remove(TransportMastersWidget::Row * row)277 TransportMastersWidget::idle_remove (TransportMastersWidget::Row* row)
278 {
279 	TransportMasterManager::instance().remove (row->tm->name());
280 	return false;
281 }
282 
283 void
update_ports()284 TransportMastersWidget::update_ports ()
285 {
286 	if (!is_mapped()) {
287 		return;
288 	}
289 
290 	{
291 		PBD::Unwinder<bool> uw (ignore_active_change, true);
292 		vector<string> inputs;
293 
294 		ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::MIDI, ARDOUR::PortFlags (ARDOUR::IsOutput), inputs);
295 		build_port_model (midi_port_store, inputs);
296 
297 		inputs.clear ();
298 
299 		ARDOUR::AudioEngine::instance()->get_ports ("", ARDOUR::DataType::AUDIO, ARDOUR::PortFlags (ARDOUR::IsOutput), inputs);
300 		build_port_model (audio_port_store, inputs);
301 	}
302 
303 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
304 		if ((*r)->tm->port()) {
305 			(*r)->build_port_list ((*r)->tm->port()->type());
306 		}
307 	}
308 }
309 
310 void
update_usability()311 TransportMastersWidget::update_usability ()
312 {
313 	for (vector<Row*>::iterator r= rows.begin(); r != rows.end(); ++r) {
314 		const bool usable = (*r)->tm->usable();
315 		(*r)->use_button.set_sensitive (usable);
316 		(*r)->request_options.set_sensitive (usable);
317 	}
318 }
319 
Row(TransportMastersWidget & p)320 TransportMastersWidget::Row::Row (TransportMastersWidget& p)
321 	: parent (p)
322 	, request_option_menu (0)
323 	, name_editor (0)
324 	, save_when (0)
325 	, save_last (" --:--:--:--")
326 {
327 	remove_button.set_icon (ArdourIcon::CloseCross);
328 	format.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
329 	last.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
330 	current.modify_font (UIConfiguration::instance().get_BigMonospaceFont());
331 
332 	uint32_t bg = UIConfigurationBase::instance().color ("clock: background");
333 	uint32_t fg = UIConfigurationBase::instance().color ("clock: text");
334 	Gdk::Color bg_color = ARDOUR_UI_UTILS::gdk_color_from_rgba (bg);
335 	Gdk::Color fg_color = ARDOUR_UI_UTILS::gdk_color_from_rgba (fg);
336 
337 	current_box.modify_bg (Gtk::STATE_NORMAL, bg_color);
338 	current.modify_fg (Gtk::STATE_NORMAL, fg_color);
339 
340 	last_box.modify_bg (Gtk::STATE_NORMAL, bg_color);
341 	last.modify_fg (Gtk::STATE_NORMAL, fg_color);
342 
343 	set_size_request_to_display_given_text (format, "999.9 BPM", 0, 0);
344 
345 }
346 
~Row()347 TransportMastersWidget::Row::~Row ()
348 {
349 	delete request_option_menu;
350 }
351 
352 bool
name_press(GdkEventButton * ev)353 TransportMastersWidget::Row::name_press (GdkEventButton* ev)
354 {
355 	if (ev->type == GDK_2BUTTON_PRESS && ev->button == 1) {
356 		Gtk::Window* toplevel = dynamic_cast<Gtk::Window*> (label.get_toplevel());
357 		if (!toplevel) {
358 			return false;
359 		}
360 		name_editor = new FloatingTextEntry (toplevel, tm->name());
361 		name_editor->use_text.connect (sigc::mem_fun (*this, &TransportMastersWidget::Row::name_edited));
362 		name_editor->show ();
363 
364 		/* Now move the floating text entry window to be perfectly
365 		 * aligned with the upper left corner of the name/label box.
366 		 */
367 
368 		Gtk::Widget* tl = label_box.get_toplevel();
369 		Gtk::Window* top_level = dynamic_cast<Gtk::Window*>(tl);
370 
371 		if (top_level) {
372 			Glib::RefPtr<Gdk::Window> win (top_level->get_window());
373 			int rx, ry;
374 			win->get_position (rx, ry);
375 			Gtk::Allocation alloc = label_box.get_allocation();
376 			name_editor->move (rx + alloc.get_x(), ry + alloc.get_y());
377 		}
378 
379 		return true;
380 	}
381 
382 	return false;
383 }
384 
385 void
build_port_model(Glib::RefPtr<Gtk::ListStore> model,vector<string> const & ports)386 TransportMastersWidget::build_port_model (Glib::RefPtr<Gtk::ListStore> model, vector<string> const & ports)
387 {
388 	TreeModel::Row row;
389 
390 	model->clear ();
391 
392 	row = *model->append ();
393 	row[port_columns.full_name] = string();
394 	row[port_columns.short_name] = _("Disconnected");
395 
396 	for (vector<string>::const_iterator p = ports.begin(); p != ports.end(); ++p) {
397 
398 		if (AudioEngine::instance()->port_is_mine (*p)) {
399 			continue;
400 		}
401 
402 		row = *model->append ();
403 		row[port_columns.full_name] = *p;
404 
405 		std::string pn = ARDOUR::AudioEngine::instance()->get_pretty_name_by_name (*p);
406 		if (pn.empty ()) {
407 			pn = (*p).substr ((*p).find (':') + 1);
408 		}
409 		row[port_columns.short_name] = pn;
410 	}
411 }
412 
413 void
remove_clicked()414 TransportMastersWidget::Row::remove_clicked ()
415 {
416 	/* have to do this via an idle callback, because it will destroy the
417 	   widget from which this callback was initiated.
418 	*/
419 	Glib::signal_idle().connect (sigc::bind (sigc::mem_fun (parent, &TransportMastersWidget::idle_remove), this));
420 }
421 
422 void
name_edited(string str,int ignored)423 TransportMastersWidget::Row::name_edited (string str, int ignored)
424 {
425 	tm->set_name (str);
426 	/* floating text entry deletes itself */
427 	name_editor = 0;
428 }
429 
430 void
prop_change(PropertyChange what_changed)431 TransportMastersWidget::Row::prop_change (PropertyChange what_changed)
432 {
433 	if (what_changed.contains (Properties::locked)) {
434 	}
435 
436 	if (what_changed.contains (Properties::fr2997)) {
437 		fr2997_button.set_active (boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm)->fr2997());
438 	}
439 
440 	if (what_changed.contains (Properties::sclock_synced)) {
441 		sclock_synced_button.set_active (boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm)->sample_clock_synced());
442 	}
443 
444 	if (what_changed.contains (Properties::collect)) {
445 	}
446 
447 	if (what_changed.contains (Properties::connected)) {
448 		populate_port_combo ();
449 	}
450 
451 	if (what_changed.contains (Properties::name)) {
452 		label.set_text (tm->name());
453 	}
454 
455 	if (what_changed.contains (Properties::allowed_transport_requests)) {
456 		request_options.set_text (tm->allowed_request_string());
457 	}
458 }
459 
460 void
use_button_toggled()461 TransportMastersWidget::Row::use_button_toggled ()
462 {
463 	if (use_button.get_active()) {
464 		parent.set_transport_master (tm);
465 	}
466 }
467 
468 void
fr2997_button_toggled()469 TransportMastersWidget::Row::fr2997_button_toggled ()
470 {
471 	boost::dynamic_pointer_cast<TimecodeTransportMaster>(tm)->set_fr2997 (fr2997_button.get_active());
472 }
473 
474 void
sync_button_toggled()475 TransportMastersWidget::Row::sync_button_toggled ()
476 {
477 	tm->set_sample_clock_synced (sclock_synced_button.get_active());
478 }
479 
480 bool
request_option_press(GdkEventButton * ev)481 TransportMastersWidget::Row::request_option_press (GdkEventButton* ev)
482 {
483 	if (ev->button == 1) {
484 		if (!request_option_menu) {
485 			build_request_options ();
486 		}
487 		request_option_menu->popup (1, ev->time);
488 		return true;
489 	}
490 	return false;
491 }
492 
493 void
build_request_options()494 TransportMastersWidget::Row::build_request_options ()
495 {
496 	using namespace Gtk::Menu_Helpers;
497 
498 	request_option_menu = new Menu;
499 
500 	MenuList& items (request_option_menu->items());
501 
502 	items.push_back (CheckMenuElem (_("Accept start/stop commands")));
503 	Gtk::CheckMenuItem* i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
504 	i->set_active (tm->request_mask() & TR_StartStop);
505 	i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_StartStop));
506 
507 	items.push_back (CheckMenuElem (_("Accept speed-changing commands")));
508 	i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
509 	i->set_active (tm->request_mask() & TR_Speed);
510 	i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_Speed));
511 
512 	items.push_back (CheckMenuElem (_("Accept locate commands")));
513 	i = dynamic_cast<Gtk::CheckMenuItem *> (&items.back ());
514 	i->set_active (tm->request_mask() & TR_Locate);
515 	i->signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &TransportMastersWidget::Row::mod_request_type), TR_Locate));
516 }
517 
518 void
mod_request_type(TransportRequestType t)519 TransportMastersWidget::Row::mod_request_type (TransportRequestType t)
520 {
521 	tm->set_request_mask (TransportRequestType ((tm->request_mask() & t) ? (tm->request_mask() & ~t) : (tm->request_mask() | t)));
522 }
523 
524 void
populate_port_combo()525 TransportMastersWidget::Row::populate_port_combo ()
526 {
527 	if (!tm->port()) {
528 		port_combo.hide ();
529 		return;
530 	} else {
531 		port_combo.show ();
532 	}
533 
534 	build_port_list (tm->port()->type());
535 }
536 
537 void
build_port_list(DataType type)538 TransportMastersWidget::Row::build_port_list (DataType type)
539 {
540 	Glib::RefPtr<Gtk::ListStore> input = (type == DataType::MIDI ? parent.midi_port_store : parent.audio_port_store);
541 	bool input_found = false;
542 	int n;
543 
544 	if (input->children().empty()) {
545 		return;
546 	}
547 
548 	port_combo.set_model (input);
549 
550 	Gtk::TreeModel::Children children = input->children();
551 	Gtk::TreeModel::Children::iterator i;
552 	i = children.begin();
553 	++i; /* skip "Disconnected" */
554 
555 	for (n = 1;  i != children.end(); ++i, ++n) {
556 		string port_name = (*i)[parent.port_columns.full_name];
557 		if (tm->port()->connected_to (port_name)) {
558 			port_combo.set_active (n);
559 			input_found = true;
560 			break;
561 		}
562 	}
563 
564 	if (!input_found) {
565 		port_combo.set_active (0); /* disconnected */
566 	}
567 }
568 
569 void
port_choice_changed()570 TransportMastersWidget::Row::port_choice_changed ()
571 {
572 	if (!tm->port()) {
573 		return;
574 	}
575 
576 	if (parent.ignore_active_change) {
577 		return;
578 	}
579 
580 	TreeModel::iterator active = port_combo.get_active ();
581 	string new_port = (*active)[parent.port_columns.full_name];
582 
583 	if (new_port.empty()) {
584 		tm->port()->disconnect_all ();
585 		return;
586 	}
587 
588 	if (!tm->port()->connected_to (new_port)) {
589 		tm->port()->disconnect_all ();
590 		tm->port()->connect (new_port);
591 	}
592 }
593 
594 void
update(Session * s,samplepos_t now)595 TransportMastersWidget::Row::update (Session* s, samplepos_t now)
596 {
597 	using namespace Timecode;
598 
599 	samplepos_t pos;
600 	double speed;
601 	samplepos_t most_recent;
602 	samplepos_t when;
603 	stringstream ss;
604 	Time t;
605 	boost::shared_ptr<TimecodeTransportMaster> ttm;
606 	boost::shared_ptr<MIDIClock_TransportMaster> mtm;
607 
608 	if (!AudioEngine::instance()->running() || !s) {
609 		return;
610 	}
611 
612 	string current_str (" --:--:--:--");
613 	string delta_str ("\u0394  ----  ");
614 	string age_str ("         ");
615 
616 	if (tm->speed_and_position (speed, pos, most_recent, when, now)) {
617 
618 		if ((ttm = boost::dynamic_pointer_cast<TimecodeTransportMaster> (tm))) {
619 			Timecode::TimecodeFormat fmt = ttm->apparent_timecode_format();
620 			format.set_text (timecode_format_name (fmt));
621 
622 			sample_to_timecode (pos, t, false, false,
623 					Timecode::timecode_to_frames_per_second (fmt),
624 					Timecode::timecode_has_drop_frames (fmt),
625 					AudioEngine::instance()->sample_rate(), 0, false, 0);
626 
627 		} else if ((mtm = boost::dynamic_pointer_cast<MIDIClock_TransportMaster> (tm))) {
628 			char buf[16];
629 			snprintf (buf, sizeof (buf), "%.1f BPM", mtm->bpm());
630 			buf[15] = '\0';
631 			format.set_text (buf);
632 			s->sample_to_timecode (pos, t, false, false);
633 		} else {
634 			format.set_text (" - ");
635 			s->sample_to_timecode (pos, t, false, false);
636 		}
637 
638 		current_str = Timecode::timecode_format_time (t);
639 
640 		delta_str = tm->delta_string ();
641 		save_when = when;
642 		save_last = current_str;
643 	} else {
644 		format.set_text ("   ?   ");
645 	}
646 
647 	if (save_when) {
648 		char gap[32];
649 		float seconds = (now - save_when) / (float) AudioEngine::instance()->sample_rate();
650 		if (seconds < 0) {
651 			seconds = 0;
652 		}
653 		if (seconds < 1.f) {
654 			snprintf (gap, sizeof (gap), "%3.02fs ago", seconds);
655 		} else if (seconds < 60.f) {
656 			snprintf (gap, sizeof (gap), "%3.0fs ago", seconds);
657 		} else if (seconds < 3600.f) {
658 			snprintf (gap, sizeof (gap), "%3.0fm ago", seconds / 60.f);
659 		} else {
660 			snprintf (gap, sizeof (gap), "%3.0fh ago", seconds/3600.f);
661 		}
662 		gap[31] = '\0';
663 		age_str = gap;
664 	}
665 
666 	last.set_text (string_compose (_("%1 %2"), save_last, age_str));
667 	current.set_text (string_compose ("%1  %2", current_str, delta_str));
668 }
669 
670 void
update(samplepos_t)671 TransportMastersWidget::update (samplepos_t /* audible */)
672 {
673 	samplepos_t now = AudioEngine::instance()->sample_time ();
674 
675 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
676 		(*r)->update (_session, now);
677 	}
678 }
679 
680 void
on_map()681 TransportMastersWidget::on_map ()
682 {
683 	update_connection = ARDOUR_UI::Clock.connect (sigc::mem_fun (*this, &TransportMastersWidget::update));
684 	Gtk::VBox::on_map ();
685 	update_ports ();
686 }
687 
688 void
on_unmap()689 TransportMastersWidget::on_unmap ()
690 {
691 	update_connection.disconnect ();
692 	Gtk::VBox::on_unmap ();
693 }
694 
TransportMastersWindow()695 TransportMastersWindow::TransportMastersWindow ()
696 	: ArdourWindow (_("Transport Masters"))
697 {
698 	add (w);
699 	w.show ();
700 }
701 
702 void
on_realize()703 TransportMastersWindow::on_realize ()
704 {
705 	ArdourWindow::on_realize ();
706 	/* (try to) ensure that resizing is possible and the window can be moved (and closed) */
707 	get_window()->set_decorations (Gdk::DECOR_BORDER | Gdk::DECOR_RESIZEH | Gdk::DECOR_TITLE | Gdk::DECOR_MENU);
708 }
709 
710 
711 void
set_session(ARDOUR::Session * s)712 TransportMastersWindow::set_session (ARDOUR::Session* s)
713 {
714 	ArdourWindow::set_session (s);
715 	w.set_session (s);
716 }
717 
718 void
set_session(ARDOUR::Session * s)719 TransportMastersWidget::set_session (ARDOUR::Session* s)
720 {
721 	session_config_connection.disconnect ();
722 
723 	SessionHandlePtr::set_session (s);
724 
725 	if (_session) {
726 		_session->config.ParameterChanged.connect (session_config_connection, invalidator (*this), boost::bind (&TransportMastersWidget::param_changed, this, _1), gui_context());
727 		allow_master_select (!_session->config.get_external_sync());
728 	}
729 }
730 
AddTransportMasterDialog()731 TransportMastersWidget::AddTransportMasterDialog::AddTransportMasterDialog ()
732 	: ArdourDialog (_("Add Transport Master"), true, false)
733 	, name_label (_("Name"))
734 	, type_label (_("Type"))
735 {
736 	name_hbox.set_spacing (6);
737 	name_hbox.pack_start (name_label, false, false);
738 	name_hbox.pack_start (name_entry, true, true);
739 
740 	type_hbox.set_spacing (6);
741 	type_hbox.pack_start (type_label, false, false);
742 	type_hbox.pack_start (type_combo, true, true);
743 
744 	vector<string> s;
745 
746 	s.push_back (X_("MTC"));
747 	s.push_back (X_("LTC"));
748 	s.push_back (X_("MIDI Clock"));
749 
750 	set_popdown_strings (type_combo, s);
751 	type_combo.set_active_text (X_("LTC"));
752 
753 	get_vbox()->pack_start (name_hbox, false, false);
754 	get_vbox()->pack_start (type_hbox, false, false);
755 
756 	add_button (_("Cancel"), RESPONSE_CANCEL);
757 	add_button (_("Add"), RESPONSE_ACCEPT);
758 
759 	name_entry.show ();
760 	type_combo.show ();
761 	name_label.show ();
762 	type_label.show ();
763 	name_hbox.show ();
764 	type_hbox.show ();
765 
766 	name_entry.signal_activate().connect (sigc::bind (sigc::mem_fun (*this, &Gtk::Dialog::response), Gtk::RESPONSE_ACCEPT));
767 }
768 
769 string
get_name() const770 TransportMastersWidget::AddTransportMasterDialog::get_name () const
771 {
772 	return name_entry.get_text ();
773 }
774 
775 SyncSource
get_type() const776 TransportMastersWidget::AddTransportMasterDialog::get_type() const
777 {
778 	string t = type_combo.get_active_text ();
779 
780 	if (t == X_("MTC")) {
781 		return MTC;
782 	} else if (t == X_("MIDI Clock")) {
783 		return MIDIClock;
784 	}
785 
786 	return LTC;
787 }
788 
789 void
lost_sync_changed()790 TransportMastersWidget::lost_sync_changed ()
791 {
792 	lost_sync_button.set_active (Config->get_transport_masters_just_roll_when_sync_lost());
793 }
794 
795 void
lost_sync_button_toggled()796 TransportMastersWidget::lost_sync_button_toggled ()
797 {
798 	bool active = lost_sync_button.get_active ();
799 	Config->set_transport_masters_just_roll_when_sync_lost (active);
800 }
801 
802 void
param_changed(string const & p)803 TransportMastersWidget::param_changed (string const & p)
804 {
805 	if (p == "transport-masters-just_roll-when-sync-lost") {
806 		lost_sync_changed ();
807 	} else if (p == "external-sync") {
808 		if (_session) {
809 			allow_master_select (!_session->config.get_external_sync());
810 		}
811 	}
812 }
813 
814 void
allow_master_select(bool yn)815 TransportMastersWidget::allow_master_select (bool yn)
816 {
817 	for (vector<Row*>::iterator r = rows.begin(); r != rows.end(); ++r) {
818 		(*r)->use_button.set_sensitive (yn);
819 	}
820 }
821