3 # LADITools - Linux Audio Desktop Integration Tools
4 # ladi-control-center - A configuration GUI for your Linux Audio Desktop
5 # Copyright (C) 2011-2012 Alessio Treglia <quadrispro@ubuntu.com>
6 # Copyright (C) 2007-2010, Marc-Olivier Barre <marco@marcochapeau.org>
7 # Copyright (C) 2007-2009, Nedko Arnaudov <nedko@arnaudov.name>
8 # Copyright (C) 2008, Krzysztof Foltman <wdev@foltman.com>
10 # This program is free software: you can redistribute it and/or modify
11 # it under the terms of the GNU General Public License as published by
12 # the Free Software Foundation, either version 3 of the License, or
13 # (at your option) any later version.
15 # This program is distributed in the hope that it will be useful,
16 # but WITHOUT ANY WARRANTY; without even the implied warranty of
17 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 # GNU General Public License for more details.
20 # You should have received a copy of the GNU General Public License
21 # along with this program. If not, see <http://www.gnu.org/licenses/>.
29 from laditools
import _gettext_domain
30 gettext
.install(_gettext_domain
)
32 from laditools
import get_version_string
33 from laditools
import check_ladish
34 from laditools
import LadishProxy
35 from laditools
import LadishStatusType
36 from laditools
import LadishProxyError
37 from laditools
import JackConfigProxy
39 from gi
.repository
import Gtk
40 from gi
.repository
import Gdk
41 from gi
.repository
import GObject
43 from laditools
.gtk
import find_data_file
45 sig_handler
= signal
.getsignal(signal
.SIGTERM
)
46 signal
.signal(signal
.SIGINT
, sig_handler
)
48 tooltips_active
= True
54 COLUMN_ISSET
) = range (5)
59 except LadishProxyError
as e
:
60 sys
.stderr
.write("%s\n" % str(e
))
63 except Exception as e
:
64 sys
.stderr
.write(_("ladish proxy creation failed: %s\n") % str(e
))
67 if ret
== LadishStatusType
.STUDIO_STOPPED
:
68 # Everything is OK, there's no need to print out a message
69 #sys.stderr.write(_("ladish studio is loaded and not started\n"))
72 elif ret
== LadishStatusType
.NOT_AVAILABLE
:
73 sys
.stderr
.write(_("ladish is not available\n"))
77 if ret
== LadishStatusType
.NO_STUDIO_LOADED
:
78 msg
= _("JACK can only be configured with a loaded and stopped studio. Please create a new studio or load and stop an existing one.")
79 sys
.stderr
.write(msg
+ "\n")
80 title
= _("No studio present")
81 elif ret
== LadishStatusType
.STUDIO_RUNNING
:
82 msg
= _("JACK can only be configured with a stopped studio. Please stop your studio first.")
83 sys
.stderr
.write(msg
+ "\n")
84 title
= _("Studio is running")
86 sys
.stderr(_("Unexpected error!\n"))
88 mdlg
= Gtk
.MessageDialog(type = Gtk
.MessageType
.ERROR
,
89 buttons
= Gtk
.ButtonsType
.CLOSE
,
100 class parameter(object):
101 def __init__(self
, path
):
103 self
.name
= path
[-1:]
109 return jack
.get_param_type(self
.path
)
112 return jack
.get_param_value(self
.path
)
114 def set_value(self
, value
):
115 jack
.set_param_value(self
.path
, value
)
117 def reset_value(self
):
118 jack
.reset_param_value(self
.path
)
120 def get_short_description(self
):
121 return jack
.get_param_short_description(self
.path
)
123 def get_long_description(self
):
124 descr
= jack
.get_param_long_description(self
.path
)
126 descr
= self
.get_short_description()
130 return jack
.param_has_range(self
.path
)
133 return jack
.param_get_range(self
.path
)
136 return jack
.param_has_enum(self
.path
)
138 def is_strict_enum(self
):
139 return jack
.param_is_strict_enum(self
.path
)
141 def is_fake_values_enum(self
):
142 return jack
.param_is_fake_value(self
.path
)
144 def get_enum_values(self
):
145 return jack
.param_get_enum_values(self
.path
)
147 class configure_command(object):
151 def get_description(self
):
154 def get_window_title(self
):
155 return self
.get_description();
158 return self
.activate()
160 class parameter_enum_value(GObject
.GObject
):
161 def __init__(self
, is_fake_value
, value
, description
):
162 GObject
.GObject
.__init
__(self
)
163 self
.is_fake_value
= is_fake_value
165 self
.description
= description
167 def get_description(self
):
168 if self
.is_fake_value
:
169 return self
.description
171 return str(self
.value
) + " - " + self
.description
173 class parameter_store(GObject
.GObject
):
174 def __init__(self
, param
):
175 GObject
.GObject
.__init
__(self
)
177 self
.name
= self
.param
.get_name()
178 self
.is_set
, self
.default_value
, self
.value
= self
.param
.get_value()
179 self
.modified
= False
180 self
.has_range
= self
.param
.has_range()
181 self
.is_strict
= self
.param
.is_strict_enum()
182 self
.is_fake_value
= self
.param
.is_fake_values_enum()
184 self
.enum_values
= []
187 self
.range_min
, self
.range_max
= self
.param
.get_range()
189 for enum_value
in self
.param
.get_enum_values():
190 self
.enum_values
.append(parameter_enum_value(self
.is_fake_value
, enum_value
[0], enum_value
[1]))
196 return self
.param
.get_type()
201 def get_default_value(self
):
202 if not self
.is_fake_value
:
203 return str(self
.default_value
)
205 for enum_value
in self
.get_enum_values():
206 if enum_value
.value
== self
.default_value
:
207 return enum_value
.get_description()
211 def set_value(self
, value
):
215 def reset_value(self
):
216 self
.value
= self
.default_value
218 def get_short_description(self
):
219 return self
.param
.get_short_description()
221 def maybe_save_value(self
):
223 self
.param
.set_value(self
.value
)
224 self
.modified
= False
227 return self
.range_min
, self
.range_max
230 return len(self
.enum_values
) != 0
232 def is_strict_enum(self
):
233 return self
.is_strict
235 def get_enum_values(self
):
236 return self
.enum_values
238 GObject
.type_register(parameter_store
)
240 def combobox_get_active_text(combobox
, model_index
= 0):
241 model
= combobox
.get_model()
242 active
= combobox
.get_active()
245 return model
[active
][model_index
]
247 class cell_renderer_param(Gtk
.CellRendererPixbuf
):
248 __gproperties__
= { "parameter": (GObject
.TYPE_OBJECT
,
251 GObject
.PARAM_READWRITE
) }
254 Gtk
.CellRendererPixbuf
.__init
__(self
)
255 self
.parameter
= None
256 self
.set_property('mode', Gtk
.CellRendererMode
.EDITABLE
)
257 self
.set_property('mode', Gtk
.CellRendererMode
.ACTIVATABLE
)
258 self
.renderer_text
= Gtk
.CellRendererText()
259 self
.renderer_toggle
= Gtk
.CellRendererToggle()
260 self
.renderer_combo
= Gtk
.CellRendererCombo()
261 self
.renderer_spinbutton
= Gtk
.CellRendererSpin()
262 for r
in (self
.renderer_text
, self
.renderer_combo
, self
.renderer_spinbutton
):
263 r
.connect("edited", self
.on_edited
)
265 self
.edit_widget
= None
267 def do_set_property(self
, pspec
, value
):
268 if pspec
.name
== 'parameter':
269 if value
.get_type() == 'b':
270 self
.set_property('mode', Gtk
.CellRendererMode
.ACTIVATABLE
)
272 self
.set_property('mode', Gtk
.CellRendererMode
.EDITABLE
)
274 sys
.stderr
.write(pspec
.name
)
275 setattr(self
, pspec
.name
, value
)
277 def do_get_property(self
, pspec
):
278 return getattr(self
, pspec
.name
)
280 def choose_renderer(self
):
281 typechar
= self
.parameter
.get_type()
282 value
= self
.parameter
.get_value()
285 self
.renderer
= self
.renderer_toggle
286 self
.renderer
.set_activatable(True)
287 self
.renderer
.set_active(value
)
288 self
.renderer
.set_property("xalign", 0.0)
291 if self
.parameter
.has_enum():
292 self
.renderer
= self
.renderer_combo
294 m
= Gtk
.ListStore(str, parameter_enum_value
)
296 for value
in self
.parameter
.get_enum_values():
297 m
.append([value
.get_description(), value
])
299 self
.renderer
.set_property("model",m
)
300 self
.renderer
.set_property('text-column', 0)
301 self
.renderer
.set_property('editable', True)
302 self
.renderer
.set_property('has_entry', not self
.parameter
.is_strict_enum())
304 value
= self
.parameter
.get_value()
305 if self
.parameter
.is_fake_value
:
307 for enum_value
in self
.parameter
.get_enum_values():
308 if enum_value
.value
== value
:
309 text
= enum_value
.get_description()
314 self
.renderer
.set_property('text', text
)
318 if typechar
== 'u' or typechar
== 'i':
319 self
.renderer
= self
.renderer_spinbutton
320 self
.renderer
.set_property('text', str(value
))
321 self
.renderer
.set_property('editable', True)
322 if self
.parameter
.has_range
:
323 range_min
, range_max
= self
.parameter
.get_range()
324 self
.renderer
.set_property('adjustment', Gtk
.Adjustment(value
, range_min
, range_max
, 1, abs(int((range_max
- range_min
) / 10))))
326 self
.renderer
.set_property('adjustment', Gtk
.Adjustment(value
, 0, 100000, 1, 1000))
329 self
.renderer
= self
.renderer_text
330 self
.renderer
.set_property('editable', True)
331 self
.renderer
.set_property('text', self
.parameter
.get_value())
333 def do_render(self
, ctx
, widget
, bg_area
, cell_area
, flags
):
334 self
.choose_renderer()
335 return self
.renderer
.render(ctx
, widget
, bg_area
, cell_area
, flags
)
337 def do_get_size(self
, widget
, cell_area
=None):
338 self
.choose_renderer()
339 return self
.renderer
.get_size(widget
, cell_area
)
341 def do_activate(self
, event
, widget
, path
, background_area
, cell_area
, flags
):
342 self
.choose_renderer()
343 if self
.parameter
.get_type() == 'b':
344 self
.parameter
.set_value(not self
.parameter
.get_value())
345 widget
.get_model()[path
][COLUMN_ISSET
] = "modified"
348 def on_edited(self
, renderer
, path
, value_str
):
349 parameter
= self
.edit_parameter
350 widget
= self
.edit_widget
351 model
= self
.edit_tree
.get_model()
352 self
.edit_widget
= None
353 typechar
= parameter
.get_type()
354 if type(widget
) == Gtk
.ComboBox
:
355 value
= combobox_get_active_text(widget
, 1)
358 value_str
= value
.value
359 elif type(widget
) == Gtk
.ComboBoxText
:
360 enum_value
= combobox_get_active_text(widget
, 1)
362 value_str
= enum_value
.value
364 value_str
= widget
.get_active_text()
366 if typechar
== 'u' or typechar
== 'i':
368 value
= int(value_str
)
369 except ValueError, e
:
370 # Hide the widget (because it may display something else than what user typed in)
373 mdlg
= Gtk
.MessageDialog(buttons
= Gtk
.ButtonsType
.OK
, message_format
= "Invalid value. Please enter an integer number.")
376 # Return the focus back to the tree to prevent buttons from stealing it
377 self
.edit_tree
.grab_focus()
381 parameter
.set_value(value
)
382 model
[path
][COLUMN_ISSET
] = "modified"
383 self
.edit_tree
.grab_focus()
385 def do_start_editing(self
, event
, widget
, path
, background_area
, cell_area
, flags
):
386 # this happens when edit requested using keyboard
388 event
= Gdk
.Event(Gdk
.NOTHING
)
390 self
.choose_renderer()
391 ret
= self
.renderer
.start_editing(event
, widget
, path
, background_area
, cell_area
, flags
)
392 self
.edit_widget
= ret
393 self
.edit_tree
= widget
394 self
.edit_parameter
= self
.parameter
397 GObject
.type_register(cell_renderer_param
)
399 class jack_params_configure_command(configure_command
):
400 def __init__(self
, path
):
402 self
.is_setting
= False
404 def reset_value(self
, row_path
):
405 row
= self
.liststore
[row_path
]
406 param
= row
[COLUMN_PARAMETER
]
408 row
[COLUMN_ISSET
] = "reset"
410 def do_row_activated(self
, treeview
, path
, view_column
):
411 if view_column
== self
.tvcolumn_is_set
:
412 self
.reset_value(path
)
414 def do_button_press_event(self
, tree
, event
):
415 if event
.type != Gdk
.EventType
._2BUTTON
_PRESS
:
417 # this is needed for proper double-click handling in the list; don't ask me why, I don't know
418 # it's probably because _2BUTTON_PRESS event is still delivered to tree view, automatically deactivating
419 # the newly created edit widget (which gets created on second BUTTON_PRESS but before _2BUTTON_PRESS)
420 # deactivating the widget causes it to be deleted
423 def do_key_press_event(self
, tree
, event
):
424 (row_path
, cur
) = self
.treeview
.get_cursor()
426 # if Delete was pressed, reset the value
427 #if event.get_state() == 0 and event.keyval == Gdk.KEY_Delete:
428 if event
.keyval
== Gdk
.KEY_Delete
:
429 self
.reset_value(row_path
)
433 # prevent ESC from activating the editor
434 if event
.string
< " ":
437 # single-key data entry: if the control is a text entry, spin button or combo box/combo box entry,
438 # then edit the current and set the text value to what user has already typed in
439 (row_path
, cur
) = self
.treeview
.get_cursor()
440 param
= self
.liststore
[row_path
][COLUMN_PARAMETER
]
441 ptype
= param
.get_type()
443 # we don't care about booleans
447 # accept only digits for integer input (or a minus, but only if it's a signed field)
448 if ptype
in ('i', 'u'):
449 if not (event
.string
.isdigit() or (event
.string
== "-" and ptype
== 'i')):
453 # MAYBE: call a specially crafted cell_renderer_param method for this
454 self
.treeview
.set_cursor_on_cell(row_path
, self
.tvcolumn_value
, self
.renderer_value
.renderer
, True)
456 # cell_renderer_param::on_start_editing() should set edit_widget.
457 # if edit operation has failed (didn't create a widget), pass the key on
458 # MAYBE: call a specially crafted cell_renderer_param method to do this check
459 if self
.renderer_value
.edit_widget
== None:
462 widget
= self
.renderer_value
.edit_widget
463 if type(widget
) in (Gtk
.Entry
, Gtk
.ComboBoxText
):
464 # (combo or plain) text entry - set the content and move the cursor to the end
465 sl
= len(event
.string
)
466 widget
.set_text(event
.string
)
467 widget
.select_region(sl
, sl
)
470 if type(self
.renderer_value
.edit_widget
) == Gtk
.SpinButton
:
471 # spin button - set the value and move the cursor to the end
472 if event
.string
== "-":
473 # special case for minus sign (which can't be converted to float)
474 widget
.set_text(event
.string
)
476 widget
.set_value(float(event
.string
))
477 sl
= len(widget
.get_text())
478 widget
.select_region(sl
, sl
)
481 if type(self
.renderer_value
.edit_widget
) == Gtk
.ComboBox
:
482 # combo box - select the first item that starts with typed character
483 model
= widget
.get_model()
485 iter = model
.get_iter_root()
487 if model
.get_value(iter, 0).startswith(event
.string
):
488 item
= model
.get_path(iter)[0]
490 iter = model
.iter_next(iter)
491 widget
.set_active(item
)
496 def on_cursor_changed(self
, tree
):
497 (row_path
, cur
) = self
.treeview
.get_cursor()
499 if not self
.is_setting
and cur
!= None and cur
.get_title() != self
.tvcolumn_value
.get_title():
500 self
.is_setting
= True
502 self
.treeview
.set_cursor_on_cell(row_path
, self
.tvcolumn_value
, self
.renderer_value
.renderer
, False)
504 self
.is_setting
= False
506 def ok_clicked(self
, dlg
):
507 if self
.renderer_value
.edit_widget
:
508 self
.renderer_value
.edit_widget
.editing_done()
510 def _on_query_tooltip(self
, treeview
, x
, y
, keyboard_tip
, tooltip
):
511 """Handle tooltips for the cells"""
513 (path
, column
, out_x
, out_y
) = treeview
.get_path_at_pos(x
, y
)
519 # Horrible-fix to skip the treeview's header row
520 intpath
= int(path
.to_string())
524 path
= Gtk
.TreePath(str(intpath
- 1))
527 if column
.get_title() in (self
.tvcolumn_value
.get_title(),
528 self
.tvcolumn_parameter
.get_title()):
529 text
= self
.liststore
[path
][COLUMN_LONGDESC
]
530 elif column
.get_title() == self
.tvcolumn_is_set
.get_title():
531 if self
.liststore
[path
][COLUMN_ISSET
] == "default":
533 if self
.liststore
[path
][COLUMN_ISSET
] == 'set':
534 text
+= _("Double-click to schedule reset of value to %s") % \
535 self
.liststore
[path
][COLUMN_PARAMETER
].get_default_value()
537 text
= _("Value will be reset to %s") % \
538 self
.liststore
[path
][COLUMN_PARAMETER
].get_default_value()
542 tooltip
.set_text(text
)
545 def do_destroy(self
, *args
):
546 for row
in self
.liststore
:
547 param
= row
[COLUMN_PARAMETER
]
548 reset
= (row
[COLUMN_ISSET
] == "reset")
550 param
.param
.reset_value()
553 param
.maybe_save_value()
555 def activate(self
, *args
, **kwargs
):
557 self
.liststore
= Gtk
.ListStore(GObject
.TYPE_STRING
,
562 self
.treeview
= Gtk
.TreeView(self
.liststore
)
563 self
.treeview
.set_rules_hint(True)
564 self
.treeview
.set_has_tooltip(True)
565 self
.treeview
.set_enable_search(False)
567 renderer_text
= Gtk
.CellRendererText()
568 renderer_toggle
= Gtk
.CellRendererToggle()
569 renderer_value
= cell_renderer_param()
570 self
.renderer_value
= renderer_value
# save for use in event handler methods
572 self
.tvcolumn_parameter
= Gtk
.TreeViewColumn('Parameter', renderer_text
, text
=COLUMN_NAME
)
573 self
.tvcolumn_is_set
= Gtk
.TreeViewColumn('Status', renderer_text
, text
=COLUMN_ISSET
)
574 self
.tvcolumn_value
= Gtk
.TreeViewColumn('Value', renderer_value
, parameter
=COLUMN_PARAMETER
)
575 self
.tvcolumn_description
= Gtk
.TreeViewColumn('Description', renderer_text
, text
=COLUMN_SHORTDESC
)
577 self
.tvcolumn_value
.set_resizable(True)
578 self
.tvcolumn_value
.set_min_width(100)
580 self
.treeview
.append_column(self
.tvcolumn_parameter
)
581 self
.treeview
.append_column(self
.tvcolumn_is_set
)
582 self
.treeview
.append_column(self
.tvcolumn_value
)
583 self
.treeview
.append_column(self
.tvcolumn_description
)
585 param_names
= jack
.get_param_names(self
.path
)
586 for name
in param_names
:
587 param
= parameter(self
.path
+ [name
])
588 store
= parameter_store(param
)
593 self
.liststore
.append([name
,
595 param
.get_short_description(),
596 param
.get_long_description(),
599 self
.treeview
.connect("row-activated", self
.do_row_activated
)
600 self
.treeview
.connect("cursor-changed", self
.on_cursor_changed
)
601 self
.treeview
.connect("key-press-event", self
.do_key_press_event
)
602 self
.treeview
.connect("button-press-event", self
.do_button_press_event
)
604 self
.treeview
.connect("query-tooltip", self
._on
_query
_tooltip
)
605 self
.treeview
.connect("destroy", self
.do_destroy
)
608 # move cursor to first row and 'value' column
609 self
.treeview
.set_cursor(Gtk
.TreePath(path
=0),
610 focus_column
=self
.tvcolumn_value
,
615 class jack_engine_params_configure_command(jack_params_configure_command
):
617 jack_params_configure_command
.__init
__(self
, ['engine'])
619 def get_description(self
):
620 return _('JACK engine')
622 class jack_driver_params_configure_command(jack_params_configure_command
):
624 jack_params_configure_command
.__init
__(self
, ['driver'])
626 def get_description(self
):
627 return _('JACK driver')
629 def get_window_title(self
):
630 return _('JACK "%s" driver') % jack
.get_selected_driver()
632 class jack_internal_params_configure_command(jack_params_configure_command
):
633 def __init__(self
, name
):
635 jack_params_configure_command
.__init
__(self
, ['internals', name
])
637 def get_description(self
):
638 return _('JACK "%s"') % self
.name
640 def show_panels(*args
, **kwargs
):
641 if not 'modules' in kwargs
:
643 mods
= kwargs
['modules']
644 window
= Gtk
.Window
.new(Gtk
.WindowType
.TOPLEVEL
)
647 notebook
= Gtk
.Notebook()
649 vbox
.pack_start(hbox
, True, True, 12)
650 hbox
.pack_start(notebook
, True, True, 2)
651 window
.set_title("LADI Settings")
652 window
.set_icon_name('preferences-system')
653 window
.set_resizable(True)
654 notebook
.set_tab_pos(Gtk
.PositionType
.LEFT
)
656 if 'select' in kwargs
:
657 selected
= kwargs
['select']
664 treeview
= mods
[mod
].run({})
665 container
= Gtk
.ScrolledWindow()
666 container
.set_min_content_width(400)
667 container
.set_min_content_height(400)
668 container
.set_policy(hscrollbar_policy
=Gtk
.PolicyType
.AUTOMATIC
,
669 vscrollbar_policy
=Gtk
.PolicyType
.AUTOMATIC
)
670 container
.add(treeview
)
674 tab_label
= mods
[mod
].get_window_title()
676 tab_label
= mods
[mod
].get_description()
677 notebook
.append_page(container
, Gtk
.Label(tab_label
))
678 if selected
and selected
== mod
:
679 notebook
.set_current_page(page_count
)
685 window
.connect('destroy', Gtk
.main_quit
)
688 if __name__
== "__main__":
691 jack
= JackConfigProxy()
694 GObject
.type_register(parameter_enum_value
)
696 parser
= argparse
.ArgumentParser(description
=_('Convenient graphical interface for configuring JACK'),
697 epilog
=_('This program is part of the LADITools suite.'))
698 parser
.add_argument('-m', '--module', nargs
=1, metavar
='MODULE', help=_('select the module to configure'))
699 parser
.add_argument('-l', '--list-modules', action
='store_true', help=_('list available modules'))
700 parser
.add_argument('--version', action
='version', version
="%(prog)s " + get_version_string())
702 options
= parser
.parse_args()
704 modules
= {'engine' : jack_engine_params_configure_command(),
705 'params' : jack_driver_params_configure_command()}
706 for internal
in jack
.read_container(['internals']):
707 modules
[str(internal
)] = jack_internal_params_configure_command(internal
)
709 if options
.list_modules
and options
.module
:
710 sys
.stderr
.write(_("Conflicting options, type %s --help for a list of options.") % sys
.argv
[0] + '\n')
714 if options
.list_modules
:
715 sys
.stderr
.write(_("Available modules: "))
716 sys
.stderr
.write(' '.join(modules
) + '\n')
719 module
= options
.module
[0]
720 if not module
in modules
:
721 sys
.stderr
.write(_("Module %(mod)s is not available, type '%(cmd)s -l' for a list of available modules.") % {'mod' : module
, 'cmd' : sys
.argv
[0]} + "\n")
725 show_panels(modules
=modules
, select
=module
)
727 show_panels(modules
=modules
)