]> git.0d.be Git - jack_mixer.git/blobdiff - channel.py
Set version to 14 in preparation for next release
[jack_mixer.git] / channel.py
index 56232b9575d7709fc9f29258b3a423e040c79c0b..f5a3c86739b7c8820f123fd2217f46b66088f5f1 100644 (file)
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 
+import logging
+
 import gi
 from gi.repository import Gtk
 from gi.repository import Gdk
 from gi.repository import GObject
-import slider
-import meter
+from gi.repository import Pango
+
 import abspeak
+import meter
+import slider
 from serialization import SerializedObject
 
-try:
-    import phat
-except:
-    phat = None
+
+log = logging.getLogger(__name__)
+button_padding = 1
+CSS = b"""
+.top_label {
+    padding: 0px .1em;
+    min-height: 1.5rem;
+}
+
+.wide {
+    font-size: medium
+}
+
+.narrow {
+    font-size: smaller
+}
+
+button {
+    padding: 0px
+}
+
+.vbox_fader {
+    border: 1px inset #111;
+}
+
+.readout {
+    font-size: 80%;
+    margin: .1em;
+    padding: 0;
+    border: 1px inset #111;
+    color: white;
+}
+"""
+css_provider = Gtk.CssProvider()
+css_provider.load_from_data(CSS)
+context = Gtk.StyleContext()
+screen = Gdk.Screen.get_default()
+context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
+
+def get_text_color(background_color):
+    """Calculates the luminance of the given color (GdkRGBA)
+       and returns an appropriate text color."""
+    # luminance coefficients taken from section C-9 from
+    # http://www.faqs.org/faqs/graphics/colorspace-faq/
+    brightess = background_color.red * 0.212671 + \
+            background_color.green * 0.715160 + \
+            background_color.blue * 0.072169
+
+    if brightess > 0.5:
+        return 'black'
+    else:
+        return 'white'
+
+
+def set_background_color(widget, name, color):
+    color_string = color.to_string()
+    css = """
+    .%s {
+        background-color: %s;
+        color: %s;
+    }
+""" % (name, color_string, get_text_color(color))
+
+    css_provider = Gtk.CssProvider()
+    css_provider.load_from_data(css.encode('utf-8'))
+    context = Gtk.StyleContext()
+    screen = Gdk.Screen.get_default()
+    context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+
+    widget_context = widget.get_style_context()
+    widget_context.add_class(name)
+
+
+def random_color():
+    from random import uniform, seed
+    seed()
+    return Gdk.RGBA(uniform(0, 1), uniform(0, 1), uniform(0, 1), 1)
 
 
 class Channel(Gtk.VBox, SerializedObject):
-    '''Widget with slider and meter used as base class for more specific
-       channel widgets'''
+    """Widget with slider and meter used as base class for more specific
+       channel widgets"""
+
     monitor_button = None
+    num_instances = 0
 
-    def __init__(self, app, name, stereo):
+    def __init__(self, app, name, stereo, value = None):
         Gtk.VBox.__init__(self)
         self.app = app
         self.mixer = app.mixer
+        self.channel = None
         self.gui_factory = app.gui_factory
         self._channel_name = name
         self.stereo = stereo
+        self.initial_value = value
         self.meter_scale = self.gui_factory.get_default_meter_scale()
         self.slider_scale = self.gui_factory.get_default_slider_scale()
-        self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0)
-        self.balance_adjustment = Gtk.Adjustment(0.0, -1.0, 1.0, 0.02)
+        self.slider_adjustment = slider.AdjustmentdBFS(self.slider_scale, 0.0, 0.02)
+        self.balance_adjustment = slider.BalanceAdjustment()
+        self.post_fader_output_channel = None
         self.future_out_mute = None
         self.future_volume_midi_cc = None
         self.future_balance_midi_cc = None
         self.future_mute_midi_cc = None
         self.future_solo_midi_cc = None
+        self.css_name = "css_name_%d" % Channel.num_instances
+        self.label_name = None
+        self.wide = True
+        self.label_chars_wide = 12
+        self.label_chars_narrow = 7
+        Channel.num_instances += 1
 
     def get_channel_name(self):
         return self._channel_name
 
-    label_name = None
-    channel = None
-    post_fader_output_channel = None
     def set_channel_name(self, name):
         self.app.on_channel_rename(self._channel_name, name);
         self._channel_name = name
         if self.label_name:
             self.label_name.set_text(name)
+            if len(name) > (self.label_chars_wide if self.wide else self.label_chars_narrow):
+                self.label_name.set_tooltip_text(name)
         if self.channel:
             self.channel.name = name
         if self.post_fader_output_channel:
             self.post_fader_output_channel.name = "%s Out" % name;
     channel_name = property(get_channel_name, set_channel_name)
 
+    def create_balance_widget(self):
+        self.balance = slider.BalanceSlider(self.balance_adjustment, (20, 20), (0, 100))
+        self.balance.show()
+
+    def create_buttons(self):
+        # Mute, Solo and Monitor buttons
+        self.hbox_mutesolo = Gtk.Box(False, 0, orientation=Gtk.Orientation.HORIZONTAL)
+
+        self.mute = Gtk.ToggleButton()
+        self.mute.set_label("M")
+        self.mute.set_name("mute")
+        self.mute.set_active(self.channel.out_mute)
+        self.mute.connect("toggled", self.on_mute_toggled)
+        self.hbox_mutesolo.pack_start(self.mute, True, True, 0)
+
+        self.pack_start(self.hbox_mutesolo, False, False, 0)
+
+        self.monitor_button = Gtk.ToggleButton('MON')
+        self.monitor_button.connect('toggled', self.on_monitor_button_toggled)
+        self.pack_start(self.monitor_button, False, False, 0)
+
+    def create_fader(self):
+        # HBox for fader and meter
+        self.vbox_fader = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.vbox_fader.get_style_context().add_class('vbox_fader')
+
+        self.hbox_readouts = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        self.hbox_readouts.set_homogeneous(True)
+        self.hbox_readouts.pack_start(self.volume_digits, False, True, 0)
+        self.hbox_readouts.pack_start(self.abspeak, False, True, 0)
+        self.vbox_fader.pack_start(self.hbox_readouts, False, False, 0)
+
+        self.hbox_fader = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        self.hbox_fader.pack_start(self.slider, True, True, 0)
+        self.hbox_fader.pack_start(self.meter, True, True, 0)
+        self.vbox_fader.pack_start(self.hbox_fader, True, True, 0)
+        self.vbox_fader.pack_start(self.balance, False, True, 0)
+
+        self.pack_start(self.vbox_fader, True, True, 0)
+
+    def create_slider_widget(self):
+        parent = None
+        if self.slider:
+            parent = self.slider.get_parent()
+            self.slider.destroy()
+
+        if self.gui_factory.use_custom_widgets:
+            self.slider = slider.CustomSliderWidget(self.slider_adjustment)
+        else:
+            self.slider = slider.VolumeSlider(self.slider_adjustment)
+
+        if parent:
+            parent.pack_start(self.slider, True, True, 0)
+            parent.reorder_child(self.slider, 0)
+
+        self.slider.show()
+
     def realize(self):
-        #print "Realizing channel \"%s\"" % self.channel_name
+        log.debug('Realizing channel "%s".', self.channel_name)
         if self.future_out_mute != None:
             self.channel.out_mute = self.future_out_mute
 
-        self.slider_adjustment.connect("volume-changed", self.on_volume_changed)
-        self.balance_adjustment.connect("value-changed", self.on_balance_changed)
+        # Widgets
+        # Channel strip label
+        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.pack_start(self.vbox, False, True, 0)
+        self.label_name = Gtk.Label()
+        self.label_name.get_style_context().add_class('top_label')
+        self.label_name.set_text(self.channel_name)
+        self.label_name.set_max_width_chars(self.label_chars_wide if self.wide else
+                                            self.label_chars_narrow)
+        self.label_name.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
+        self.label_name_event_box = Gtk.EventBox()
+        self.label_name_event_box.connect('button-press-event', self.on_label_mouse)
+        self.label_name_event_box.add(self.label_name)
 
+        # Volume fader
         self.slider = None
         self.create_slider_widget()
