Include manpages.
[laditools.git] / ladi-control-center
blobaf3d302b1c8a4608d8af6de1f482ec719178eb57
1 #!/usr/bin/python
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/>.
23 import os
24 import sys
25 import gettext
26 import argparse
28 from laditools import _gettext_domain
29 gettext.install(_gettext_domain)
31 from laditools import get_version_string
32 from laditools import check_ladish
33 from laditools import LadishProxy
34 from laditools import LadishStatusType
35 from laditools import LadishProxyError
36 from laditools import JackConfigProxy
37 from laditools import JackConfigParameter as parameter
38 from laditools import LadiApp
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 tooltips_active = True
48 (COLUMN_NAME,
49 COLUMN_PARAMETER,
50 COLUMN_SHORTDESC,
51 COLUMN_LONGDESC,
52 COLUMN_ISSET) = range (5)
54 def _check_ladish():
55 try:
56 ret = check_ladish()
57 except LadishProxyError as e:
58 sys.stderr.write("%s\n" % str(e))
59 sys.stderr.flush()
60 sys.exit(1)
61 except Exception as e:
62 sys.stderr.write(_("ladish proxy creation failed: %s\n") % str(e))
63 sys.stderr.flush()
64 sys.exit(1)
65 if ret == LadishStatusType.STUDIO_STOPPED:
66 # Everything is OK, there's no need to print out a message
67 #sys.stderr.write(_("ladish studio is loaded and not started\n"))
68 #sys.stderr.flush()
69 return False
70 elif ret == LadishStatusType.NOT_AVAILABLE:
71 sys.stderr.write(_("ladish is not available\n"))
72 sys.stderr.flush()
73 sys.exit(1)
74 else:
75 if ret == LadishStatusType.NO_STUDIO_LOADED:
76 msg = _("JACK can only be configured with a loaded and stopped studio. Please create a new studio or load and stop an existing one.")
77 sys.stderr.write(msg + "\n")
78 title = _("No studio present")
79 elif ret == LadishStatusType.STUDIO_RUNNING:
80 msg = _("JACK can only be configured with a stopped studio. Please stop your studio first.")
81 sys.stderr.write(msg + "\n")
82 title = _("Studio is running")
83 else:
84 sys.stderr(_("Unexpected error!\n"))
85 try:
86 mdlg = Gtk.MessageDialog(type = Gtk.MessageType.ERROR,
87 buttons = Gtk.ButtonsType.CLOSE,
88 message_format = msg)
89 mdlg.set_title(title)
90 mdlg.run()
91 mdlg.hide()
92 except:
93 pass
94 finally:
95 sys.stderr.flush()
96 sys.exit(1)
98 class configure_command(object):
99 def __init__(self):
100 pass
102 def get_description(self):
103 pass
105 def get_window_title(self):
106 return self.get_description();
108 def run(self, args):
109 return self.activate()
111 class parameter_enum_value(GObject.GObject):
112 def __init__(self, is_fake_value, value, description):
113 GObject.GObject.__init__(self)
114 self.is_fake_value = is_fake_value
115 self.value = value
116 self.description = description
118 def get_description(self):
119 if self.is_fake_value:
120 return self.description
122 return str(self.value) + " - " + self.description
124 class parameter_store(GObject.GObject):
125 def __init__(self, param):
126 GObject.GObject.__init__(self)
127 self.param = param
128 self.name = self.param.get_name()
129 self.is_set, self.default_value, self.value = self.param.get_value()
130 self.modified = False
131 self.has_range = self.param.has_range()
132 self.is_strict = self.param.is_strict_enum()
133 self.is_fake_value = self.param.is_fake_values_enum()
135 self.enum_values = []
137 if self.has_range:
138 self.range_min, self.range_max = self.param.get_range()
139 else:
140 for enum_value in self.param.get_enum_values():
141 self.enum_values.append(parameter_enum_value(self.is_fake_value, enum_value[0], enum_value[1]))
143 def get_name(self):
144 return self.name
146 def get_type(self):
147 return self.param.get_type()
149 def get_value(self):
150 return self.value
152 def get_default_value(self):
153 if not self.is_fake_value:
154 return str(self.default_value)
156 for enum_value in self.get_enum_values():
157 if enum_value.value == self.default_value:
158 return enum_value.get_description()
160 return "???"
162 def set_value(self, value):
163 self.value = value
164 self.modified = True
166 def reset_value(self):
167 self.value = self.default_value
169 def get_short_description(self):
170 return self.param.get_short_description()
172 def maybe_save_value(self):
173 if self.modified:
174 self.param.set_value(self.value)
175 self.modified = False
177 def get_range(self):
178 return self.range_min, self.range_max
180 def has_enum(self):
181 return len(self.enum_values) != 0
183 def is_strict_enum(self):
184 return self.is_strict
186 def get_enum_values(self):
187 return self.enum_values
189 GObject.type_register(parameter_store)
191 def combobox_get_active_text(combobox, model_index = 0):
192 model = combobox.get_model()
193 active = combobox.get_active()
194 if active < 0:
195 return None
196 return model[active][model_index]
198 class cell_renderer_param(Gtk.CellRendererPixbuf):
199 __gproperties__ = { "parameter": (GObject.TYPE_OBJECT,
200 "Parameter",
201 "Parameter",
202 GObject.PARAM_READWRITE) }
204 def __init__(self):
205 Gtk.CellRendererPixbuf.__init__(self)
206 self.parameter = None
207 self.set_property('mode', Gtk.CellRendererMode.EDITABLE)
208 self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE)
209 self.renderer_text = Gtk.CellRendererText()
210 self.renderer_toggle = Gtk.CellRendererToggle()
211 self.renderer_combo = Gtk.CellRendererCombo()
212 self.renderer_spinbutton = Gtk.CellRendererSpin()
213 for r in (self.renderer_text, self.renderer_combo, self.renderer_spinbutton):
214 r.connect("edited", self.on_edited)
215 self.renderer = None
216 self.edit_widget = None
218 def do_set_property(self, pspec, value):
219 if pspec.name == 'parameter':
220 if value.get_type() == 'b':
221 self.set_property('mode', Gtk.CellRendererMode.ACTIVATABLE)
222 else:
223 self.set_property('mode', Gtk.CellRendererMode.EDITABLE)
224 else:
225 sys.stderr.write(pspec.name)
226 setattr(self, pspec.name, value)
228 def do_get_property(self, pspec):
229 return getattr(self, pspec.name)
231 def choose_renderer(self):
232 typechar = self.parameter.get_type()
233 value = self.parameter.get_value()
235 if typechar == "b":
236 self.renderer = self.renderer_toggle
237 self.renderer.set_activatable(True)
238 self.renderer.set_active(value)
239 self.renderer.set_property("xalign", 0.0)
240 return
242 if self.parameter.has_enum():
243 self.renderer = self.renderer_combo
245 m = Gtk.ListStore(str, parameter_enum_value)
247 for value in self.parameter.get_enum_values():
248 m.append([value.get_description(), value])
250 self.renderer.set_property("model",m)
251 self.renderer.set_property('text-column', 0)
252 self.renderer.set_property('editable', True)
253 self.renderer.set_property('has_entry', not self.parameter.is_strict_enum())
255 value = self.parameter.get_value()
256 if self.parameter.is_fake_value:
257 text = "???"
258 for enum_value in self.parameter.get_enum_values():
259 if enum_value.value == value:
260 text = enum_value.get_description()
261 break
262 else:
263 text = str(value)
265 self.renderer.set_property('text', text)
267 return
269 if typechar == 'u' or typechar == 'i':
270 self.renderer = self.renderer_spinbutton
271 self.renderer.set_property('text', str(value))
272 self.renderer.set_property('editable', True)
273 if self.parameter.has_range:
274 range_min, range_max = self.parameter.get_range()
275 self.renderer.set_property('adjustment', Gtk.Adjustment(value, range_min, range_max, 1, abs(int((range_max - range_min) / 10))))
276 else:
277 self.renderer.set_property('adjustment', Gtk.Adjustment(value, 0, 100000, 1, 1000))
278 return
280 self.renderer = self.renderer_text
281 self.renderer.set_property('editable', True)
282 self.renderer.set_property('text', self.parameter.get_value())
284 def do_render(self, ctx, widget, bg_area, cell_area, flags):
285 self.choose_renderer()
286 return self.renderer.render(ctx, widget, bg_area, cell_area, flags)
288 def do_get_size(self, widget, cell_area=None):
289 self.choose_renderer()
290 return self.renderer.get_size(widget, cell_area)
292 def do_activate(self, event, widget, path, background_area, cell_area, flags):
293 self.choose_renderer()
294 if self.parameter.get_type() == 'b':
295 self.parameter.set_value(not self.parameter.get_value())
296 widget.get_model()[path][COLUMN_ISSET] = "modified"
297 return True
299 def on_edited(self, renderer, path, value_str):
300 parameter = self.edit_parameter
301 widget = self.edit_widget
302 model = self.edit_tree.get_model()
303 self.edit_widget = None
304 typechar = parameter.get_type()
305 if type(widget) == Gtk.ComboBox:
306 value = combobox_get_active_text(widget, 1)
307 if value == None:
308 return
309 value_str = value.value
310 elif type(widget) == Gtk.ComboBoxText:
311 enum_value = combobox_get_active_text(widget, 1)
312 if enum_value:
313 value_str = enum_value.value
314 else:
315 value_str = widget.get_active_text()
317 if typechar == 'u' or typechar == 'i':
318 try:
319 value = int(value_str)
320 except ValueError, e:
321 # Hide the widget (because it may display something else than what user typed in)
322 widget.hide()
323 # Display the error
324 mdlg = Gtk.MessageDialog(buttons = Gtk.ButtonsType.OK, message_format = "Invalid value. Please enter an integer number.")
325 mdlg.run()
326 mdlg.hide()
327 # Return the focus back to the tree to prevent buttons from stealing it
328 self.edit_tree.grab_focus()
329 return
330 else:
331 value = value_str
332 parameter.set_value(value)
333 model[path][COLUMN_ISSET] = "modified"
334 self.edit_tree.grab_focus()
336 def do_start_editing(self, event, widget, path, background_area, cell_area, flags):
337 # this happens when edit requested using keyboard
338 if not event:
339 event = Gdk.Event(Gdk.NOTHING)
341 self.choose_renderer()
342 ret = self.renderer.start_editing(event, widget, path, background_area, cell_area, flags)
343 self.edit_widget = ret
344 self.edit_tree = widget
345 self.edit_parameter = self.parameter
346 return ret
348 GObject.type_register(cell_renderer_param)
350 class jack_params_configure_command(configure_command):
351 def __init__(self, jack, path):
352 self.jack = jack
353 self.path = path
354 self.is_setting = False
356 def reset_value(self, row_path):
357 row = self.liststore[row_path]
358 param = row[COLUMN_PARAMETER]
359 param.reset_value()
360 row[COLUMN_ISSET] = "reset"
362 def do_row_activated(self, treeview, path, view_column):
363 if view_column == self.tvcolumn_is_set:
364 self.reset_value(path)
366 def do_button_press_event(self, tree, event):
367 if event.type != Gdk.EventType._2BUTTON_PRESS:
368 return False
369 # this is needed for proper double-click handling in the list; don't ask me why, I don't know
370 # it's probably because _2BUTTON_PRESS event is still delivered to tree view, automatically deactivating
371 # the newly created edit widget (which gets created on second BUTTON_PRESS but before _2BUTTON_PRESS)
372 # deactivating the widget causes it to be deleted
373 return True
375 def do_key_press_event(self, tree, event):
376 (row_path, cur) = self.treeview.get_cursor()
378 # if Delete was pressed, reset the value
379 #if event.get_state() == 0 and event.keyval == Gdk.KEY_Delete:
380 if event.keyval == Gdk.KEY_Delete:
381 self.reset_value(row_path)
382 tree.queue_draw()
383 return True
385 # prevent ESC from activating the editor
386 if event.string < " ":
387 return False
389 # single-key data entry: if the control is a text entry, spin button or combo box/combo box entry,
390 # then edit the current and set the text value to what user has already typed in
391 (row_path, cur) = self.treeview.get_cursor()
392 param = self.liststore[row_path][COLUMN_PARAMETER]
393 ptype = param.get_type()
395 # we don't care about booleans
396 if ptype == 'b':
397 return False
399 # accept only digits for integer input (or a minus, but only if it's a signed field)
400 if ptype in ('i', 'u'):
401 if not (event.string.isdigit() or (event.string == "-" and ptype == 'i')):
402 return False
404 # Start cell editing
405 # MAYBE: call a specially crafted cell_renderer_param method for this
406 self.treeview.set_cursor_on_cell(row_path, self.tvcolumn_value, self.renderer_value.renderer, True)
408 # cell_renderer_param::on_start_editing() should set edit_widget.
409 # if edit operation has failed (didn't create a widget), pass the key on
410 # MAYBE: call a specially crafted cell_renderer_param method to do this check
411 if self.renderer_value.edit_widget == None:
412 return
414 widget = self.renderer_value.edit_widget
415 if type(widget) in (Gtk.Entry, Gtk.ComboBoxText):
416 # (combo or plain) text entry - set the content and move the cursor to the end
417 sl = len(event.string)
418 widget.set_text(event.string)
419 widget.select_region(sl, sl)
420 return True
422 if type(self.renderer_value.edit_widget) == Gtk.SpinButton:
423 # spin button - set the value and move the cursor to the end
424 if event.string == "-":
425 # special case for minus sign (which can't be converted to float)
426 widget.set_text(event.string)
427 else:
428 widget.set_value(float(event.string))
429 sl = len(widget.get_text())
430 widget.select_region(sl, sl)
431 return True
433 if type(self.renderer_value.edit_widget) == Gtk.ComboBox:
434 # combo box - select the first item that starts with typed character
435 model = widget.get_model()
436 item = -1
437 iter = model.get_iter_root()
438 while iter != None:
439 if model.get_value(iter, 0).startswith(event.string):
440 item = model.get_path(iter)[0]
441 break
442 iter = model.iter_next(iter)
443 widget.set_active(item)
444 return True
446 return False
448 def on_cursor_changed(self, tree):
449 (row_path, cur) = self.treeview.get_cursor()
451 if not self.is_setting and cur != None and cur.get_title() != self.tvcolumn_value.get_title():
452 self.is_setting = True
453 try:
454 self.treeview.set_cursor_on_cell(row_path, self.tvcolumn_value, self.renderer_value.renderer, False)
455 finally:
456 self.is_setting = False
458 def ok_clicked(self, dlg):
459 if self.renderer_value.edit_widget:
460 self.renderer_value.edit_widget.editing_done()
462 def _on_query_tooltip(self, treeview, x, y, keyboard_tip, tooltip):
463 """Handle tooltips for the cells"""
464 try:
465 (path, column, out_x, out_y) = treeview.get_path_at_pos(x, y)
466 if not path:
467 return False
468 except TypeError:
469 return False
471 # Horrible-fix to skip the treeview's header row
472 intpath = int(path.to_string())
473 if intpath == 0:
474 return False
475 elif intpath > 0:
476 path = Gtk.TreePath(str(intpath - 1))
478 text = ''
479 if column.get_title() in (self.tvcolumn_value.get_title(),
480 self.tvcolumn_parameter.get_title()):
481 text = self.liststore[path][COLUMN_LONGDESC]
482 elif column.get_title() == self.tvcolumn_is_set.get_title():
483 if self.liststore[path][COLUMN_ISSET] == "default":
484 return False
485 if self.liststore[path][COLUMN_ISSET] == 'set':
486 text += _("Double-click to schedule reset of value to %s") % \
487 self.liststore[path][COLUMN_PARAMETER].get_default_value()
488 else:
489 text = _("Value will be reset to %s") % \
490 self.liststore[path][COLUMN_PARAMETER].get_default_value()
491 else:
492 return False
494 tooltip.set_text(text)
495 return True
497 def do_destroy(self, *args):
498 for row in self.liststore:
499 param = row[COLUMN_PARAMETER]
500 reset = (row[COLUMN_ISSET] == "reset")
501 if reset:
502 param.param.reset_value()
503 else:
504 if param.modified:
505 param.maybe_save_value()
507 def activate(self, *args, **kwargs):
509 jack = self.jack
511 self.liststore = Gtk.ListStore(GObject.TYPE_STRING,
512 parameter_store,
513 GObject.TYPE_STRING,
514 GObject.TYPE_STRING,
515 GObject.TYPE_STRING)
516 self.treeview = Gtk.TreeView(self.liststore)
517 self.treeview.set_rules_hint(True)
518 self.treeview.set_has_tooltip(True)
519 self.treeview.set_enable_search(False)
521 renderer_text = Gtk.CellRendererText()
522 renderer_toggle = Gtk.CellRendererToggle()
523 renderer_value = cell_renderer_param()
524 self.renderer_value = renderer_value # save for use in event handler methods
526 self.tvcolumn_parameter = Gtk.TreeViewColumn('Parameter', renderer_text, text=COLUMN_NAME)
527 self.tvcolumn_is_set = Gtk.TreeViewColumn('Status', renderer_text, text=COLUMN_ISSET)
528 self.tvcolumn_value = Gtk.TreeViewColumn('Value', renderer_value, parameter=COLUMN_PARAMETER)
529 self.tvcolumn_description = Gtk.TreeViewColumn('Description', renderer_text, text=COLUMN_SHORTDESC)
531 self.tvcolumn_value.set_resizable(True)
532 self.tvcolumn_value.set_min_width(100)
534 self.treeview.append_column(self.tvcolumn_parameter)
535 self.treeview.append_column(self.tvcolumn_is_set)
536 self.treeview.append_column(self.tvcolumn_value)
537 self.treeview.append_column(self.tvcolumn_description)
539 param_names = jack.get_param_names(self.path)
540 for name in param_names:
541 param = parameter(jack, self.path + [name])
542 store = parameter_store(param)
543 if store.is_set:
544 is_set = "set"
545 else:
546 is_set = "default"
547 self.liststore.append([name,
548 store,
549 param.get_short_description(),
550 param.get_long_description(),
551 is_set])
553 self.treeview.connect("row-activated", self.do_row_activated)
554 self.treeview.connect("cursor-changed", self.on_cursor_changed)
555 self.treeview.connect("key-press-event", self.do_key_press_event)
556 self.treeview.connect("button-press-event", self.do_button_press_event)
557 if tooltips_active:
558 self.treeview.connect("query-tooltip", self._on_query_tooltip)
559 self.treeview.connect("destroy", self.do_destroy)
561 if len(param_names):
562 # move cursor to first row and 'value' column
563 self.treeview.set_cursor(Gtk.TreePath(path=0),
564 focus_column=self.tvcolumn_value,
565 start_editing=False)
567 return self.treeview
569 class jack_engine_params_configure_command(jack_params_configure_command):
570 def __init__(self, jack):
571 jack_params_configure_command.__init__(self, jack, ['engine'])
573 def get_description(self):
574 return _('JACK engine')
576 class jack_driver_params_configure_command(jack_params_configure_command):
577 def __init__(self, jack):
578 jack_params_configure_command.__init__(self, jack, ['driver'])
580 def get_description(self):
581 return _('JACK driver')
583 def get_window_title(self):
584 return _('JACK "%s" driver') % jack.get_selected_driver()
586 class jack_internal_params_configure_command(jack_params_configure_command):
587 def __init__(self, jack, name):
588 self.name = name
589 jack_params_configure_command.__init__(self, jack, ['internals', name])
591 def get_description(self):
592 return _('JACK "%s"') % self.name
594 class LadiControlCenter(LadiApp):
596 _appname = 'ladi-control-center'
597 _appname_long = _("LADI Control Center")
598 _appid = 'org.linuxaudio.ladi.controlcenter'
600 def quit(self, *args, **kwargs):
601 self.window.destroy()
602 Gtk.main_quit()
604 @property
605 def modules(self): return self._modules
607 def __init__(self):
609 LadiApp.__init__(self)
610 jack = JackConfigProxy()
612 # Init modules list
613 modules = {'engine' : jack_engine_params_configure_command(jack),
614 'params' : jack_driver_params_configure_command(jack)}
615 for internal in jack.read_container(['internals']):
616 modules[str(internal)] = jack_internal_params_configure_command(jack, internal)
617 self._modules = modules
619 def _activate(self, **kwargs):
621 if 'select' in kwargs:
622 selected = kwargs['select']
623 else:
624 selected = None
626 # Init UI
627 self.window = window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
628 vbox = Gtk.VBox()
629 hbox = Gtk.HBox()
630 notebook = Gtk.Notebook()
632 vbox.pack_start(hbox, True, True, 12)
633 hbox.pack_start(notebook, True, True, 2)
634 window.set_title("LADI Settings")
635 window.set_icon_name('preferences-system')
636 window.set_resizable(True)
637 notebook.set_tab_pos(Gtk.PositionType.LEFT)
639 page_count = 0
641 modules = self.modules
642 for mod in modules:
643 treeview = modules[mod].run({})
644 container = Gtk.ScrolledWindow()
645 container.set_min_content_width(400)
646 container.set_min_content_height(400)
647 container.set_policy(hscrollbar_policy=Gtk.PolicyType.AUTOMATIC,
648 vscrollbar_policy=Gtk.PolicyType.AUTOMATIC)
649 container.add(treeview)
650 container.show_all()
651 #vbox.show_all()
652 try:
653 tab_label = modules[mod].get_window_title()
654 except:
655 tab_label = modules[mod].get_description()
656 notebook.append_page(container, Gtk.Label(tab_label))
657 if selected and selected == mod:
658 notebook.set_current_page(page_count)
659 page_count += 1
661 window.add(vbox)
662 window.connect('destroy', self.quit)
664 self.connect_signals_quit()
666 def run(self, **kwargs):
667 self._activate(**kwargs)
668 self.window.show_all()
669 Gtk.main()
671 if __name__ == "__main__":
672 _check_ladish()
674 GObject.type_register(parameter_enum_value)
676 parser = argparse.ArgumentParser(description=_('Convenient graphical interface for configuring JACK'),
677 epilog=_('This program is part of the LADITools suite.'))
678 parser.add_argument('-m',
679 '--module',
680 nargs=1,
681 metavar='MODULE',
682 help=_('select the module to configure'))
683 parser.add_argument('-l',
684 '--list-modules',
685 action='store_true',
686 help=_('list available modules'))
687 parser.add_argument('--version',
688 action='version',
689 version="%(prog)s " + get_version_string())
691 options = parser.parse_args()
693 app = LadiControlCenter()
695 if options.list_modules and options.module:
696 sys.stderr.write(_("Conflicting options, type %s --help for a list of options.") % sys.argv[0] + '\n')
697 sys.stderr.flush()
698 sys.exit(2)
700 modules = app.modules
701 if options.list_modules:
702 sys.stderr.write(_("Available modules: "))
703 sys.stderr.write(' '.join(modules) + '\n')
704 sys.stderr.flush()
705 elif options.module:
706 module = options.module[0]
707 if not module in modules:
708 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")
709 sys.stderr.flush()
710 sys.exit(2)
711 else:
712 app.run(select=module)
713 else:
714 app.run()
716 sys.exit(0)