df2759ef6b97021dadd2802b82fe035ec961ac13
[dcpomatic.git] / src / wx / audio_mapping_view.cc
1 /*
2     Copyright (C) 2013-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21 /** @file  src/wx/audio_mapping_view.cc
22  *  @brief AudioMappingView class and helpers.
23  */
24
25 #include "audio_mapping_view.h"
26 #include "wx_util.h"
27 #include "audio_gain_dialog.h"
28 #include "lib/audio_mapping.h"
29 #include "lib/util.h"
30 #include <dcp/locale_convert.h>
31 #include <dcp/types.h>
32 #include <wx/wx.h>
33 #include <wx/renderer.h>
34 #include <wx/grid.h>
35 #include <wx/graphics.h>
36 #include <boost/foreach.hpp>
37 #include <iostream>
38
39 using std::cout;
40 using std::list;
41 using std::string;
42 using std::min;
43 using std::max;
44 using std::vector;
45 using std::pair;
46 using std::make_pair;
47 using boost::shared_ptr;
48 using boost::optional;
49 using dcp::locale_convert;
50
51 #define INDICATOR_SIZE 20
52 #define GRID_SPACING 32
53 #define LEFT_WIDTH (GRID_SPACING * 3)
54 #define TOP_HEIGHT (GRID_SPACING * 2)
55
56 enum {
57         ID_off = 1,
58         ID_full = 2,
59         ID_minus6dB = 3,
60         ID_edit = 4
61 };
62
63 AudioMappingView::AudioMappingView (wxWindow* parent, wxString left_label, wxString from, wxString top_label, wxString to)
64         : wxPanel (parent, wxID_ANY)
65         , _menu_input (0)
66         , _menu_output (1)
67         , _left_label (left_label)
68         , _from (from)
69         , _top_label (top_label)
70         , _to (to)
71 {
72         _menu = new wxMenu;
73         _menu->Append (ID_off, _("Off"));
74         _menu->Append (ID_full, _("Full"));
75         _menu->Append (ID_minus6dB, _("-6dB"));
76         _menu->Append (ID_edit, _("Edit..."));
77
78         _body = new wxPanel (this, wxID_ANY);
79         _vertical_scroll = new wxScrollBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
80         _horizontal_scroll = new wxScrollBar (this, wxID_ANY);
81
82 #ifndef __WXOSX__
83         SetDoubleBuffered (true);
84 #endif
85
86         Bind (wxEVT_SIZE, boost::bind(&AudioMappingView::size, this, _1));
87         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::set_gain_from_menu, this, 0), ID_off);
88         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::set_gain_from_menu, this, 1), ID_full);
89         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::set_gain_from_menu, this, db_to_linear(-6)), ID_minus6dB);
90         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::edit, this), ID_edit);
91         Bind (wxEVT_MOUSEWHEEL, boost::bind(&AudioMappingView::mouse_wheel, this, _1));
92         _body->Bind (wxEVT_PAINT, boost::bind(&AudioMappingView::paint, this));
93         _body->Bind (wxEVT_LEFT_DOWN, boost::bind(&AudioMappingView::left_down, this, _1));
94         _body->Bind (wxEVT_RIGHT_DOWN, boost::bind(&AudioMappingView::right_down, this, _1));
95         _body->Bind (wxEVT_MOTION, boost::bind(&AudioMappingView::motion, this, _1));
96         _vertical_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
97         _vertical_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
98         _vertical_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
99         _vertical_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
100         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
101         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
102         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
103         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
104         _horizontal_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
105         _horizontal_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
106         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
107         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
108         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
109         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
110         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
111         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
112 }
113
114 void
115 AudioMappingView::size (wxSizeEvent& ev)
116 {
117         setup ();
118         ev.Skip ();
119 }
120
121 void
122 AudioMappingView::setup ()
123 {
124         wxSize const s = GetSize();
125         int const w = _vertical_scroll->GetSize().GetWidth();
126         int const h = _horizontal_scroll->GetSize().GetHeight();
127
128         _vertical_scroll->SetPosition (wxPoint(s.GetWidth() - w, 0));
129         _vertical_scroll->SetSize (wxSize(w, max(0, s.GetHeight() - h)));
130
131         _body->SetSize (wxSize(max(0, s.GetWidth() - w), max(0, s.GetHeight() - h)));
132
133         _horizontal_scroll->SetPosition (wxPoint(0, s.GetHeight() - h));
134         _horizontal_scroll->SetSize (wxSize(max(0, s.GetWidth() - w), h));
135
136         _vertical_scroll->SetScrollbar (
137                 _vertical_scroll->GetThumbPosition(),
138                 s.GetHeight() - h - 8,
139                 GRID_SPACING * (2 + _input_channels.size()),
140                 GRID_SPACING,
141                 true
142                 );
143
144         _horizontal_scroll->SetScrollbar (
145                 _horizontal_scroll->GetThumbPosition(),
146                 s.GetWidth() - w - 8,
147                 GRID_SPACING * (3 + _output_channels.size()),
148                 GRID_SPACING,
149                 true);
150 }
151
152 void
153 AudioMappingView::scroll ()
154 {
155         Refresh ();
156 }
157
158 void
159 AudioMappingView::paint_static (wxDC& dc, wxGraphicsContext* gc)
160 {
161         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
162         dc.SetFont (wxSWISS_FONT->Bold());
163         wxCoord label_width;
164         wxCoord label_height;
165
166         dc.GetTextExtent (_top_label, &label_width, &label_height);
167         dc.DrawText (_top_label, LEFT_WIDTH + (_output_channels.size() * GRID_SPACING - label_width) / 2, (GRID_SPACING - label_height) / 2);
168
169         dc.GetTextExtent (_left_label, &label_width, &label_height);
170         dc.DrawRotatedText (
171                 _left_label,
172                 (GRID_SPACING - label_height) / 2,
173                 TOP_HEIGHT + (_input_channels.size() * GRID_SPACING + label_width) / 2,
174                 90
175                 );
176
177         dc.SetFont (*wxSWISS_FONT);
178         gc->SetPen (*wxBLACK_PEN);
179 }
180
181 void
182 AudioMappingView::paint_column_labels (wxDC& dc, wxGraphicsContext* gc)
183 {
184         wxCoord label_width;
185         wxCoord label_height;
186         int N = 0;
187         BOOST_FOREACH (string i, _output_channels) {
188                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
189                 dc.DrawText (std_to_wx(i), LEFT_WIDTH + GRID_SPACING * N + (GRID_SPACING - label_width) / 2, GRID_SPACING + (GRID_SPACING - label_height) / 2);
190                 ++N;
191         }
192
193         wxGraphicsPath lines = gc->CreatePath ();
194         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING);
195         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, GRID_SPACING);
196         lines.MoveToPoint (LEFT_WIDTH, GRID_SPACING * 2);
197         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, GRID_SPACING * 2);
198         gc->StrokePath (lines);
199 }
200
201 void
202 AudioMappingView::paint_column_lines (wxGraphicsContext* gc)
203 {
204         wxGraphicsPath lines = gc->CreatePath ();
205         for (size_t i = 0; i < _output_channels.size(); ++i) {
206                 lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * i, GRID_SPACING);
207                 lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * i, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
208         }
209         lines.MoveToPoint    (LEFT_WIDTH + GRID_SPACING * _output_channels.size(), GRID_SPACING);
210         lines.AddLineToPoint (LEFT_WIDTH + GRID_SPACING * _output_channels.size(), TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
211         gc->StrokePath (lines);
212 }
213
214 void
215 AudioMappingView::paint_row_labels (wxDC& dc, wxGraphicsContext* gc)
216 {
217         wxCoord label_width;
218         wxCoord label_height;
219         wxGraphicsPath lines = gc->CreatePath ();
220
221         /* Row channel labels */
222
223         int N = 0;
224         BOOST_FOREACH (string i, _input_channels) {
225                 dc.GetTextExtent (std_to_wx(i), &label_width, &label_height);
226                 dc.DrawText (std_to_wx(i), GRID_SPACING * 2 + (GRID_SPACING - label_width) / 2, TOP_HEIGHT + GRID_SPACING * N + (GRID_SPACING - label_height) / 2);
227                 ++N;
228         }
229
230         /* Vertical lines on the left */
231
232         for (int i = 1; i < 3; ++i) {
233                 lines.MoveToPoint    (GRID_SPACING * i, TOP_HEIGHT);
234                 lines.AddLineToPoint (GRID_SPACING * i, TOP_HEIGHT + _input_channels.size() * GRID_SPACING);
235         }
236
237         /* Group labels and lines */
238
239         int y = TOP_HEIGHT;
240         BOOST_FOREACH (Group i, _input_groups) {
241                 int const height = (i.to - i.from + 1) * GRID_SPACING;
242                 dc.GetTextExtent (std_to_wx(i.name), &label_width, &label_height);
243                 if (label_width > height) {
244                         label_width = height - 8;
245                 }
246
247                 {
248                         int yp = y;
249                         if ((yp - 2 * GRID_SPACING) < dc.GetLogicalOrigin().y) {
250                                 yp += dc.GetLogicalOrigin().y;
251                         }
252
253                         wxCoord old_x, old_y, old_width, old_height;
254                         dc.GetClippingBox (&old_x, &old_y, &old_width, &old_height);
255                         dc.DestroyClippingRegion ();
256                         dc.SetClippingRegion (GRID_SPACING, yp + 4, GRID_SPACING, height - 8);
257
258                         dc.DrawRotatedText (
259                                 std_to_wx(i.name),
260                                 GRID_SPACING + (GRID_SPACING - label_height) / 2,
261                                 y + (height + label_width) / 2,
262                                 90
263                                 );
264
265                         dc.DestroyClippingRegion ();
266                         dc.SetClippingRegion (old_x, old_y, old_width, old_height);
267                 }
268
269                 lines.MoveToPoint    (GRID_SPACING,     y);
270                 lines.AddLineToPoint (GRID_SPACING * 2, y);
271                 y += height;
272         }
273
274         lines.MoveToPoint    (GRID_SPACING,     y);
275         lines.AddLineToPoint (GRID_SPACING * 2, y);
276
277         gc->StrokePath (lines);
278 }
279
280 void
281 AudioMappingView::paint_row_lines (wxGraphicsContext* gc)
282 {
283         wxGraphicsPath lines = gc->CreatePath ();
284         for (size_t i = 0; i < _input_channels.size(); ++i) {
285                 lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * i);
286                 lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, TOP_HEIGHT + GRID_SPACING * i);
287         }
288         lines.MoveToPoint (GRID_SPACING * 2, TOP_HEIGHT + GRID_SPACING * _input_channels.size());
289         lines.AddLineToPoint (LEFT_WIDTH + _output_channels.size() * GRID_SPACING, TOP_HEIGHT + GRID_SPACING * _input_channels.size());
290         gc->StrokePath (lines);
291 }
292
293 void
294 AudioMappingView::paint_indicators (wxDC& dc)
295 {
296         /* _{input,output}_channels and _map may not always be in sync, be careful here */
297         size_t const output = min(_output_channels.size(), size_t(_map.output_channels()));
298         size_t const input = min(_input_channels.size(), size_t(_map.input_channels()));
299
300         for (size_t x = 0; x < output; ++x) {
301                 for (size_t y = 0; y < input; ++y) {
302                         dc.SetBrush (*wxWHITE_BRUSH);
303                         dc.DrawRectangle (
304                                 wxRect(
305                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
306                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
307                                         INDICATOR_SIZE, INDICATOR_SIZE
308                                         )
309                                 );
310
311                         float const value_dB = linear_to_db(_map.get(y, x));
312                         int const range = 18;
313                         int height = 0;
314                         if (value_dB > -range) {
315                                 height = INDICATOR_SIZE * (1 + value_dB / range);
316                         }
317
318                         dc.SetBrush (*wxTheBrushList->FindOrCreateBrush(wxColour (0, 255, 0), wxBRUSHSTYLE_SOLID));
319                         dc.DrawRectangle (
320                                 wxRect(
321                                         LEFT_WIDTH + x * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2,
322                                         TOP_HEIGHT + y * GRID_SPACING + (GRID_SPACING - INDICATOR_SIZE) / 2 + INDICATOR_SIZE - height,
323                                         INDICATOR_SIZE, height
324                                         )
325                                 );
326                 }
327         }
328 }
329
330 static
331 void clip (wxDC& dc, wxGraphicsContext* gc, int x, int y, int w, int h)
332 {
333         dc.SetClippingRegion (x, y, w, h);
334         gc->Clip (x, y, w, h);
335 }
336
337 static
338 void translate (wxDC& dc, wxGraphicsContext* gc, int x, int y)
339 {
340         gc->PushState ();
341         gc->Translate (-x, -y);
342         dc.SetLogicalOrigin (x, y);
343 }
344
345 static
346 void restore (wxDC& dc, wxGraphicsContext* gc)
347 {
348         dc.SetLogicalOrigin (0, 0);
349         gc->PopState ();
350         dc.DestroyClippingRegion ();
351         gc->ResetClip ();
352 }
353
354 void
355 AudioMappingView::paint ()
356 {
357         wxPaintDC dc (_body);
358
359         wxGraphicsContext* gc = wxGraphicsContext::Create (dc);
360         if (!gc) {
361                 return;
362         }
363
364         int const hs = _horizontal_scroll->GetThumbPosition ();
365         int const vs = _vertical_scroll->GetThumbPosition ();
366
367         paint_static (dc, gc);
368
369         clip (dc, gc, LEFT_WIDTH, 0, GRID_SPACING * _output_channels.size(), GRID_SPACING * (2 + _input_channels.size()));
370         translate (dc, gc, hs, 0);
371         paint_column_labels (dc, gc);
372         restore (dc, gc);
373
374         clip (dc, gc, 0, TOP_HEIGHT, GRID_SPACING * (3 + _output_channels.size()), GRID_SPACING * _input_channels.size() + 1);
375         translate (dc, gc, 0, vs);
376         paint_row_labels (dc, gc);
377         restore (dc, gc);
378
379         clip (dc, gc, GRID_SPACING * 2, TOP_HEIGHT, GRID_SPACING * (1 + _output_channels.size()), GRID_SPACING * _input_channels.size() + 1);
380         translate (dc, gc, hs, vs);
381         paint_row_lines (gc);
382         restore (dc, gc);
383
384         clip (dc, gc, LEFT_WIDTH, GRID_SPACING, GRID_SPACING * (1 + _output_channels.size()), GRID_SPACING * (1 + _input_channels.size()));
385         translate (dc, gc, hs, vs);
386         paint_column_lines (gc);
387         restore (dc, gc);
388
389         clip (dc, gc, LEFT_WIDTH, TOP_HEIGHT, GRID_SPACING * _output_channels.size(), GRID_SPACING * _input_channels.size());
390         translate (dc, gc, hs, vs);
391         paint_indicators (dc);
392         restore (dc, gc);
393
394         delete gc;
395 }
396
397 optional<pair<int, int> >
398 AudioMappingView::mouse_event_to_channels (wxMouseEvent& ev) const
399 {
400         int const x = ev.GetX() + _horizontal_scroll->GetThumbPosition();
401         int const y = ev.GetY() + _vertical_scroll->GetThumbPosition();
402
403         if (x <= LEFT_WIDTH || y < TOP_HEIGHT) {
404                 return optional<pair<int, int> >();
405         }
406
407         int const input = (y - TOP_HEIGHT) / GRID_SPACING;
408         int const output = (x - LEFT_WIDTH) / GRID_SPACING;
409
410         if (input >= int(_input_channels.size()) || output >= int(_output_channels.size())) {
411                 return optional<pair<int, int> >();
412         }
413
414         return make_pair (input, output);
415 }
416
417 optional<string>
418 AudioMappingView::mouse_event_to_input_group_name (wxMouseEvent& ev) const
419 {
420         int const x = ev.GetX() + _horizontal_scroll->GetThumbPosition();
421         if (x < GRID_SPACING || x > (2 * GRID_SPACING)) {
422                 return optional<string>();
423         }
424
425         int y = (ev.GetY() + _vertical_scroll->GetThumbPosition() - (GRID_SPACING * 2)) / GRID_SPACING;
426         BOOST_FOREACH (Group i, _input_groups) {
427                 if (i.from <= y && y <= i.to) {
428                         return i.name;
429                 }
430         }
431
432         return optional<string>();
433 }
434
435 void
436 AudioMappingView::left_down (wxMouseEvent& ev)
437 {
438         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
439         if (!channels) {
440                 return;
441         }
442
443         if (_map.get(channels->first, channels->second) > 0) {
444                 _map.set (channels->first, channels->second, 0);
445         } else {
446                 _map.set (channels->first, channels->second, 1);
447         }
448
449         map_values_changed ();
450 }
451
452 void
453 AudioMappingView::right_down (wxMouseEvent& ev)
454 {
455         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
456         if (!channels) {
457                 return;
458         }
459
460         _menu_input = channels->first;
461         _menu_output = channels->second;
462         PopupMenu (_menu, ev.GetPosition());
463 }
464
465 void
466 AudioMappingView::mouse_wheel (wxMouseEvent& ev)
467 {
468         if (ev.ShiftDown()) {
469                 _horizontal_scroll->SetThumbPosition (
470                         _horizontal_scroll->GetThumbPosition() + (ev.GetWheelRotation() > 0 ? -GRID_SPACING : GRID_SPACING)
471                         );
472
473         } else {
474                 _vertical_scroll->SetThumbPosition (
475                         _vertical_scroll->GetThumbPosition() + (ev.GetWheelRotation() > 0 ? -GRID_SPACING : GRID_SPACING)
476                         );
477         }
478         Refresh ();
479 }
480
481 /** Called when any gain value has changed */
482 void
483 AudioMappingView::map_values_changed ()
484 {
485         Changed (_map);
486         _last_tooltip_channels = optional<pair<int, int> >();
487         Refresh ();
488 }
489
490 void
491 AudioMappingView::set_gain_from_menu (double linear)
492 {
493         _map.set (_menu_input, _menu_output, linear);
494         map_values_changed ();
495 }
496
497 void
498 AudioMappingView::edit ()
499 {
500         AudioGainDialog* dialog = new AudioGainDialog (this, _menu_input, _menu_output, _map.get(_menu_input, _menu_output));
501         if (dialog->ShowModal() == wxID_OK) {
502                 _map.set (_menu_input, _menu_output, dialog->value ());
503                 map_values_changed ();
504         }
505
506         dialog->Destroy ();
507 }
508
509 void
510 AudioMappingView::set (AudioMapping map)
511 {
512         _map = map;
513         Refresh ();
514 }
515
516 void
517 AudioMappingView::set_input_channels (vector<string> const & names)
518 {
519         _input_channels = names;
520         setup ();
521         Refresh ();
522 }
523
524 void
525 AudioMappingView::set_output_channels (vector<string> const & names)
526 {
527         _output_channels = names;
528         setup ();
529         Refresh ();
530 }
531
532 wxString
533 AudioMappingView::safe_input_channel_name (int n) const
534 {
535         if (n >= int(_input_channels.size())) {
536                 return wxString::Format ("%d", n + 1);
537         }
538
539         optional<wxString> group;
540         BOOST_FOREACH (Group i, _input_groups) {
541                 if (i.from <= n && n <= i.to) {
542                         group = std_to_wx (i.name);
543                 }
544         }
545
546         if (group && !group->IsEmpty()) {
547                 return wxString::Format ("%s/%s", group->data(), std_to_wx(_input_channels[n]).data());
548         }
549
550         return std_to_wx(_input_channels[n]);
551 }
552
553 wxString
554 AudioMappingView::safe_output_channel_name (int n) const
555 {
556         if (n >= int(_output_channels.size())) {
557                 return wxString::Format ("%d", n + 1);
558         }
559
560         return std_to_wx(_output_channels[n]);
561 }
562
563 void
564 AudioMappingView::motion (wxMouseEvent& ev)
565 {
566         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
567         if (channels) {
568                 if (channels != _last_tooltip_channels) {
569                         wxString s;
570                         float const gain = _map.get(channels->first, channels->second);
571                         if (gain == 0) {
572                                 s = wxString::Format (
573                                         _("No audio will be passed from %s channel '%s' to %s channel '%s'."),
574                                         _from,
575                                         safe_input_channel_name(channels->first),
576                                         _to,
577                                         safe_output_channel_name(channels->second)
578                                         );
579                         } else if (gain == 1) {
580                                 s = wxString::Format (
581                                         _("Audio will be passed from %s channel %s to %s channel %s unaltered."),
582                                         _from,
583                                         safe_input_channel_name(channels->first),
584                                         _to,
585                                         safe_output_channel_name(channels->second)
586                                         );
587                         } else {
588                                 float const dB = linear_to_db(gain);
589                                 s = wxString::Format (
590                                         _("Audio will be passed from %s channel %s to %s channel %s with gain %.1fdB."),
591                                         _from,
592                                         safe_input_channel_name(channels->first),
593                                         _to,
594                                         safe_output_channel_name(channels->second),
595                                         dB
596                                         );
597                         }
598
599                         SetToolTip (s + " " + _("Right click to change gain."));
600                 }
601         } else {
602                 optional<string> group = mouse_event_to_input_group_name (ev);
603                 if (group) {
604                         SetToolTip (std_to_wx(*group));
605                 } else {
606                         SetToolTip ("");
607                 }
608         }
609
610         _last_tooltip_channels = channels;
611         ev.Skip ();
612 }
613
614 void
615 AudioMappingView::set_input_groups (vector<Group> const & groups)
616 {
617         _input_groups = groups;
618 }