+        self.create_balance_widget()
 
+        # Volume entry
+        self.volume_digits = Gtk.Entry()
+        self.volume_digits.set_has_frame(False)
+        self.volume_digits.set_width_chars(5)
+        self.volume_digits.set_property('xalign', 0.5)
+        self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed)
+        self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out)
+        self.volume_digits.get_style_context().add_class('readout')
+
+        # Peak level label
+        self.abspeak = abspeak.AbspeakWidget()
+        self.abspeak.connect("reset", self.on_abspeak_reset)
+        self.abspeak.connect("volume-adjust", self.on_abspeak_adjust)
+        self.abspeak.get_style_context().add_class('readout')
+
+        # Level meter
         if self.stereo:
             self.meter = meter.StereoMeterWidget(self.meter_scale)
         else:
             self.meter = meter.MonoMeterWidget(self.meter_scale)
-        self.on_vumeter_color_changed(self.gui_factory)
 
         self.meter.set_events(Gdk.EventMask.SCROLL_MASK)
+        self.on_vumeter_color_changed(self.gui_factory)
+
+        if self.initial_value != None:
+            if self.initial_value == True:
+                self.slider_adjustment.set_value(0)
+            else:
+                self.slider_adjustment.set_value_db(0)
+
+        self.slider_adjustment.connect("volume-changed", self.on_volume_changed)
+        self.slider_adjustment.connect("volume-changed-from-midi", self.on_volume_changed_from_midi)
+        self.balance_adjustment.connect("balance-changed", self.on_balance_changed)
 
         self.gui_factory.connect("default-meter-scale-changed", self.on_default_meter_scale_changed)
         self.gui_factory.connect("default-slider-scale-changed", self.on_default_slider_scale_changed)
@@ -94,64 +278,61 @@ class Channel(Gtk.VBox, SerializedObject):
         self.gui_factory.connect('vumeter-color-scheme-changed', self.on_vumeter_color_changed)
         self.gui_factory.connect('use-custom-widgets-changed', self.on_custom_widgets_changed)
 
-        self.abspeak = abspeak.AbspeakWidget()
-        self.abspeak.connect("reset", self.on_abspeak_reset)
-        self.abspeak.connect("volume-adjust", self.on_abspeak_adjust)
-
-        self.volume_digits = Gtk.Entry()
-        self.volume_digits.connect("key-press-event", self.on_volume_digits_key_pressed)
-        self.volume_digits.connect("focus-out-event", self.on_volume_digits_focus_out)
-
         self.connect("key-press-event", self.on_key_pressed)
         self.connect("scroll-event", self.on_scroll)
 
     def unrealize(self):
-        #print "Unrealizing channel \"%s\"" % self.channel_name
-        pass
+        log.debug('Unrealizing channel "%s".', self.channel_name)
 
-    def balance_preferred_width(self):
-        return 2000, 2000
+    def widen(self, flag=True):
+        self.wide = flag
+        ctx = self.label_name.get_style_context()
 
-
-    def create_balance_widget(self):
-        if self.gui_factory.use_custom_widgets and phat:
-            self.balance = phat.HFanSlider()
-            self.balance.set_default_value(0)
-            self.balance.set_adjustment(self.balance_adjustment)
+        if flag:
+            ctx.remove_class('narrow')
+            ctx.add_class('wide')
         else:
-            self.balance = Gtk.Scale()
-            self.balance.get_preferred_width = self.balance_preferred_width
-            self.balance.set_orientation(Gtk.Orientation.HORIZONTAL)
-            self.balance.set_adjustment(self.balance_adjustment)
-            self.balance.set_draw_value(False)
-        self.pack_start(self.balance, False, True, 0)
-        if self.monitor_button:
-            self.reorder_child(self.monitor_button, -1)
-        self.balance.show()
+            ctx.remove_class('wide')
+            ctx.add_class('narrow')
 
-    def create_slider_widget(self):
-        parent = None
-        if self.slider:
-            parent = self.slider.get_parent()
-            self.slider.destroy()
-        if self.gui_factory.use_custom_widgets:
-            self.slider = slider.CustomSliderWidget(self.slider_adjustment)
-        else:
-            self.slider = slider.GtkSlider(self.slider_adjustment)
-        if parent:
-            parent.pack_start(self.slider, True, True, 0)
-            parent.reorder_child(self.slider, 0)
-        self.slider.show()
+        label = self.label_name.get_label()
+        label_width = self.label_chars_wide if flag else self.label_chars_narrow
+        self.label_name.set_max_width_chars(label_width)
+
+        if len(label) > label_width:
+            self.label_name.set_tooltip_text(label)
+
+        self.meter.widen(flag)
+        self.hbox_readouts.set_orientation(
+            Gtk.Orientation.HORIZONTAL if flag else Gtk.Orientation.VERTICAL)
+
+    def narrow(self):
+        self.widen(False)
+
+    def on_label_mouse(self, widget, event):
+        if event.type == Gdk.EventType._2BUTTON_PRESS:
+            if event.button == 1:
+                self.on_channel_properties()
+            return True
+        elif (event.state & Gdk.ModifierType.CONTROL_MASK and
+              event.type == Gdk.EventType.BUTTON_PRESS and
+              event.button == 1):
+            if self.wide:
+                self.narrow()
+            else:
+                self.widen()
+            return True
 
     def on_default_meter_scale_changed(self, gui_factory, scale):
-        #print "Default meter scale change detected."
+        log.debug("Default meter scale change detected.")
         self.meter.set_scale(scale)
 
     def on_default_slider_scale_changed(self, gui_factory, scale):
-        #print "Default slider scale change detected."
+        log.debug("Default slider scale change detected.")
         self.slider_scale = scale
         self.slider_adjustment.set_scale(scale)
-        self.channel.midi_scale = self.slider_scale.scale
+        if self.channel:
+            self.channel.midi_scale = self.slider_scale.scale
 
     def on_vumeter_color_changed(self, gui_factory, *args):
         color = gui_factory.get_vumeter_color()
@@ -167,13 +348,13 @@ class Channel(Gtk.VBox, SerializedObject):
         self.create_slider_widget()
 
     def on_abspeak_adjust(self, abspeak, adjust):
-        #print "abspeak adjust %f" % adjust
+        log.debug("abspeak adjust %f", adjust)
         self.slider_adjustment.set_value_db(self.slider_adjustment.get_value_db() + adjust)
         self.channel.abspeak = None
         #self.update_volume(False)   # We want to update gui even if actual decibels have not changed (scale wrap for example)
 
     def on_abspeak_reset(self, abspeak):
-        #print "abspeak reset"
+        log.debug("abspeak reset")
         self.channel.abspeak = None
 
     def on_volume_digits_key_pressed(self, widget, event):
@@ -181,9 +362,9 @@ class Channel(Gtk.VBox, SerializedObject):
             db_text = self.volume_digits.get_text()
             try:
                 db = float(db_text)
-                #print "Volume digits confirmation \"%f dBFS\"" % db
-            except (ValueError), e:
-                #print "Volume digits confirmation ignore, reset to current"
+                log.debug('Volume digits confirmation "%f dBFS".', db)
+            except (ValueError) as e:
+                log.debug("Volume digits confirmation ignore, reset to current.")
                 self.update_volume(False)
                 return
             self.slider_adjustment.set_value_db(db)
@@ -191,17 +372,18 @@ class Channel(Gtk.VBox, SerializedObject):
             #self.update_volume(False)   # We want to update gui even if actual decibels have not changed (scale wrap for example)
 
     def on_volume_digits_focus_out(self, widget, event):
-        #print "volume digits focus out detected"
+        log.debug("Volume digits focus out detected.")
         self.update_volume(False)
 
     def read_meter(self):
         if not self.channel:
             return
         if self.stereo:
-            meter_left, meter_right = self.channel.meter
-            self.meter.set_values(meter_left, meter_right)
+            peak_left, peak_right, rms_left, rms_right = self.channel.kmeter
+            self.meter.set_values(peak_left, peak_right, rms_left, rms_right)
         else:
-            self.meter.set_value(self.channel.meter[0])
+            peak, rms = self.channel.kmeter
+            self.meter.set_values(peak, rms)
 
         self.abspeak.set_peak(self.channel.abspeak)
 
@@ -212,32 +394,36 @@ class Channel(Gtk.VBox, SerializedObject):
             self.slider_adjustment.step_up()
         return True
 
-    def update_volume(self, update_engine):
+    def update_volume(self, update_engine, from_midi = False):
         db = self.slider_adjustment.get_value_db()
 
         db_text = "%.2f" % db
         self.volume_digits.set_text(db_text)
 
         if update_engine:
-            self.channel.volume = db
+            if not from_midi:
+                self.channel.volume = db
             self.app.update_monitor(self)
 
     def on_volume_changed(self, adjustment):
         self.update_volume(True)
 
+    def on_volume_changed_from_midi(self, adjustment):
+        self.update_volume(True, from_midi = True)
+
     def on_balance_changed(self, adjustment):
         balance = self.balance_adjustment.get_value()
-        #print "%s balance: %f" % (self.channel_name, balance)
+        log.debug("%s balance: %f", self.channel_name, balance)
         self.channel.balance = balance
         self.app.update_monitor(self)
 
     def on_key_pressed(self, widget, event):
         if (event.keyval == Gdk.KEY_Up):
-            #print self.channel_name + " Up"
+            log.debug(self.channel_name + " Up")
             self.slider_adjustment.step_up()
             return True
         elif (event.keyval == Gdk.KEY_Down):
-            #print self.channel_name + " Down"
+            log.debug(self.channel_name + " Down")
             self.slider_adjustment.step_down()
             return True
 
@@ -246,6 +432,7 @@ class Channel(Gtk.VBox, SerializedObject):
     def serialize(self, object_backend):
         object_backend.add_property("volume", "%f" % self.slider_adjustment.get_value_db())
         object_backend.add_property("balance", "%f" % self.balance_adjustment.get_value())
+        object_backend.add_property("wide", "%s" % str(self.wide))
 
         if hasattr(self.channel, 'out_mute'):
             object_backend.add_property('out_mute', str(self.channel.out_mute))
@@ -258,7 +445,6 @@ class Channel(Gtk.VBox, SerializedObject):
         if self.channel.solo_midi_cc != -1:
             object_backend.add_property('solo_midi_cc', str(self.channel.solo_midi_cc))
 
-
     def unserialize_property(self, name, value):
         if name == "volume":
             self.slider_adjustment.set_value_db(float(value))
@@ -281,11 +467,14 @@ class Channel(Gtk.VBox, SerializedObject):
         if name == 'solo_midi_cc':
             self.future_solo_midi_cc = int(value)
             return True
+        if name == "wide":
+            self.wide = value == "True"
+            return True
         return False
 
     def on_midi_event_received(self, *args):
-        self.slider_adjustment.set_value_db(self.channel.volume)
-        self.balance_adjustment.set_value(self.channel.balance)
+        self.slider_adjustment.set_value_db(self.channel.volume, from_midi = True)
+        self.balance_adjustment.set_balance(self.channel.balance, from_midi = True)
 
     def on_monitor_button_toggled(self, button):
         if button.get_active():
@@ -297,23 +486,42 @@ class Channel(Gtk.VBox, SerializedObject):
                     channel.monitor_button.handler_unblock_by_func(
                                 channel.on_monitor_button_toggled)
             self.app.set_monitored_channel(self)
+        else:
+            if self.app._monitored_channel.channel.name == self.channel.name:
+                self.monitor_button.handler_block_by_func(self.on_monitor_button_toggled)
+                self.monitor_button.set_active(True)
+                self.monitor_button.handler_unblock_by_func(self.on_monitor_button_toggled)
 
     def set_monitored(self):
         if self.channel:
             self.app.set_monitored_channel(self)
         self.monitor_button.set_active(True)
 
+    def set_color(self, color):
+        self.color = color
+        set_background_color(self.label_name_event_box, self.css_name, self.color)
+
+
 class InputChannel(Channel):
     post_fader_output_channel = None
 
-    def __init__(self, app, name, stereo):
-        Channel.__init__(self, app, name, stereo)
+    def create_buttons(self):
+        super().create_buttons()
+        self.solo = Gtk.ToggleButton()
+        self.solo.set_label("S")
+        self.solo.set_name("solo")
+        self.solo.set_active(self.channel.solo)
+        self.solo.connect("toggled", self.on_solo_toggled)
+        self.hbox_mutesolo.pack_start(self.solo, True, True, 0)
 
     def realize(self):
         self.channel = self.mixer.add_channel(self.channel_name, self.stereo)
+
         if self.channel == None:
-            raise Exception,"Cannot create a channel"
-        Channel.realize(self)
+            raise Exception("Cannot create a channel")
+
+        super().realize()
+
         if self.future_volume_midi_cc != None:
             self.channel.volume_midi_cc = self.future_volume_midi_cc
         if self.future_balance_midi_cc != None:
@@ -330,70 +538,53 @@ class InputChannel(Channel):
         self.on_volume_changed(self.slider_adjustment)
         self.on_balance_changed(self.balance_adjustment)
 
-        # vbox child at upper part
-        self.vbox = Gtk.VBox()
-        self.pack_start(self.vbox, False, True, 0)
-        self.label_name = Gtk.Label()
-        self.label_name.set_text(self.channel_name)
-        self.label_name.set_size_request(0, -1)
-        self.label_name_event_box = Gtk.EventBox()
-        self.label_name_event_box.connect("button-press-event", self.on_label_mouse)
-        self.label_name_event_box.add(self.label_name)
-        self.vbox.pack_start(self.label_name_event_box, True, True, 0)
-#         self.label_stereo = Gtk.Label()
-#         if self.stereo:
-#             self.label_stereo.set_text("stereo")
-#         else:
-#             self.label_stereo.set_text("mono")
-#         self.label_stereo.set_size_request(0, -1)
-#         self.vbox.pack_start(self.label_stereo, True)
-
-        self.hbox_mutesolo = Gtk.HBox()
+        entries = [Gtk.TargetEntry.new("INPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
+        self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
+                Gdk.DragAction.MOVE)
+        self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
+        self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
+        self.connect_after("drag-data-received", self.on_drag_data_received)
 
-        self.mute = Gtk.ToggleButton()
-        self.mute.set_label("M")
-        self.mute.set_active(self.channel.out_mute)
-        self.mute.connect("toggled", self.on_mute_toggled)
-        self.hbox_mutesolo.pack_start(self.mute, True, True, 0)
+        self.vbox.pack_start(self.label_name_event_box, True, True, 0)
 
-        self.solo = Gtk.ToggleButton()
-        self.solo.set_label("S")
-        self.solo.set_active(self.channel.solo)
-        self.solo.connect("toggled", self.on_solo_toggled)
-        self.hbox_mutesolo.pack_start(self.solo, True, True, 0)
+        self.create_fader()
+        self.create_buttons()
 
-        self.vbox.pack_start(self.hbox_mutesolo, False, True, 0)
+        if not self.wide:
+            self.narrow()
 
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.abspeak);
-        self.pack_start(frame, False, True, 0)
+    def unrealize(self):
+        super().unrealize()
+        if self.post_fader_output_channel:
+            self.post_fader_output_channel.remove()
+            self.post_fader_output_channel = None
+        self.channel.remove()
+        self.channel = None
 
-        # hbox child at lower part
-        self.hbox = Gtk.HBox()
-        self.hbox.pack_start(self.slider, True, True, 0)
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.meter);
-        self.hbox.pack_start(frame, True, True, 0)
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.hbox);
-        self.pack_start(frame, True, True, 0)
+    def narrow(self):
+        super().narrow()
+        for cg in self.get_control_groups():
+            cg.narrow()
 
-        self.volume_digits.set_size_request(0, -1)
-        self.pack_start(self.volume_digits, False, True, 0)
+    def widen(self, flag=True):
+        super().widen(flag)
+        for cg in self.get_control_groups():
+            cg.widen()
 
-        self.create_balance_widget()
+    def on_drag_data_get(self, widget, drag_context, data, info, time):
+        channel = widget.get_parent().get_parent()
+        data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
 
