#!/usr/bin/env python

# LADITools - Linux Audio Desktop Integration Tools
# ladiconf - A configuration GUI for your Linux Audio Desktop
# Copyright (C) 2007-2010, Marc-Olivier Barre <marco@marcochapeau.org>
# Copyright (C) 2007-2009, Nedko Arnaudov <nedko@arnaudov.name>
# Copyright (C) 2008, Krzysztof Foltman <wdev@foltman.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import pygtk
pygtk.require ('2.0')
import gtk
import laditools
import gobject
import sys

jack = laditools.jack_configure()

def check_ladish():
    try:
        proxy = laditools.ladish_proxy()
    except:
        print "ladish proxy creation failed"
        return

    if not proxy.is_available():
        print "ladish is not available"
        return

    if proxy.studio_is_loaded():
        if not proxy.studio_is_started():
            print "ladish studio is loaded and not started"
            return
        else:
            msg = "JACK can only be configured with a stopped studio. Please stop your studio first."
            title = "Studio is running"
    else:
        msg = "JACK can only be configured with a loaded and stopped studio. Please create a new studio or load and stop an existing one."
        title = "No studio present"

    # studio is not loaded or loaded and started
    mdlg = gtk.MessageDialog(type = gtk.MESSAGE_ERROR, buttons = gtk.BUTTONS_CLOSE, message_format = msg)
    mdlg.set_title(title)
    mdlg.run()
    mdlg.hide()
    sys.exit(0)

class parameter:
    def __init__(self, path):
        self.path = path
        self.name = path[-1:]

    def get_name(self):
        return self.name

    def get_type(self):
        return jack.get_param_type(self.path)

    def get_value(self):
        return jack.get_param_value(self.path)

    def set_value(self, value):
        jack.set_param_value(self.path, value)

    def reset_value(self):
        jack.reset_param_value(self.path)

    def get_short_description(self):
        return jack.get_param_short_description(self.path)

    def get_long_description(self):
        descr = jack.get_param_long_description(self.path)
        if not descr:
            descr = self.get_short_description()
        return descr

    def has_range(self):
        return jack.param_has_range(self.path)

    def get_range(self):
        return jack.param_get_range(self.path)

    def has_enum(self):
        return jack.param_has_enum(self.path)

    def is_strict_enum(self):
        return jack.param_is_strict_enum(self.path)

    def is_fake_values_enum(self):
        return jack.param_is_fake_value(self.path)

    def get_enum_values(self):
        return jack.param_get_enum_values(self.path)

class configure_command:
    def __init__(self):
        pass

    def get_description(self):
        pass

    def get_window_title(self):
        return self.get_description();

    def run(self, arg):
        pass

class parameter_enum_value(gobject.GObject):
    def __init__(self, is_fake_value, value, description):
        gobject.GObject.__init__(self)
        self.is_fake_value = is_fake_value
        self.value = value
        self.description = description

    def get_description(self):
        if self.is_fake_value:
            return self.description

        return str(self.value) + " - " + self.description

gobject.type_register(parameter_enum_value)

class parameter_store(gobject.GObject):
    def __init__(self, param):
        gobject.GObject.__init__(self)
        self.param = param
        self.name = self.param.get_name()
        self.is_set, self.default_value, self.value = self.param.get_value()
        self.modified = False
        self.has_range = self.param.has_range()
        self.is_strict = self.param.is_strict_enum()
        self.is_fake_value = self.param.is_fake_values_enum()

        self.enum_values = []

        if self.has_range:
            self.range_min, self.range_max = self.param.get_range()
        else:
            for enum_value in self.param.get_enum_values():
                self.enum_values.append(parameter_enum_value(self.is_fake_value, enum_value[0], enum_value[1]))

    def get_name(self):
        return self.name

    def get_type(self):
        return self.param.get_type()

    def get_value(self):
        return self.value

    def get_default_value(self):
        if not self.is_fake_value:
            return str(self.default_value)

        for enum_value in self.get_enum_values():
            if enum_value.value == self.default_value:
                return enum_value.get_description()

        return "???"

    def set_value(self, value):
        #print "%s -> %s" % (self.name, value)
        self.value = value
        self.modified = True

    def reset_value(self):
        self.value = self.default_value

    def get_short_description(self):
        return self.param.get_short_description()

    def maybe_save_value(self):
        if self.modified:
            self.param.set_value(self.value)
            self.modified = False

    def get_range(self):
        return self.range_min, self.range_max

    def has_enum(self):
        return len(self.enum_values) != 0

    def is_strict_enum(self):
        return self.is_strict

    def get_enum_values(self):
        return self.enum_values

gobject.type_register(parameter_store)

