Try to reduce audio mapping flicker on Windows.
[dcpomatic.git] / src / wx / audio_mapping_view.cc
1 /*
2     Copyright (C) 2013-2019 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)
64         : wxPanel (parent, wxID_ANY)
65         , _menu_input (0)
66         , _menu_output (1)
67 {
68         _menu = new wxMenu;
69         _menu->Append (ID_off, _("Off"));
70         _menu->Append (ID_full, _("Full"));
71         _menu->Append (ID_minus6dB, _("-6dB"));
72         _menu->Append (ID_edit, _("Edit..."));
73
74         _body = new wxPanel (this, wxID_ANY);
75         _vertical_scroll = new wxScrollBar (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
76         _horizontal_scroll = new wxScrollBar (this, wxID_ANY);
77
78 #ifndef __WXOSX__
79         SetDoubleBuffered (true);
80 #endif
81
82         Bind (wxEVT_SIZE, boost::bind(&AudioMappingView::size, this, _1));
83         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::off, this), ID_off);
84         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::full, this), ID_full);
85         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::minus6dB, this), ID_minus6dB);
86         Bind (wxEVT_MENU, boost::bind(&AudioMappingView::edit, this), ID_edit);
87         Bind (wxEVT_MOUSEWHEEL, boost::bind(&AudioMappingView::mouse_wheel, this, _1));
88         _body->Bind (wxEVT_PAINT, boost::bind(&AudioMappingView::paint, this));
89         _body->Bind (wxEVT_LEFT_DOWN, boost::bind(&AudioMappingView::left_down, this, _1));
90         _body->Bind (wxEVT_RIGHT_DOWN, boost::bind(&AudioMappingView::right_down, this, _1));
91         _body->Bind (wxEVT_MOTION, boost::bind(&AudioMappingView::motion, this, _1));
92         _vertical_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
93         _vertical_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
94         _vertical_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
95         _vertical_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
96         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
97         _vertical_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
98         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
99         _vertical_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
100         _horizontal_scroll->Bind (wxEVT_SCROLL_TOP, boost::bind(&AudioMappingView::scroll, this));
101         _horizontal_scroll->Bind (wxEVT_SCROLL_BOTTOM, boost::bind(&AudioMappingView::scroll, this));
102         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEUP, boost::bind(&AudioMappingView::scroll, this));
103         _horizontal_scroll->Bind (wxEVT_SCROLL_LINEDOWN, boost::bind(&AudioMappingView::scroll, this));
104         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEUP, boost::bind(&AudioMappingView::scroll, this));
105         _horizontal_scroll->Bind (wxEVT_SCROLL_PAGEDOWN, boost::bind(&AudioMappingView::scroll, this));
106         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBTRACK, boost::bind(&AudioMappingView::scroll, this));
107         _horizontal_scroll->Bind (wxEVT_SCROLL_THUMBRELEASE, boost::bind(&AudioMappingView::scroll, this));
108 }
109
110 void
111 AudioMappingView::size (wxSizeEvent& ev)
112 {
113         setup ();
114         ev.Skip ();
115 }
116
117 void
118 AudioMappingView::setup ()
119 {
120         wxSize const s = GetSize();
121         int const w = _vertical_scroll->GetSize().GetWidth();
122         int const h = _horizontal_scroll->GetSize().GetHeight();
123
124         _vertical_scroll->SetPosition (wxPoint(s.GetWidth() - w, 0));
125         _vertical_scroll->SetSize (wxSize(w, max(0, s.GetHeight() - h)));
126
127         _body->SetSize (wxSize(max(0, s.GetWidth() - w), max(0, s.GetHeight() - h)));
128
129         _horizontal_scroll->SetPosition (wxPoint(0, s.GetHeight() - h));
130         _horizontal_scroll->SetSize (wxSize(max(0, s.GetWidth() - w), h));
131
132         _vertical_scroll->SetScrollbar (
133                 _vertical_scroll->GetThumbPosition(),
134                 s.GetHeight() - h - 8,
135                 GRID_SPACING * (2 + _input_channels.size()),
136                 GRID_SPACING,
137                 true
138                 );
139
140         _horizontal_scroll->SetScrollbar (
141                 _horizontal_scroll->GetThumbPosition(),
142                 s.GetWidth() - w - 8,
143                 GRID_SPACING * (3 + _output_channels.size()),
144                 GRID_SPACING,
145                 true);
146 }
147
148 void
149 AudioMappingView::scroll ()
150 {
151         Refresh ();
152 }
153
154 void
155 AudioMappingView::paint_static (wxDC& dc, wxGraphicsContext* gc)
156 {
157         gc->SetAntialiasMode (wxANTIALIAS_DEFAULT);
158         dc.SetFont (wxSWISS_FONT->Bold());
159         wxCoord label_width;
160         wxCoord label_height;
161
162         /* DCP label at the top */
163
164         dc.GetTextExtent (_("DCP"), &label_width, &label_height);
165         dc.DrawText (_("DCP"), LEFT_WIDTH + (_output_channels.size() * GRID_SPACING - label_width) / 2, (GRID_SPACING - label_height) / 2);
166
167         /* Content label on the left */
168
169         dc.GetTextExtent (_("Content"), &label_width, &label_height);
170         dc.DrawRotatedText (
171                 _("Content"),
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 = 20 * log10 (_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::off ()
492 {
493         _map.set (_menu_input, _menu_output, 0);
494         map_values_changed ();
495 }
496
497 void
498 AudioMappingView::full ()
499 {
500         _map.set (_menu_input, _menu_output, 1);
501         map_values_changed ();
502 }
503
504 void
505 AudioMappingView::minus6dB ()
506 {
507         _map.set (_menu_input, _menu_output, pow (10, -6.0 / 20));
508         map_values_changed ();
509 }
510
511 void
512 AudioMappingView::edit ()
513 {
514         AudioGainDialog* dialog = new AudioGainDialog (this, _menu_input, _menu_output, _map.get(_menu_input, _menu_output));
515         if (dialog->ShowModal() == wxID_OK) {
516                 _map.set (_menu_input, _menu_output, dialog->value ());
517                 map_values_changed ();
518         }
519
520         dialog->Destroy ();
521 }
522
523 void
524 AudioMappingView::set (AudioMapping map)
525 {
526         _map = map;
527         Refresh ();
528 }
529
530 void
531 AudioMappingView::set_input_channels (vector<string> const & names)
532 {
533         _input_channels = names;
534         setup ();
535         Refresh ();
536 }
537
538 void
539 AudioMappingView::set_output_channels (vector<string> const & names)
540 {
541         _output_channels = names;
542         setup ();
543         Refresh ();
544 }
545
546 wxString
547 AudioMappingView::safe_input_channel_name (int n) const
548 {
549         if (n >= int(_input_channels.size())) {
550                 return wxString::Format ("%d", n + 1);
551         }
552
553         optional<wxString> group;
554         BOOST_FOREACH (Group i, _input_groups) {
555                 if (i.from <= n && n <= i.to) {
556                         group = std_to_wx (i.name);
557                 }
558         }
559
560         if (group) {
561                 return wxString::Format ("%s/%s", group->data(), std_to_wx(_input_channels[n]).data());
562         }
563
564         return std_to_wx(_input_channels[n]);
565 }
566
567 wxString
568 AudioMappingView::safe_output_channel_name (int n) const
569 {
570         if (n >= int(_output_channels.size())) {
571                 return wxString::Format ("%d", n + 1);
572         }
573
574         return std_to_wx(_output_channels[n]);
575 }
576
577 void
578 AudioMappingView::motion (wxMouseEvent& ev)
579 {
580         optional<pair<int, int> > channels = mouse_event_to_channels (ev);
581         if (channels) {
582                 if (channels != _last_tooltip_channels) {
583                         wxString s;
584                         float const gain = _map.get(channels->first, channels->second);
585                         if (gain == 0) {
586                                 s = wxString::Format (
587                                         _("No audio will be passed from content channel '%s' to DCP channel '%s'."),
588                                         safe_input_channel_name(channels->first),
589                                         safe_output_channel_name(channels->second)
590                                         );
591                         } else if (gain == 1) {
592                                 s = wxString::Format (
593                                         _("Audio will be passed from content channel %s to DCP channel %s unaltered."),
594                                         safe_input_channel_name(channels->first),
595                                         safe_output_channel_name(channels->second)
596                                         );
597                         } else {
598                                 float const dB = 20 * log10 (gain);
599                                 s = wxString::Format (
600                                         _("Audio will be passed from content channel %s to DCP channel %s with gain %.1fdB."),
601                                         safe_input_channel_name(channels->first),
602                                         safe_output_channel_name(channels->second),
603                                         dB
604                                         );
605                         }
606
607                         SetToolTip (s + " " + _("Right click to change gain."));
608                 }
609         } else {
610                 optional<string> group = mouse_event_to_input_group_name (ev);
611                 if (group) {
612                         SetToolTip (std_to_wx(*group));
613                 } else {
614                         SetToolTip ("");
615                 }
616         }
617
618         _last_tooltip_channels = channels;
619         ev.Skip ();
620 }
621
622 void
623 AudioMappingView::set_input_groups (vector<Group> const & groups)
624 {
625         _input_groups = groups;
626 }