]> git.0d.be Git - jack_mixer.git/blob - jack_mixer.py
When under NSM, menu quit or window close only hides UI
[jack_mixer.git] / jack_mixer.py
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 #
4 # This file is part of jack_mixer
5 #
6 # Copyright (C) 2006-2009 Nedko Arnaudov <nedko@arnaudov.name>
7 # Copyright (C) 2009 Frederic Peters <fpeters@0d.be>
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; version 2 of the License
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
21
22 import logging
23 import os
24 import signal
25 import sys
26 from argparse import ArgumentParser
27
28 import gi
29 gi.require_version('Gtk', '3.0')
30 from gi.repository import Gtk
31 from gi.repository import GObject
32 from gi.repository import GLib
33
34 # temporary change Python modules lookup path to look into installation
35 # directory ($prefix/share/jack_mixer/)
36 old_path = sys.path
37 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', 'share', 'jack_mixer'))
38
39 import jack_mixer_c
40
41 import gui
42 import scale
43 from channel import *
44 from nsmclient import NSMClient
45 from serialization_xml import XmlSerialization
46 from serialization import SerializedObject, Serializator
47 from preferences import PreferencesDialog
48
49 # restore Python modules lookup path
50 sys.path = old_path
51 log = logging.getLogger("jack_mixer")
52
53 class JackMixer(SerializedObject):
54
55     # scales suitable as meter scales
56     meter_scales = [scale.K20(), scale.K14(), scale.IEC268(), scale.Linear70dB(), scale.IEC268Minimalistic()]
57
58     # scales suitable as volume slider scales
59     slider_scales = [scale.Linear30dB(), scale.Linear70dB()]
60
61     # name of settngs file that is currently open
62     current_filename = None
63
64     _init_solo_channels = None
65
66     def __init__(self, client_name='jack_mixer'):
67         self.visible = False
68         self.nsm_client = None
69
70         if os.environ.get('NSM_URL'):
71             self.nsm_client = NSMClient(prettyName = "jack_mixer",
72                                         saveCallback = self.nsm_save_cb,
73                                         openOrNewCallback = self.nsm_open_cb,
74                                         supportsSaveStatus = False,
75                                         hideGUICallback = self.nsm_hide_cb,
76                                         showGUICallback = self.nsm_show_cb,
77                                         exitProgramCallback = self.nsm_exit_cb,
78                                         loggingLevel = "error",
79                                        )
80             self.nsm_client.announceGuiVisibility(self.visible)
81         else:
82             self.visible = True
83             self.create_mixer(client_name, with_nsm = False)
84
85     def create_mixer(self, client_name, with_nsm = True):
86         self.mixer = jack_mixer_c.Mixer(client_name)
87         self.create_ui(with_nsm)
88         if not self.mixer:
89             sys.exit(1)
90
91         self.window.set_title(client_name)
92
93         self.monitor_channel = self.mixer.add_output_channel("Monitor", True, True)
94         self.save = False
95
96         GLib.timeout_add(33, self.read_meters)
97         if with_nsm:
98             GLib.timeout_add(200, self.nsm_react)
99         GLib.timeout_add(50, self.midi_events_check)
100
101     def new_menu_item(self, title, callback=None, accel=None, enabled=True):
102         menuitem = Gtk.MenuItem.new_with_mnemonic(title)
103         menuitem.set_sensitive(enabled)
104         if callback:
105             menuitem.connect("activate", callback)
106         if accel:
107             key, mod = Gtk.accelerator_parse(accel)
108             menuitem.add_accelerator("activate", self.menu_accelgroup, key, mod,
109                                      Gtk.AccelFlags.VISIBLE)
110         return menuitem
111
112     def create_ui(self, with_nsm):
113         self.channels = []
114         self.output_channels = []
115         self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
116         self.window.set_icon_name('jack_mixer')
117         self.gui_factory = gui.Factory(self.window, self.meter_scales, self.slider_scales)
118         self.gui_factory.connect('midi-behavior-mode-changed', self.on_midi_behavior_mode_changed)
119         self.gui_factory.emit_midi_behavior_mode()
120
121         self.vbox_top = Gtk.VBox()
122         self.window.add(self.vbox_top)
123
124         self.menu_accelgroup = Gtk.AccelGroup()
125         self.window.add_accel_group(self.menu_accelgroup)
126
127         self.menubar = Gtk.MenuBar()
128         self.vbox_top.pack_start(self.menubar, False, True, 0)
129
130         mixer_menu_item = Gtk.MenuItem.new_with_mnemonic("_Mixer")
131         self.menubar.append(mixer_menu_item)
132         edit_menu_item = Gtk.MenuItem.new_with_mnemonic('_Edit')
133         self.menubar.append(edit_menu_item)
134         help_menu_item = Gtk.MenuItem.new_with_mnemonic('_Help')
135         self.menubar.append(help_menu_item)
136
137         self.width = 420
138         self.height = 420
139         self.paned_position = 210
140         self.window.set_default_size(self.width, self.height)
141
142         self.mixer_menu = Gtk.Menu()
143         mixer_menu_item.set_submenu(self.mixer_menu)
144
145         self.mixer_menu.append(self.new_menu_item('New _Input Channel',
146                                                   self.on_add_input_channel, "<Control>N"))
147         self.mixer_menu.append(self.new_menu_item('New Output _Channel',
148                                                   self.on_add_output_channel, "<Shift><Control>N"))
149
150         self.mixer_menu.append(Gtk.SeparatorMenuItem())
151         if not with_nsm:
152             self.mixer_menu.append(self.new_menu_item('_Open...', self.on_open_cb, "<Control>O"))
153
154         self.mixer_menu.append(self.new_menu_item('_Save', self.on_save_cb, "<Control>S"))
155
156         if not with_nsm:
157             self.mixer_menu.append(self.new_menu_item('Save _As...', self.on_save_as_cb,
158                                                       "<Shift><Control>S"))
159
160         self.mixer_menu.append(Gtk.SeparatorMenuItem())
161         if with_nsm:
162             self.mixer_menu.append(self.new_menu_item('_Hide', self.nsm_hide_cb, "<Control>W"))
163         else:
164             self.mixer_menu.append(self.new_menu_item('_Quit', self.on_quit_cb, "<Control>Q"))
165
166         edit_menu = Gtk.Menu()
167         edit_menu_item.set_submenu(edit_menu)
168
169         self.channel_edit_input_menu_item = self.new_menu_item('_Edit Input Channel',
170                                                                enabled=False)
171         edit_menu.append(self.channel_edit_input_menu_item)
172         self.channel_edit_input_menu = Gtk.Menu()
173         self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu)
174
175         self.channel_edit_output_menu_item = self.new_menu_item('E_dit Output Channel',
176                                                                 enabled=False)
177         edit_menu.append(self.channel_edit_output_menu_item)
178         self.channel_edit_output_menu = Gtk.Menu()
179         self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu)
180
181         self.channel_remove_input_menu_item = self.new_menu_item('_Remove Input Channel',
182                                                                  enabled=False)
183         edit_menu.append(self.channel_remove_input_menu_item)
184         self.channel_remove_input_menu = Gtk.Menu()
185         self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu)
186
187         self.channel_remove_output_menu_item = self.new_menu_item('Re_move Output Channel',
188                                                                   enabled=False)
189         edit_menu.append(self.channel_remove_output_menu_item)
190         self.channel_remove_output_menu = Gtk.Menu()
191         self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu)
192
193         edit_menu.append(Gtk.SeparatorMenuItem())
194         edit_menu.append(self.new_menu_item('Shrink Input Channels', self.on_narrow_input_channels_cb, "<Control>minus"))
195         edit_menu.append(self.new_menu_item('Expand Input Channels', self.on_widen_input_channels_cb, "<Control>plus"))
196         edit_menu.append(Gtk.SeparatorMenuItem())
197
198         edit_menu.append(self.new_menu_item('_Clear', self.on_channels_clear, "<Control>X"))
199         edit_menu.append(Gtk.SeparatorMenuItem())
200         edit_menu.append(self.new_menu_item('_Preferences', self.on_preferences_cb, "<Control>P"))
201
202         help_menu = Gtk.Menu()
203         help_menu_item.set_submenu(help_menu)
204
205         help_menu.append(self.new_menu_item('_About', self.on_about, "F1"))
206
207         self.hbox_top = Gtk.HBox()
208         self.vbox_top.pack_start(self.hbox_top, True, True, 0)
209
210         self.scrolled_window = Gtk.ScrolledWindow()
211         self.scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
212
213         self.hbox_inputs = Gtk.Box()
214         self.hbox_inputs.set_spacing(0)
215         self.hbox_inputs.set_border_width(0)
216         self.hbox_top.set_spacing(0)
217         self.hbox_top.set_border_width(0)
218         self.scrolled_window.add(self.hbox_inputs)
219         self.hbox_outputs = Gtk.Box()
220         self.hbox_outputs.set_spacing(0)
221         self.hbox_outputs.set_border_width(0)
222         self.scrolled_output = Gtk.ScrolledWindow()
223         self.scrolled_output.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
224         self.scrolled_output.add(self.hbox_outputs)
225         self.paned = Gtk.HPaned()
226         self.paned.set_wide_handle(True)
227         self.hbox_top.pack_start(self.paned, True, True, 0)
228         self.paned.pack1(self.scrolled_window, True, False)
229         self.paned.pack2(self.scrolled_output, True, False)
230         self.window.connect("destroy", Gtk.main_quit)
231         self.window.connect('delete-event', self.on_delete_event)
232
233     def nsm_react(self):
234         self.nsm_client.reactToMessage()
235         return True
236
237     def nsm_hide_cb(self, *args):
238         self.window.hide()
239         self.visible = False
240         self.nsm_client.announceGuiVisibility(False)
241
242     def nsm_show_cb(self):
243         width, height = self.window.get_size()
244         self.window.show_all()
245         self.paned.set_position(self.paned_position/self.width*width)
246
247         self.visible = True
248         self.nsm_client.announceGuiVisibility(True)
249
250     def nsm_open_cb(self, path, session_name, client_name):
251         self.create_mixer(client_name, with_nsm = True)
252         self.current_filename = path + '.xml'
253         if os.path.isfile(self.current_filename):
254             f = open(self.current_filename, 'r')
255             self.load_from_xml(f, from_nsm=True)
256             f.close()
257         else:
258             f = open(self.current_filename, 'w')
259             f.close()
260
261     def nsm_save_cb(self, path, session_name, client_name):
262         self.current_filename = path + '.xml'
263         f = open(self.current_filename, 'w')
264         self.save_to_xml(f)
265         f.close()
266
267     def nsm_exit_cb(self, path, session_name, client_name):
268         Gtk.main_quit()
269
270     def on_midi_behavior_mode_changed(self, gui_factory, value):
271         self.mixer.midi_behavior_mode = value
272
273     def on_delete_event(self, widget, event):
274         if self.nsm_client:
275             self.nsm_hide_cb()
276             return True
277
278         return False
279
280     def sighandler(self, signum, frame):
281         log.debug("Signal %d received.", signum)
282         if signum == signal.SIGUSR1:
283             self.save = True
284         elif signum == signal.SIGTERM:
285             Gtk.main_quit()
286         elif signum == signal.SIGINT:
287             Gtk.main_quit()
288         else:
289             log.warning("Unknown signal %d received.", signum)
290
291     def cleanup(self):
292         log.debug("Cleaning jack_mixer.")
293         if not self.mixer:
294             return
295
296         for channel in self.channels:
297             channel.unrealize()
298
299         self.mixer.destroy()
300
301     def on_open_cb(self, *args):
302         dlg = Gtk.FileChooserDialog(title='Open', parent=self.window,
303                         action=Gtk.FileChooserAction.OPEN)
304         dlg.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
305                         Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
306         dlg.set_default_response(Gtk.ResponseType.OK)
307         if dlg.run() == Gtk.ResponseType.OK:
308             filename = dlg.get_filename()
309             try:
310                 f = open(filename, 'r')
311                 self.load_from_xml(f)
312             except Exception as e:
313                 error_dialog(self.window, "Failed loading settings (%s)", e)
314             else:
315                 self.current_filename = filename
316             finally:
317                 f.close()
318         dlg.destroy()
319
320     def on_save_cb(self, *args):
321         if not self.current_filename:
322             return self.on_save_as_cb()
323         f = open(self.current_filename, 'w')
324         self.save_to_xml(f)
325         f.close()
326
327     def on_save_as_cb(self, *args):
328         dlg = Gtk.FileChooserDialog(title='Save', parent=self.window,
329                         action=Gtk.FileChooserAction.SAVE)
330         dlg.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
331                         Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
332         dlg.set_default_response(Gtk.ResponseType.OK)
333         if dlg.run() == Gtk.ResponseType.OK:
334             self.current_filename = dlg.get_filename()
335             self.on_save_cb()
336         dlg.destroy()
337
338     def on_quit_cb(self, *args):
339         Gtk.main_quit()
340
341     def on_narrow_input_channels_cb(self, widget):
342         for channel in self.channels:
343             channel.narrow()
344
345     def on_widen_input_channels_cb(self, widget):
346         for channel in self.channels:
347             channel.widen()
348
349     preferences_dialog = None
350     def on_preferences_cb(self, widget):
351         if not self.preferences_dialog:
352             self.preferences_dialog = PreferencesDialog(self)
353         self.preferences_dialog.show()
354         self.preferences_dialog.present()
355
356     def on_add_input_channel(self, widget):
357         dialog = NewInputChannelDialog(app=self)
358         dialog.set_transient_for(self.window)
359         dialog.show()
360         ret = dialog.run()
361         dialog.hide()
362
363         if ret == Gtk.ResponseType.OK:
364             result = dialog.get_result()
365             channel = self.add_channel(**result)
366             if self.visible or self.nsm_client == None:
367                 self.window.show_all()
368
369     def on_add_output_channel(self, widget):
370         dialog = NewOutputChannelDialog(app=self)
371         dialog.set_transient_for(self.window)
372         dialog.show()
373         ret = dialog.run()
374         dialog.hide()
375
376         if ret == Gtk.ResponseType.OK:
377             result = dialog.get_result()
378             channel = self.add_output_channel(**result)
379             if self.visible or self.nsm_client == None:
380                 self.window.show_all()
381
382     def on_edit_input_channel(self, widget, channel):
383         log.debug('Editing input channel "%s".', channel.channel_name)
384         channel.on_channel_properties()
385
386     def remove_channel_edit_input_menuitem_by_label(self, widget, label):
387         if (widget.get_label() == label):
388             self.channel_edit_input_menu.remove(widget)
389
390     def on_remove_input_channel(self, widget, channel):
391         log.debug('Removing input channel "%s".', channel.channel_name)
392         self.channel_remove_input_menu.remove(widget)
393         self.channel_edit_input_menu.foreach(
394             self.remove_channel_edit_input_menuitem_by_label,
395             channel.channel_name);
396         if self.monitored_channel is channel:
397             channel.monitor_button.set_active(False)
398         for i in range(len(self.channels)):
399             if self.channels[i] is channel:
400                 channel.unrealize()
401                 del self.channels[i]
402                 self.hbox_inputs.remove(channel.get_parent())
403                 break
404         if not self.channels:
405             self.channel_edit_input_menu_item.set_sensitive(False)
406             self.channel_remove_input_menu_item.set_sensitive(False)
407
408     def on_edit_output_channel(self, widget, channel):
409         log.debug('Editing output channel "%s".', channel.channel_name)
410         channel.on_channel_properties()
411
412     def remove_channel_edit_output_menuitem_by_label(self, widget, label):
413         if (widget.get_label() == label):
414             self.channel_edit_output_menu.remove(widget)
415
416     def on_remove_output_channel(self, widget, channel):
417         log.debug('Removing output channel "%s".', channel.channel_name)
418         self.channel_remove_output_menu.remove(widget)
419         self.channel_edit_output_menu.foreach(
420             self.remove_channel_edit_output_menuitem_by_label,
421             channel.channel_name);
422         if self.monitored_channel is channel:
423             channel.monitor_button.set_active(False)
424         for i in range(len(self.channels)):
425             if self.output_channels[i] is channel:
426                 channel.unrealize()
427                 del self.output_channels[i]
428                 self.hbox_outputs.remove(channel.get_parent())
429                 break
430         if not self.output_channels:
431             self.channel_edit_output_menu_item.set_sensitive(False)
432             self.channel_remove_output_menu_item.set_sensitive(False)
433
434     def rename_channels(self, container, parameters):
435         if (container.get_label() == parameters['oldname']):
436             container.set_label(parameters['newname'])
437
438     def on_channel_rename(self, oldname, newname):
439         rename_parameters = { 'oldname' : oldname, 'newname' : newname }
440         self.channel_edit_input_menu.foreach(self.rename_channels,
441             rename_parameters)
442         self.channel_edit_output_menu.foreach(self.rename_channels,
443             rename_parameters)
444         self.channel_remove_input_menu.foreach(self.rename_channels,
445             rename_parameters)
446         self.channel_remove_output_menu.foreach(self.rename_channels,
447             rename_parameters)
448         log.debug('Renaming channel from "%s" to "%s".', oldname, newname)
449
450     def on_channels_clear(self, widget):
451         dlg = Gtk.MessageDialog(parent = self.window,
452                 modal = True,
453                 message_type = Gtk.MessageType.WARNING,
454                 text = "Are you sure you want to clear all channels?",
455                 buttons = Gtk.ButtonsType.OK_CANCEL)
456         if not widget or dlg.run() == Gtk.ResponseType.OK:
457             for channel in self.output_channels:
458                 channel.unrealize()
459                 self.hbox_outputs.remove(channel.get_parent())
460             for channel in self.channels:
461                 channel.unrealize()
462                 self.hbox_inputs.remove(channel.get_parent())
463             self.channels = []
464             self.output_channels = []
465             self.channel_edit_input_menu = Gtk.Menu()
466             self.channel_edit_input_menu_item.set_submenu(self.channel_edit_input_menu)
467             self.channel_edit_input_menu_item.set_sensitive(False)
468             self.channel_remove_input_menu = Gtk.Menu()
469             self.channel_remove_input_menu_item.set_submenu(self.channel_remove_input_menu)
470             self.channel_remove_input_menu_item.set_sensitive(False)
471             self.channel_edit_output_menu = Gtk.Menu()
472             self.channel_edit_output_menu_item.set_submenu(self.channel_edit_output_menu)
473             self.channel_edit_output_menu_item.set_sensitive(False)
474             self.channel_remove_output_menu = Gtk.Menu()
475             self.channel_remove_output_menu_item.set_submenu(self.channel_remove_output_menu)
476             self.channel_remove_output_menu_item.set_sensitive(False)
477         dlg.destroy()
478
479     def add_channel(self, name, stereo, volume_cc, balance_cc, mute_cc, solo_cc, value):
480         try:
481             channel = InputChannel(self, name, stereo, value)
482             self.add_channel_precreated(channel)
483         except Exception:
484             error_dialog(self.window, "Channel creation failed.")
485             return
486         if volume_cc != -1:
487             channel.channel.volume_midi_cc = volume_cc
488         else:
489             channel.channel.autoset_volume_midi_cc()
490         if balance_cc != -1:
491             channel.channel.balance_midi_cc = balance_cc
492         else:
493             channel.channel.autoset_balance_midi_cc()
494         if mute_cc != -1:
495             channel.channel.mute_midi_cc = mute_cc
496         else:
497             channel.channel.autoset_mute_midi_cc()
498         if solo_cc != -1:
499             channel.channel.solo_midi_cc = solo_cc
500         else:
501             channel.channel.autoset_solo_midi_cc()
502
503         return channel
504
505     def add_channel_precreated(self, channel):
506         frame = Gtk.Frame()
507         frame.add(channel)
508         self.hbox_inputs.pack_start(frame, False, True, 0)
509         channel.realize()
510
511         channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name)
512         self.channel_edit_input_menu.append(channel_edit_menu_item)
513         channel_edit_menu_item.connect("activate", self.on_edit_input_channel, channel)
514         self.channel_edit_input_menu_item.set_sensitive(True)
515
516         channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name)
517         self.channel_remove_input_menu.append(channel_remove_menu_item)
518         channel_remove_menu_item.connect("activate", self.on_remove_input_channel, channel)
519         self.channel_remove_input_menu_item.set_sensitive(True)
520
521         self.channels.append(channel)
522
523         for outputchannel in self.output_channels:
524             channel.add_control_group(outputchannel)
525
526         # create post fader output channel matching the input channel
527         channel.post_fader_output_channel = self.mixer.add_output_channel(
528                         channel.channel.name + ' Out', channel.channel.is_stereo, True)
529         channel.post_fader_output_channel.volume = 0
530         channel.post_fader_output_channel.set_solo(channel.channel, True)
531
532         channel.connect('input-channel-order-changed', self.on_input_channel_order_changed)
533
534     def on_input_channel_order_changed(self, widget, source_name, dest_name):
535         self.channels.clear()
536
537         channel_box = self.hbox_inputs
538         frames = channel_box.get_children()
539
540         for f in frames:
541             c = f.get_child()
542             if source_name == c._channel_name:
543                 source_frame = f
544                 break
545
546         for f in frames:
547             c = f.get_child()
548             if (dest_name == c._channel_name):
549                 pos = frames.index(f)
550                 channel_box.reorder_child(source_frame, pos)
551                 break
552
553         for frame in self.hbox_inputs.get_children():
554             c = frame.get_child()
555             self.channels.append(c)
556
557     def read_meters(self):
558         for channel in self.channels:
559             channel.read_meter()
560         for channel in self.output_channels:
561             channel.read_meter()
562         return True
563
564     def midi_events_check(self):
565         for channel in self.channels + self.output_channels:
566             channel.midi_events_check()
567         return True
568
569     def add_output_channel(self, name, stereo, volume_cc, balance_cc, mute_cc,
570             display_solo_buttons, color, value):
571         try:
572             channel = OutputChannel(self, name, stereo, value)
573             channel.display_solo_buttons = display_solo_buttons
574             channel.color = color
575             self.add_output_channel_precreated(channel)
576         except Exception:
577             error_dialog(self.window, "Channel creation failed")
578             return
579
580         if volume_cc != -1:
581             channel.channel.volume_midi_cc = volume_cc
582         else:
583             channel.channel.autoset_volume_midi_cc()
584         if balance_cc != -1:
585             channel.channel.balance_midi_cc = balance_cc
586         else:
587             channel.channel.autoset_balance_midi_cc()
588         if mute_cc != -1:
589             channel.channel.mute_midi_cc = mute_cc
590         else:
591             channel.channel.autoset_mute_midi_cc()
592
593         return channel
594
595     def add_output_channel_precreated(self, channel):
596         frame = Gtk.Frame()
597         frame.add(channel)
598         self.hbox_outputs.pack_end(frame, False, True, 0)
599         self.hbox_outputs.reorder_child(frame, 0)
600         channel.realize()
601
602         channel_edit_menu_item = Gtk.MenuItem(label=channel.channel_name)
603         self.channel_edit_output_menu.append(channel_edit_menu_item)
604         channel_edit_menu_item.connect("activate", self.on_edit_output_channel, channel)
605         self.channel_edit_output_menu_item.set_sensitive(True)
606
607         channel_remove_menu_item = Gtk.MenuItem(label=channel.channel_name)
608         self.channel_remove_output_menu.append(channel_remove_menu_item)
609         channel_remove_menu_item.connect("activate", self.on_remove_output_channel, channel)
610         self.channel_remove_output_menu_item.set_sensitive(True)
611
612         self.output_channels.append(channel)
613         channel.connect('output-channel-order-changed', self.on_output_channel_order_changed)
614
615     def on_output_channel_order_changed(self, widget, source_name, dest_name):
616         self.output_channels.clear()
617         channel_box = self.hbox_outputs
618
619         frames = channel_box.get_children()
620
621         for f in frames:
622             c = f.get_child()
623             if source_name == c._channel_name:
624                  source_frame = f
625                  break
626
627         for f in frames:
628             c = f.get_child()
629             if (dest_name == c._channel_name):
630                 pos = len(frames) - 1 - frames.index(f)
631                 channel_box.reorder_child(source_frame, pos)
632                 break
633
634         for frame in self.hbox_outputs.get_children():
635             c = frame.get_child()
636             self.output_channels.append(c)
637
638     _monitored_channel = None
639     def get_monitored_channel(self):
640         return self._monitored_channel
641
642     def set_monitored_channel(self, channel):
643         if self._monitored_channel:
644             if channel.channel.name == self._monitored_channel.channel.name:
645                 return
646         self._monitored_channel = channel
647         if type(channel) is InputChannel:
648             # reset all solo/mute settings
649             for in_channel in self.channels:
650                 self.monitor_channel.set_solo(in_channel.channel, False)
651                 self.monitor_channel.set_muted(in_channel.channel, False)
652             self.monitor_channel.set_solo(channel.channel, True)
653             self.monitor_channel.prefader = True
654         else:
655             self.monitor_channel.prefader = False
656         self.update_monitor(channel)
657     monitored_channel = property(get_monitored_channel, set_monitored_channel)
658
659     def update_monitor(self, channel):
660         if self._monitored_channel is not channel:
661             return
662         self.monitor_channel.volume = channel.channel.volume
663         self.monitor_channel.balance = channel.channel.balance
664         if type(self.monitored_channel) is OutputChannel:
665             # sync solo/muted channels
666             for input_channel in self.channels:
667                 self.monitor_channel.set_solo(input_channel.channel,
668                                 channel.channel.is_solo(input_channel.channel))
669                 self.monitor_channel.set_muted(input_channel.channel,
670                                 channel.channel.is_muted(input_channel.channel))
671
672     def get_input_channel_by_name(self, name):
673         for input_channel in self.channels:
674             if input_channel.channel.name == name:
675                 return input_channel
676         return None
677
678     def on_about(self, *args):
679         about = Gtk.AboutDialog()
680         about.set_name('jack_mixer')
681         about.set_copyright('Copyright © 2006-2020\nNedko Arnaudov, Frédéric Péters, Arnout Engelen, Daniel Sheeler')
682         about.set_license('''\
683 jack_mixer is free software; you can redistribute it and/or modify it
684 under the terms of the GNU General Public License as published by the
685 Free Software Foundation; either version 2 of the License, or (at your
686 option) any later version.
687
688 jack_mixer is distributed in the hope that it will be useful, but
689 WITHOUT ANY WARRANTY; without even the implied warranty of
690 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
691 General Public License for more details.
692
693 You should have received a copy of the GNU General Public License along
694 with jack_mixer; if not, write to the Free Software Foundation, Inc., 51
695 Franklin Street, Fifth Floor, Boston, MA 02110-130159 USA''')
696         about.set_authors([
697             'Nedko Arnaudov <nedko@arnaudov.name>',
698             'Christopher Arndt <chris@chrisarndt.de>',
699             'Arnout Engelen <arnouten@bzzt.net>',
700             'John Hedges <john@drystone.co.uk>',
701             'Olivier Humbert <trebmuh@tuxfamily.org>',
702             'Sarah Mischke <sarah@spooky-online.de>',
703             'Frédéric Péters <fpeters@0d.be>',
704             'Daniel Sheeler <dsheeler@pobox.com>',
705             'Athanasios Silis <athanasios.silis@gmail.com>',
706         ])
707         about.set_logo_icon_name('jack_mixer')
708         about.set_website('https://rdio.space/jackmixer/')
709
710         about.run()
711         about.destroy()
712
713     def save_to_xml(self, file):
714         log.debug("Saving to XML...")
715         b = XmlSerialization()
716         s = Serializator()
717         s.serialize(self, b)
718         b.save(file)
719
720     def load_from_xml(self, file, silence_errors=False, from_nsm=False):
721         log.debug("Loading from XML...")
722         self.unserialized_channels = []
723         b = XmlSerialization()
724         try:
725             b.load(file)
726         except:
727             if silence_errors:
728                 return
729             raise
730         self.on_channels_clear(None)
731         s = Serializator()
732         s.unserialize(self, b)
733         for channel in self.unserialized_channels:
734             if isinstance(channel, InputChannel):
735                 if self._init_solo_channels and channel.channel_name in self._init_solo_channels:
736                     channel.solo = True
737                 self.add_channel_precreated(channel)
738         self._init_solo_channels = None
739         for channel in self.unserialized_channels:
740             if isinstance(channel, OutputChannel):
741                 self.add_output_channel_precreated(channel)
742         del self.unserialized_channels
743         width, height = self.window.get_size()
744         if self.visible or not from_nsm:
745             self.window.show_all()
746         self.paned.set_position(self.paned_position/self.width*width)
747         self.window.resize(self.width, self.height)
748
749     def serialize(self, object_backend):
750         width, height = self.window.get_size()
751         object_backend.add_property('geometry',
752                         '%sx%s' % (width, height))
753         pos = self.paned.get_position()
754         object_backend.add_property('paned_position', '%s' % pos)
755         solo_channels = []
756         for input_channel in self.channels:
757             if input_channel.channel.solo:
758                 solo_channels.append(input_channel)
759         if solo_channels:
760             object_backend.add_property('solo_channels', '|'.join([x.channel.name for x in solo_channels]))
761         object_backend.add_property('visible', '%s' % str(self.visible))
762
763     def unserialize_property(self, name, value):
764         if name == 'geometry':
765             width, height = value.split('x')
766             self.width = int(width)
767             self.height = int(height)
768             return True
769         if name == 'solo_channels':
770             self._init_solo_channels = value.split('|')
771             return True
772         if name == 'visible':
773             self.visible = value == 'True'
774             return True
775         if name == 'paned_position':
776             self.paned_position = int(value)
777             return True
778         return False
779
780     def unserialize_child(self, name):
781         if name == InputChannel.serialization_name():
782             channel = InputChannel(self, "", True)
783             self.unserialized_channels.append(channel)
784             return channel
785
786         if name == OutputChannel.serialization_name():
787             channel = OutputChannel(self, "", True)
788             self.unserialized_channels.append(channel)
789             return channel
790
791         if name == gui.Factory.serialization_name():
792             return self.gui_factory
793
794     def serialization_get_childs(self):
795         '''Get child objects that required and support serialization'''
796         childs = self.channels[:] + self.output_channels[:] + [self.gui_factory]
797         return childs
798
799     def serialization_name(self):
800         return "jack_mixer"
801
802     def main(self):
803         if not self.mixer:
804             return
805
806         if self.visible or self.nsm_client == None:
807             width, height = self.window.get_size()
808             self.window.show_all()
809             if hasattr(self, 'paned_position'):
810                 self.paned.set_position(self.paned_position/self.width*width)
811
812         signal.signal(signal.SIGUSR1, self.sighandler)
813         signal.signal(signal.SIGTERM, self.sighandler)
814         signal.signal(signal.SIGINT, self.sighandler)
815         signal.signal(signal.SIGHUP, signal.SIG_IGN)
816
817         Gtk.main()
818
819 def error_dialog(parent, msg, *args):
820     log.exception(msg, *args)
821     err = Gtk.MessageDialog(parent=parent, modal=True, destroy_with_parent=True,
822         message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=msg % args)
823     err.run()
824     err.destroy()
825
826 def main():
827     parser = ArgumentParser()
828     parser.add_argument('-c', '--config', metavar="FILE", help='load mixer project configuration from FILE')
829     parser.add_argument('-d', '--debug', action="store_true", help='enable debug logging messages')
830     parser.add_argument('client_name', metavar='NAME', nargs='?', default='jack_mixer',
831                         help='set JACK client name')
832     args = parser.parse_args()
833
834     logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO,
835                         format="%(levelname)s: %(message)s")
836
837     try:
838         mixer = JackMixer(args.client_name)
839     except Exception as e:
840         error_dialog(None, "Mixer creation failed (%s).", e)
841         sys.exit(1)
842
843     if not mixer.nsm_client and args.config:
844         f = open(args.config)
845         mixer.current_filename = args.config
846
847         try:
848             mixer.load_from_xml(f)
849         except Exception as e:
850             error_dialog(mixer.window, "Failed loading settings (%s).", e)
851
852         mixer.window.set_default_size(60*(1+len(mixer.channels)+len(mixer.output_channels)), 300)
853         f.close()
854
855     mixer.main()
856
857     mixer.cleanup()
858
859 if __name__ == "__main__":
860     main()