def combobox_get_active_text(combobox, model_index = 0):
    model = combobox.get_model()
    active = combobox.get_active()
    if active < 0:
        return None
    return model[active][model_index]

class cell_renderer_param(gtk.GenericCellRenderer):
    __gproperties__ = { "parameter": (gobject.TYPE_OBJECT, "Parameter", "Parameter", gobject.PARAM_READWRITE) }

    def __init__(self):
        self.__gobject_init__()
        self.parameter = None
        #self.set_property('mode', gtk.CELL_RENDERER_MODE_EDITABLE)
        #self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
        self.renderer_text = gtk.CellRendererText()
        self.renderer_toggle = gtk.CellRendererToggle()
        self.renderer_combo = gtk.CellRendererCombo()
        self.renderer_spinbutton = gtk.CellRendererSpin()
        for r in (self.renderer_text, self.renderer_combo, self.renderer_spinbutton):
            r.connect("edited", self.on_edited)
        self.renderer = None
        self.edit_widget = None

    def do_set_property(self, pspec, value):
        if pspec.name == 'parameter':
            if value.get_type() == 'b':
                self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE)
            else: 
                self.set_property('mode', gtk.CELL_RENDERER_MODE_EDITABLE)
        else:
            print pspec.name
        setattr(self, pspec.name, value)

    def do_get_property(self, pspec):
        return getattr(self, pspec.name)

    def choose_renderer(self):
        typechar = self.parameter.get_type()
        value = self.parameter.get_value()

        if typechar == "b":
            self.renderer = self.renderer_toggle
            self.renderer.set_property('activatable', True)
            self.renderer.set_property('active', value)
            self.renderer.set_property("xalign", 0.0)
            return

        if self.parameter.has_enum():
            self.renderer = self.renderer_combo

            m = gtk.ListStore(str, parameter_enum_value)

            for value in self.parameter.get_enum_values():
                m.append([value.get_description(), value])

            self.renderer.set_property("model",m)
            self.renderer.set_property('text-column', 0)
            self.renderer.set_property('editable', True)
            self.renderer.set_property('has_entry', not self.parameter.is_strict_enum())

            value = self.parameter.get_value()
            if self.parameter.is_fake_value:
                text = "???"
                for enum_value in self.parameter.get_enum_values():
                    if enum_value.value == value:
                        text = enum_value.get_description()
                        break
            else:
                text = str(value)

            self.renderer.set_property('text', text)

            return

        if typechar == 'u' or typechar == 'i':
            self.renderer = self.renderer_spinbutton
            self.renderer.set_property('text', str(value))
            self.renderer.set_property('editable', True)
            if self.parameter.has_range:
                range_min, range_max = self.parameter.get_range()
                self.renderer.set_property('adjustment', gtk.Adjustment(value, range_min, range_max, 1, abs(int((range_max - range_min) / 10))))
            else:
                self.renderer.set_property('adjustment', gtk.Adjustment(value, 0, 100000, 1, 1000))
            return

        self.renderer = self.renderer_text
        self.renderer.set_property('editable', True)
        self.renderer.set_property('text', self.parameter.get_value())

    def on_render(self, window, widget, background_area, cell_area, expose_area, flags):
        self.choose_renderer()
        return self.renderer.render(window, widget, background_area, cell_area, expose_area, flags)

    def on_get_size(self, widget, cell_area=None):
        self.choose_renderer()
        return self.renderer.get_size(widget, cell_area)

    def on_activate(self, event, widget, path, background_area, cell_area, flags):
        self.choose_renderer()
        if self.parameter.get_type() == 'b':
            self.parameter.set_value(not self.parameter.get_value())
            widget.get_model()[path][4] = "modified"
        return True

    def on_edited(self, renderer, path, value_str):
        parameter = self.edit_parameter
        widget = self.edit_widget
        model = self.edit_tree.get_model()
        self.edit_widget = None
        typechar = parameter.get_type()
        if type(widget) == gtk.ComboBox:
            value = combobox_get_active_text(widget, 1)
            if value == None:
                return
            value_str = value.value
        elif type(widget) == gtk.ComboBoxEntry:
            enum_value = combobox_get_active_text(widget, 1)
            if enum_value:
                value_str = enum_value.value
            else:
                value_str = widget.get_active_text()

        if typechar == 'u' or typechar == 'i':
            try:
                value = int(value_str)
            except ValueError, e:
                # Hide the widget (because it may display something else than what user typed in)
                widget.hide()
                # Display the error
                mdlg = gtk.MessageDialog(buttons = gtk.BUTTONS_OK, message_format = "Invalid value. Please enter an integer number.")
                mdlg.run()
                mdlg.hide()
                # Return the focus back to the tree to prevent buttons from stealing it
                self.edit_tree.grab_focus()
                return
        else:
            value = value_str
        parameter.set_value(value)
        model[path][4] = "modified"
        self.edit_tree.grab_focus()

    def on_start_editing(self, event, widget, path, background_area, cell_area, flags):
        # this happens when edit requested using keyboard
        if not event:
            event = gtk.gdk.Event(gtk.gdk.NOTHING)

        self.choose_renderer()
        ret = self.renderer.start_editing(event, widget, path, background_area, cell_area, flags)
        self.edit_widget = ret
        self.edit_tree = widget
        self.edit_parameter = self.parameter
        return ret

