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