-        self.monitor_button = Gtk.ToggleButton('MON')
-        self.monitor_button.connect('toggled', self.on_monitor_button_toggled)
-        self.pack_start(self.monitor_button, False, False, 0)
+    def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
+        source_name = data.get_data().decode('utf-8')
+        if source_name == self._channel_name:
+            return
+        self.emit("input-channel-order-changed", source_name, self._channel_name)
 
     def add_control_group(self, channel):
         control_group = ControlGroup(channel, self)
         control_group.show_all()
-        self.vbox.pack_start(control_group, False, True, 0)
+        self.vbox.pack_start(control_group, True, True, 0)
         return control_group
 
     def remove_control_group(self, channel):
@@ -407,32 +598,26 @@ class InputChannel(Channel):
                     control_group.update()
 
     def get_control_group(self, channel):
-        for control_group in self.vbox.get_children():
-            if isinstance(control_group, ControlGroup):
-                if control_group.output_channel is channel:
-                    return control_group
+        for control_group in self.get_control_groups():
+            if control_group.output_channel is channel:
+                return control_group
         return None
 
-    def unrealize(self):
-        Channel.unrealize(self)
-        if self.post_fader_output_channel:
-            self.post_fader_output_channel.remove()
-            self.post_fader_output_channel = None
-        self.channel.remove()
-        self.channel = None
+    def get_control_groups(self):
+        ctlgroups = []
+        for c in self.vbox.get_children():
+            if isinstance(c, ControlGroup):
+                ctlgroups.append(c)
+        return ctlgroups
 
     channel_properties_dialog = None
+
     def on_channel_properties(self):
         if not self.channel_properties_dialog:
             self.channel_properties_dialog = ChannelPropertiesDialog(self, self.app)
         self.channel_properties_dialog.show()
         self.channel_properties_dialog.present()
 
-    def on_label_mouse(self, widget, event):
-        if event.type == Gdk._2BUTTON_PRESS:
-            if event.button == 1:
-                self.on_channel_properties()
-
     def on_mute_toggled(self, button):
         self.channel.out_mute = self.mute.get_active()
 
@@ -443,7 +628,7 @@ class InputChannel(Channel):
         if hasattr(self, 'channel') and self.channel.midi_in_got_events:
             self.mute.set_active(self.channel.out_mute)
             self.solo.set_active(self.channel.solo)
-            Channel.on_midi_event_received(self)
+            super().on_midi_event_received()
 
     def on_solo_button_pressed(self, button, event, *args):
         if event.button == 3:
@@ -478,7 +663,7 @@ class InputChannel(Channel):
             object_backend.add_property("type", "stereo")
         else:
             object_backend.add_property("type", "mono")
-        Channel.serialize(self, object_backend)
+        super().serialize(object_backend)
 
     def unserialize_property(self, name, value):
         if name == "name":
@@ -491,28 +676,22 @@ class InputChannel(Channel):
             if value == "mono":
                 self.stereo = False
                 return True
-        return Channel.unserialize_property(self, name, value)
+        return super().unserialize_property(name, value)
+
 
+GObject.signal_new("input-channel-order-changed", InputChannel,
+                GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
+                None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
 
-available_colours = [
-    ('#000000', '#204c98', '#000000'),
-    ('#000000', '#542656', '#000000'),
-    ('#ef2929', '#3f5677', '#840000'),
-    ('#729fcf', '#7b4d5b', '#204a67'),
-    ('#8aa234', '#762945', '#4e7a06'),
-    ('#ad7fa8', '#0c5156', '#4c3556'),
-    ('#e9b96e', '#166280', '#6f4902'),
-]
 
 class OutputChannel(Channel):
-    colours = available_colours[:]
     _display_solo_buttons = False
 
     _init_muted_channels = None
     _init_solo_channels = None
+    _init_prefader_channels = None
 
-    def __init__(self, app, name, stereo):
-        Channel.__init__(self, app, name, stereo)
+    channel_properties_dialog = None
 
     def get_display_solo_buttons(self):
         return self._display_solo_buttons
@@ -527,9 +706,12 @@ class OutputChannel(Channel):
 
     def realize(self):
         self.channel = self.mixer.add_output_channel(self.channel_name, self.stereo)
+
         if self.channel == None:
-            raise Exception,"Cannot create a channel"
-        Channel.realize(self)
+            raise Exception("Cannot create a channel")
+
+        super().realize()
+
         if self.future_volume_midi_cc != None:
             self.channel.volume_midi_cc = self.future_volume_midi_cc
         if self.future_balance_midi_cc != None:
@@ -541,54 +723,20 @@ class OutputChannel(Channel):
         self.on_volume_changed(self.slider_adjustment)
         self.on_balance_changed(self.balance_adjustment)
 
-        # vbox child at upper part
-        self.vbox = Gtk.VBox()
-        self.pack_start(self.vbox, False, True, 0)
-        self.label_name = Gtk.Label()
-        self.label_name.set_text(self.channel_name)
-        self.label_name.set_size_request(0, -1)
-        self.label_name_event_box = Gtk.EventBox()
-        self.label_name_event_box.connect('button-press-event', self.on_label_mouse)
-        self.label_name_event_box.add(self.label_name)
-        if not self.colours:
-            OutputChannel.colours = available_colours[:]
-        for color in self.colours:
-            self.color_tuple = [Gdk.color_parse(color[x]) for x in range(3)]
-            self.colours.remove(color)
-            break
-        self.label_name_event_box.modify_bg(Gtk.StateType.NORMAL, self.color_tuple[1])
-        self.vbox.pack_start(self.label_name_event_box, True, True, 0)
-        self.mute = Gtk.ToggleButton()
-        self.mute.set_label("M")
-        self.mute.set_active(self.channel.out_mute)
-        self.mute.connect("toggled", self.on_mute_toggled)
-        self.vbox.pack_start(self.mute, False, True, 0)
-
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.abspeak);
-        self.vbox.pack_start(frame, False, True, 0)
-
-        # hbox child at lower part
-        self.hbox = Gtk.HBox()
-        self.hbox.pack_start(self.slider, True, True, 0)
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.meter);
-        self.hbox.pack_start(frame, True, True, 0)
-        frame = Gtk.Frame()
-        frame.set_shadow_type(Gtk.ShadowType.IN)
-        frame.add(self.hbox);
-        self.pack_start(frame, True, True, 0)
-
-        self.volume_digits.set_size_request(0, -1)
-        self.pack_start(self.volume_digits, False, True, 0)
+        entries = [Gtk.TargetEntry.new("OUTPUT_CHANNEL", Gtk.TargetFlags.SAME_APP, 0)]
+        self.label_name_event_box.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, entries,
+                Gdk.DragAction.MOVE)
+        self.label_name_event_box.connect("drag-data-get", self.on_drag_data_get)
+        self.drag_dest_set(Gtk.DestDefaults.ALL, entries, Gdk.DragAction.MOVE)
+        self.connect_after("drag-data-received", self.on_drag_data_received)
 
-        self.create_balance_widget()
+        if not hasattr(self, 'color'):
+            self.color = random_color()
+        set_background_color(self.label_name_event_box, self.css_name, self.color)
+        self.vbox.pack_start(self.label_name_event_box, True, True, 0)
 
-        self.monitor_button = Gtk.ToggleButton('MON')
-        self.monitor_button.connect('toggled', self.on_monitor_button_toggled)
-        self.pack_start(self.monitor_button, False, False, 0)
+        self.create_fader()
+        self.create_buttons()
 
         # add control groups to the input channels, and initialize them
         # appropriately
@@ -598,37 +746,50 @@ class OutputChannel(Channel):
                 ctlgroup.mute.set_active(True)
             if self._init_solo_channels and input_channel.channel.name in self._init_solo_channels:
                 ctlgroup.solo.set_active(True)
+            if self._init_prefader_channels and input_channel.channel.name in self._init_prefader_channels:
+                ctlgroup.prefader.set_active(True)
+            if not input_channel.wide:
+                ctlgroup.narrow()
+
         self._init_muted_channels = None
         self._init_solo_channels = None
