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 LadiConfiguration
35 from laditools
import LadishProxy
36 from laditools
import LadishStatusType
37 from laditools
import LadishProxyError
38 from laditools
import JackConfigProxy
40 from gi
.repository
import Gtk
41 from gi
.repository
import Gdk
42 from gi
.repository
import GObject
44 from laditools
.gtk
import find_data_file
46 sig_handler
= signal
.getsignal(signal
.SIGTERM
)
47 signal
.signal(signal
.SIGINT
, sig_handler
)
53 COLUMN_ISSET
) = range (5)
58 except LadishProxyError
as e
:
59 sys
.stderr
.write("%s\n" % str(e
))
62 except Exception as e
:
63 sys
.stderr
.write(_("ladish proxy creation failed: %s\n") % str(e
))
66 if ret
== LadishStatusType
.STUDIO_STOPPED
:
67 # Everything is OK, there's no need to print out a message
68 #sys.stderr.write(_("ladish studio is loaded and not started\n"))
71 elif ret
== LadishStatusType
.NOT_AVAILABLE
:
72 sys
.stderr
.write(_("ladish is not available\n"))
76 if ret
== LadishStatusType
.NO_STUDIO_LOADED
:
77 msg
= _("JACK can only be configured with a loaded and stopped studio. Please create a new studio or load and stop an existing one.")
78 sys
.stderr
.write(msg
+ "\n")
79 title
= _("No studio present")
80 elif ret
== LadishStatusType
.STUDIO_RUNNING
:
81 msg
= _("JACK can only be configured with a stopped studio. Please stop your studio first.")
82 sys
.stderr
.write(msg
+ "\n")
83 title
= _("Studio is running")
85 sys
.stderr(_("Unexpected error!\n"))
87 mdlg
= Gtk
.MessageDialog(type = Gtk
.MessageType
.ERROR
,
88 buttons
= Gtk
.ButtonsType
.CLOSE
,
99 class parameter(object):
100 def __init__(self
, path
):
102 self
.name
= path
[-1:]
108 return jack
.get_param_type(self
.path
)
111 return jack
.get_param_value(self
.path
)
113 def set_value(self
, value
):
114 jack
.set_param_value(self
.path
, value
)
116 def reset_value(self
):
117 jack
.reset_param_value(self
.path
)
119 def get_short_description(self
):
120 return jack
.get_param_short_description(self
.path
)
122 def get_long_description(self
):
123 descr
= jack
.get_param_long_description(self
.path
)
125 descr
= self
.get_short_description()
129 return jack
.param_has_range(self
.path
)
132 return jack
.param_get_range(self
.path
)
135 return jack
.param_has_enum(self
.path
)
137 def is_strict_enum(self
):
138 return jack
.param_is_strict_enum(self
.path
)
140 def is_fake_values_enum(self
):
141 return jack
.param_is_fake_value(self
.path
)
143 def get_enum_values(self
):
144 return jack
.param_get_enum_values(self
.path
)
146 class configure_command(object):
150 def get_description(self
):
153 def get_window_title(self
):
154 return self
.get_description();
157 return self
.activate()
159 class parameter_enum_value(GObject
.GObject
):
160 def __init__(self
, is_fake_value
, value
, description
):
161 GObject
.GObject
.__init
__(self
)
162 self
.is_fake_value
= is_fake_value
164 self
.description
= description
166 def get_description(self
):
167 if self
.is_fake_value
:
168 return self
.description
170 return str(self
.value
) + " - " + self
.description
172 class parameter_store(GObject
.GObject
):
173 def __init__(self
, param
):
174 GObject
.GObject
.__init
__(self
)
176 self
.name
= self
.param
.get_name()
177 self
.is_set
, self
.default_value
, self
.value
= self
.param
.get_value()
178 self
.modified
= False
179 self
.has_range
= self
.param
.has_range()
180 self
.is_strict
= self
.param
.is_strict_enum()
181 self
.is_fake_value
= self
.param
.is_fake_values_enum()
183 self
.enum_values
= []
186 self
.range_min
, self
.range_max
= self
.param
.get_range()
188 for enum_value
in self
.param
.get_enum_values():
189 self
.enum_values
.append(parameter_enum_value(self
.is_fake_value
, enum_value
[0], enum_value
[1]))
195 return self
.param
.get_type()
200 def get_default_value(self
):
201 if not self
.is_fake_value
:
202 return str(self
.default_value
)
204 for enum_value
in self
.get_enum_values():
205 if enum_value
.value
== self
.default_value
:
206 return enum_value
.get_description()
210 def set_value(self
, value
):
214 def reset_value(self
):
215 self
.value
= self
.default_value
217 def get_short_description(self
):
218 return self
.param
.get_short_description()
220 def maybe_save_value(self
):
222 self
.param
.set_value(self
.value
)
223 self
.modified
= False
226 return self
.range_min
, self
.range_max
229 return len(self
.enum_values
) != 0
231 def is_strict_enum(self
):
232 return self
.is_strict
234 def get_enum_values(self
):
235 return self
.enum_values
237 GObject
.type_register(parameter_store
)
239 def combobox_get_active_text(combobox
, model_index
= 0):
240 model
= combobox
.get_model()
241 active
= combobox
.get_active()
244 return model
[active
][model_index
]
246 class cell_renderer_param(Gtk
.CellRendererPixbuf
):
247 __gproperties__
= { "parameter": (GObject
.TYPE_OBJECT
,
250 GObject
.PARAM_READWRITE
) }
253 Gtk
.CellRendererPixbuf
.__init
__(self
)
254 self
.parameter
= None
255 self
.set_property('mode', Gtk
.CellRendererMode
.EDITABLE
)
256 self
.set_property('mode', Gtk
.CellRendererMode
.ACTIVATABLE
)
257 self
.renderer_text
= Gtk
.CellRendererText()
258 self
.renderer_toggle
= Gtk
.CellRendererToggle()
259 self
.renderer_combo
= Gtk
.CellRendererCombo()
260 self
.renderer_spinbutton
= Gtk
.CellRendererSpin()
261 for r
in (self
.renderer_text
, self
.renderer_combo
, self
.renderer_spinbutton
):
262 r
.connect("edited", self
.on_edited
)
264 self
.edit_widget
= None
266 def do_set_property(self
, pspec
, value
):
267 if pspec
.name
== 'parameter':
268 if value
.get_type() == 'b':
269 self
.set_property('mode', Gtk
.CellRendererMode
.ACTIVATABLE
)
271 self
.set_property('mode', Gtk
.CellRendererMode
.EDITABLE
)
273 sys
.stderr
.write(pspec
.name
)
274 setattr(self
, pspec
.name
, value
)
276 def do_get_property(self
, pspec
):
277 return getattr(self
, pspec
.name
)
279 def choose_renderer(self
):
280 typechar
= self
.parameter
.get_type()
281 value
= self
.parameter
.get_value()
284 self
.renderer
= self
.renderer_toggle
285 self
.renderer
.set_activatable(True)
286 self
.renderer
.set_active(value
)
287 self
.renderer
.set_property("xalign", 0.0)
290 if self
.parameter
.has_enum():
291 self
.renderer
= self
.renderer_combo
293 m
= Gtk
.ListStore(str, parameter_enum_value
)
295 for value
in self
.parameter
.get_enum_values():
296 m
.append([value
.get_description(), value
])
298 self
.renderer
.set_property("model",m
)
299 self
.renderer
.set_property('text-column', 0)
300 self
.renderer
.set_property('editable', True)
301 self
.renderer
.set_property('has_entry', not self
.parameter
.is_strict_enum())
303 value
= self
.parameter
.get_value()
304 if self
.parameter
.is_fake_value
:
306 for enum_value
in self
.parameter
.get_enum_values():
307 if enum_value
.value
== value
:
308 text
= enum_value
.get_description()
313 self
.renderer
.set_property('text', text
)
317 if typechar
== 'u' or typechar
== 'i':
318 self
.renderer
= self
.renderer_spinbutton
319 self
.renderer
.set_property('text', str(value
))
320 self
.renderer
.set_property('editable', True)
321 if self
.parameter
.has_range
:
322 range_min
, range_max
= self
.parameter
.get_range()
323 self
.renderer
.set_property('adjustment', Gtk
.Adjustment(value
, range_min
, range_max
, 1, abs(int((range_max
- range_min
) / 10))))
325 self
.renderer
.set_property('adjustment', Gtk
.Adjustment(value
, 0, 100000, 1, 1000))
328 self
.renderer
= self
.renderer_text
329 self
.renderer
.set_property('editable', True)
330 self
.renderer
.set_property('text', self
.parameter
.get_value())
332 def do_render(self
, ctx
, widget
, bg_area
, cell_area
, flags
):
333 self
.choose_renderer()
334 return self
.renderer
.render(ctx
, widget
, bg_area
, cell_area
, flags
)
336 def do_get_size(self
, widget
, cell_area
=None):
337 self
.choose_renderer()
338 return self
.renderer
.get_size(widget
, cell_area
)
340 def do_activate(self
, event
, widget
, path
, background_area
, cell_area
, flags
):
341 self
.choose_renderer()
342 if self
.parameter
.get_type() == 'b':
343 self
.parameter
.set_value(not self
.parameter
.get_value())
344 widget
.get_model()[path
][COLUMN_ISSET
] = "modified"
347 def on_edited(self
, renderer
, path
, value_str
):
348 parameter
= self
.edit_parameter
349 widget
= self
.edit_widget
350 model
= self
.edit_tree
.get_model()
351 self
.edit_widget
= None
352 typechar
= parameter
.get_type()
353 if type(widget
) == Gtk
.ComboBox
:
354 value
= combobox_get_active_text(widget
, 1)
357 value_str
= value
.value
358 elif type(widget
) == Gtk
.ComboBoxText
:
359 enum_value
= combobox_get_active_text(widget
, 1)
361 value_str
= enum_value
.value
363 value_str
= widget
.get_active_text()
365 if typechar
== 'u' or typechar
== 'i':
367 value
= int(value_str
)
368 except ValueError, e
:
369 # Hide the widget (because it may display something else than what user typed in)
372 mdlg
= Gtk
.MessageDialog(buttons
= Gtk
.ButtonsType
.OK
, message_format
= "Invalid value. Please enter an integer number.")
375 # Return the focus back to the tree to prevent buttons from stealing it
376 self
.edit_tree
.grab_focus()
380 parameter
.set_value(value
)
381 model
[path
][COLUMN_ISSET
] = "modified"
382 self
.edit_tree
.grab_focus()
384 def do_start_editing(self
, event
, widget
, path
, background_area
, cell_area
, flags
):
385 # this happens when edit requested using keyboard
387 event
= Gdk
.Event(Gdk
.NOTHING
)
389 self
.choose_renderer()
390 ret
= self
.renderer
.start_editing(event
, widget
, path
, background_area
, cell_area
, flags
)
391 self
.edit_widget
= ret
392 self
.edit_tree
= widget
393 self
.edit_parameter
= self
.parameter
396 GObject
.type_register(cell_renderer_param
)
398 class jack_params_configure_command(configure_command
):
399 def __init__(self
, path
):
401 self
.is_setting
= False
403 def reset_value(self
, row_path
):
404 row
= self
.liststore
[row_path
]
405 param
= row
[COLUMN_PARAMETER
]
407 row
[COLUMN_ISSET
] = "reset"
409 def do_row_activated(self
, treeview
, path
, view_column
):
410 if view_column
== self
.tvcolumn_is_set
:
411 self
.reset_value(path
)
413 def do_button_press_event(self
, tree
, event
):
414 if event
.type != Gdk
.EventType
._2BUTTON
_PRESS
:
416 # this is needed for proper double-click handling in the list; don't ask me why, I don't know
417 # it's probably because _2BUTTON_PRESS event is still delivered to tree view, automatically deactivating
418 # the newly created edit widget (which gets created on second BUTTON_PRESS but before _2BUTTON_PRESS)
419 # deactivating the widget causes it to be deleted
422 def do_key_press_event(self
, tree
, event
):
423 (row_path
, cur
) = self
.treeview
.get_cursor()
425 # if Delete was pressed, reset the value
426 #if event.get_state() == 0 and event.keyval == Gdk.KEY_Delete:
427 if event
.keyval
== Gdk
.KEY_Delete
:
428 self
.reset_value(row_path
)
432 # prevent ESC from activating the editor
433 if event
.string
< " ":
436 # single-key data entry: if the control is a text entry, spin button or combo box/combo box entry,
437 # then edit the current and set the text value to what user has already typed in
438 (row_path
, cur
) = self
.treeview
.get_cursor()
439 param
= self
.liststore
[row_path
][COLUMN_PARAMETER
]
440 ptype
= param
.get_type()
442 # we don't care about booleans
446 # accept only digits for integer input (or a minus, but only if it's a signed field)
447 if ptype
in ('i', 'u'):
448 if not (event
.string
.isdigit() or (event
.string
== "-" and ptype
== 'i')):
452 # MAYBE: call a specially crafted cell_renderer_param method for this
453 self
.treeview
.set_cursor_on_cell(row_path
, self
.tvcolumn_value
, self
.renderer_value
.renderer
, True)
455 # cell_renderer_param::on_start_editing() should set edit_widget.
456 # if edit operation has failed (didn't create a widget), pass the key on
457 # MAYBE: call a specially crafted cell_renderer_param method to do this check
458 if self
.renderer_value
.edit_widget
== None:
461 widget
= self
.renderer_value
.edit_widget
462 if type(widget
) in (Gtk
.Entry
, Gtk
.ComboBoxText
):
463 # (combo or plain) text entry - set the content and move the cursor to the end
464 sl
= len(event
.string
)
465 widget
.set_text(event
.string
)
466 widget
.select_region(sl
, sl
)
469 if type(self
.renderer_value
.edit_widget
) == Gtk
.SpinButton
:
470 # spin button - set the value and move the cursor to the end
471 if event
.string
== "-":
472 # special case for minus sign (which can't be converted to float)
473 widget
.set_text(event
.string
)
475 widget
.set_value(float(event
.string
))
476 sl
= len(widget
.get_text())
477 widget
.select_region(sl
, sl
)
480 if type(self
.renderer_value
.edit_widget
) == Gtk
.ComboBox
:
481 # combo box - select the first item that starts with typed character
482 model
= widget
.get_model()
484 iter = model
.get_iter_root()
486 if model
.get_value(iter, 0).startswith(event
.string
):
487 item
= model
.get_path(iter)[0]
489 iter = model
.iter_next(iter)
490 widget
.set_active(item
)
495 def on_cursor_changed(self
, tree
):
496 (row_path
, cur
) = self
.treeview
.get_cursor()
498 if not self
.is_setting
and cur
!= None and cur
.get_title() != self
.tvcolumn_value
.get_title():
499 self
.is_setting
= True
501 self
.treeview
.set_cursor_on_cell(row_path
, self
.tvcolumn_value
, self
.renderer_value
.renderer
, False)
503 self
.is_setting
= False
505 def ok_clicked(self
, dlg
):
506 if self
.renderer_value
.edit_widget
:
507 self
.renderer_value
.edit_widget
.editing_done()
509 def _on_query_tooltip(self
, treeview
, x
, y
, keyboard_tip
, tooltip
):
510 """Handle tooltips for the cells"""
512 result
, out_x
, out_y
, model
, path
, iter = treeview
.get_tooltip_context(x
, y
, keyboard_tip
)
518 text
= self
.liststore
[path
][COLUMN_LONGDESC
]
519 if self
.liststore
[path
][COLUMN_ISSET
] == "default":
523 if self
.liststore
[path
][COLUMN_ISSET
] == "reset":
524 text
+= _("<i>Value will be reset to %s</i>") % \
525 self
.liststore
[path
][COLUMN_PARAMETER
].get_default_value()
527 text
+= _("<i>Double-click to schedule reset of value to %s</i>") % \
528 self
.liststore
[path
][COLUMN_PARAMETER
].get_default_value()
530 tooltip
.set_icon_from_icon_name(Gtk
.STOCK_DIALOG_INFO
, Gtk
.IconSize
.DIALOG
)
531 tooltip
.set_markup(text
)
532 treeview
.set_tooltip_row(tooltip
, path
)
535 def do_destroy(self
, *args
):
536 for row
in self
.liststore
:
537 param
= row
[COLUMN_PARAMETER
]
538 reset
= (row
[COLUMN_ISSET
] == "reset")
540 param
.param
.reset_value()
543 param
.maybe_save_value()
545 def activate(self
, *args
, **kwargs
):
547 self
.liststore
= Gtk
.ListStore(GObject
.TYPE_STRING
,
552 self
.treeview
= Gtk
.TreeView(self
.liststore
)
553 self
.treeview
.set_rules_hint(True)
554 self
.treeview
.set_has_tooltip(True)
555 self
.treeview
.set_enable_search(False)
557 renderer_text
= Gtk
.CellRendererText()
558 renderer_toggle
= Gtk
.CellRendererToggle()
559 renderer_value
= cell_renderer_param()
560 self
.renderer_value
= renderer_value
# save for use in event handler methods
562 self
.tvcolumn_parameter
= Gtk
.TreeViewColumn('Parameter', renderer_text
, text
=COLUMN_NAME
)
563 self
.tvcolumn_is_set
= Gtk
.TreeViewColumn('Status', renderer_text
, text
=COLUMN_ISSET
)
564 self
.tvcolumn_value
= Gtk
.TreeViewColumn('Value', renderer_value
, parameter
=COLUMN_PARAMETER
)
565 self
.tvcolumn_description
= Gtk
.TreeViewColumn('Description', renderer_text
, text
=COLUMN_SHORTDESC
)
567 self
.tvcolumn_value
.set_resizable(True)
568 self
.tvcolumn_value
.set_min_width(100)
570 self
.treeview
.append_column(self
.tvcolumn_parameter
)
571 self
.treeview
.append_column(self
.tvcolumn_is_set
)
572 self
.treeview
.append_column(self
.tvcolumn_value
)
573 self
.treeview
.append_column(self
.tvcolumn_description
)
575 param_names
= jack
.get_param_names(self
.path
)
576 for name
in param_names
:
577 param
= parameter(self
.path
+ [name
])
578 store
= parameter_store(param
)
583 self
.liststore
.append([name
, store
, param
.get_short_description(), param
.get_long_description(), is_set
])
585 self
.treeview
.connect("row-activated", self
.do_row_activated
)
586 self
.treeview
.connect("cursor-changed", self
.on_cursor_changed
)
587 self
.treeview
.connect("key-press-event", self
.do_key_press_event
)
588 self
.treeview
.connect("button-press-event", self
.do_button_press_event
)
589 self
.treeview
.connect("query-tooltip", self
._on
_query
_tooltip
)
590 self
.treeview
.connect("destroy", self
.do_destroy
)
593 # move cursor to first row and 'value' column
594 self
.treeview
.set_cursor(Gtk
.TreePath(path
=0),
595 focus_column
=self
.tvcolumn_value
,
600 class jack_engine_params_configure_command(jack_params_configure_command
):
602 jack_params_configure_command
.__init
__(self
, ['engine'])
604 def get_description(self
):
605 return _('JACK engine parameters')
607 class jack_driver_params_configure_command(jack_params_configure_command
):
609 jack_params_configure_command
.__init
__(self
, ['driver'])
611 def get_description(self
):
612 return _('JACK driver parameters')
614 def get_window_title(self
):
615 return _('JACK "%s" driver parameters') % jack
.get_selected_driver()
617 class jack_internal_params_configure_command(jack_params_configure_command
):
618 def __init__(self
, name
):
620 jack_params_configure_command
.__init
__(self
, ['internals', name
])
622 def get_description(self
):
623 return _('JACK "%s" parameters') % self
.name
625 def show_panels(*args
, **kwargs
):
626 if not 'modules' in kwargs
:
628 mods
= kwargs
['modules']
629 window
= Gtk
.Window
.new(Gtk
.WindowType
.TOPLEVEL
)
630 window
.set_title("LADI Settings")
631 window
.set_icon_name('preferences-system')
632 window
.set_resizable(True)
633 notebook
= Gtk
.Notebook()
634 notebook
.set_tab_pos(Gtk
.PositionType
.LEFT
)
636 if 'select' in kwargs
:
637 selected
= kwargs
['select']
644 treeview
= mods
[mod
].run({})
645 container
= Gtk
.ScrolledWindow()
646 container
.set_min_content_width(400)
647 container
.set_min_content_height(400)
648 container
.set_policy(hscrollbar_policy
=Gtk
.PolicyType
.AUTOMATIC
,
649 vscrollbar_policy
=Gtk
.PolicyType
.AUTOMATIC
)
650 container
.add(treeview
)
653 notebook
.append_page(container
, Gtk
.Label(mod
))
654 if selected
and selected
== mod
:
655 notebook
.set_current_page(page_count
)
661 window
.connect('destroy', Gtk
.main_quit
)
664 if __name__
== "__main__":
667 jack
= JackConfigProxy()
670 GObject
.type_register(parameter_enum_value
)
672 modules
= {'engine' : jack_engine_params_configure_command(),
673 'params' : jack_driver_params_configure_command()}
674 for internal
in jack
.read_container(['internals']):
675 modules
[str(internal
)] = jack_internal_params_configure_command(internal
)
677 modules
= {'engine' : jack_engine_params_configure_command(),
678 'params' : jack_driver_params_configure_command()}
679 for internal
in jack
.read_container(['internals']):
680 modules
[str(internal
)] = jack_internal_params_configure_command(internal
)
682 parser
= argparse
.ArgumentParser(description
=_('Convenient graphical interface for configuring JACK'),
683 epilog
=_('This program is part of the LADITools suite.'))
684 parser
.add_argument('-m', '--module', nargs
=1, metavar
='MODULE', help=_('select the module to configure'))
685 parser
.add_argument('-l', '--list-modules', action
='store_true', help=_('list available modules'))
686 parser
.add_argument('--version', action
='version', version
="%(prog)s " + get_version_string())
688 options
= parser
.parse_args()
690 if options
.list_modules
and options
.module
:
691 sys
.stderr
.write(_("Conflicting options, type %s --help for a list of options.") % sys
.argv
[0] + '\n')
695 if options
.list_modules
:
696 sys
.stderr
.write(_("Available modules: "))
697 sys
.stderr
.write(' '.join(modules
) + '\n')
700 module
= options
.module
[0]
701 if not module
in modules
:
702 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")
706 show_panels(modules
=modules
, select
=module
)
708 show_panels(modules
=modules
)