Move icons under icons/* and update code and buildsystem.
[laditools.git] / ladi-control-center
blobf56b89a418817a60ea18fef884f7fd2fb526596b
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 signal
26 import gettext
27 import argparse
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)
49 (COLUMN_NAME,
50 COLUMN_PARAMETER,
51 COLUMN_SHORTDESC,
52 COLUMN_LONGDESC,
53 COLUMN_ISSET) = range (5)
55 def _check_ladish():
56 try:
57 ret = check_ladish()
58 except LadishProxyError as e:
59 sys.stderr.write("%s\n" % str(e))
60 sys.stderr.flush()
61 sys.exit(1)
62 except Exception as e:
63 sys.stderr.write(_("ladish proxy creation failed: %s\n") % str(e))
64 sys.stderr.flush()
65 sys.exit(1)
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"))
69 #sys.stderr.flush()
70 return False
71 elif ret == LadishStatusType.NOT_AVAILABLE:
72 sys.stderr.write(_("ladish is not available\n"))
73 sys.stderr.flush()
74 sys.exit(1)
75 else:
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")
84 else:
85 sys.stderr(_("Unexpected error!\n"))
86 try:
87 mdlg = Gtk.MessageDialog(type = Gtk.MessageType.ERROR,
88 buttons = Gtk.ButtonsType.CLOSE,
89 message_format = msg)
90 mdlg.set_title(title)
91 mdlg.run()
92 mdlg.hide()
93 except:
94 pass
95 finally:
96 sys.stderr.flush()
97 sys.exit(1)
99 class parameter(object):
100 def __init__(self, path):
101 self.path = path
102 self.name = path[-1:]
104 def get_name(self):
105 return self.name
107 def get_type(self):
108 return jack.get_param_type(self.path)
110 def get_value(self):
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)
124 if not descr:
125 descr = self.get_short_description()
126 return descr
128 def has_range(self):
129 return jack.param_has_range(self.path)
131 def get_range(self):
132 return jack.param_get_range(self.path)
134 def has_enum(self):
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):
147 def __init__(self):
148 pass
150 def get_description(self):
151 pass
153 def get_window_title(self):
154 return self.get_description();
156 def run(self, args):
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
163 self.value = 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)
175 self.param = param
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 = []
185 if self.has_range:
186 self.range_min, self.range_max = self.param.get_range()
187 else:
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]))
191 def get_name(self):
192 return self.name
194 def get_type(self):
195 return self.param.get_type()
197 def get_value(self):
198 return self.value
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()
208 return "???"
210 def set_value(self, value):
211 self.value = value
212 self.modified = True
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):
221 if self.modified:
222 self.param.set_value(self.value)
223 self.modified = False
225 def get_range(self):
226 return self.range_min, self.range_max
228 def has_enum(self):
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()
242 if active < 0:
243 return None
244 return model[active][model_index]
246 class cell_renderer_param(Gtk.CellRendererPixbuf):
247 __gproperties__ = { "parameter": (GObject.TYPE_OBJECT,
248 "Parameter",
249 "Parameter",
250 GObject.PARAM_READWRITE) }
252 def __init__(self):
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)
263 self.renderer = None
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)
270 else:
271 self.set_property('mode', Gtk.CellRendererMode.EDITABLE)
272 else:
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()
283 if typechar == "b":
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)
288 return
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:
305 text = "???"
306 for enum_value in self.parameter.get_enum_values():
307 if enum_value.value == value:
308 text = enum_value.get_description()
309 break
310 else:
311 text = str(value)
313 self.renderer.set_property('text', text)
315 return
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))))
324 else:
325 self.renderer.set_property('adjustment', Gtk.Adjustment(value, 0, 100000, 1, 1000))
326 return
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"
345 return True
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)
355 if value == None:
356 return
357 value_str = value.value
358 elif type(widget) == Gtk.ComboBoxText:
359 enum_value = combobox_get_active_text(widget, 1)
360 if enum_value:
361 value_str = enum_value.value
362 else:
363 value_str = widget.get_active_text()
365 if typechar == 'u' or typechar == 'i':
366 try:
367 value = int(value_str)
368 except ValueError, e:
369 # Hide the widget (because it may display something else than what user typed in)
370 widget.hide()
371 # Display the error
372 mdlg = Gtk.MessageDialog(buttons = Gtk.ButtonsType.OK, message_format = "Invalid value. Please enter an integer number.")
373 mdlg.run()
374 mdlg.hide()
375 # Return the focus back to the tree to prevent buttons from stealing it
376 self.edit_tree.grab_focus()
377 return
378 else:
379 value = value_str
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
386 if not event:
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
394 return ret
396 GObject.type_register(cell_renderer_param)
398 class jack_params_configure_command(configure_command):
399 def __init__(self, path):
400 self.path = path
401 self.is_setting = False
403 def reset_value(self, row_path):
404 row = self.liststore[row_path]
405 param = row[COLUMN_PARAMETER]
406 param.reset_value()
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:
415 return False
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
420 return True
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)
429 tree.queue_draw()
430 return True
432 # prevent ESC from activating the editor
433 if event.string < " ":
434 return False
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
443 if ptype == 'b':
444 return False
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')):
449 return False
451 # Start cell editing
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:
459 return
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)
467 return True
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)
474 else:
475 widget.set_value(float(event.string))
476 sl = len(widget.get_text())
477 widget.select_region(sl, sl)
478 return True
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()
483 item = -1
484 iter = model.get_iter_root()
485 while iter != None:
486 if model.get_value(iter, 0).startswith(event.string):
487 item = model.get_path(iter)[0]
488 break
489 iter = model.iter_next(iter)
490 widget.set_active(item)
491 return True
493 return False
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
500 try:
501 self.treeview.set_cursor_on_cell(row_path, self.tvcolumn_value, self.renderer_value.renderer, False)
502 finally:
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"""
511 try:
512 result, out_x, out_y, model, path, iter = treeview.get_tooltip_context(x, y, keyboard_tip)
513 if not path:
514 return False
515 except TypeError:
516 return False
518 text = self.liststore[path][COLUMN_LONGDESC]
519 if self.liststore[path][COLUMN_ISSET] == "default":
520 pass
521 else:
522 text += '\n\n'
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()
526 else:
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)
533 return True
535 def do_destroy(self, *args):
536 for row in self.liststore:
537 param = row[COLUMN_PARAMETER]
538 reset = (row[COLUMN_ISSET] == "reset")
539 if reset:
540 param.param.reset_value()
541 else:
542 if param.modified:
543 param.maybe_save_value()
545 def activate(self, *args, **kwargs):
547 self.liststore = Gtk.ListStore(GObject.TYPE_STRING,
548 parameter_store,
549 GObject.TYPE_STRING,
550 GObject.TYPE_STRING,
551 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)
579 if store.is_set:
580 is_set = "set"
581 else:
582 is_set = "default"
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)
592 if len(param_names):
593 # move cursor to first row and 'value' column
594 self.treeview.set_cursor(Gtk.TreePath(path=0),
595 focus_column=self.tvcolumn_value,
596 start_editing=False)
598 return self.treeview
600 class jack_engine_params_configure_command(jack_params_configure_command):
601 def __init__(self):
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):
608 def __init__(self):
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):
619 self.name = 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:
627 return None
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']
638 else:
639 selected = None
641 page_count = 0
643 for mod in mods:
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)
651 container.show_all()
652 #vbox.show_all()
653 notebook.append_page(container, Gtk.Label(mod))
654 if selected and selected == mod:
655 notebook.set_current_page(page_count)
656 page_count += 1
658 window.add(notebook)
660 window.show_all()
661 window.connect('destroy', Gtk.main_quit)
662 Gtk.main()
664 if __name__ == "__main__":
665 global jack
667 jack = JackConfigProxy()
668 _check_ladish()
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')
692 sys.stderr.flush()
693 sys.exit(2)
695 if options.list_modules:
696 sys.stderr.write(_("Available modules: "))
697 sys.stderr.write(' '.join(modules) + '\n')
698 sys.stderr.flush()
699 elif options.module:
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")
703 sys.stderr.flush()
704 sys.exit(2)
705 else:
706 show_panels(modules=modules, select=module)
707 else:
708 show_panels(modules=modules)
710 sys.exit(0)