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