+        self._init_prefader_channels = None
+
+        if not self.wide:
+            self.narrow()
+
+    def unrealize(self):
+        # remove control groups from input channels
+        for input_channel in self.app.channels:
+            input_channel.remove_control_group(self)
+        # then remove itself
+        super().unrealize()
+        self.channel.remove()
+        self.channel = None
+
+    def on_drag_data_get(self, widget, drag_context, data, info, time):
+        channel = widget.get_parent().get_parent()
+        data.set(data.get_target(), 8, channel._channel_name.encode('utf-8'))
+
+    def on_drag_data_received(self, widget, drag_context, x, y, data, info, time):
+        source_name = data.get_data().decode('utf-8')
+        if source_name == self._channel_name:
+            return
+        self.emit("output-channel-order-changed", source_name, self._channel_name)
 
-    channel_properties_dialog = None
     def on_channel_properties(self):
         if not self.channel_properties_dialog:
             self.channel_properties_dialog = OutputChannelPropertiesDialog(self, self.app)
         self.channel_properties_dialog.show()
         self.channel_properties_dialog.present()
 
-    def on_label_mouse(self, widget, event):
-        if event.type == Gdk._2BUTTON_PRESS:
-            if event.button == 1:
-                self.on_channel_properties()
-
     def on_mute_toggled(self, button):
         self.channel.out_mute = self.mute.get_active()
 
     def midi_events_check(self):
-        if self.channel.midi_in_got_events:
+        if self.channel != None and self.channel.midi_in_got_events:
             self.mute.set_active(self.channel.out_mute)
-            Channel.on_midi_event_received(self)
-
-    def unrealize(self):
-        # remove control groups from input channels
-        for input_channel in self.app.channels:
-            input_channel.remove_control_group(self)
-        # then remove itself
-        Channel.unrealize(self)
-        self.channel.remove()
-        self.channel = None
+            super().on_midi_event_received()
 
     @classmethod
     def serialization_name(cls):
@@ -644,16 +805,22 @@ class OutputChannel(Channel):
             object_backend.add_property("solo_buttons", "true")
         muted_channels = []
         solo_channels = []
+        prefader_in_channels = []
         for input_channel in self.app.channels:
             if self.channel.is_muted(input_channel.channel):
                 muted_channels.append(input_channel)
             if self.channel.is_solo(input_channel.channel):
                 solo_channels.append(input_channel)
+            if self.channel.is_in_prefader(input_channel.channel):
+                prefader_in_channels.append(input_channel)
         if muted_channels:
             object_backend.add_property('muted_channels', '|'.join([x.channel.name for x in muted_channels]))
         if solo_channels:
             object_backend.add_property('solo_channels', '|'.join([x.channel.name for x in solo_channels]))
-        Channel.serialize(self, object_backend)
+        if prefader_in_channels:
+            object_backend.add_property('prefader_channels', '|'.join([x.channel.name for x in prefader_in_channels]))
+        object_backend.add_property("color", self.color.to_string())
+        super().serialize(object_backend)
 
     def unserialize_property(self, name, value):
         if name == "name":
@@ -676,7 +843,16 @@ class OutputChannel(Channel):
         if name == 'solo_channels':
             self._init_solo_channels = value.split('|')
             return True
-        return Channel.unserialize_property(self, name, value)
+        if name == 'prefader_channels':
+            self._init_prefader_channels = value.split('|')
+            return True
+        if name == 'color':
+            c = Gdk.RGBA()
+            c.parse(value)
+            self.color = c
+            return True
+        return super().unserialize_property(name, value)
+
 
 class ChannelPropertiesDialog(Gtk.Dialog):
     channel = None
@@ -685,9 +861,8 @@ class ChannelPropertiesDialog(Gtk.Dialog):
         self.channel = parent
         self.app = app
         self.mixer = self.channel.mixer
-        GObject.GObject.__init__(self,
-                        'Channel "%s" Properties' % self.channel.channel_name,
-                        self.channel.gui_factory.topwindow)
+        Gtk.Dialog.__init__(self, 'Channel "%s" Properties' % self.channel.channel_name, app.window)
+        self.set_default_size(365, -1)
 
         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
         self.ok_button = self.add_button(Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY)
@@ -699,93 +874,100 @@ class ChannelPropertiesDialog(Gtk.Dialog):
         self.connect('response', self.on_response_cb)
         self.connect('delete-event', self.on_response_cb)
 
-    def create_frame(self, label, child):
-        frame = Gtk.Frame()
-        frame.set_label('')
-        frame.set_border_width(3)
-        #frame.set_shadow_type(Gtk.ShadowType.NONE)
+    def create_frame(self, label, child, padding=8):
+        # need to pass an empty label, otherwise no label widget is created
+        frame = Gtk.Frame(label='')
         frame.get_label_widget().set_markup('<b>%s</b>' % label)
+        frame.set_border_width(3)
+        frame.set_shadow_type(Gtk.ShadowType.NONE)
 
-        alignment = Gtk.Alignment.new(0, 0, 1, 1)
-        alignment.set_padding(0, 0, 12, 0)
+        alignment = Gtk.Alignment.new(0.5, 0, 1, 1)
+        alignment.set_padding(padding, padding, padding, padding)
         frame.add(alignment)
         alignment.add(child)
 
         return frame
 
     def create_ui(self):
-        vbox = Gtk.VBox()
-        self.vbox.add(vbox)
+        vbox = self.get_content_area()
 
-        table = Gtk.Table(2, 3, False)
-        vbox.pack_start(self.create_frame('Properties', table), True, True, 0)
-        table.set_row_spacings(5)
-        table.set_col_spacings(5)
+        self.properties_grid = grid = Gtk.Grid()
+        vbox.pack_start(self.create_frame('Properties', grid), True, True, 0)
+        grid.set_row_spacing(8)
+        grid.set_column_spacing(8)
+        grid.set_column_homogeneous(True)
 
-        table.attach(Gtk.Label(label='Name'), 0, 1, 0, 1)
+        name_label = Gtk.Label.new_with_mnemonic('_Name')
+        name_label.set_halign(Gtk.Align.START)
+        grid.attach(name_label, 0, 0, 1, 1)
         self.entry_name = Gtk.Entry()
         self.entry_name.set_activates_default(True)
         self.entry_name.connect('changed', self.on_entry_name_changed)
-        table.attach(self.entry_name, 1, 2, 0, 1)
-
-        table.attach(Gtk.Label(label='Mode'), 0, 1, 1, 2)
-        self.mode_hbox = Gtk.HBox()
-        table.attach(self.mode_hbox, 1, 2, 1, 2)
-        self.mono = Gtk.RadioButton(label='Mono')
-        self.stereo = Gtk.RadioButton(label='Stereo', group=self.mono)
-        self.mode_hbox.pack_start(self.mono, True, True, 0)
-        self.mode_hbox.pack_start(self.stereo, True, True, 0)
-
-        table = Gtk.Table(2, 3, False)
-        vbox.pack_start(self.create_frame('MIDI Control Channels', table), True, True, 0)
-        table.set_row_spacings(5)
-        table.set_col_spacings(5)
-
-        table.attach(Gtk.Label(label='Volume'), 0, 1, 0, 1)
-        self.entry_volume_cc = Gtk.Entry()
-        self.entry_volume_cc.set_activates_default(True)
-        self.entry_volume_cc.set_editable(False)
-        self.entry_volume_cc.set_width_chars(3)
-        table.attach(self.entry_volume_cc, 1, 2, 0, 1)
-        self.button_sense_midi_volume = Gtk.Button('Autoset')
+        name_label.set_mnemonic_widget(self.entry_name)
+        grid.attach(self.entry_name, 1, 0, 2, 1)
+
+        grid.attach(Gtk.Label(label='Mode', halign=Gtk.Align.START), 0, 1, 1, 1)
+        self.mono = Gtk.RadioButton.new_with_mnemonic(None, '_Mono')
+        self.stereo = Gtk.RadioButton.new_with_mnemonic_from_widget(self.mono, '_Stereo')
+        grid.attach(self.mono, 1, 1, 1, 1)
+        grid.attach(self.stereo, 2, 1, 1, 1)
+
+        grid = Gtk.Grid()
+        vbox.pack_start(self.create_frame('MIDI Control Changes', grid), True, True, 0)
+        grid.set_row_spacing(8)
+        grid.set_column_spacing(8)
+        grid.set_column_homogeneous(True)
+
+        cc_tooltip = "{} MIDI Control Change number (0-127, set to -1 to assign next free CC #)"
+        volume_label = Gtk.Label.new_with_mnemonic('_Volume')
+        volume_label.set_halign(Gtk.Align.START)
+        grid.attach(volume_label, 0, 0, 1, 1)
+        self.entry_volume_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
+        self.entry_volume_cc.set_tooltip_text(cc_tooltip.format("Volume"))
+        volume_label.set_mnemonic_widget(self.entry_volume_cc)
+        grid.attach(self.entry_volume_cc, 1, 0, 1, 1)
+        self.button_sense_midi_volume = Gtk.Button('Learn')
         self.button_sense_midi_volume.connect('clicked',
                         self.on_sense_midi_volume_clicked)
