]> git.0d.be Git - jack_mixer.git/blob - channel.py
Set version to 14 in preparation for next release
[jack_mixer.git] / channel.py
1 # This file is part of jack_mixer
2 #
3 # Copyright (C) 2006 Nedko Arnaudov <nedko@arnaudov.name>
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; version 2 of the License
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
17
18 import logging
19
20 import gi
21 from gi.repository import Gtk
22 from gi.repository import Gdk
23 from gi.repository import GObject
24 from gi.repository import Pango
25
26 import abspeak
27 import meter
28 import slider
29 from serialization import SerializedObject
30
31
32 log = logging.getLogger(__name__)
33 button_padding = 1
34 CSS = b"""
35 .top_label {
36     padding: 0px .1em;
37     min-height: 1.5rem;
38 }
39
40 .wide {
41     font-size: medium
42 }
43
44 .narrow {
45     font-size: smaller
46 }
47
48 button {
49     padding: 0px
50 }
51
52 .vbox_fader {
53     border: 1px inset #111;
54 }
55
56 .readout {
57     font-size: 80%;
58     margin: .1em;
59     padding: 0;
60     border: 1px inset #111;
61     color: white;
62 }
63 """
64 css_provider = Gtk.CssProvider()
65 css_provider.load_from_data(CSS)
66 context = Gtk.StyleContext()
67 screen = Gdk.Screen.get_default()
68 context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
69
70
71 def get_text_color(background_color):
72     """Calculates the luminance of the given color (GdkRGBA)
73        and returns an appropriate text color."""
74     # luminance coefficients taken from section C-9 from
75     # http://www.faqs.org/faqs/graphics/colorspace-faq/
76     brightess = background_color.red * 0.212671 + \
77             background_color.green * 0.715160 + \
78             background_color.blue * 0.072169
79
80     if brightess > 0.5:
81         return 'black'
82     else:
83         return 'white'
84
85
86 def set_background_color(widget, name, color):
87     color_string = color.to_string()
88     css = """
89     .%s {
90         background-color: %s;
91         color: %s;
92     }
93 """ % (name, color_string, get_text_color(color))
94
95     css_provider = Gtk.CssProvider()
96     css_provider.load_from_data(css.encode('utf-8'))
97     context = Gtk.StyleContext()
98     screen = Gdk.Screen.get_default()
99     context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
100
101     widget_context = widget.get_style_context()
102     widget_context.add_class(name)
103
104
105 def random_color():
106     from random import uniform, seed
107     seed()
108     return Gdk.RGBA(uniform(0, 1), uniform(0, 1), uniform(0, 1), 1)
109
110
111 class Channel(Gtk.VBox, SerializedObject):
112     """Widget with slider and meter used as base class for more specific
113        channel widgets"""
114
115     monitor_button = None
116     num_instances = 0
117
118     def __init__(self, app, name, stereo, value = None):
119         Gtk.VBox.__init__(self)
120         self.app = app
121         self.mixer = app.mixer
122         self.channel = None
123         self.gui_factory = app.gui_factory
124         self._channel_name = name
125         self.stereo = stereo
126         self.initial_value = value
127         self.meter_scale = self.gui_factory.get_default_meter_scale()
128         self.slider_scale = self.gui_factory.get_default_slider_scale()
129         self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0, 0.02)
130         self.balance_adjustment = slider.BalanceAdjustment()
131         self.post_fader_output_channel = None
132         self.future_out_mute = None
133         self.future_volume_midi_cc = None
134         self.future_balance_midi_cc = None
135         self.future_mute_midi_cc = None
136         self.future_solo_midi_cc = None
137         self.css_name = "css_name_%d" % Channel.num_instances
138         self.label_name = None
139         self.wide = True
140         self.label_chars_wide = 12
141         self.label_chars_narrow = 7
142         Channel.num_instances += 1
143
144     def get_channel_name(self):
145         return self._channel_name
146
147     def set_channel_name(self, name):
148         self.app.on_channel_rename(self._channel_name, name);
149         self._channel_name = name
150         if self.label_name:
151             self.label_name.set_text(name)
152             if len(name) > (self.label_chars_wide if self.wide else self.label_chars_narrow):
153                 self.label_name.set_tooltip_text(name)
154         if self.channel:
155             self.channel.name = name
156         if self.post_fader_output_channel:
157             self.post_fader_output_channel.name = "%s Out" % name;
158     channel_name = property(get_channel_name, set_channel_name)
159
160     def create_balance_widget(self):
161         self.balance = slider.BalanceSlider(self.balance_adjustment, (20, 20), (0, 100))
162         self.balance.show()
163
164     def create_buttons(self):
165         # Mute, Solo and Monitor buttons
166         self.hbox_mutesolo = Gtk.Box(False, 0, orientation=Gtk.Orientation.HORIZONTAL)
167
168         self.mute = Gtk.ToggleButton()
169         self.mute.set_label("M")
170         self.mute.set_name("mute")
171         self.mute.set_active(self.channel.out_mute)
172         self.mute.connect("toggled", self.on_mute_toggled)
173         self.hbox_mutesolo.pack_start(self.mute, True, True, 0)
174
175         self.pack_start(self.hbox_mutesolo, False, False, 0)
176
177         self.monitor_button = Gtk.ToggleButton('MON')
178         self.monitor_button.connect('toggled', self.on_monitor_button_toggled)
179         self.pack_start(self.monitor_button, False, False, 0)
180
181     def create_fader(self):
182         # HBox for fader and meter
183         self.vbox_fader = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
184         self.vbox_fader.get_style_context().add_class('vbox_fader')
185
186         self.hbox_readouts = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
187         self.hbox_readouts.set_homogeneous(True)
188         self.hbox_readouts.pack_start(self.volume_digits, False, True, 0)
189         self.hbox_readouts.pack_start(self.abspeak, False, True, 0)
190         self.vbox_fader.pack_start(self.hbox_readouts, False, False, 0)
191
192         self.hbox_fader = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
193         self.hbox_fader.pack_start(self.slider, True, True, 0)
194         self.hbox_fader.pack_start(self.meter, True, True, 0)
195         self.vbox_fader.pack_start(self.hbox_fader, True, True, 0)
196         self.vbox_fader.pack_start(self.balance, False, True, 0)
197
198         self.pack_start(self.vbox_fader, True, True, 0)
199
200     def create_slider_widget(self):
201         parent = None
202         if self.slider:
203             parent = self.slider.get_parent()
204             self.slider.destroy()
205
206         if self.gui_factory.use_custom_widgets:
207             self.slider = slider.CustomSliderWidget(self.slider_adjustment)
208         else:
209             self.slider = slider.VolumeSlider(self.slider_adjustment)
210
211         if parent:
212             parent.pack_start(self.slider, True, True, 0)
213             parent.reorder_child(self.slider, 0)
214
215         self.slider.show()
216
217     def realize(self):
218         log.debug('Realizing channel "%s".', self.channel_name)
219         if self.future_out_mute != None:
220             self.channel.out_mute = self.future_out_mute
221
222         # Widgets
223         # Channel strip label
224         self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
225         self.pack_start(self.vbox, False, True, 0)
226         self.label_name = Gtk.Label()
227         self.label_name.get_style_context().add_class('top_label')
228         self.label_name.set_text(self.channel_name)
229         self.label_name.set_max_width_chars(self.label_chars_wide if self.wide else
230                                             self.label_chars_narrow)
231         self.label_name.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
232         self.label_name_event_box = Gtk.EventBox()
233         self.label_name_event_box.connect('button-press-event', self.on_label_mouse)
234         self.label_name_event_box.add(self.label_name)
235
236         # Volume fader
237         self.slider = None
238         self.create_slider_widget()
239         self.create_balance_widget()
240
241         # Volume entry
242         self.volume_digits = Gtk.Entry()
243         self.volume_digits.set_has_frame(False)
244         self.volume_digits.set_width_chars(5)
245         self.volume_digits.set_property('xalign', 0.5)
246         self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed)
247         self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out)
248         self.volume_digits.get_style_context().add_class('readout')
249
250         # Peak level label
251         self.abspeak = abspeak.AbspeakWidget()
252         self.abspeak.connect("reset", self.on_abspeak_reset)
253         self.abspeak.connect("volume-adjust", self.on_abspeak_adjust)
254         self.abspeak.get_style_context().add_class('readout')
255
256         # Level meter
257         if self.stereo:
258             self.meter = meter.StereoMeterWidget(self.meter_scale)
259         else:
260             self.meter = meter.MonoMeterWidget(self.meter_scale)
261
262         self.meter.set_events(Gdk.EventMask.SCROLL_MASK)
263         self.on_vumeter_color_changed(self.gui_factory)
264
265         if self.initial_value != None:
266             if self.initial_value == True:
267                 self.slider_adjustment.set_value(0)
268             else:
269                 self.slider_adjustment.set_value_db(0)
270
271         self.slider_adjustment.connect("volume-changed", self.on_volume_changed)
272         self.slider_adjustment.connect("volume-changed-from-midi", self.on_volume_changed_from_midi)
273         self.balance_adjustment.connect("balance-changed", self.on_balance_changed)
274
275         self.gui_factory.connect("default-meter-scale-changed", self.on_default_meter_scale_changed)
276         self.gui_factory.connect("default-slider-scale-changed", self.on_default_slider_scale_changed)
277         self.gui_factory.connect('vumeter-color-changed', self.on_vumeter_color_changed)
278         self.gui_factory.connect('vumeter-color-scheme-changed', self.on_vumeter_color_changed)
279         self.gui_factory.connect('use-custom-widgets-changed', self.on_custom_widgets_changed)
280
281         self.connect("key-press-event", self.on_key_pressed)
282         self.connect("scroll-event", self.on_scroll)
283
284     def unrealize(self):
285         log.debug('Unrealizing channel "%s".', self.channel_name)
286
287     def widen(self, flag=True):
288         self.wide = flag
289         ctx = self.label_name.get_style_context()
290
291         if flag:
292             ctx.remove_class('narrow')
293             ctx.add_class('wide')
294         else:
295             ctx.remove_class('wide')
296             ctx.add_class('narrow')
297
298         label = self.label_name.get_label()
299         label_width = self.label_chars_wide if flag else self.label_chars_narrow
300         self.label_name.set_max_width_chars(label_width)
301
302         if len(label) > label_width:
303             self.label_name.set_tooltip_text(label)
304
305         self.meter.widen(flag)
306         self.hbox_readouts.set_orientation(
307             Gtk.Orientation.HORIZONTAL if flag else Gtk.Orientation.VERTICAL)
308
309     def narrow(self):
310         self.widen(False)
311
312     def on_label_mouse(self, widget, event):
313         if event.type == Gdk.EventType._2BUTTON_PRESS:
314             if event.button == 1:
315                 self.on_channel_properties()
316             return True
317         elif (event.state & Gdk.ModifierType.CONTROL_MASK and
318               event.type == Gdk.EventType.BUTTON_PRESS and
319               event.button == 1):
320             if self.wide:
321                 self.narrow()
322             else:
323                 self.widen()
324             return True
325
326     def on_default_meter_scale_changed(self, gui_factory, scale):
327         log.debug("Default meter scale change detected.")
328         self.meter.set_scale(scale)
329
330     def on_default_slider_scale_changed(self, gui_factory, scale):
331         log.debug("Default slider scale change detected.")
332         self.slider_scale = scale
333         self.slider_adjustment.set_scale(scale)
334         if self.channel:
335             self.channel.midi_scale = self.slider_scale.scale
336
337     def on_vumeter_color_changed(self, gui_factory, *args):
338         color = gui_factory.get_vumeter_color()
339         color_scheme = gui_factory.get_vumeter_color_scheme()
340         if color_scheme != 'solid':
341             self.meter.set_color(None)
342         else:
343             self.meter.set_color(Gdk.color_parse(color))
344
345     def on_custom_widgets_changed(self, gui_factory, value):
346         self.balance.destroy()
347         self.create_balance_widget()
348         self.create_slider_widget()
349
350     def on_abspeak_adjust(self, abspeak, adjust):
351         log.debug("abspeak adjust %f", adjust)
352         self.slider_adjustment.set_value_db(self.slider_adjustment.get_value_db() + adjust)
353         self.channel.abspeak = None
354         #self.update_volume(False)   # We want to update gui even if actual decibels have not changed (scale wrap for example)
355
356     def on_abspeak_reset(self, abspeak):
357         log.debug("abspeak reset")
358         self.channel.abspeak = None
359
360     def on_volume_digits_key_pressed(self, widget, event):
361         if (event.keyval == Gdk.KEY_Return or event.keyval == Gdk.KEY_KP_Enter):
362             db_text = self.volume_digits.get_text()
363             try:
364                 db = float(db_text)
365                 log.debug('Volume digits confirmation "%f dBFS".', db)
366             except (ValueError) as e:
367                 log.debug("Volume digits confirmation ignore, reset to current.")
368                 self.update_volume(False)
369                 return
370             self.slider_adjustment.set_value_db(db)
371             #self.grab_focus()
372             #self.update_volume(False)   # We want to update gui even if actual decibels have not changed (scale wrap for example)
373
374     def on_volume_digits_focus_out(self, widget, event):
375         log.debug("Volume digits focus out detected.")
376         self.update_volume(False)
377
378     def read_meter(self):
379         if not self.channel:
380             return
381         if self.stereo:
382             peak_left, peak_right, rms_left, rms_right = self.channel.kmeter
383             self.meter.set_values(peak_left, peak_right, rms_left, rms_right)
384         else:
385             peak, rms = self.channel.kmeter
386             self.meter.set_values(peak, rms)
387
388         self.abspeak.set_peak(self.channel.abspeak)
389
390     def on_scroll(self, widget, event):
391         if event.direction == Gdk.ScrollDirection.DOWN:
392             self.slider_adjustment.step_down()
393         elif event.direction == Gdk.ScrollDirection.UP:
394             self.slider_adjustment.step_up()
395         return True
396
397     def update_volume(self, update_engine, from_midi = False):
398         db = self.slider_adjustment.get_value_db()
399
400         db_text = "%.2f" % db
401         self.volume_digits.set_text(db_text)
402
403         if update_engine:
404             if not from_midi:
405                 self.channel.volume = db
406             self.app.update_monitor(self)
407
408     def on_volume_changed(self, adjustment):
409         self.update_volume(True)
410
411     def on_volume_changed_from_midi(self, adjustment):
412         self.update_volume(True, from_midi = True)
413
414     def on_balance_changed(self, adjustment):
415         balance = self.balance_adjustment.get_value()
416         log.debug("%s balance: %f", self.channel_name, balance)
417         self.channel.balance = balance
418         self.app.update_monitor(self)
419
420     def on_key_pressed(self, widget, event):
421         if (event.keyval == Gdk.KEY_Up):
422             log.debug(self.channel_name + " Up")
423             self.slider_adjustment.step_up()
424             return True
425         elif (event.keyval == Gdk.KEY_Down):
426             log.debug(self.channel_name + " Down")
427             self.slider_adjustment.step_down()
428             return True
429
430         return False
431
432     def serialize(self, object_backend):
433         object_backend.add_property("volume", "%f" % self.slider_adjustment.get_value_db())
434         object_backend.add_property("balance", "%f" % self.balance_adjustment.get_value())
435         object_backend.add_property("wide", "%s" % str(self.wide))
436
437         if hasattr(self.channel, 'out_mute'):
438             object_backend.add_property('out_mute', str(self.channel.out_mute))
439         if self.channel.volume_midi_cc != -1:
440             object_backend.add_property('volume_midi_cc', str(self.channel.volume_midi_cc))
441         if self.channel.balance_midi_cc != -1:
442             object_backend.add_property('balance_midi_cc', str(self.channel.balance_midi_cc))
443         if self.channel.mute_midi_cc != -1:
444             object_backend.add_property('mute_midi_cc', str(self.channel.mute_midi_cc))
445         if self.channel.solo_midi_cc != -1:
446             object_backend.add_property('solo_midi_cc', str(self.channel.solo_midi_cc))
447
448     def unserialize_property(self, name, value):
449         if name == "volume":
450             self.slider_adjustment.set_value_db(float(value))
451             return True
452         if name == "balance":
453             self.balance_adjustment.set_value(float(value))
454             return True
455         if name == 'out_mute':
456             self.future_out_mute = (value == 'True')
457             return True
458         if name == 'volume_midi_cc':
459             self.future_volume_midi_cc = int(value)
460             return True
461         if name == 'balance_midi_cc':
462             self.future_balance_midi_cc = int(value)
463             return True
464         if name == 'mute_midi_cc':
465             self.future_mute_midi_cc = int(value)
466             return True
467         if name == 'solo_midi_cc':
468             self.future_solo_midi_cc = int(value)
469             return True
470         if name == "wide":
471             self.wide = value == "True"
472             return True
473         return False
474
475     def on_midi_event_received(self, *args):
476         self.slider_adjustment.set_value_db(self.channel.volume, from_midi = True)
477         self.balance_adjustment.set_balance(self.channel.balance, from_midi = True)
478
479     def on_monitor_button_toggled(self, button):
480         if button.get_active():
481             for channel in self.app.channels + self.app.output_channels:
482                 if channel.monitor_button.get_active() and channel.monitor_button is not button:
483                     channel.monitor_button.handler_block_by_func(
484                                 channel.on_monitor_button_toggled)
485                     channel.monitor_button.set_active(False)
486                     channel.monitor_button.handler_unblock_by_func(
487                                 channel.on_monitor_button_toggled)
488             self.app.set_monitored_channel(self)
489         else:
490             if self.app._monitored_channel.channel.name == self.channel.name:
491                 self.monitor_button.handler_block_by_func(self.on_monitor_button_toggled)
492                 self.monitor_button.set_active(True)
493                 self.monitor_button.handler_unblock_by_func(self.on_monitor_button_toggled)
494
495     def set_monitored(self):
496         if self.channel:
497             self.app.set_monitored_channel(self)
498         self.monitor_button.set_active(True)
499
500     def set_color(self, color):
501         self.color = color
502         set_background_color(self.label_name_event_box, self.css_name, self.color)
503
504
505 class InputChannel(Channel):
506     post_fader_output_channel = None
507
508     def create_buttons(self):
509         super().create_buttons()
510         self.solo = Gtk.ToggleButton()
511         self.solo.set_label("S")
512         self.solo.set_name("solo")
513         self.solo.set_active(self.channel.solo)
514         self.solo.connect("toggled", self.on_solo_toggled)
515         self.hbox_mutesolo.pack_start(self.solo, True, True, 0)
516
517     def realize(self):
518         self.channel = self.mixer.add_channel(self.channel_name, self.stereo)
519
520         if self.channel == None:
521             raise Exception("Cannot create a channel")
522
523         super().realize()
524
525         if self.future_volume_midi_cc != None:
526             self.channel.volume_midi_cc = self.future_volume_midi_cc
527         if self.future_balance_midi_cc != None:
528             self.channel.balance_midi_cc = self.future_balance_midi_cc
529         if self.future_mute_midi_cc != None:
530             self.channel.mute_midi_cc = self.future_mute_midi_cc
531         if self.future_solo_midi_cc != None:
532             self.channel.solo_midi_cc = self.future_solo_midi_cc
533         if self.app._init_solo_channels and self.channel_name in self.app._init_solo_channels:
534             self.channel.solo = True
535
536         self.channel.midi_scale = self.slider_scale.scale
537
538         self.on_volume_changed(self.slider_adjustment)
539         self.on_balance_changed(self.balance_adjustment)
540
541         entries = [Gtk.TargetEntry.new("INPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
542         self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
543                 Gdk.DragAction.MOVE)
544         self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
545         self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
546         self.connect_after("drag-data-received", self.on_drag_data_received)
547
548         self.vbox.pack_start(self.label_name_event_box, True, True, 0)
549
550         self.create_fader()
551         self.create_buttons()
552
553         if not self.wide:
554             self.narrow()
555
556     def unrealize(self):
557         super().unrealize()
558         if self.post_fader_output_channel:
559             self.post_fader_output_channel.remove()
560             self.post_fader_output_channel = None
561         self.channel.remove()
562         self.channel = None
563
564     def narrow(self):
565         super().narrow()
566         for cg in self.get_control_groups():
567             cg.narrow()
568
569     def widen(self, flag=True):
570         super().widen(flag)
571         for cg in self.get_control_groups():
572             cg.widen()
573
574     def on_drag_data_get(self, widget, drag_context, data, info, time):
575         channel = widget.get_parent().get_parent()
576         data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
577
578     def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
579         source_name = data.get_data().decode('utf-8')
580         if source_name == self._channel_name:
581             return
582         self.emit("input-channel-order-changed", source_name, self._channel_name)
583
584     def add_control_group(self, channel):
585         control_group = ControlGroup(channel, self)
586         control_group.show_all()
587         self.vbox.pack_start(control_group, True, True, 0)
588         return control_group
589
590     def remove_control_group(self, channel):
591         ctlgroup = self.get_control_group(channel)
592         self.vbox.remove(ctlgroup)
593
594     def update_control_group(self, channel):
595         for control_group in self.vbox.get_children():
596             if isinstance(control_group, ControlGroup):
597                 if control_group.output_channel is channel:
598                     control_group.update()
599
600     def get_control_group(self, channel):
601         for control_group in self.get_control_groups():
602             if control_group.output_channel is channel:
603                 return control_group
604         return None
605
606     def get_control_groups(self):
607         ctlgroups = []
608         for c in self.vbox.get_children():
609             if isinstance(c, ControlGroup):
610                 ctlgroups.append(c)
611         return ctlgroups
612
613     channel_properties_dialog = None
614
615     def on_channel_properties(self):
616         if not self.channel_properties_dialog:
617             self.channel_properties_dialog = ChannelPropertiesDialog(self, self.app)
618         self.channel_properties_dialog.show()
619         self.channel_properties_dialog.present()
620
621     def on_mute_toggled(self, button):
622         self.channel.out_mute = self.mute.get_active()
623
624     def on_solo_toggled(self, button):
625         self.channel.solo = self.solo.get_active()
626
627     def midi_events_check(self):
628         if hasattr(self, 'channel') and self.channel.midi_in_got_events:
629             self.mute.set_active(self.channel.out_mute)
630             self.solo.set_active(self.channel.solo)
631             super().on_midi_event_received()
632
633     def on_solo_button_pressed(self, button, event, *args):
634         if event.button == 3:
635             # right click on the solo button, act on all output channels
636             if button.get_active(): # was soloed
637                 button.set_active(False)
638                 if hasattr(button, 'touched_channels'):
639                     touched_channels = button.touched_channels
640                     for chan in touched_channels:
641                         ctlgroup = self.get_control_group(chan)
642                         ctlgroup.solo.set_active(False)
643                     del button.touched_channels
644             else: # was not soloed
645                 button.set_active(True)
646                 touched_channels = []
647                 for chan in self.app.output_channels:
648                     ctlgroup = self.get_control_group(chan)
649                     if not ctlgroup.solo.get_active():
650                         ctlgroup.solo.set_active(True)
651                         touched_channels.append(chan)
652                 button.touched_channels = touched_channels
653             return True
654         return False
655
656     @classmethod
657     def serialization_name(cls):
658         return 'input_channel'
659
660     def serialize(self, object_backend):
661         object_backend.add_property("name", self.channel_name)
662         if self.stereo:
663             object_backend.add_property("type", "stereo")
664         else:
665             object_backend.add_property("type", "mono")
666         super().serialize(object_backend)
667
668     def unserialize_property(self, name, value):
669         if name == "name":
670             self.channel_name = str(value)
671             return True
672         if name == "type":
673             if value == "stereo":
674                 self.stereo = True
675                 return True
676             if value == "mono":
677                 self.stereo = False
678                 return True
679         return super().unserialize_property(name, value)
680
681
682 GObject.signal_new("input-channel-order-changed", InputChannel,
683                 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
684                 None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
685
686
687 class OutputChannel(Channel):
688     _display_solo_buttons = False
689
690     _init_muted_channels = None
691     _init_solo_channels = None
692     _init_prefader_channels = None
693
694     channel_properties_dialog = None
695
696     def get_display_solo_buttons(self):
697         return self._display_solo_buttons
698
699     def set_display_solo_buttons(self, value):
700         self._display_solo_buttons = value
701         # notifying control groups
702         for inputchannel in self.app.channels:
703             inputchannel.update_control_group(self)
704
705     display_solo_buttons = property(get_display_solo_buttons, set_display_solo_buttons)
706
707     def realize(self):
708         self.channel = self.mixer.add_output_channel(self.channel_name, self.stereo)
709
710         if self.channel == None:
711             raise Exception("Cannot create a channel")
712
713         super().realize()
714
715         if self.future_volume_midi_cc != None:
716             self.channel.volume_midi_cc = self.future_volume_midi_cc
717         if self.future_balance_midi_cc != None:
718             self.channel.balance_midi_cc = self.future_balance_midi_cc
719         if self.future_mute_midi_cc != None:
720             self.channel.mute_midi_cc = self.future_mute_midi_cc
721         self.channel.midi_scale = self.slider_scale.scale
722
723         self.on_volume_changed(self.slider_adjustment)
724         self.on_balance_changed(self.balance_adjustment)
725
726         entries = [Gtk.TargetEntry.new("OUTPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
727         self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
728                 Gdk.DragAction.MOVE)
729         self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
730         self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
731         self.connect_after("drag-data-received", self.on_drag_data_received)
732
733         if not hasattr(self, 'color'):
734             self.color = random_color()
735         set_background_color(self.label_name_event_box, self.css_name, self.color)
736         self.vbox.pack_start(self.label_name_event_box, True, True, 0)
737
738         self.create_fader()
739         self.create_buttons()
740
741         # add control groups to the input channels, and initialize them
742         # appropriately
743         for input_channel in self.app.channels:
744             ctlgroup = input_channel.add_control_group(self)
745             if self._init_muted_channels and input_channel.channel.name in self._init_muted_channels:
746                 ctlgroup.mute.set_active(True)
747             if self._init_solo_channels and input_channel.channel.name in self._init_solo_channels:
748                 ctlgroup.solo.set_active(True)
749             if self._init_prefader_channels and input_channel.channel.name in self._init_prefader_channels:
750                 ctlgroup.prefader.set_active(True)
751             if not input_channel.wide:
752                 ctlgroup.narrow()
753
754         self._init_muted_channels = None
755         self._init_solo_channels = None
756         self._init_prefader_channels = None
757
758         if not self.wide:
759             self.narrow()
760
761     def unrealize(self):
762         # remove control groups from input channels
763         for input_channel in self.app.channels:
764             input_channel.remove_control_group(self)
765         # then remove itself
766         super().unrealize()
767         self.channel.remove()
768         self.channel = None
769
770     def on_drag_data_get(self, widget, drag_context, data, info, time):
771         channel = widget.get_parent().get_parent()
772         data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
773
774     def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
775         source_name = data.get_data().decode('utf-8')
776         if source_name == self._channel_name:
777             return
778         self.emit("output-channel-order-changed", source_name, self._channel_name)
779
780     def on_channel_properties(self):
781         if not self.channel_properties_dialog:
782             self.channel_properties_dialog = OutputChannelPropertiesDialog(self, self.app)
783         self.channel_properties_dialog.show()
784         self.channel_properties_dialog.present()
785
786     def on_mute_toggled(self, button):
787         self.channel.out_mute = self.mute.get_active()
788
789     def midi_events_check(self):
790         if self.channel != None and self.channel.midi_in_got_events:
791             self.mute.set_active(self.channel.out_mute)
792             super().on_midi_event_received()
793
794     @classmethod
795     def serialization_name(cls):
796         return 'output_channel'
797
798     def serialize(self, object_backend):
799         object_backend.add_property("name", self.channel_name)
800         if self.stereo:
801             object_backend.add_property("type", "stereo")
802         else:
803             object_backend.add_property("type", "mono")
804         if self.display_solo_buttons:
805             object_backend.add_property("solo_buttons", "true")
806         muted_channels = []
807         solo_channels = []
808         prefader_in_channels = []
809         for input_channel in self.app.channels:
810             if self.channel.is_muted(input_channel.channel):
811                 muted_channels.append(input_channel)
812             if self.channel.is_solo(input_channel.channel):
813                 solo_channels.append(input_channel)
814             if self.channel.is_in_prefader(input_channel.channel):
815                 prefader_in_channels.append(input_channel)
816         if muted_channels:
817             object_backend.add_property('muted_channels', '|'.join([x.channel.name for x in muted_channels]))
818         if solo_channels:
819             object_backend.add_property('solo_channels', '|'.join([x.channel.name for x in solo_channels]))
820         if prefader_in_channels:
821             object_backend.add_property('prefader_channels', '|'.join([x.channel.name for x in prefader_in_channels]))
822         object_backend.add_property("color", self.color.to_string())
823         super().serialize(object_backend)
824
825     def unserialize_property(self, name, value):
826         if name == "name":
827             self.channel_name = str(value)
828             return True
829         if name == "type":
830             if value == "stereo":
831                 self.stereo = True
832                 return True
833             if value == "mono":
834                 self.stereo = False
835                 return True
836         if name == "solo_buttons":
837             if value == "true":
838                 self.display_solo_buttons = True
839                 return True
840         if name == 'muted_channels':
841             self._init_muted_channels = value.split('|')
842             return True
843         if name == 'solo_channels':
844             self._init_solo_channels = value.split('|')
845             return True
846         if name == 'prefader_channels':
847             self._init_prefader_channels = value.split('|')
848             return True
849         if name == 'color':
850             c = Gdk.RGBA()
851             c.parse(value)
852             self.color = c
853             return True
854         return super().unserialize_property(name, value)
855
856
857 class ChannelPropertiesDialog(Gtk.Dialog):
858     channel = None
859
860     def __init__(self, parent, app):
861         self.channel = parent
862         self.app = app
863         self.mixer = self.channel.mixer
864         Gtk.Dialog.__init__(self, 'Channel "%s" Properties' % self.channel.channel_name, app.window)
865         self.set_default_size(365, -1)
866
867         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
868         self.ok_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY)
869         self.set_default_response(Gtk.ResponseType.APPLY);
870
871         self.create_ui()
872         self.fill_ui()
873
874         self.connect('response', self.on_response_cb)
875         self.connect('delete-event', self.on_response_cb)
876
877     def create_frame(self, label, child, padding=8):
878         # need to pass an empty label, otherwise no label widget is created
879         frame = Gtk.Frame(label='')
880         frame.get_label_widget().set_markup('<b>%s</b>' % label)
881         frame.set_border_width(3)
882         frame.set_shadow_type(Gtk.ShadowType.NONE)
883
884         alignment = Gtk.Alignment.new(0.5, 0, 1, 1)
885         alignment.set_padding(padding, padding, padding, padding)
886         frame.add(alignment)
887         alignment.add(child)
888
889         return frame
890
891     def create_ui(self):
892         vbox = self.get_content_area()
893
894         self.properties_grid = grid = Gtk.Grid()
895         vbox.pack_start(self.create_frame('Properties', grid), True, True, 0)
896         grid.set_row_spacing(8)
897         grid.set_column_spacing(8)
898         grid.set_column_homogeneous(True)
899
900         name_label = Gtk.Label.new_with_mnemonic('_Name')
901         name_label.set_halign(Gtk.Align.START)
902         grid.attach(name_label, 0, 0, 1, 1)
903         self.entry_name = Gtk.Entry()
904         self.entry_name.set_activates_default(True)
905         self.entry_name.connect('changed', self.on_entry_name_changed)
906         name_label.set_mnemonic_widget(self.entry_name)
907         grid.attach(self.entry_name, 1, 0, 2, 1)
908
909         grid.attach(Gtk.Label(label='Mode', halign=Gtk.Align.START), 0, 1, 1, 1)
910         self.mono = Gtk.RadioButton.new_with_mnemonic(None, '_Mono')
911         self.stereo = Gtk.RadioButton.new_with_mnemonic_from_widget(self.mono, '_Stereo')
912         grid.attach(self.mono, 1, 1, 1, 1)
913         grid.attach(self.stereo, 2, 1, 1, 1)
914
915         grid = Gtk.Grid()
916         vbox.pack_start(self.create_frame('MIDI Control Changes', grid), True, True, 0)
917         grid.set_row_spacing(8)
918         grid.set_column_spacing(8)
919         grid.set_column_homogeneous(True)
920
921         cc_tooltip = "{} MIDI Control Change number (0-127, set to -1 to assign next free CC #)"
922         volume_label = Gtk.Label.new_with_mnemonic('_Volume')
923         volume_label.set_halign(Gtk.Align.START)
924         grid.attach(volume_label, 0, 0, 1, 1)
925         self.entry_volume_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
926         self.entry_volume_cc.set_tooltip_text(cc_tooltip.format("Volume"))
927         volume_label.set_mnemonic_widget(self.entry_volume_cc)
928         grid.attach(self.entry_volume_cc, 1, 0, 1, 1)
929         self.button_sense_midi_volume = Gtk.Button('Learn')
930         self.button_sense_midi_volume.connect('clicked',
931                         self.on_sense_midi_volume_clicked)
932         grid.attach(self.button_sense_midi_volume, 2, 0, 1, 1)
933
934         balance_label = Gtk.Label.new_with_mnemonic('_Balance')
935         balance_label.set_halign(Gtk.Align.START)
936         grid.attach(balance_label, 0, 1, 1, 1)
937         self.entry_balance_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
938         self.entry_balance_cc.set_tooltip_text(cc_tooltip.format("Balance"))
939         balance_label.set_mnemonic_widget(self.entry_balance_cc)
940         grid.attach(self.entry_balance_cc, 1, 1, 1, 1)
941         self.button_sense_midi_balance = Gtk.Button('Learn')
942         self.button_sense_midi_balance.connect('clicked',
943                         self.on_sense_midi_balance_clicked)
944         grid.attach(self.button_sense_midi_balance, 2, 1, 1, 1)
945
946         mute_label = Gtk.Label.new_with_mnemonic('M_ute')
947         mute_label.set_halign(Gtk.Align.START)
948         grid.attach(mute_label, 0, 2, 1, 1)
949         self.entry_mute_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
950         self.entry_mute_cc.set_tooltip_text(cc_tooltip.format("Mute"))
951         mute_label.set_mnemonic_widget(self.entry_mute_cc)
952         grid.attach(self.entry_mute_cc, 1, 2, 1, 1)
953         self.button_sense_midi_mute = Gtk.Button('Learn')
954         self.button_sense_midi_mute.connect('clicked',
955                         self.on_sense_midi_mute_clicked)
956         grid.attach(self.button_sense_midi_mute, 2, 2, 1, 1)
957
958         if (isinstance(self, NewChannelDialog) or (self.channel and
959             isinstance(self.channel, InputChannel))):
960             solo_label = Gtk.Label.new_with_mnemonic('S_olo')
961             solo_label.set_halign(Gtk.Align.START)
962             grid.attach(solo_label, 0, 3, 1, 1)
963             self.entry_solo_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
964             self.entry_solo_cc.set_tooltip_text(cc_tooltip.format("Solo"))
965             solo_label.set_mnemonic_widget(self.entry_solo_cc)
966             grid.attach(self.entry_solo_cc, 1, 3, 1, 1)
967             self.button_sense_midi_solo = Gtk.Button('Learn')
968             self.button_sense_midi_solo.connect('clicked',
969                             self.on_sense_midi_solo_clicked)
970             grid.attach(self.button_sense_midi_solo, 2, 3, 1, 1)
971
972         self.vbox.show_all()
973
974     def fill_ui(self):
975         self.entry_name.set_text(self.channel.channel_name)
976         if self.channel.channel.is_stereo:
977             self.stereo.set_active(True)
978         else:
979             self.mono.set_active(True)
980         self.mono.set_sensitive(False)
981         self.stereo.set_sensitive(False)
982         self.entry_volume_cc.set_value(self.channel.channel.volume_midi_cc)
983         self.entry_balance_cc.set_value(self.channel.channel.balance_midi_cc)
984         self.entry_mute_cc.set_value(self.channel.channel.mute_midi_cc)
985         if (self.channel and isinstance(self.channel, InputChannel)):
986             self.entry_solo_cc.set_value(self.channel.channel.solo_midi_cc)
987
988     def sense_popup_dialog(self, entry):
989         window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
990         window.set_destroy_with_parent(True)
991         window.set_transient_for(self)
992         window.set_decorated(False)
993         window.set_modal(True)
994         window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
995         window.set_border_width(10)
996
997         vbox = Gtk.Box(10, orientation=Gtk.Orientation.VERTICAL)
998         window.add(vbox)
999         window.timeout = 5
1000         vbox.pack_start(Gtk.Label(label='Please move the MIDI control you want to use for this function.'), True, True, 0)
1001         timeout_label = Gtk.Label(label='This window will close in 5 seconds')
1002         vbox.pack_start(timeout_label, True, True, 0)
1003         def close_sense_timeout(window, entry):
1004             window.timeout -= 1
1005             timeout_label.set_text('This window will close in %d seconds.' % window.timeout)
1006             if window.timeout == 0:
1007                 window.destroy()
1008                 entry.set_value(self.mixer.last_midi_channel)
1009                 return False
1010             return True
1011         window.show_all()
1012         GObject.timeout_add_seconds(1, close_sense_timeout, window, entry)
1013
1014     def on_sense_midi_volume_clicked(self, *args):
1015         self.mixer.last_midi_channel = int(self.entry_volume_cc.get_value())
1016         self.sense_popup_dialog(self.entry_volume_cc)
1017
1018     def on_sense_midi_balance_clicked(self, *args):
1019         self.mixer.last_midi_channel = int(self.entry_balance_cc.get_value())
1020         self.sense_popup_dialog(self.entry_balance_cc)
1021
1022     def on_sense_midi_mute_clicked(self, *args):
1023         self.mixer.last_midi_channel = int(self.entry_mute_cc.get_value())
1024         self.sense_popup_dialog(self.entry_mute_cc)
1025
1026     def on_sense_midi_solo_clicked(self, *args):
1027         self.mixer.last_midi_channel = int(self.entry_solo_cc.get_value())
1028         self.sense_popup_dialog(self.entry_solo_cc)
1029
1030     def on_response_cb(self, dlg, response_id, *args):
1031         self.channel.channel_properties_dialog = None
1032         name = self.entry_name.get_text()
1033         if response_id == Gtk.ResponseType.APPLY:
1034             if name != self.channel.channel_name:
1035                 self.channel.channel_name = name
1036             for control in ('volume', 'balance', 'mute', 'solo'):
1037                 widget = getattr(self, 'entry_{}_cc'.format(control), None)
1038                 if widget is not None:
1039                     value = int(widget.get_value())
1040                     if value != -1:
1041                         setattr(self.channel.channel, '{}_midi_cc'.format(control), value)
1042         self.destroy()
1043
1044     def on_entry_name_changed(self, entry):
1045         sensitive = False
1046         if len(entry.get_text()):
1047             if self.channel and self.channel.channel.name == entry.get_text():
1048                 sensitive = True
1049             elif entry.get_text() not in [x.channel.name for x in self.app.channels] + \
1050                         [x.channel.name for x in self.app.output_channels] + ['MAIN']:
1051                 sensitive = True
1052         self.ok_button.set_sensitive(sensitive)
1053
1054
1055 GObject.signal_new("output-channel-order-changed", OutputChannel,
1056                 GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
1057                 None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
1058
1059
1060 class NewChannelDialog(ChannelPropertiesDialog):
1061     def create_ui(self):
1062         ChannelPropertiesDialog.create_ui(self)
1063         self.add_initial_value_radio()
1064         self.vbox.show_all()
1065
1066     def add_initial_value_radio(self):
1067         grid = self.properties_grid
1068         grid.attach(Gtk.Label(label='Value', halign=Gtk.Align.START), 0, 2, 1, 1)
1069         self.minus_inf = Gtk.RadioButton.new_with_mnemonic(None, '-_Inf')
1070         self.zero_dB = Gtk.RadioButton.new_with_mnemonic_from_widget(self.minus_inf, '_0dB')
1071         grid.attach(self.minus_inf, 1, 2, 1, 1)
1072         grid.attach(self.zero_dB, 2, 2, 1, 1)
1073
1074
1075 class NewInputChannelDialog(NewChannelDialog):
1076     def __init__(self, app):
1077         Gtk.Dialog.__init__(self, 'New Input Channel', app.window)
1078         self.set_default_size(365, -1)
1079         self.mixer = app.mixer
1080         self.app = app
1081         self.create_ui()
1082
1083         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1084         self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
1085         self.ok_button.set_sensitive(False)
1086         self.set_default_response(Gtk.ResponseType.OK);
1087
1088     def fill_ui(self, **values):
1089         self.entry_name.set_text(values.get('name', ''))
1090         # don't set MIDI CCs to previously used values, because they
1091         # would overwrite existing mappings, if accepted.
1092         self.entry_volume_cc.set_value(-1)
1093         self.entry_balance_cc.set_value(-1)
1094         self.entry_mute_cc.set_value(-1)
1095         self.entry_solo_cc.set_value(-1)
1096         self.stereo.set_active(values.get('stereo', True))
1097         self.minus_inf.set_active(values.get('value', False))
1098         self.entry_name.grab_focus()
1099
1100     def get_result(self):
1101         return {
1102             'name': self.entry_name.get_text(),
1103             'stereo': self.stereo.get_active(),
1104             'volume_cc': int(self.entry_volume_cc.get_value()),
1105             'balance_cc': int(self.entry_balance_cc.get_value()),
1106             'mute_cc': int(self.entry_mute_cc.get_value()),
1107             'solo_cc': int(self.entry_solo_cc.get_value()),
1108             'value': self.minus_inf.get_active()
1109         }
1110
1111
1112 class OutputChannelPropertiesDialog(ChannelPropertiesDialog):
1113     def create_ui(self):
1114         ChannelPropertiesDialog.create_ui(self)
1115
1116         grid = self.properties_grid
1117         color_label = Gtk.Label.new_with_mnemonic('_Color')
1118         color_label.set_halign(Gtk.Align.START)
1119         grid.attach(color_label, 0, 3, 1, 1)
1120         self.color_chooser_button = Gtk.ColorButton()
1121         self.color_chooser_button.set_use_alpha(True)
1122         self.color_chooser_button.set_rgba(Gdk.RGBA(0, 0, 0, 0))
1123         color_label.set_mnemonic_widget(self.color_chooser_button)
1124         grid.attach(self.color_chooser_button, 1, 3, 2, 1)
1125
1126         vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1127         self.vbox.pack_start(self.create_frame('Input Channels', vbox), True, True, 0)
1128
1129         self.display_solo_buttons = Gtk.CheckButton.new_with_mnemonic('_Display solo buttons')
1130         vbox.pack_start(self.display_solo_buttons, True, True, 0)
1131
1132         self.vbox.show_all()
1133
1134     def fill_ui(self):
1135         ChannelPropertiesDialog.fill_ui(self)
1136         self.display_solo_buttons.set_active(self.channel.display_solo_buttons)
1137         self.color_chooser_button.set_rgba(self.channel.color)
1138
1139     def on_response_cb(self, dlg, response_id, *args):
1140         ChannelPropertiesDialog.on_response_cb(self, dlg, response_id, *args)
1141         if response_id == Gtk.ResponseType.APPLY:
1142             self.channel.display_solo_buttons = self.display_solo_buttons.get_active()
1143             self.channel.set_color(self.color_chooser_button.get_rgba())
1144             for inputchannel in self.app.channels:
1145                 inputchannel.update_control_group(self.channel)
1146
1147
1148 class NewOutputChannelDialog(NewChannelDialog, OutputChannelPropertiesDialog):
1149     def __init__(self, app):
1150         Gtk.Dialog.__init__(self, 'New Output Channel', app.window)
1151         self.mixer = app.mixer
1152         self.app = app
1153         OutputChannelPropertiesDialog.create_ui(self)
1154         self.add_initial_value_radio()
1155         self.vbox.show_all()
1156         self.set_default_size(365, -1)
1157
1158         # TODO: disable mode for output channels as mono output channels may
1159         # not be correctly handled yet.
1160         self.mono.set_sensitive(False)
1161         self.stereo.set_sensitive(False)
1162
1163         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
1164         self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
1165         self.ok_button.set_sensitive(False)
1166         self.set_default_response(Gtk.ResponseType.OK);
1167
1168     def fill_ui(self, **values):
1169         self.entry_name.set_text(values.get('name', ''))
1170         # don't set MIDI CCs to previously used values, because they
1171         # would overwrite existing mappings, if accepted.
1172         self.entry_volume_cc.set_value(-1)
1173         self.entry_balance_cc.set_value(-1)
1174         self.entry_mute_cc.set_value(-1)
1175         self.stereo.set_active(values.get('stereo', True))
1176         self.minus_inf.set_active(values.get('value', False))
1177         self.color_chooser_button.set_rgba(values.get('color', Gdk.RGBA(0, 0, 0, 0)))
1178         self.display_solo_buttons.set_active(values.get('display_solo_buttons', False))
1179         self.entry_name.grab_focus()
1180
1181     def get_result(self):
1182         return {
1183             'name': self.entry_name.get_text(),
1184             'stereo': self.stereo.get_active(),
1185             'volume_cc': int(self.entry_volume_cc.get_value()),
1186             'balance_cc': int(self.entry_balance_cc.get_value()),
1187             'mute_cc': int(self.entry_mute_cc.get_value()),
1188             'display_solo_buttons': self.display_solo_buttons.get_active(),
1189             'color': self.color_chooser_button.get_rgba(),
1190             'value': self.minus_inf.get_active()
1191         }
1192
1193
1194 class ControlGroup(Gtk.Alignment):
1195     def __init__(self, output_channel, input_channel):
1196         GObject.GObject.__init__(self)
1197         self.set(0.5, 0.5, 1, 1)
1198         self.output_channel = output_channel
1199         self.input_channel = input_channel
1200         self.app = input_channel.app
1201
1202         self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
1203         self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
1204         self.add(self.vbox)
1205         self.buttons_box = Gtk.Box(False, button_padding, orientation=Gtk.Orientation.HORIZONTAL)
1206
1207         set_background_color(self.vbox, output_channel.css_name, output_channel.color)
1208
1209         self.vbox.pack_start(self.hbox, True, True, button_padding)
1210         css = b"""
1211 .control_group {
1212     min-width: 0px;
1213     padding: 0px;
1214 }
1215
1216 .control_group #label,
1217 .control_group #mute,
1218 .control_group #pre_fader,
1219 .control_group #solo {
1220     font-size: smaller;
1221     padding: 0px .1em;
1222 }
1223 """
1224
1225         css_provider = Gtk.CssProvider()
1226         css_provider.load_from_data(css)
1227         context = Gtk.StyleContext()
1228         screen = Gdk.Screen.get_default()
1229         context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
1230         hbox_context = self.hbox.get_style_context()
1231         hbox_context.add_class('control_group')
1232
1233         name = output_channel.channel.name
1234         self.label = Gtk.Label(name)
1235         self.label.set_name("label")
1236         self.label.set_max_width_chars(self.input_channel.label_chars_narrow)
1237         self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
1238         if len(name) > self.input_channel.label_chars_narrow:
1239             self.label.set_tooltip_text(name)
1240         self.hbox.pack_start(self.label, False, False, button_padding)
1241         self.hbox.pack_end(self.buttons_box, False, False, button_padding)
1242         mute = Gtk.ToggleButton()
1243         mute.set_label("M")
1244         mute.set_name("mute")
1245         mute.set_tooltip_text("Mute output channel send")
1246         mute.connect("toggled", self.on_mute_toggled)
1247         self.mute = mute
1248         solo = Gtk.ToggleButton()
1249         solo.set_name("solo")
1250         solo.set_label("S")
1251         solo.set_tooltip_text("Solo output send")
1252         solo.connect("toggled", self.on_solo_toggled)
1253         self.solo = solo
1254         pre = Gtk.ToggleButton("P")
1255         pre.set_name("pre_fader")
1256         pre.set_tooltip_text("Pre (on) / Post (off) fader send")
1257         pre.connect("toggled", self.on_prefader_toggled)
1258         self.prefader = pre
1259         self.buttons_box.pack_start(pre, True, True, button_padding)
1260         self.buttons_box.pack_start(mute, True, True, button_padding)
1261         if self.output_channel.display_solo_buttons:
1262             self.buttons_box.pack_start(solo, True, True, button_padding)
1263
1264     def update(self):
1265         if self.output_channel.display_solo_buttons:
1266             if not self.solo in self.buttons_box.get_children():
1267                 self.buttons_box.pack_start(self.solo, True, True, button_padding)
1268                 self.solo.show()
1269         else:
1270             if self.solo in self.buttons_box.get_children():
1271                 self.buttons_box.remove(self.solo)
1272
1273         name = self.output_channel.channel.name
1274         self.label.set_text(name)
1275         if len(name) > self.input_channel.label_chars_narrow:
1276             self.label.set_tooltip_text(name)
1277
1278         set_background_color(self.vbox, self.output_channel.css_name, self.output_channel.color)
1279
1280     def on_mute_toggled(self, button):
1281         self.output_channel.channel.set_muted(self.input_channel.channel, button.get_active())
1282         self.app.update_monitor(self)
1283
1284     def on_solo_toggled(self, button):
1285         self.output_channel.channel.set_solo(self.input_channel.channel, button.get_active())
1286         self.app.update_monitor(self)
1287
1288     def on_prefader_toggled(self, button):
1289         self.output_channel.channel.set_in_prefader(self.input_channel.channel, button.get_active())
1290
1291     def narrow(self):
1292         self.hbox.remove(self.label)
1293         self.hbox.set_child_packing(self.buttons_box, True, True, button_padding, Gtk.PackType.END)
1294
1295     def widen(self):
1296         self.hbox.pack_start(self.label, False, False, button_padding)
1297         self.hbox.set_child_packing(self.buttons_box, False, False, button_padding, Gtk.PackType.END)