gobject.type_register(cell_renderer_param)

class ladiconf_tooltips(laditools.TreeViewTooltips):
        def __init__(self, name_column, is_set_column):
            self.name_column = name_column
            self.is_set_column = is_set_column
            laditools.TreeViewTooltips.__init__(self)

        def location(self, x, y, w, h):
            return x + 10, y + 10

        def get_tooltip(self, view, column, path):
            if column is self.name_column:
                model = view.get_model()
                tooltip = model[path][3]
                return tooltip

            if column is self.is_set_column:
                model = view.get_model()
                param = model[path][1]
                is_set = model[path][4]

                if is_set == "default":
                    return None

                if is_set == "reset":
                    return "Value will be reset to %s" % param.get_default_value()

                return "Double-click to schedule reset of value to %s" % param.get_default_value()

            return None

class jack_params_configure_command(configure_command):
    def __init__(self, path):
        self.path = path
        self.is_setting = False

    def reset_value(self, row_path):
        row = self.liststore[row_path]
        param = row[1]
        param.reset_value()
        row[4] = "reset"

    def on_row_activated(self, treeview, path, view_column):
        if view_column == self.tvcolumn_is_set:
            self.reset_value(path)
            
    def on_button_press_event(self, tree, event):
        if event.type != gtk.gdk._2BUTTON_PRESS:
            return False
        # this is needed for proper double-click handling in the list; don't ask me why, I don't know
        # it's probably because _2BUTTON_PRESS event is still delivered to tree view, automatically deactivating
        # the newly created edit widget (which gets created on second BUTTON_PRESS but before _2BUTTON_PRESS)
        # deactivating the widget causes it to be deleted
        return True

    def on_key_press_event(self, tree, event):
        cur = self.treeview.get_cursor()
        row_path = cur[0][0]

        # if Delete was pressed, reset the value
        if event.state == 0 and event.keyval == gtk.keysyms.Delete:
            self.reset_value(row_path)
            tree.queue_draw()
            return True

        # prevent ESC from activating the editor
        if event.string < " ":
            return False

        # single-key data entry: if the control is a text entry, spin button or combo box/combo box entry,
        # then edit the current and set the text value to what user has already typed in
        cur = self.treeview.get_cursor()
        param = self.liststore[row_path][1]
        ptype = param.get_type()

        # we don't care about booleans
        if ptype == 'b':
            return False

        # accept only digits for integer input (or a minus, but only if it's a signed field)
        if ptype in ('i', 'u'):
            if not (event.string.isdigit() or (event.string == "-" and ptype == 'i')):
                return False

        # Start cell editing
        # MAYBE: call a specially crafted cell_renderer_param method for this
        self.treeview.set_cursor_on_cell(cur[0], self.tvcolumn_value, self.renderer_value.renderer, True)

        # cell_renderer_param::on_start_editing() should set edit_widget.
        # if edit operation has failed (didn't create a widget), pass the key on
        # MAYBE: call a specially crafted cell_renderer_param method to do this check
        if self.renderer_value.edit_widget == None:
            return

        widget = self.renderer_value.edit_widget
        if type(widget) in (gtk.Entry, gtk.ComboBoxEntry):
            # (combo or plain) text entry - set the content and move the cursor to the end
            sl = len(event.string)
            widget.set_text(event.string)
            widget.select_region(sl, sl)
            return True

        if type(self.renderer_value.edit_widget) == gtk.SpinButton:
            # spin button - set the value and move the cursor to the end
            if event.string == "-":
                # special case for minus sign (which can't be converted to float)
                widget.set_text(event.string)
            else:
                widget.set_value(float(event.string))
            sl = len(widget.get_text())
            widget.select_region(sl, sl)
            return True
            
        if type(self.renderer_value.edit_widget) == gtk.ComboBox:
            # combo box - select the first item that starts with typed character
            model = widget.get_model()
            item = -1
            iter = model.get_iter_root()
            while iter != None:
                if model.get_value(iter, 0).startswith(event.string):
                    item = model.get_path(iter)[0]
                    break
                iter = model.iter_next(iter)
            widget.set_active(item)
            return True

        return False
        
    def on_cursor_changed(self, tree):
        cur = self.treeview.get_cursor()
        if not self.is_setting and cur[1] != None and cur[1].get_title() != self.tvcolumn_value.get_title():
            self.is_setting = True
            try:
                self.treeview.set_cursor_on_cell(cur[0], self.tvcolumn_value)
            finally:
                self.is_setting = False

    def ok_clicked(self, dlg):
        if self.renderer_value.edit_widget:
            self.renderer_value.edit_widget.editing_done()

    def run(self, arg):
        dlg = gtk.Dialog()
        dlg.set_title(self.get_window_title())
        dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
        dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK).connect("clicked", self.ok_clicked)

        #dlg.set_transient_for(window)

        self.liststore = gtk.ListStore(str, parameter_store, str, str, str)
        self.treeview = gtk.TreeView(self.liststore)
        self.treeview.set_rules_hint(True)
        self.treeview.set_enable_search(False)

        renderer_text = gtk.CellRendererText()
        renderer_toggle = gtk.CellRendererToggle()
        renderer_value = cell_renderer_param()
        self.renderer_value = renderer_value # save for use in event handler methods

        self.tvcolumn_parameter = gtk.TreeViewColumn('Parameter', renderer_text, text=0)
        self.tvcolumn_is_set = gtk.TreeViewColumn('Status', renderer_text, text=4)
        self.tvcolumn_value = gtk.TreeViewColumn('Value', renderer_value, parameter=1)
        self.tvcolumn_description = gtk.TreeViewColumn('Description', renderer_text, text=2)

        self.tvcolumn_value.set_resizable(True)
        self.tvcolumn_value.set_min_width(100)

        self.treeview.append_column(self.tvcolumn_parameter)
        self.treeview.append_column(self.tvcolumn_is_set)
        self.treeview.append_column(self.tvcolumn_value)
        self.treeview.append_column(self.tvcolumn_description)

        dlg.vbox.pack_start(self.treeview, True)

        param_names = jack.get_param_names(self.path)
        for name in param_names:
            param = parameter(self.path + [name])
            store = parameter_store(param)
            if store.is_set:
                is_set = "set"
            else:
                is_set = "default"
            self.liststore.append([name, store, param.get_short_description(), param.get_long_description(), is_set])

        self.treeview.connect("row-activated", self.on_row_activated)
        self.treeview.connect("cursor-changed", self.on_cursor_changed)
        self.treeview.connect("key-press-event", self.on_key_press_event)
        self.treeview.connect("button-press-event", self.on_button_press_event)

        self.tooltips = ladiconf_tooltips(self.tvcolumn_parameter, self.tvcolumn_is_set)
        self.tooltips.add_view(self.treeview)
        if len(param_names):
            # move cursor to first row and 'value' column
            self.treeview.set_cursor((0,), self.tvcolumn_value)

        dlg.show_all()
        ret = dlg.run()
        if ret == gtk.RESPONSE_OK:
            for row in self.liststore:
                param = row[1]
                reset = row[4] == "reset"
                if reset:
                    #print "%s -> reset" % param.get_name()
                    param.param.reset_value()
                else:
                    if param.modified:
                        #print "%s -> %s" % (param.get_name(), param.get_value())
                        param.maybe_save_value()
        dlg.hide()