-        table.attach(self.button_sense_midi_volume, 2, 3, 0, 1)
-
-        table.attach(Gtk.Label(label='Balance'), 0, 1, 1, 2)
-        self.entry_balance_cc = Gtk.Entry()
-        self.entry_balance_cc.set_activates_default(True)
-        self.entry_balance_cc.set_width_chars(3)
-        self.entry_balance_cc.set_editable(False)
-        table.attach(self.entry_balance_cc, 1, 2, 1, 2)
-        self.button_sense_midi_balance = Gtk.Button('Autoset')
+        grid.attach(self.button_sense_midi_volume, 2, 0, 1, 1)
+
+        balance_label = Gtk.Label.new_with_mnemonic('_Balance')
+        balance_label.set_halign(Gtk.Align.START)
+        grid.attach(balance_label, 0, 1, 1, 1)
+        self.entry_balance_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
+        self.entry_balance_cc.set_tooltip_text(cc_tooltip.format("Balance"))
+        balance_label.set_mnemonic_widget(self.entry_balance_cc)
+        grid.attach(self.entry_balance_cc, 1, 1, 1, 1)
+        self.button_sense_midi_balance = Gtk.Button('Learn')
         self.button_sense_midi_balance.connect('clicked',
                         self.on_sense_midi_balance_clicked)
-        table.attach(self.button_sense_midi_balance, 2, 3, 1, 2)
-
-        table.attach(Gtk.Label(label='Mute'), 0, 1, 2, 3)
-        self.entry_mute_cc = Gtk.Entry()
-        self.entry_mute_cc.set_activates_default(True)
-        self.entry_mute_cc.set_editable(False)
-        self.entry_mute_cc.set_width_chars(3)
-        table.attach(self.entry_mute_cc, 1, 2, 2, 3)
-        self.button_sense_midi_mute = Gtk.Button('Autoset')
+        grid.attach(self.button_sense_midi_balance, 2, 1, 1, 1)
+
+        mute_label = Gtk.Label.new_with_mnemonic('M_ute')
+        mute_label.set_halign(Gtk.Align.START)
+        grid.attach(mute_label, 0, 2, 1, 1)
+        self.entry_mute_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
+        self.entry_mute_cc.set_tooltip_text(cc_tooltip.format("Mute"))
+        mute_label.set_mnemonic_widget(self.entry_mute_cc)
+        grid.attach(self.entry_mute_cc, 1, 2, 1, 1)
+        self.button_sense_midi_mute = Gtk.Button('Learn')
         self.button_sense_midi_mute.connect('clicked',
                         self.on_sense_midi_mute_clicked)
-        table.attach(self.button_sense_midi_mute, 2, 3, 2, 3)
+        grid.attach(self.button_sense_midi_mute, 2, 2, 1, 1)
 
         if (isinstance(self, NewChannelDialog) or (self.channel and
             isinstance(self.channel, InputChannel))):
-            table.attach(Gtk.Label(label='Solo'), 0, 1, 3, 4)
-            self.entry_solo_cc = Gtk.Entry()
-            self.entry_solo_cc.set_activates_default(True)
-            self.entry_solo_cc.set_editable(False)
-            self.entry_solo_cc.set_width_chars(3)
-            table.attach(self.entry_solo_cc, 1, 2, 3, 4)
-            self.button_sense_midi_solo = Gtk.Button('Autoset')
+            solo_label = Gtk.Label.new_with_mnemonic('S_olo')
+            solo_label.set_halign(Gtk.Align.START)
+            grid.attach(solo_label, 0, 3, 1, 1)
+            self.entry_solo_cc = Gtk.SpinButton.new_with_range(-1, 127, 1)
+            self.entry_solo_cc.set_tooltip_text(cc_tooltip.format("Solo"))
+            solo_label.set_mnemonic_widget(self.entry_solo_cc)
+            grid.attach(self.entry_solo_cc, 1, 3, 1, 1)
+            self.button_sense_midi_solo = Gtk.Button('Learn')
             self.button_sense_midi_solo.connect('clicked',
                             self.on_sense_midi_solo_clicked)
-            table.attach(self.button_sense_midi_solo, 2, 3, 3, 4)
+            grid.attach(self.button_sense_midi_solo, 2, 3, 1, 1)
 
         self.vbox.show_all()
 
@@ -795,15 +977,16 @@ class ChannelPropertiesDialog(Gtk.Dialog):
             self.stereo.set_active(True)
         else:
             self.mono.set_active(True)
-        self.mode_hbox.set_sensitive(False)
-        self.entry_volume_cc.set_text('%s' % self.channel.channel.volume_midi_cc)
-        self.entry_balance_cc.set_text('%s' % self.channel.channel.balance_midi_cc)
-        self.entry_mute_cc.set_text('%s' % self.channel.channel.mute_midi_cc)
+        self.mono.set_sensitive(False)
+        self.stereo.set_sensitive(False)
+        self.entry_volume_cc.set_value(self.channel.channel.volume_midi_cc)
+        self.entry_balance_cc.set_value(self.channel.channel.balance_midi_cc)
+        self.entry_mute_cc.set_value(self.channel.channel.mute_midi_cc)
         if (self.channel and isinstance(self.channel, InputChannel)):
-            self.entry_solo_cc.set_text('%s' % self.channel.channel.solo_midi_cc)
+            self.entry_solo_cc.set_value(self.channel.channel.solo_midi_cc)
 
     def sense_popup_dialog(self, entry):
-        window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
+        window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
         window.set_destroy_with_parent(True)
         window.set_transient_for(self)
         window.set_decorated(False)
@@ -811,10 +994,10 @@ class ChannelPropertiesDialog(Gtk.Dialog):
         window.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
         window.set_border_width(10)
 
-        vbox = Gtk.VBox(10)
+        vbox = Gtk.Box(10, orientation=Gtk.Orientation.VERTICAL)
         window.add(vbox)
         window.timeout = 5
-        vbox.pack_start(Gtk.Label('Please move the MIDI control you want to use for this function.', True, True, 0))
+        vbox.pack_start(Gtk.Label(label='Please move the MIDI control you want to use for this function.'), True, True, 0)
         timeout_label = Gtk.Label(label='This window will close in 5 seconds')
         vbox.pack_start(timeout_label, True, True, 0)
         def close_sense_timeout(window, entry):
@@ -822,53 +1005,40 @@ class ChannelPropertiesDialog(Gtk.Dialog):
             timeout_label.set_text('This window will close in %d seconds.' % window.timeout)
             if window.timeout == 0:
                 window.destroy()
-                entry.set_text('%s' % self.mixer.last_midi_channel)
+                entry.set_value(self.mixer.last_midi_channel)
                 return False
             return True
         window.show_all()
         GObject.timeout_add_seconds(1, close_sense_timeout, window, entry)
 
     def on_sense_midi_volume_clicked(self, *args):
-        self.mixer.last_midi_channel = int(self.entry_volume_cc.get_text())
+        self.mixer.last_midi_channel = int(self.entry_volume_cc.get_value())
         self.sense_popup_dialog(self.entry_volume_cc)
 
     def on_sense_midi_balance_clicked(self, *args):
