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/>.
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
52 COLUMN_ISSET
) = range (5)
57 except LadishProxyError
as e
:
58 sys
.stderr
.write("%s\n" % str(e
))
61 except Exception as e
:
62 sys
.stderr
.write(_("ladish proxy creation failed: %s\n") % str(e
))
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"))
70 elif ret
== LadishStatusType
.NOT_AVAILABLE
:
71 sys
.stderr
.write(_("ladish is not available\n"))
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")
84 sys
.stderr(_("Unexpected error!\n"))
86 mdlg
= Gtk
.MessageDialog(type = Gtk
.MessageType
.ERROR
,
87 buttons
= Gtk
.ButtonsType
.CLOSE
,
98 class configure_command(object):
102 def get_description(self
):
105 def get_window_title(self
):
106 return self
.get_description();
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
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
)
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
= []
138 self
.range_min
, self
.range_max
= self
.param
.get_range()
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]))
147 return self
.param
.get_type()
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()
162 def set_value(self
, value
):
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
):
174 self
.param
.set_value(self
.value
)
175 self
.modified
= False
178 return self
.range_min
, self
.range_max
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()
196 return model
[active
][model_index
]
198 class cell_renderer_param(Gtk
.CellRendererPixbuf
):
199 __gproperties__
= { "parameter": (GObject
.TYPE_OBJECT
,
202 GObject
.PARAM_READWRITE
) }
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
)
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
)
223 self
.set_property('mode', Gtk
.CellRendererMode
.EDITABLE
)
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()
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)
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
:
258 for enum_value
in self
.parameter
.get_enum_values():
259 if enum_value
.value
== value
:
260 text
= enum_value
.get_description()
265 self
.renderer
.set_property('text', text
)
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))))
277 self
.renderer
.set_property('adjustment', Gtk
.Adjustment(value
, 0, 100000, 1, 1000))
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"
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)
309 value_str
= value
.value
310 elif type(widget
) == Gtk
.ComboBoxText
:
311 enum_value
= combobox_get_active_text(widget
, 1)
313 value_str
= enum_value
.value
315 value_str
= widget
.get_active_text()
317 if typechar
== 'u' or typechar
== 'i':
319 value
= int(value_str
)
320 except ValueError, e
:
321 # Hide the widget (because it may display something else than what user typed in)
324 mdlg
= Gtk
.MessageDialog(buttons
= Gtk
.ButtonsType
.OK
, message_format
= "Invalid value. Please enter an integer number.")
327 # Return the focus back to the tree to prevent buttons from stealing it
328 self
.edit_tree
.grab_focus()
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
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
348 GObject
.type_register(cell_renderer_param
)
350 class jack_params_configure_command(configure_command
):
351 def __init__(self
, jack
, path
):
354 self
.is_setting
= False
356 def reset_value(self
, row_path
):
357 row
= self
.liststore
[row_path
]
358 param
= row
[COLUMN_PARAMETER
]
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
:
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
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
)
385 # prevent ESC from activating the editor
386 if event
.string
< " ":
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
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')):
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:
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
)
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
)
428 widget
.set_value(float(event
.string
))
429 sl
= len(widget
.get_text())
430 widget
.select_region(sl
, sl
)
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()
437 iter = model
.get_iter_root()
439 if model
.get_value(iter, 0).startswith(event
.string
):
440 item
= model
.get_path(iter)[0]
442 iter = model
.iter_next(iter)
443 widget
.set_active(item
)
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
454 self
.treeview
.set_cursor_on_cell(row_path
, self
.tvcolumn_value
, self
.renderer_value
.renderer
, False)
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"""
465 (path
, column
, out_x
, out_y
) = treeview
.get_path_at_pos(x
, y
)
471 # Horrible-fix to skip the treeview's header row
472 intpath
= int(path
.to_string())
476 path
= Gtk
.TreePath(str(intpath
- 1))
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":
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()
489 text
= _("Value will be reset to %s") % \
490 self
.liststore
[path
][COLUMN_PARAMETER
].get_default_value()
494 tooltip
.set_text(text
)
497 def do_destroy(self
, *args
):
498 for row
in self
.liststore
:
499 param
= row
[COLUMN_PARAMETER
]
500 reset
= (row
[COLUMN_ISSET
] == "reset")
502 param
.param
.reset_value()
505 param
.maybe_save_value()
507 def activate(self
, *args
, **kwargs
):
511 self
.liststore
= Gtk
.ListStore(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
)
547 self
.liststore
.append([name
,
549 param
.get_short_description(),
550 param
.get_long_description(),
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
)
558 self
.treeview
.connect("query-tooltip", self
._on
_query
_tooltip
)
559 self
.treeview
.connect("destroy", self
.do_destroy
)
562 # move cursor to first row and 'value' column
563 self
.treeview
.set_cursor(Gtk
.TreePath(path
=0), # path
564 self
.tvcolumn_value
, # focus_column
565 False) # start_editing
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
):
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()
605 def modules(self
): return self
._modules
609 LadiApp
.__init
__(self
)
610 jack
= JackConfigProxy()
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']
627 self
.window
= window
= Gtk
.Window
.new(Gtk
.WindowType
.TOPLEVEL
)
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
)
641 modules
= self
.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
)
653 tab_label
= modules
[mod
].get_window_title()
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
)
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()
671 if __name__
== "__main__":
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',
682 help=_('select the module to configure'))
683 parser
.add_argument('-l',
686 help=_('list available modules'))
687 parser
.add_argument('--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')
700 modules
= app
.modules
701 if options
.list_modules
:
702 sys
.stderr
.write(_("Available modules: "))
703 sys
.stderr
.write(' '.join(modules
) + '\n')
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")
712 app
.run(select
=module
)