1 /*
2  * Copyright (C) 2007-2012 Carl Hetherington <carl@carlh.net>
3  * Copyright (C) 2008-2011 David Robillard <d@drobilla.net>
4  * Copyright (C) 2008-2016 Paul Davis <paul@linuxaudiosystems.com>
5  * Copyright (C) 2008 Hans Baier <hansfbaier@googlemail.com>
6  * Copyright (C) 2013-2015 Nick Mainsbridge <mainsbridge@gmail.com>
7  * Copyright (C) 2013-2019 Robin Gareus <robin@gareus.org>
8  *
9  * This program is free software; you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation; either version 2 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License along
20  * with this program; if not, write to the Free Software Foundation, Inc.,
21  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22  */
23 
24 #include <iostream>
25 
26 #include <gtkmm/scrolledwindow.h>
27 #include <gtkmm/adjustment.h>
28 #include <gtkmm/label.h>
29 #include <gtkmm/menu.h>
30 #include <gtkmm/menushell.h>
31 #include <gtkmm/menu_elems.h>
32 #include <gtkmm/window.h>
33 #include <gtkmm/stock.h>
34 
35 #include "ardour/bundle.h"
36 #include "ardour/types.h"
37 #include "ardour/session.h"
38 #include "ardour/route.h"
39 #include "ardour/audioengine.h"
40 
41 #include "gtkmm2ext/utils.h"
42 
43 #include "ardour_dialog.h"
44 #include "ardour_message.h"
45 #include "gui_thread.h"
46 #include "port_matrix.h"
47 #include "port_matrix_body.h"
48 #include "port_matrix_component.h"
49 #include "utils.h"
50 #include "ui_config.h"
51 
52 #include "pbd/i18n.h"
53 
54 using namespace std;
55 using namespace Gtk;
56 using namespace ARDOUR;
57 using namespace ARDOUR_UI_UTILS;
58 
59 /** PortMatrix constructor.
60  *  @param session Our session.
61  *  @param type Port type that we are handling.
62  */
PortMatrix(Window * parent,Session * session,DataType type)63 PortMatrix::PortMatrix (Window* parent, Session* session, DataType type)
64 	: Table (4, 4)
65 	, _parent (parent)
66 	, _type (type)
67 	, _menu (0)
68 	, _arrangement (TOP_TO_RIGHT)
69 	, _row_index (0)
70 	, _column_index (1)
71 	, _min_height_divisor (1)
72 	, _show_only_bundles (false)
73 	, _inhibit_toggle_show_only_bundles (false)
74 	, _ignore_notebook_page_selected (false)
75 {
76 	set_session (session);
77 
78 	_body = new PortMatrixBody (this);
79 	_body->DimensionsChanged.connect (sigc::mem_fun (*this, &PortMatrix::body_dimensions_changed));
80 
81 	_hbox.pack_end (_hspacer, true, true);
82 	_hbox.pack_end (_hnotebook, false, false);
83 	_hbox.pack_end (_hlabel, false, false);
84 
85 	_vnotebook.signal_switch_page().connect (sigc::mem_fun (*this, &PortMatrix::notebook_page_selected));
86 	_vnotebook.property_tab_border() = 4;
87 	_vnotebook.set_name (X_("PortMatrixLabel"));
88 	_hnotebook.signal_switch_page().connect (sigc::mem_fun (*this, &PortMatrix::notebook_page_selected));
89 	_hnotebook.property_tab_border() = 4;
90 	_hnotebook.set_name (X_("PortMatrixLabel"));
91 
92 	_vlabel.set_use_markup ();
93 	_vlabel.set_alignment (1, 1);
94 	_vlabel.set_padding (4, 16);
95 	_vlabel.set_name (X_("PortMatrixLabel"));
96 	_hlabel.set_use_markup ();
97 	_hlabel.set_alignment (1, 0.5);
98 	_hlabel.set_padding (16, 4);
99 	_hlabel.set_name (X_("PortMatrixLabel"));
100 
101 	set_row_spacing (0, 8);
102 	set_col_spacing (0, 8);
103 	set_row_spacing (2, 8);
104 	set_col_spacing (2, 8);
105 
106 	_body->show ();
107 	_vbox.show ();
108 	_hbox.show ();
109 	_vscroll.show ();
110 	_hscroll.show ();
111 	_vlabel.show ();
112 	_hlabel.show ();
113 	_hspacer.show ();
114 	_vspacer.show ();
115 	_vnotebook.show ();
116 	_hnotebook.show ();
117 
118 	UIConfiguration::instance().ParameterChanged.connect (sigc::mem_fun (*this, &PortMatrix::parameter_changed));
119 }
120 
~PortMatrix()121 PortMatrix::~PortMatrix ()
122 {
123 	delete _body;
124 	delete _menu;
125 }
126 
127 /** Perform initial and once-only setup.  This must be called by
128  *  subclasses after they have set up _ports[] to at least some
129  *  reasonable extent.  Two-part initialisation is necessary because
130  *  setting up _ports is largely done by virtual functions in
131  *  subclasses.
132  */
133 
134 void
init()135 PortMatrix::init ()
136 {
137 	select_arrangement ();
138 
139 	/* Signal handling is kind of split into three parts:
140 	 *
141 	 * 1.  When _ports[] changes, we call setup().  This essentially sorts out our visual
142 	 *     representation of the information in _ports[].
143 	 *
144 	 * 2.  When certain other things change, we need to get our subclass to clear and
145 	 *     re-fill _ports[], which in turn causes appropriate signals to be raised to
146 	 *     hook into part (1).
147 	 *
148 	 * 3.  Assorted other signals.
149 	 */
150 
151 
152 	/* Part 1: the basic _ports[] change -> reset visuals */
153 
154 	for (int i = 0; i < 2; ++i) {
155 		/* watch for the content of _ports[] changing */
156 		_ports[i].Changed.connect (_changed_connections, invalidator (*this), boost::bind (&PortMatrix::setup, this), gui_context());
157 
158 		/* and for bundles in _ports[] changing */
159 		_ports[i].BundleChanged.connect (_bundle_changed_connections, invalidator (*this), boost::bind (&PortMatrix::setup, this), gui_context());
160 	}
161 
162 	/* Part 2: notice when things have changed that require our subclass to clear and refill _ports[] */
163 
164 	/* watch for routes being added or removed */
165 	_session->RouteAdded.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::routes_changed, this), gui_context());
166 
167 	/* and also bundles */
168 	_session->BundleAddedOrRemoved.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::setup_global_ports, this), gui_context());
169 
170 	/* and also ports */
171 	_session->engine().PortRegisteredOrUnregistered.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::setup_global_ports, this), gui_context());
172 
173 	_session->engine().PortPrettyNameChanged.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::setup_all_ports, this), gui_context());
174 
175 	/* watch for route order keys changing, which changes the order of things in our global ports list(s) */
176 	PresentationInfo::Change.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::setup_global_ports_proxy, this), gui_context());
177 
178 	/* Part 3: other stuff */
179 
180 	_session->engine().PortConnectedOrDisconnected.connect (_session_connections, invalidator (*this), boost::bind (&PortMatrix::port_connected_or_disconnected, this), gui_context ());
181 
182 	_hscroll.signal_value_changed().connect (sigc::mem_fun (*this, &PortMatrix::hscroll_changed));
183 	_vscroll.signal_value_changed().connect (sigc::mem_fun (*this, &PortMatrix::vscroll_changed));
184 
185 	reconnect_to_routes ();
186 
187 	setup ();
188 }
189 
190 /** Disconnect from and reconnect to routes' signals that we need to watch for things that affect the matrix */
191 void
reconnect_to_routes()192 PortMatrix::reconnect_to_routes ()
193 {
194 	_route_connections.drop_connections ();
195 
196 	boost::shared_ptr<RouteList> routes = _session->get_routes ();
197 	for (RouteList::iterator i = routes->begin(); i != routes->end(); ++i) {
198 		(*i)->processors_changed.connect (_route_connections, invalidator (*this), boost::bind (&PortMatrix::route_processors_changed, this, _1), gui_context());
199 		(*i)->DropReferences.connect (_route_connections, invalidator (*this), boost::bind (&PortMatrix::routes_changed, this), gui_context());
200 	}
201 }
202 
203 void
route_processors_changed(RouteProcessorChange c)204 PortMatrix::route_processors_changed (RouteProcessorChange c)
205 {
206 	if (c.type == RouteProcessorChange::MeterPointChange) {
207 		/* this change has no impact on the port matrix */
208 		return;
209 	}
210 
211 	setup_global_ports ();
212 }
213 
214 /** A route has been added to or removed from the session */
215 void
routes_changed()216 PortMatrix::routes_changed ()
217 {
218 	if (!_session) return;
219 	reconnect_to_routes ();
220 	setup_global_ports ();
221 }
222 
223 /** Set up everything that depends on the content of _ports[] */
224 void
setup()225 PortMatrix::setup ()
226 {
227 	if (!_session) {
228 		_route_connections.drop_connections ();
229 		return; // session went away
230 	}
231 
232 	/* this needs to be done first, as the visible_ports() method uses the
233 	   notebook state to decide which ports are being shown */
234 
235 	setup_notebooks ();
236 
237 	_body->setup ();
238 	setup_scrollbars ();
239 	update_tab_highlighting ();
240 	queue_draw ();
241 }
242 
243 void
set_type(DataType t)244 PortMatrix::set_type (DataType t)
245 {
246 	_type = t;
247 }
248 
249 void
hscroll_changed()250 PortMatrix::hscroll_changed ()
251 {
252 	_body->set_xoffset (_hscroll.get_adjustment()->get_value());
253 }
254 
255 void
vscroll_changed()256 PortMatrix::vscroll_changed ()
257 {
258 	_body->set_yoffset (_vscroll.get_adjustment()->get_value());
259 }
260 
261 void
setup_scrollbars()262 PortMatrix::setup_scrollbars ()
263 {
264 	Adjustment* a = _hscroll.get_adjustment ();
265 	a->set_lower (0);
266 	a->set_page_size (_body->alloc_scroll_width());
267 	a->set_step_increment (32);
268 	a->set_page_increment (128);
269 
270 	/* Set the adjustment to zero if the size has changed.*/
271 	if (a->get_upper() != _body->full_scroll_width()) {
272 		a->set_upper (_body->full_scroll_width());
273 		a->set_value (0);
274 	}
275 
276 	a = _vscroll.get_adjustment ();
277 	a->set_lower (0);
278 	a->set_page_size (_body->alloc_scroll_height());
279 	a->set_step_increment (32);
280 	a->set_page_increment (128);
281 
282 	if (a->get_upper() != _body->full_scroll_height()) {
283 		a->set_upper (_body->full_scroll_height());
284 		a->set_value (0);
285 	}
286 }
287 
288 /** Disassociate all of our ports from each other */
289 void
disassociate_all()290 PortMatrix::disassociate_all ()
291 {
292 	PortGroup::BundleList a = _ports[0].bundles ();
293 	PortGroup::BundleList b = _ports[1].bundles ();
294 
295 	for (PortGroup::BundleList::iterator i = a.begin(); i != a.end(); ++i) {
296 		for (uint32_t j = 0; j < (*i)->bundle->nchannels().n_total(); ++j) {
297 			for (PortGroup::BundleList::iterator k = b.begin(); k != b.end(); ++k) {
298 				for (uint32_t l = 0; l < (*k)->bundle->nchannels().n_total(); ++l) {
299 
300 					if (!should_show ((*i)->bundle->channel_type(j)) || !should_show ((*k)->bundle->channel_type(l))) {
301 						continue;
302 					}
303 
304 					BundleChannel c[2] = {
305 						BundleChannel ((*i)->bundle, j),
306 						BundleChannel ((*k)->bundle, l)
307 							};
308 
309 					if (get_state (c) == PortMatrixNode::ASSOCIATED) {
310 						set_state (c, false);
311 					}
312 
313 				}
314 			}
315 		}
316 	}
317 
318 	_body->rebuild_and_draw_grid ();
319 }
320 
321 /* Decide how to arrange the components of the matrix */
322 void
select_arrangement()323 PortMatrix::select_arrangement ()
324 {
325 	uint32_t const N[2] = {
326 		count_of_our_type_min_1 (_ports[0].total_channels()),
327 		count_of_our_type_min_1 (_ports[1].total_channels())
328 	};
329 
330 	/* XXX: shirley there's an easier way than this */
331 
332 	if (_vspacer.get_parent()) {
333 		_vbox.remove (_vspacer);
334 	}
335 
336 	if (_vnotebook.get_parent()) {
337 		_vbox.remove (_vnotebook);
338 	}
339 
340 	if (_vlabel.get_parent()) {
341 		_vbox.remove (_vlabel);
342 	}
343 
344 	/* The list with the most channels goes on left or right, so that the most channel
345 	   names are printed horizontally and hence more readable.  However we also
346 	   maintain notional `signal flow' vaguely from left to right.  Subclasses
347 	   should choose where to put ports based on signal flowing from _ports[0]
348 	   to _ports[1] */
349 
350 	if (N[0] > N[1]) {
351 
352 		_row_index = 0;
353 		_column_index = 1;
354 		_arrangement = LEFT_TO_BOTTOM;
355 		_vlabel.set_label (_("<b>Sources</b>"));
356 		_hlabel.set_label (_("<b>Destinations</b>"));
357 		_vlabel.set_angle (90);
358 
359 		_vbox.pack_end (_vlabel, false, false);
360 		_vbox.pack_end (_vnotebook, false, false);
361 		_vbox.pack_end (_vspacer, true, true);
362 
363 #define REMOVE_FROM_GTK_PARENT(WGT) if ((WGT).get_parent()) { (WGT).get_parent()->remove(WGT);}
364 		REMOVE_FROM_GTK_PARENT(*_body)
365 		REMOVE_FROM_GTK_PARENT(_vscroll)
366 		REMOVE_FROM_GTK_PARENT(_hscroll)
367 		REMOVE_FROM_GTK_PARENT(_vbox)
368 		REMOVE_FROM_GTK_PARENT(_hbox)
369 
370 		attach (*_body, 2, 3, 1, 2, FILL | EXPAND, FILL | EXPAND);
371 		attach (_vscroll, 3, 4, 1, 2, SHRINK);
372 		attach (_hscroll, 2, 3, 3, 4, FILL | EXPAND, SHRINK);
373 		attach (_vbox, 1, 2, 1, 2, SHRINK);
374 		attach (_hbox, 2, 3, 2, 3, FILL | EXPAND, SHRINK);
375 
376 	} else {
377 
378 		_row_index = 1;
379 		_column_index = 0;
380 		_arrangement = TOP_TO_RIGHT;
381 		_hlabel.set_label (_("<b>Sources</b>"));
382 		_vlabel.set_label (_("<b>Destinations</b>"));
383 		_vlabel.set_angle (-90);
384 
385 		_vbox.pack_end (_vspacer, true, true);
386 		_vbox.pack_end (_vnotebook, false, false);
387 		_vbox.pack_end (_vlabel, false, false);
388 
389 		REMOVE_FROM_GTK_PARENT(*_body)
390 		REMOVE_FROM_GTK_PARENT(_vscroll)
391 		REMOVE_FROM_GTK_PARENT(_hscroll)
392 		REMOVE_FROM_GTK_PARENT(_vbox)
393 		REMOVE_FROM_GTK_PARENT(_hbox)
394 
395 		attach (*_body, 1, 2, 2, 3, FILL | EXPAND, FILL | EXPAND);
396 		attach (_vscroll, 3, 4, 2, 3, SHRINK);
397 		attach (_hscroll, 1, 2, 3, 4, FILL | EXPAND, SHRINK);
398 		attach (_vbox, 2, 3, 2, 3, SHRINK);
399 		attach (_hbox, 1, 2, 1, 2, FILL | EXPAND, SHRINK);
400 	}
401 }
402 
403 /** @return columns list */
404 PortGroupList const *
columns() const405 PortMatrix::columns () const
406 {
407 	return &_ports[_column_index];
408 }
409 
410 boost::shared_ptr<const PortGroup>
visible_columns() const411 PortMatrix::visible_columns () const
412 {
413 	return visible_ports (_column_index);
414 }
415 
416 /* @return rows list */
417 PortGroupList const *
rows() const418 PortMatrix::rows () const
419 {
420 	return &_ports[_row_index];
421 }
422 
423 boost::shared_ptr<const PortGroup>
visible_rows() const424 PortMatrix::visible_rows () const
425 {
426 	return visible_ports (_row_index);
427 }
428 
429 /** @param column Column; its bundle may be 0 if we are over a row heading.
430  *  @param row Row; its bundle may be 0 if we are over a column heading.
431  */
432 void
popup_menu(BundleChannel column,BundleChannel row,uint32_t t)433 PortMatrix::popup_menu (BundleChannel column, BundleChannel row, uint32_t t)
434 {
435 	using namespace Menu_Helpers;
436 
437 	delete _menu;
438 
439 	_menu = new Menu;
440 	_menu->set_name ("ArdourContextMenu");
441 
442 	MenuList& items = _menu->items ();
443 
444 	BundleChannel bc[2];
445 	bc[_column_index] = column;
446 	bc[_row_index] = row;
447 
448 	char buf [64];
449 	bool need_separator = false;
450 
451 	for (int dim = 0; dim < 2; ++dim) {
452 
453 		if (bc[dim].bundle) {
454 
455 			Menu* m = manage (new Menu);
456 			MenuList& sub = m->items ();
457 
458 			boost::weak_ptr<Bundle> w (bc[dim].bundle);
459 
460 			if (can_add_channels (bc[dim].bundle)) {
461 				/* Start off with options for the `natural' port type */
462 				for (DataType::iterator i = DataType::begin(); i != DataType::end(); ++i) {
463 					if (should_show (*i) && can_add_port_proxy (w, *i)) {
464 						snprintf (buf, sizeof (buf), _("Add %s %s"), (*i).to_i18n_string(), channel_noun().c_str());
465 						sub.push_back (MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::add_channel_proxy), w, *i)));
466 					}
467 				}
468 
469 				/* Now add other ones */
470 				for (DataType::iterator i = DataType::begin(); i != DataType::end(); ++i) {
471 					if (!should_show (*i) && can_add_port_proxy (w, *i)) {
472 						snprintf (buf, sizeof (buf), _("Add %s %s"), (*i).to_i18n_string(), channel_noun().c_str());
473 						sub.push_back (MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::add_channel_proxy), w, *i)));
474 					}
475 				}
476 			}
477 
478 			if (can_rename_channels (bc[dim].bundle) && bc[dim].channel != -1) {
479 				snprintf (
480 					buf, sizeof (buf), _("Rename '%s'..."),
481 					escape_underscores (bc[dim].bundle->channel_name (bc[dim].channel)).c_str()
482 					);
483 				sub.push_back (
484 					MenuElem (
485 						buf,
486 						sigc::bind (sigc::mem_fun (*this, &PortMatrix::rename_channel_proxy), w, bc[dim].channel)
487 						)
488 					);
489 			}
490 
491 			if (can_remove_channels (bc[dim].bundle) && bc[dim].bundle->nchannels() != ARDOUR::ChanCount::ZERO) {
492 				if (bc[dim].channel != -1) {
493 					add_remove_option (sub, w, bc[dim].channel);
494 				} else {
495 					sub.push_back (
496 						MenuElem (_("Remove all"), sigc::bind (sigc::mem_fun (*this, &PortMatrix::remove_all_channels), w))
497 						);
498 
499 					if (bc[dim].bundle->nchannels().n_total() > 1) {
500 						for (uint32_t i = 0; i < bc[dim].bundle->nchannels().n_total(); ++i) {
501 							if (should_show (bc[dim].bundle->channel_type(i))) {
502 								add_remove_option (sub, w, i);
503 							}
504 						}
505 					}
506 				}
507 			}
508 
509 			uint32_t c = count_of_our_type (bc[dim].bundle->nchannels ());
510 			if ((_show_only_bundles && c > 0) || c == 1) {
511 
512 				/* we're looking just at bundles, or our bundle has only one channel, so just offer
513 				   to disassociate all on the bundle.
514 				*/
515 
516 				snprintf (buf, sizeof (buf), _("%s all"), disassociation_verb().c_str());
517 				sub.push_back (
518 					MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::disassociate_all_on_bundle), w, dim))
519 					);
520 
521 			} else if (c != 0) {
522 
523 				if (bc[dim].channel != -1) {
524 					/* specific channel under the menu, so just offer to disassociate that */
525 					add_disassociate_option (sub, w, dim, bc[dim].channel);
526 				} else {
527 					/* no specific channel; offer to disassociate all, or any one in particular */
528 					snprintf (buf, sizeof (buf), _("%s all"), disassociation_verb().c_str());
529 					sub.push_back (
530 						MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::disassociate_all_on_bundle), w, dim))
531 						);
532 
533 					for (uint32_t i = 0; i < bc[dim].bundle->nchannels().n_total(); ++i) {
534 						if (should_show (bc[dim].bundle->channel_type(i))) {
535 							add_disassociate_option (sub, w, dim, i);
536 						}
537 					}
538 				}
539 			}
540 
541 			items.push_back (MenuElem (escape_underscores (bc[dim].bundle->name()).c_str(), *m));
542 			need_separator = true;
543 		}
544 
545 	}
546 
547 	if (need_separator) {
548 		items.push_back (SeparatorElem ());
549 	}
550 
551 	items.push_back (MenuElem (_("Rescan"), sigc::mem_fun (*this, &PortMatrix::setup_all_ports)));
552 
553 	items.push_back (CheckMenuElem (_("Show individual ports"), sigc::mem_fun (*this, &PortMatrix::toggle_show_only_bundles)));
554 	Gtk::CheckMenuItem* i = dynamic_cast<Gtk::CheckMenuItem*> (&items.back());
555 	_inhibit_toggle_show_only_bundles = true;
556 	i->set_active (!_show_only_bundles);
557 	_inhibit_toggle_show_only_bundles = false;
558 
559 	items.push_back (MenuElem (_("Flip"), sigc::mem_fun (*this, &PortMatrix::flip)));
560 	items.back().set_sensitive (can_flip ());
561 
562 	_menu->popup (1, t);
563 }
564 
565 void
remove_channel_proxy(boost::weak_ptr<Bundle> b,uint32_t c)566 PortMatrix::remove_channel_proxy (boost::weak_ptr<Bundle> b, uint32_t c)
567 {
568 	boost::shared_ptr<Bundle> sb = b.lock ();
569 	if (!sb) {
570 		return;
571 	}
572 
573 	remove_channel (BundleChannel (sb, c));
574 
575 }
576 
577 void
rename_channel_proxy(boost::weak_ptr<Bundle> b,uint32_t c)578 PortMatrix::rename_channel_proxy (boost::weak_ptr<Bundle> b, uint32_t c)
579 {
580 	boost::shared_ptr<Bundle> sb = b.lock ();
581 	if (!sb) {
582 		return;
583 	}
584 
585 	rename_channel (BundleChannel (sb, c));
586 }
587 
588 void
disassociate_all_on_bundle(boost::weak_ptr<Bundle> bundle,int dim)589 PortMatrix::disassociate_all_on_bundle (boost::weak_ptr<Bundle> bundle, int dim)
590 {
591 	boost::shared_ptr<Bundle> sb = bundle.lock ();
592 	if (!sb) {
593 		return;
594 	}
595 
596 	for (uint32_t i = 0; i < sb->nchannels().n_total(); ++i) {
597 		if (should_show (sb->channel_type(i))) {
598 			disassociate_all_on_channel (bundle, i, dim);
599 		}
600 	}
601 }
602 
603 void
disassociate_all_on_channel(boost::weak_ptr<Bundle> bundle,uint32_t channel,int dim)604 PortMatrix::disassociate_all_on_channel (boost::weak_ptr<Bundle> bundle, uint32_t channel, int dim)
605 {
606 	boost::shared_ptr<Bundle> sb = bundle.lock ();
607 	if (!sb) {
608 		return;
609 	}
610 
611 	PortGroup::BundleList a = _ports[1-dim].bundles ();
612 
613 	for (PortGroup::BundleList::iterator i = a.begin(); i != a.end(); ++i) {
614 		for (uint32_t j = 0; j < (*i)->bundle->nchannels().n_total(); ++j) {
615 
616 			if (!should_show ((*i)->bundle->channel_type(j))) {
617 				continue;
618 			}
619 
620 			BundleChannel c[2];
621 			c[dim] = BundleChannel (sb, channel);
622 			c[1-dim] = BundleChannel ((*i)->bundle, j);
623 
624 			if (get_state (c) == PortMatrixNode::ASSOCIATED) {
625 				set_state (c, false);
626 			}
627 		}
628 	}
629 
630 	_body->rebuild_and_draw_grid ();
631 }
632 
633 void
setup_global_ports()634 PortMatrix::setup_global_ports ()
635 {
636 	if (!_session || _session->deletion_in_progress()) return;
637 	ENSURE_GUI_THREAD (*this, &PortMatrix::setup_global_ports)
638 
639 	for (int i = 0; i < 2; ++i) {
640 		if (list_is_global (i)) {
641 			setup_ports (i);
642 		}
643 	}
644 }
645 
646 void
setup_global_ports_proxy()647 PortMatrix::setup_global_ports_proxy ()
648 {
649 	/* Avoid a deadlock by calling this in an idle handler: see IOSelector::io_changed_proxy
650 	   for a discussion.
651 	*/
652 
653 	Glib::signal_idle().connect_once (sigc::mem_fun (*this, &PortMatrix::setup_global_ports));
654 }
655 
656 void
setup_all_ports()657 PortMatrix::setup_all_ports ()
658 {
659 	if (_session->deletion_in_progress()) {
660 		return;
661 	}
662 
663 	ENSURE_GUI_THREAD (*this, &PortMatrix::setup_all_ports)
664 
665 	setup_ports (0);
666 	setup_ports (1);
667 }
668 
669 void
toggle_show_only_bundles()670 PortMatrix::toggle_show_only_bundles ()
671 {
672 	if (_inhibit_toggle_show_only_bundles) {
673 		return;
674 	}
675 
676 	_show_only_bundles = !_show_only_bundles;
677 
678 	setup ();
679 
680 	/* The way in which hardware ports are grouped changes depending on the _show_only_bundles
681 	   setting, so we need to set things up again now.
682 	*/
683 	setup_all_ports ();
684 }
685 
686 pair<uint32_t, uint32_t>
max_size() const687 PortMatrix::max_size () const
688 {
689 	pair<uint32_t, uint32_t> m = _body->max_size ();
690 
691 	m.first += _vscroll.get_width () + _vbox.get_width () + 4;
692 	m.second += _hscroll.get_height () + _hbox.get_height () + 4;
693 
694 	return m;
695 }
696 
697 bool
on_scroll_event(GdkEventScroll * ev)698 PortMatrix::on_scroll_event (GdkEventScroll* ev)
699 {
700 	double const h = _hscroll.get_value ();
701 	double const v = _vscroll.get_value ();
702 
703 	switch (ev->direction) {
704 	case GDK_SCROLL_UP:
705 		_vscroll.set_value (v - PortMatrixComponent::grid_spacing ());
706 		break;
707 	case GDK_SCROLL_DOWN:
708 		_vscroll.set_value (v + PortMatrixComponent::grid_spacing ());
709 		break;
710 	case GDK_SCROLL_LEFT:
711 		_hscroll.set_value (h - PortMatrixComponent::grid_spacing ());
712 		break;
713 	case GDK_SCROLL_RIGHT:
714 		_hscroll.set_value (h + PortMatrixComponent::grid_spacing ());
715 		break;
716 	}
717 
718 	return true;
719 }
720 
721 boost::shared_ptr<IO>
io_from_bundle(boost::shared_ptr<Bundle> b) const722 PortMatrix::io_from_bundle (boost::shared_ptr<Bundle> b) const
723 {
724 	boost::shared_ptr<IO> io = _ports[0].io_from_bundle (b);
725 	if (!io) {
726 		io = _ports[1].io_from_bundle (b);
727 	}
728 
729 	return io;
730 }
731 
732 bool
can_add_channels(boost::shared_ptr<Bundle> b) const733 PortMatrix::can_add_channels (boost::shared_ptr<Bundle> b) const
734 {
735 	return io_from_bundle (b) != 0;
736 }
737 
738 bool
can_add_port(boost::shared_ptr<Bundle> b,DataType t) const739 PortMatrix::can_add_port (boost::shared_ptr<Bundle> b, DataType t) const
740 {
741 	boost::shared_ptr<IO> io = io_from_bundle (b);
742 	return io && io->can_add_port (t);
743 }
744 
745 void
add_channel(boost::shared_ptr<Bundle> b,DataType t)746 PortMatrix::add_channel (boost::shared_ptr<Bundle> b, DataType t)
747 {
748 	boost::shared_ptr<IO> io = io_from_bundle (b);
749 
750 	if (io) {
751 		int const r = io->add_port ("", this, t);
752 		if (r == -1) {
753 			ArdourMessageDialog msg (_("It is not possible to add a port here."));
754 			msg.set_title (_("Cannot add port"));
755 			msg.run ();
756 		}
757 	}
758 }
759 
760 bool
can_remove_channels(boost::shared_ptr<Bundle> b) const761 PortMatrix::can_remove_channels (boost::shared_ptr<Bundle> b) const
762 {
763 	return io_from_bundle (b) != 0;
764 }
765 
766 void
remove_channel(ARDOUR::BundleChannel b)767 PortMatrix::remove_channel (ARDOUR::BundleChannel b)
768 {
769 	std::string errmsg;
770 	boost::shared_ptr<IO> io = io_from_bundle (b.bundle);
771 	boost::shared_ptr<Port> p = io->nth (b.channel);
772 
773 	if (!io || !p) {
774 		return;
775 	}
776 
777 	if (io->n_ports ().n_total () == 1) {
778 		errmsg = _("The last port cannot be removed");
779 	} else {
780 		if (-1 == io->remove_port (p, this)) {
781 			errmsg = _("This port cannot be removed.");
782 		}
783 	}
784 
785 	if (!errmsg.empty ()) {
786 		ArdourDialog d (_("Port removal not allowed"));
787 		Label l (errmsg);
788 		d.get_vbox()->pack_start (l);
789 		d.add_button (Stock::OK, RESPONSE_ACCEPT);
790 		d.set_modal (true);
791 		d.show_all ();
792 		d.run ();
793 	}
794 }
795 
796 void
remove_all_channels(boost::weak_ptr<Bundle> w)797 PortMatrix::remove_all_channels (boost::weak_ptr<Bundle> w)
798 {
799 	boost::shared_ptr<Bundle> b = w.lock ();
800 	if (!b) {
801 		return;
802 	}
803 
804 	/* Remove channels backwards so that we don't renumber channels
805 	   that we are about to remove.
806 	*/
807 	for (int i = (b->nchannels().n_total() - 1); i >= 0; --i) {
808 		if (should_show (b->channel_type(i))) {
809 			remove_channel (ARDOUR::BundleChannel (b, i));
810 		}
811 	}
812 }
813 
814 bool
can_add_port_proxy(boost::weak_ptr<Bundle> w,DataType t) const815 PortMatrix::can_add_port_proxy (boost::weak_ptr<Bundle> w, DataType t) const
816 {
817 	boost::shared_ptr<Bundle> b = w.lock ();
818 	if (!b) {
819 		return false;
820 	}
821 	return can_add_port (b, t);
822 }
823 
824 void
add_channel_proxy(boost::weak_ptr<Bundle> w,DataType t)825 PortMatrix::add_channel_proxy (boost::weak_ptr<Bundle> w, DataType t)
826 {
827 	boost::shared_ptr<Bundle> b = w.lock ();
828 	if (!b) {
829 		return;
830 	}
831 
832 	add_channel (b, t);
833 }
834 
835 void
setup_notebooks()836 PortMatrix::setup_notebooks ()
837 {
838 	int const h_current_page = _hnotebook.get_current_page ();
839 	int const v_current_page = _vnotebook.get_current_page ();
840 
841 	/* for some reason best known to GTK, erroneous switch_page signals seem to be generated
842 	   when adding or removing pages to or from notebooks, so ignore them */
843 
844 	_ignore_notebook_page_selected = true;
845 
846 	remove_notebook_pages (_hnotebook);
847 	remove_notebook_pages (_vnotebook);
848 
849 	for (PortGroupList::List::const_iterator i = _ports[_row_index].begin(); i != _ports[_row_index].end(); ++i) {
850 		HBox* dummy = manage (new HBox);
851 		dummy->show ();
852 		Label* label = manage (new Label ((*i)->name));
853 		label->set_angle (_arrangement == LEFT_TO_BOTTOM ? 90 : -90);
854 		label->set_use_markup ();
855 		label->show ();
856 		if (_arrangement == LEFT_TO_BOTTOM) {
857 			_vnotebook.prepend_page (*dummy, *label);
858 		} else {
859 			/* Reverse the order of vertical tabs when they are on the right hand side
860 			   so that from top to bottom it is the same order as that from left to right
861 			   for the top tabs.
862 			*/
863 			_vnotebook.append_page (*dummy, *label);
864 		}
865 	}
866 
867 	for (PortGroupList::List::const_iterator i = _ports[_column_index].begin(); i != _ports[_column_index].end(); ++i) {
868 		HBox* dummy = manage (new HBox);
869 		dummy->show ();
870 		Label* label = manage (new Label ((*i)->name));
871 		label->set_use_markup ();
872 		label->show ();
873 		_hnotebook.append_page (*dummy, *label);
874 	}
875 
876 	_ignore_notebook_page_selected = false;
877 
878 	if (_arrangement == TOP_TO_RIGHT) {
879 		_vnotebook.set_tab_pos (POS_RIGHT);
880 		_hnotebook.set_tab_pos (POS_TOP);
881 	} else {
882 		_vnotebook.set_tab_pos (POS_LEFT);
883 		_hnotebook.set_tab_pos (POS_BOTTOM);
884 	}
885 
886 	if (h_current_page != -1 && _hnotebook.get_n_pages() > h_current_page) {
887 		_hnotebook.set_current_page (h_current_page);
888 	} else {
889 		_hnotebook.set_current_page (0);
890 	}
891 
892 	if (v_current_page != -1 && _vnotebook.get_n_pages() > v_current_page) {
893 		_vnotebook.set_current_page (v_current_page);
894 	} else {
895 		_vnotebook.set_current_page (0);
896 	}
897 
898 	if (_hnotebook.get_n_pages() <= 1) {
899 		_hbox.hide ();
900 	} else {
901 		_hbox.show ();
902 	}
903 
904 	if (_vnotebook.get_n_pages() <= 1) {
905 		_vbox.hide ();
906 	} else {
907 		_vbox.show ();
908 	}
909 }
910 
911 void
remove_notebook_pages(Notebook & n)912 PortMatrix::remove_notebook_pages (Notebook& n)
913 {
914 	int const N = n.get_n_pages ();
915 
916 	for (int i = 0; i < N; ++i) {
917 		n.remove_page ();
918 	}
919 }
920 
921 void
notebook_page_selected(GtkNotebookPage *,guint)922 PortMatrix::notebook_page_selected (GtkNotebookPage *, guint)
923 {
924 	if (_ignore_notebook_page_selected) {
925 		return;
926 	}
927 
928 	_body->setup ();
929 	setup_scrollbars ();
930 	queue_draw ();
931 }
932 
933 void
session_going_away()934 PortMatrix::session_going_away ()
935 {
936 	_session = 0;
937 }
938 
939 void
body_dimensions_changed()940 PortMatrix::body_dimensions_changed ()
941 {
942 	_hspacer.set_size_request (_body->column_labels_border_x (), -1);
943 	if (_arrangement == TOP_TO_RIGHT) {
944 		_vspacer.set_size_request (-1, _body->column_labels_height ());
945 		_vspacer.show ();
946 	} else {
947 		_vspacer.hide ();
948 	}
949 
950 	int curr_width;
951 	int curr_height;
952 	_parent->get_size (curr_width, curr_height);
953 
954 	pair<uint32_t, uint32_t> m = max_size ();
955 
956 	/* Don't shrink the window */
957 	m.first = max (int (m.first), curr_width);
958 	m.second = max (int (m.second), curr_height);
959 
960 	resize_window_to_proportion_of_monitor (_parent, m.first, m.second);
961 }
962 
963 /** @return The PortGroup that is currently visible (ie selected by
964  *  the notebook) along a given axis.
965  */
966 boost::shared_ptr<const PortGroup>
visible_ports(int d) const967 PortMatrix::visible_ports (int d) const
968 {
969 	PortGroupList const & p = _ports[d];
970 	PortGroupList::List::const_iterator j = p.begin ();
971 
972 	/* The logic to compute the index here is a bit twisty because for
973 	   the TOP_TO_RIGHT arrangement we reverse the order of the vertical
974 	   tabs in setup_notebooks ().
975 	*/
976 
977 	int n = 0;
978 	if (d == _row_index) {
979 		if (_arrangement == LEFT_TO_BOTTOM) {
980 			n = p.size() - _vnotebook.get_current_page () - 1;
981 		} else {
982 			n = _vnotebook.get_current_page ();
983 		}
984 	} else {
985 		n = _hnotebook.get_current_page ();
986 	}
987 
988 	int i = 0;
989 	while (i != int (n) && j != p.end ()) {
990 		++i;
991 		++j;
992 	}
993 
994 	if (j == p.end()) {
995 		return boost::shared_ptr<const PortGroup> ();
996 	}
997 
998 	return *j;
999 }
1000 
1001 void
add_remove_option(Menu_Helpers::MenuList & m,boost::weak_ptr<Bundle> w,int c)1002 PortMatrix::add_remove_option (Menu_Helpers::MenuList& m, boost::weak_ptr<Bundle> w, int c)
1003 {
1004 	using namespace Menu_Helpers;
1005 
1006 	boost::shared_ptr<Bundle> b = w.lock ();
1007 	if (!b) {
1008 		return;
1009 	}
1010 
1011 	char buf [64];
1012 	snprintf (buf, sizeof (buf), _("Remove '%s'"), escape_underscores (b->channel_name (c)).c_str());
1013 	m.push_back (MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::remove_channel_proxy), w, c)));
1014 }
1015 
1016 void
add_disassociate_option(Menu_Helpers::MenuList & m,boost::weak_ptr<Bundle> w,int d,int c)1017 PortMatrix::add_disassociate_option (Menu_Helpers::MenuList& m, boost::weak_ptr<Bundle> w, int d, int c)
1018 {
1019 	using namespace Menu_Helpers;
1020 
1021 	boost::shared_ptr<Bundle> b = w.lock ();
1022 	if (!b) {
1023 		return;
1024 	}
1025 
1026 	char buf [64];
1027 	snprintf (buf, sizeof (buf), _("%s all from '%s'"), disassociation_verb().c_str(), escape_underscores (b->channel_name (c)).c_str());
1028 	m.push_back (MenuElem (buf, sigc::bind (sigc::mem_fun (*this, &PortMatrix::disassociate_all_on_channel), w, c, d)));
1029 }
1030 
1031 void
port_connected_or_disconnected()1032 PortMatrix::port_connected_or_disconnected ()
1033 {
1034 	_body->rebuild_and_draw_grid ();
1035 	update_tab_highlighting ();
1036 }
1037 
1038 /** Update the highlighting of tab names to reflect which ones
1039  *  have connections.  This is pretty inefficient, unfortunately,
1040  *  but maybe that doesn't matter too much.
1041  */
1042 void
update_tab_highlighting()1043 PortMatrix::update_tab_highlighting ()
1044 {
1045 	if (!_session) {
1046 		return;
1047 	}
1048 
1049 	for (int i = 0; i < 2; ++i) {
1050 
1051 		Gtk::Notebook* notebook = row_index() == i ? &_vnotebook : &_hnotebook;
1052 
1053 		PortGroupList const * gl = ports (i);
1054 		int p = 0;
1055 		for (PortGroupList::List::const_iterator j = gl->begin(); j != gl->end(); ++j) {
1056 			bool has_connection = false;
1057 			PortGroup::BundleList const & bl = (*j)->bundles ();
1058 			PortGroup::BundleList::const_iterator k = bl.begin ();
1059 			while (k != bl.end()) {
1060 				if ((*k)->bundle->connected_to_anything (_session->engine())) {
1061 					has_connection = true;
1062 					break;
1063 				}
1064 				++k;
1065 			}
1066 
1067 			/* Find the page index that we should update; this is backwards
1068 			   for the vertical tabs in the LEFT_TO_BOTTOM arrangement.
1069 			*/
1070 			int page = p;
1071 			if (i == row_index() && _arrangement == LEFT_TO_BOTTOM) {
1072 				page = notebook->get_n_pages() - p - 1;
1073 			}
1074 
1075 			Gtk::Label* label = dynamic_cast<Gtk::Label*> (notebook->get_tab_label(*notebook->get_nth_page (page)));
1076 			string c = label->get_label ();
1077 			if (c.length() && c[0] == '<' && !has_connection) {
1078 				/* this label is marked up with <b> but shouldn't be */
1079 				label->set_text ((*j)->name);
1080 			} else if (c.length() && c[0] != '<' && has_connection) {
1081 				/* this label is not marked up with <b> but should be */
1082 				label->set_markup (string_compose ("<b>%1</b>", Gtkmm2ext::markup_escape_text ((*j)->name)));
1083 			}
1084 
1085 			++p;
1086 		}
1087 	}
1088 }
1089 
1090 string
channel_noun() const1091 PortMatrix::channel_noun () const
1092 {
1093 	return _("channel");
1094 }
1095 
1096 /** @return true if this matrix should show bundles / ports of type \t */
1097 bool
should_show(DataType t) const1098 PortMatrix::should_show (DataType t) const
1099 {
1100 	return (_type == DataType::NIL || t == _type);
1101 }
1102 
1103 uint32_t
count_of_our_type(ChanCount c) const1104 PortMatrix::count_of_our_type (ChanCount c) const
1105 {
1106 	if (_type == DataType::NIL) {
1107 		return c.n_total ();
1108 	}
1109 
1110 	return c.get (_type);
1111 }
1112 
1113 /** @return The number of ports of our type in the given channel count,
1114  *  but returning 1 if there are no ports.
1115  */
1116 uint32_t
count_of_our_type_min_1(ChanCount c) const1117 PortMatrix::count_of_our_type_min_1 (ChanCount c) const
1118 {
1119 	uint32_t n = count_of_our_type (c);
1120 	if (n == 0) {
1121 		n = 1;
1122 	}
1123 
1124 	return n;
1125 }
1126 
1127 PortMatrixNode::State
get_association(PortMatrixNode node) const1128 PortMatrix::get_association (PortMatrixNode node) const
1129 {
1130 	if (show_only_bundles ()) {
1131 
1132 		bool have_off_diagonal_association = false;
1133 		bool have_diagonal_association = false;
1134 		bool have_diagonal_not_association = false;
1135 
1136 		for (uint32_t i = 0; i < node.row.bundle->nchannels().n_total(); ++i) {
1137 
1138 			for (uint32_t j = 0; j < node.column.bundle->nchannels().n_total(); ++j) {
1139 
1140 				if (!should_show (node.row.bundle->channel_type(i)) || !should_show (node.column.bundle->channel_type(j))) {
1141 					continue;
1142 				}
1143 
1144 				ARDOUR::BundleChannel c[2];
1145 				c[row_index()] = ARDOUR::BundleChannel (node.row.bundle, i);
1146 				c[column_index()] = ARDOUR::BundleChannel (node.column.bundle, j);
1147 
1148 				PortMatrixNode::State const s = get_state (c);
1149 
1150 				switch (s) {
1151 				case PortMatrixNode::ASSOCIATED:
1152 					if (i == j) {
1153 						have_diagonal_association = true;
1154 					} else {
1155 						have_off_diagonal_association = true;
1156 					}
1157 					break;
1158 
1159 				case PortMatrixNode::NOT_ASSOCIATED:
1160 					if (i == j) {
1161 						have_diagonal_not_association = true;
1162 					}
1163 					break;
1164 
1165 				default:
1166 					break;
1167 				}
1168 			}
1169 		}
1170 
1171 		if (have_diagonal_association && !have_off_diagonal_association && !have_diagonal_not_association) {
1172 			return PortMatrixNode::ASSOCIATED;
1173 		} else if (!have_diagonal_association && !have_off_diagonal_association) {
1174 			return PortMatrixNode::NOT_ASSOCIATED;
1175 		}
1176 
1177 		return PortMatrixNode::PARTIAL;
1178 
1179 	} else {
1180 
1181 		ARDOUR::BundleChannel c[2];
1182 		c[column_index()] = node.column;
1183 		c[row_index()] = node.row;
1184 		return get_state (c);
1185 
1186 	}
1187 
1188 	abort(); /* NOTREACHED */
1189 	return PortMatrixNode::NOT_ASSOCIATED;
1190 }
1191 
1192 /** @return true if b is a non-zero pointer and the bundle it points to has some channels */
1193 bool
bundle_with_channels(boost::shared_ptr<ARDOUR::Bundle> b)1194 PortMatrix::bundle_with_channels (boost::shared_ptr<ARDOUR::Bundle> b)
1195 {
1196 	return b && b->nchannels() != ARDOUR::ChanCount::ZERO;
1197 }
1198 
1199 /** See if a `flip' is possible.
1200  *  @return If flip is possible, the new (row, column) notebook indices that
1201  *  should be selected; otherwise, (-1, -1)
1202  */
1203 pair<int, int>
check_flip() const1204 PortMatrix::check_flip () const
1205 {
1206 	/* Look for the row's port group name in the columns */
1207 
1208 	int new_column = 0;
1209 	boost::shared_ptr<const PortGroup> r = visible_ports (_row_index);
1210 	PortGroupList::List::const_iterator i = _ports[_column_index].begin();
1211 	while (i != _ports[_column_index].end() && (*i)->name != r->name) {
1212 		++i;
1213 		++new_column;
1214 	}
1215 
1216 	if (i == _ports[_column_index].end ()) {
1217 		return make_pair (-1, -1);
1218 	}
1219 
1220 	/* Look for the column's port group name in the rows */
1221 
1222 	int new_row = 0;
1223 	boost::shared_ptr<const PortGroup> c = visible_ports (_column_index);
1224 	i = _ports[_row_index].begin();
1225 	while (i != _ports[_row_index].end() && (*i)->name != c->name) {
1226 		++i;
1227 		++new_row;
1228 	}
1229 
1230 	if (i == _ports[_row_index].end ()) {
1231 		return make_pair (-1, -1);
1232 	}
1233 
1234 	if (_arrangement == LEFT_TO_BOTTOM) {
1235 		new_row = _ports[_row_index].size() - new_row - 1;
1236 	}
1237 
1238 	return make_pair (new_row, new_column);
1239 }
1240 
1241 bool
can_flip() const1242 PortMatrix::can_flip () const
1243 {
1244 	return check_flip().first != -1;
1245 }
1246 
1247 /** Flip the column and row pages around, if possible */
1248 void
flip()1249 PortMatrix::flip ()
1250 {
1251 	pair<int, int> n = check_flip ();
1252 	if (n.first == -1) {
1253 		return;
1254 	}
1255 
1256 	_vnotebook.set_current_page (n.first);
1257 	_hnotebook.set_current_page (n.second);
1258 }
1259 
1260 bool
key_press(GdkEventKey * k)1261 PortMatrix::key_press (GdkEventKey* k)
1262 {
1263 	if (k->keyval == GDK_f) {
1264 		flip ();
1265 		return true;
1266 	}
1267 
1268 	return false;
1269 }
1270 
1271 void
parameter_changed(string p)1272 PortMatrix::parameter_changed (string p)
1273 {
1274 	if (p == "font-scale") {
1275 		setup ();
1276 	}
1277 }
1278