-        self.mixer.last_midi_channel = int(self.entry_balance_cc.get_text())
+        self.mixer.last_midi_channel = int(self.entry_balance_cc.get_value())
         self.sense_popup_dialog(self.entry_balance_cc)
 
     def on_sense_midi_mute_clicked(self, *args):
-        self.mixer.last_midi_channel = int(self.entry_mute_cc.get_text())
+        self.mixer.last_midi_channel = int(self.entry_mute_cc.get_value())
         self.sense_popup_dialog(self.entry_mute_cc)
 
     def on_sense_midi_solo_clicked(self, *args):
-        self.mixer.last_midi_channel = int(self.entry_solo_cc.get_text())
+        self.mixer.last_midi_channel = int(self.entry_solo_cc.get_value())
         self.sense_popup_dialog(self.entry_solo_cc)
 
     def on_response_cb(self, dlg, response_id, *args):
         self.channel.channel_properties_dialog = None
         name = self.entry_name.get_text()
         if response_id == Gtk.ResponseType.APPLY:
-            self.channel.channel_name = name
-            try:
-                if self.entry_volume_cc.get_text() != '-1':
-                    self.channel.channel.volume_midi_cc = int(self.entry_volume_cc.get_text())
-            except ValueError:
-                pass
-            try:
-                if self.entry_balance_cc.get_text() != '-1':
-                    self.channel.channel.balance_midi_cc = int(self.entry_balance_cc.get_text())
-            except ValueError:
-                pass
-            try:
-                if self.entry_mute_cc.get_text() != '-1':
-                    self.channel.channel.mute_midi_cc = int(self.entry_mute_cc.get_text())
-            except ValueError:
-                pass
-            try:
-                if hasattr(self, 'entry_solo_cc') and self.entry_solo_cc.get_text() != '-1':
-                    self.channel.channel.solo_midi_cc = int(self.entry_solo_cc.get_text())
-            except ValueError:
-                pass
+            if name != self.channel.channel_name:
+                self.channel.channel_name = name
+            for control in ('volume', 'balance', 'mute', 'solo'):
+                widget = getattr(self, 'entry_{}_cc'.format(control), None)
+                if widget is not None:
+                    value = int(widget.get_value())
+                    if value != -1:
+                        setattr(self.channel.channel, '{}_midi_cc'.format(control), value)
         self.destroy()
 
     def on_entry_name_changed(self, entry):
@@ -882,44 +1052,81 @@ class ChannelPropertiesDialog(Gtk.Dialog):
         self.ok_button.set_sensitive(sensitive)
 
 
+GObject.signal_new("output-channel-order-changed", OutputChannel,
+                GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.ACTION,
+                None, [GObject.TYPE_STRING, GObject.TYPE_STRING])
+
+
 class NewChannelDialog(ChannelPropertiesDialog):
+    def create_ui(self):
+        ChannelPropertiesDialog.create_ui(self)
+        self.add_initial_value_radio()
+        self.vbox.show_all()
+
+    def add_initial_value_radio(self):
+        grid = self.properties_grid
+        grid.attach(Gtk.Label(label='Value', halign=Gtk.Align.START), 0, 2, 1, 1)
+        self.minus_inf = Gtk.RadioButton.new_with_mnemonic(None, '-_Inf')
+        self.zero_dB = Gtk.RadioButton.new_with_mnemonic_from_widget(self.minus_inf, '_0dB')
+        grid.attach(self.minus_inf, 1, 2, 1, 1)
+        grid.attach(self.zero_dB, 2, 2, 1, 1)
+
+
+class NewInputChannelDialog(NewChannelDialog):
     def __init__(self, app):
-        Gtk.Dialog.__init__(self, 'New Channel', app.window)
+        Gtk.Dialog.__init__(self, 'New Input Channel', app.window)
+        self.set_default_size(365, -1)
         self.mixer = app.mixer
         self.app = app
         self.create_ui()
-        self.fill_ui()
-
-        self.stereo.set_active(True) # default to stereo
 
         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
         self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
         self.ok_button.set_sensitive(False)
         self.set_default_response(Gtk.ResponseType.OK);
 
-    def fill_ui(self):
-        self.entry_volume_cc.set_text('-1')
-        self.entry_balance_cc.set_text('-1')
-        self.entry_mute_cc.set_text('-1')
-        self.entry_solo_cc.set_text('-1')
+    def fill_ui(self, **values):
+        self.entry_name.set_text(values.get('name', ''))
+        # don't set MIDI CCs to previously used values, because they
+        # would overwrite existing mappings, if accepted.
+        self.entry_volume_cc.set_value(-1)
+        self.entry_balance_cc.set_value(-1)
+        self.entry_mute_cc.set_value(-1)
+        self.entry_solo_cc.set_value(-1)
+        self.stereo.set_active(values.get('stereo', True))
+        self.minus_inf.set_active(values.get('value', False))
+        self.entry_name.grab_focus()
 
     def get_result(self):
-        return {'name': self.entry_name.get_text(),
-                'stereo': self.stereo.get_active(),
-                'volume_cc': self.entry_volume_cc.get_text(),
-                'balance_cc': self.entry_balance_cc.get_text(),
-                'mute_cc': self.entry_mute_cc.get_text(),
-                'solo_cc': self.entry_solo_cc.get_text()
-               }
+        return {
+            'name': self.entry_name.get_text(),
+            'stereo': self.stereo.get_active(),
+            'volume_cc': int(self.entry_volume_cc.get_value()),
+            'balance_cc': int(self.entry_balance_cc.get_value()),
+            'mute_cc': int(self.entry_mute_cc.get_value()),
+            'solo_cc': int(self.entry_solo_cc.get_value()),
+            'value': self.minus_inf.get_active()
+        }
+
 
 class OutputChannelPropertiesDialog(ChannelPropertiesDialog):
     def create_ui(self):
         ChannelPropertiesDialog.create_ui(self)
 
-        vbox = Gtk.VBox()
+        grid = self.properties_grid
+        color_label = Gtk.Label.new_with_mnemonic('_Color')
+        color_label.set_halign(Gtk.Align.START)
+        grid.attach(color_label, 0, 3, 1, 1)
+        self.color_chooser_button = Gtk.ColorButton()
+        self.color_chooser_button.set_use_alpha(True)
+        self.color_chooser_button.set_rgba(Gdk.RGBA(0, 0, 0, 0))
+        color_label.set_mnemonic_widget(self.color_chooser_button)
+        grid.attach(self.color_chooser_button, 1, 3, 2, 1)
+
+        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
         self.vbox.pack_start(self.create_frame('Input Channels', vbox), True, True, 0)
 
-        self.display_solo_buttons = Gtk.CheckButton('Display solo buttons')
+        self.display_solo_buttons = Gtk.CheckButton.new_with_mnemonic('_Display solo buttons')
         vbox.pack_start(self.display_solo_buttons, True, True, 0)
 
         self.vbox.show_all()
@@ -927,91 +1134,148 @@ class OutputChannelPropertiesDialog(ChannelPropertiesDialog):
     def fill_ui(self):
         ChannelPropertiesDialog.fill_ui(self)
         self.display_solo_buttons.set_active(self.channel.display_solo_buttons)
+        self.color_chooser_button.set_rgba(self.channel.color)
 
     def on_response_cb(self, dlg, response_id, *args):
+        ChannelPropertiesDialog.on_response_cb(self, dlg, response_id, *args)
         if response_id == Gtk.ResponseType.APPLY:
             self.channel.display_solo_buttons = self.display_solo_buttons.get_active()
-        ChannelPropertiesDialog.on_response_cb(self, dlg, response_id, *args)
+            self.channel.set_color(self.color_chooser_button.get_rgba())
+            for inputchannel in self.app.channels:
+                inputchannel.update_control_group(self.channel)
 
 
-class NewOutputChannelDialog(OutputChannelPropertiesDialog):
+class NewOutputChannelDialog(NewChannelDialog, OutputChannelPropertiesDialog):
     def __init__(self, app):
         Gtk.Dialog.__init__(self, 'New Output Channel', app.window)
         self.mixer = app.mixer
         self.app = app