class jack_engine_params_configure_command(jack_params_configure_command):
    def __init__(self):
        jack_params_configure_command.__init__(self, ['engine'])

    def get_description(self):
        return 'JACK engine parameters'

class jack_driver_params_configure_command(jack_params_configure_command):
    def __init__(self):
        jack_params_configure_command.__init__(self, ['driver'])

    def get_description(self):
        return 'JACK driver parameters'

    def get_window_title(self):
        return 'JACK "%s" driver parameters' % jack.get_selected_driver()

class jack_internal_params_configure_command(jack_params_configure_command):
    def __init__(self, name):
        self.name = name
        jack_params_configure_command.__init__(self, ['internals', name])

    def get_description(self):
        return 'JACK "%s" parameters' % self.name

check_ladish()

commands = [
    jack_engine_params_configure_command(),
    jack_driver_params_configure_command(),
    ]

for internal in jack.read_container(['internals']):
    commands.append(jack_internal_params_configure_command(internal))

window = gtk.Window()

buttons_widget = gtk.VBox()

for command in commands:
    button = gtk.Button(command.get_description())
    button.connect('clicked', command.run)
    buttons_widget.pack_start(button)

window.add(buttons_widget)

window.show_all()
window.connect('destroy', gtk.main_quit)

gtk.main()