-        self.create_ui()
-        self.fill_ui()
+        OutputChannelPropertiesDialog.create_ui(self)
+        self.add_initial_value_radio()
+        self.vbox.show_all()
+        self.set_default_size(365, -1)
 
         # TODO: disable mode for output channels as mono output channels may
         # not be correctly handled yet.
-        self.mode_hbox.set_sensitive(False)
-        self.stereo.set_active(True) # default to stereo
+        self.mono.set_sensitive(False)
+        self.stereo.set_sensitive(False)
 
         self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
         self.ok_button = self.add_button(Gtk.STOCK_ADD, Gtk.ResponseType.OK)
         self.ok_button.set_sensitive(False)
         self.set_default_response(Gtk.ResponseType.OK);
 
-    def fill_ui(self):
-        self.entry_volume_cc.set_text('-1')
-        self.entry_balance_cc.set_text('-1')
-        self.entry_mute_cc.set_text('-1')
+    def fill_ui(self, **values):
+        self.entry_name.set_text(values.get('name', ''))
+        # don't set MIDI CCs to previously used values, because they
+        # would overwrite existing mappings, if accepted.
+        self.entry_volume_cc.set_value(-1)
+        self.entry_balance_cc.set_value(-1)
+        self.entry_mute_cc.set_value(-1)
+        self.stereo.set_active(values.get('stereo', True))
+        self.minus_inf.set_active(values.get('value', False))
+        self.color_chooser_button.set_rgba(values.get('color', Gdk.RGBA(0, 0, 0, 0)))
+        self.display_solo_buttons.set_active(values.get('display_solo_buttons', False))
+        self.entry_name.grab_focus()
 
     def get_result(self):
-        return {'name': self.entry_name.get_text(),
-                'stereo': self.stereo.get_active(),
-                'volume_cc': self.entry_volume_cc.get_text(),
-                'balance_cc': self.entry_balance_cc.get_text(),
-                'mute_cc': self.entry_mute_cc.get_text(),
-                'display_solo_buttons': self.display_solo_buttons.get_active(),
-               }
+        return {
+            'name': self.entry_name.get_text(),
+            'stereo': self.stereo.get_active(),
+            'volume_cc': int(self.entry_volume_cc.get_value()),
+            'balance_cc': int(self.entry_balance_cc.get_value()),
+            'mute_cc': int(self.entry_mute_cc.get_value()),
+            'display_solo_buttons': self.display_solo_buttons.get_active(),
+            'color': self.color_chooser_button.get_rgba(),
+            'value': self.minus_inf.get_active()
+        }
 
 
 class ControlGroup(Gtk.Alignment):
     def __init__(self, output_channel, input_channel):
         GObject.GObject.__init__(self)
-        self.set(0.5, 0.5, 0, 0)
+        self.set(0.5, 0.5, 1, 1)
         self.output_channel = output_channel
         self.input_channel = input_channel
         self.app = input_channel.app
 
-        hbox = Gtk.HBox()
-        self.hbox = hbox
-        self.add(hbox)
-
-        mute_button = Gtk.ToggleButton()
-        mute_button.set_label("M")
-        mute_button.connect("toggled", self.on_mute_toggled)
-        mute = Gtk.EventBox()
+        self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
+        self.vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
+        self.add(self.vbox)
+        self.buttons_box = Gtk.Box(False, button_padding, orientation=Gtk.Orientation.HORIZONTAL)
+
+        set_background_color(self.vbox, output_channel.css_name, output_channel.color)
+
+        self.vbox.pack_start(self.hbox, True, True, button_padding)
+        css = b"""
+.control_group {
+    min-width: 0px;
+    padding: 0px;
+}
+
+.control_group #label,
+.control_group #mute,
+.control_group #pre_fader,
+.control_group #solo {
+    font-size: smaller;
+    padding: 0px .1em;
+}
+"""
+
+        css_provider = Gtk.CssProvider()
+        css_provider.load_from_data(css)
+        context = Gtk.StyleContext()
+        screen = Gdk.Screen.get_default()
+        context.add_provider_for_screen(screen, css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
+        hbox_context = self.hbox.get_style_context()
+        hbox_context.add_class('control_group')
+
+        name = output_channel.channel.name
+        self.label = Gtk.Label(name)
+        self.label.set_name("label")
+        self.label.set_max_width_chars(self.input_channel.label_chars_narrow)
+        self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE)
+        if len(name) > self.input_channel.label_chars_narrow:
+            self.label.set_tooltip_text(name)
+        self.hbox.pack_start(self.label, False, False, button_padding)
+        self.hbox.pack_end(self.buttons_box, False, False, button_padding)
+        mute = Gtk.ToggleButton()
+        mute.set_label("M")
+        mute.set_name("mute")
+        mute.set_tooltip_text("Mute output channel send")
+        mute.connect("toggled", self.on_mute_toggled)
         self.mute = mute
-        mute.add(mute_button)
-        hbox.pack_start(mute, False, True, 0)
-
-        solo_button = Gtk.ToggleButton()
-        solo_button.set_label("S")
-        solo_button.set_name("button1")
-        solo_button.connect("toggled", self.on_solo_toggled)
-        solo = Gtk.EventBox()
+        solo = Gtk.ToggleButton()
+        solo.set_name("solo")
+        solo.set_label("S")
+        solo.set_tooltip_text("Solo output send")
+        solo.connect("toggled", self.on_solo_toggled)
         self.solo = solo
-        solo.add(solo_button)
+        pre = Gtk.ToggleButton("P")
+        pre.set_name("pre_fader")
+        pre.set_tooltip_text("Pre (on) / Post (off) fader send")
+        pre.connect("toggled", self.on_prefader_toggled)
+        self.prefader = pre
+        self.buttons_box.pack_start(pre, True, True, button_padding)
+        self.buttons_box.pack_start(mute, True, True, button_padding)
         if self.output_channel.display_solo_buttons:
-            hbox.pack_start(solo, True, True, 0)
-
-        mute.modify_bg(Gtk.StateType.PRELIGHT, output_channel.color_tuple[0])
-        mute.modify_bg(Gtk.StateType.NORMAL, output_channel.color_tuple[1])
-        mute.modify_bg(Gtk.StateType.ACTIVE, output_channel.color_tuple[2])
-        solo.modify_bg(Gtk.StateType.PRELIGHT, output_channel.color_tuple[0])
-        solo.modify_bg(Gtk.StateType.NORMAL, output_channel.color_tuple[1])
-        solo.modify_bg(Gtk.StateType.ACTIVE, output_channel.color_tuple[2])
+            self.buttons_box.pack_start(solo, True, True, button_padding)
 
     def update(self):
         if self.output_channel.display_solo_buttons:
-            if not self.solo in self.hbox.get_children():
-                self.hbox.pack_start(self.solo, True, True, 0)
+            if not self.solo in self.buttons_box.get_children():
+                self.buttons_box.pack_start(self.solo, True, True, button_padding)
                 self.solo.show()
         else:
-            if self.solo in self.hbox.get_children():
-                self.hbox.remove(self.solo)
+            if self.solo in self.buttons_box.get_children():
+                self.buttons_box.remove(self.solo)
+
+        name = self.output_channel.channel.name
+        self.label.set_text(name)
+        if len(name) > self.input_channel.label_chars_narrow:
+            self.label.set_tooltip_text(name)
+
+        set_background_color(self.vbox, self.output_channel.css_name, self.output_channel.color)
 
     def on_mute_toggled(self, button):
         self.output_channel.channel.set_muted(self.input_channel.channel, button.get_active())
@@ -1021,3 +1285,13 @@ class ControlGroup(Gtk.Alignment):
         self.output_channel.channel.set_solo(self.input_channel.channel, button.get_active())
         self.app.update_monitor(self)
 
+    def on_prefader_toggled(self, button):
+        self.output_channel.channel.set_in_prefader(self.input_channel.channel, button.get_active())
+
+    def narrow(self):
+        self.hbox.remove(self.label)
+        self.hbox.set_child_packing(self.buttons_box, True, True, button_padding, Gtk.PackType.END)
+
+    def widen(self):
+        self.hbox.pack_start(self.label, False, False, button_padding)
+        self.hbox.set_child_packing(self.buttons_box, False, False, button_padding, Gtk.PackType.END)