/*
 * All content on this website (including text, images, source
 * code and any other original works), unless otherwise noted,
 * is licensed under a Creative Commons License.
 * 
 * http://creativecommons.org/licenses/by-nc-sa/2.5/
 * 
 * Copyright (C) Open-Xchange Inc., 2006-2011
 * Mail: info@open-xchange.com
 */
 
/** 
 * @fileOverview Open-Xchange GUI Plugin API
 * @author Viktor Pracht <Viktor.Pracht@open-xchange.org>
 */

/**
 * @namespace
 */
ox.UI = {};
    
/**
 * @namespace
 */
ox.Configuration = {
    
    /**
     * Displays an error message to the user.
     * @param {I18nString} text The error message to display.
     */
    error: function(text) {
        setTimeout(function() {
            triggerEvent("OX_New_Error", 4, expectI18n(text));
        }, 0);
    },

    /**
     * Displays an information message to the user.
     * @param {I18nString} text The information message to display.
     */
    info: function(text) {
        triggerEvent("OX_New_Info", 4, expectI18n(text));
    },

    /**
     * Adds a new node to the configuration tree.
     * The parent node must already exist.
     * @class Leaf node in the configuration tree
     * @param {String} path The path in the configuration tree.
     * Must start with "configuration" and contain at least one more
     * element. Path elements are separated by a slash ("/"). There must be
     * no slash at the end. If the parent of the node does not already
     * exist, an Error exception is thrown.
     * @param {I18nString} name The name of the page.
     */
    LeafNode: function(path, name) {
        var Self = new ox.Configuration.Node(path, name, false, function() {
            if (Self.click) return Self.click();
        });
        return Self;
    },

    /**
     * This callback is called when the user clicks on the node.
     * @name ox.Configuration.LeafNode.prototype.click
     * @type Function
     */

    /**
     * Adds a new inner node to the configuration tree.
     * The parent node must already exist.
     * @class Inner node in the configuration tree.
     * @param {String} path The path in the configuration tree.
     * Must start with "configuration" and contain at least one more
     * element. Path elements are separated by a slash ("/"). There must be
     * no slash at the end. If the parent of the node does not already
     * exist, an Error exception is thrown.
     * @param {I18nString} name The name of the page.
     */
    InnerNode: function(path, name) {
        return new ox.Configuration.Node(path, name, true);
    },
    
    /**
     * @class
     * @private
     */
    Node: function(path, name, folder, click) {
        if (path in ox.Configuration.nodes)
            return ox.Configuration.nodes[path];
        var match = /^(.*)\/[^\/]+$/.exec(path);
        if (!match) throw Error("Invalid path: '" + path + "'");
        this.id = folder ? path
            : "configuration/modules/" + ox.Configuration.id++;
        var txtNode = (name instanceof I18nString) || typeof(name) == "string"
                    ? addTranslated(name) : name;
        var node = ox.widgets.configTree.add({ parent: match[1], id: this.id,
                                    domNode: txtNode, innerNode: folder,
                                    onClick: click });
        ox.Configuration.nodes[path] = this;
    },

    /**
     * A map from paths to configuration tree nodes.
     * @private
     */
    nodes: {},
    
    /**
     * A counter for unique node IDs.
     * @private
     */
    id: 0,
    
    /**
     * An array with all created configuration pages.
     * @private
     */
    pages: []
    
};
    
/**
 * @namespace
 */
ox.JSON = {};

/**
 * Asynchronously requests a JSON object from the server.
 * This method retrieves a JSON object from the server by issuing an HTTP
 * GET request to the specified URI and calling the specified callback when
 * the retrieval is complete.
 * @param {String} uri The URI for the HTTP GET request.
 * @param {Function} ok A callback function which is called with the
 * received JSON object or raw data as parameter. If there was any error
 * then this function is not called.
 * @param {Function} error An optional callback function wihch is called when
 * the server returns an error. The function takes two parameters: result and
 * status. If the HTTP status code was 200, then result is the JSON object and
 * status is not set. If the HTTP status was not 200, then result is the status
 * string and status is the HTTP status code. The function should return true
 * when it handles the error. Otherwise, the default error handler specified by
 * JSON.errorHandler will be called after this function returns. If this
 * parameter is not specified, the default error handler is called directly.
 * @param {Boolean} raw Specifies whether the response data should be
 * passed to the callback as-is or first parsed as a JSON object. Defaults
 * to the latter.
 */
ox.JSON.get = function(uri, ok, error, raw) {
    (new JSON()).get(uri, null, ok, error, raw);
};

/**
 * Asynchronously posts url-encoded data and retrieves a JSON object from
 * the server.
 * This method posts an object to the server by issuing an HTTP
 * POST request to the specified URI and calling the specified callback when
 * the reply arrives.
 * @param {String} uri The URI for the HTTP POST request.
 * @param {Object} data An object which is serialized using the
 * application/x-www-form-urlencoded encoding and sent as the body of the
 * request.
 * @param {Function} ok A callback function which is called with the received
 * JSON object or raw data as parameter. If there was any error then this
 * function is not called.
 * @param {Function} error An optional callback function wihch is called when
 * the server returns an error. The function takes two parameters: result and
 * status. If the HTTP status code was 200, then result is the JSON object and
 * status is not set. If the HTTP status was not 200, then result is the status
 * string and status is the HTTP status code. The function should return true
 * when it handles the error. Otherwise, the default error handler specified by
 * JSON.errorHandler will be called after this function returns. If this
 * parameter is not specified, the default error handler is called directly.
 * @param {Boolean} raw Specifies whether the response data should be
 * passed to the callback as-is or first parsed as a JSON object. Defaults
 * to the latter.
 */
ox.JSON.post = function(uri, data, ok, error, raw) {
    (new JSON()).post(uri, data, null, ok, error, raw);
};

/**
 * Asynchronously sends a JSON object and retrieves a JSON object from
 * the server.
 * This method sends a JSON object to the server by issuing an HTTP
 * PUT request to the specified URI and calling the specified callback when
 * the reply arrives.
 * @param {String} uri The URI for the HTTP POST request.
 * @param {Object} data An object which is serialized using JSON syntax and
 * sent as the body of the request.
 * @param {Function} ok A callback function which is called with the received
 * JSON object or raw data as parameter. If there was any error then this
 * function is not called.
 * @param {Function} error An optional callback function wihch is called when
 * the server returns an error. The function takes two parameters: result and
 * status. If the HTTP status code was 200, then result is the JSON object and
 * status is not set. If the HTTP status was not 200, then result is the status
 * string and status is the HTTP status code. The function should return true
 * when it handles the error. Otherwise, the default error handler specified by
 * JSON.errorHandler will be called after this function returns. If this
 * parameter is not specified, the default error handler is called directly.
 * @param {Boolean} raw Specifies whether the response data should be
 * passed to the callback as-is or first parsed as a JSON object. Defaults
 * to the latter.
 */
ox.JSON.put = function(uri, data, ok, error, raw) {
    (new JSON()).put(uri, data, null, ok, error, raw);
};

/**
 * @class Abstract base class of all widgets.
 */
ox.UI.Widget = function() {
    /**
     * Specifies whether the widget was already initialized.
     * @type Boolean
     * @default false
     */
    this.initialized = false;
};

/**
 * The default value of this widget, which is used when the model does
 * not contain the field for this widget.
 * @name ox.UI.Widget.prototype.default_value
 */

/**
 * The topmost DOM node of the widget. It is used by the default implementations
 * of {@link #show}, {@link #hide} and {@link #remove}.
 * @name ox.UI.Widget.prototype.node
 * @type DOM Node
 */

/**
 * A DOM node which is used by the default implementation of {@link #enable} and
 * {@link #disable} to control the disabled state of the widget. The DOM node
 * should have a property named "disabled". If the value equals false, disabling
 * will have no effect besides updating the {@link #enabled} field and applying
 * the disabled CSS.
 * @name ox.UI.Widget.prototype.formnode
 * @type DOM Node
 */

/**
 * The width of the widget as a CSS length specification.
 * If not specified, defaults to the {@link ox.UI.Container#childWidth} of
 * the parent.
 * @name ox.UI.Widget.prototype.width
 * @type String
 */

ox.UI.Widget.setDisabledClass = classNameSetter("font-color-disabled");

ox.UI.Widget.prototype = {

    /**
     * Adds the DOM nodes of this widget to its parent container.
     * @param {String} node_id The ID of the current page. This ID is required
     * for adding menu entries.
     */
    addContent: function(node_id) {
        this.initialized = true;
        if (!this.isEnabled) this.applyEnabled();
        if (!this.isVisible) this.applyVisible();
    },
    
    /**
     * Returns the current value of the widget. The returned type depends on
     * the actual widget class.
     */
    get: function() {},
    
    /**
     * Sets the value which is displayed by the widget.
     * @param value The new value of the widget. The type depends on the actual
     * widget class.
     */
    set: function(value) {},
    
    /**
     * Resizes the widget.
     */
    resize: function() {},
    
    /**
     * Removes the widget's DOM nodes from the form.
     */
    remove: function() {
        if (this.node) this.node.parentNode.removeChild(this.node);
    },
    
    /**
     * Displays the widget if it was previously hidden.
     */
    show: function() { this.setVisible(true); },
    
    /**
     * Hides the widget.
     */
    hide: function() { this.setVisible(false); },
    
    /**
     * Sets the visibility of the widget.
     * @param {Boolean} visible The new visibility status.
     */
    setVisible: function(visible) {
        this.visible = visible;
        visible = visible && (!this.parent || this.parent.isVisible);
        if (this.isVisible != visible) {
            this.isVisible = visible;
            if (this.initialized) this.applyVisible();
        }
    },
    
    /**
     * Applies the visibility status to the actual widget.
     * Only called when the widget is initialized.
     * @protected
     */
    applyVisible: function() {
        if (this.node) this.node.style.display = this.isVisible ? "" : "none";
    },
    
    /**
     * Specifies whether the widget is visible.
     * @type Boolean
     * @default true
     */
    visible: true,
    
    /**
     * The actual visibility of the widget, which can be false even if visible
     * is set to true, because the widget's container is invisible.
     */
    isVisible: true,

    /**
     * Enables the widget for user interaction.
     */
    enable: function() { this.setEnabled(true); },
    
    /**
     * Disables the widget for user interaction. 
     */
    disable: function() { this.setEnabled(false); },
    
    /**
     * Enables or disables the widget.
     * @param {Boolean} enabled The new enabled status.
     */
    setEnabled: function(enabled) {
        this.enabled = enabled;
        enabled = enabled && (!this.parent || this.parent.isEnabled);
        if (this.isEnabled != enabled) {
            this.isEnabled = enabled;
            if (this.initialized) this.applyEnabled();
        }
    },
    
    /**
     * Applies the value of this.isEnabled to the actual widget.
     * When this method is called, the widget is already initialized.
     * @protected
     */
    applyEnabled: function() {
        if (this.formnode) this.formnode.disabled = !this.isEnabled;
        if (this.node)
            ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
    },
    
    /**
     * Specifies whether the widget is enabled.
     * @type Boolean
     * @default true
     */
    enabled: true,
    
    /**
     * The actual enabled state of the widget, which can be false even if
     * enabled is set to true, because the widget's container is disabled.
     */
    isEnabled: true,
    
    /**
     * Sets the widget's container.
     * @param {ox.UI.Container} parent The new parent container.
     */
    setParent: function(parent) {
        this.parent = parent;
        this.setEnabled(this.enabled);
        this.setVisible(this.visible);
    },
    
    /**
     * The widget which contains this widget. null if this widget is not a child
     * of another widget.
     * @default null
     */
    parent: null

};

/**
 * @class Static explanation text.
 * Do not specify a field name when adding instances of this class to
 * containers.
 * @augments ox.UI.Widget
 * @param {I18nString} text The displayed text.
 */
ox.UI.Text = function(text) {
    ox.UI.Widget.apply(this);
    this.text = text;
};

ox.UI.Text.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.Text.prototype */
    {

        addContent: function(node_id) {
            this.node = this.parent.addRow(addTranslated(this.text), false);
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        }

    });

/**
 * @class A text input field.
 * @param {I18nString} label The label for the input field.
 */
ox.UI.Input = function(label) {
    ox.UI.Widget.apply(this);
    this.label = label;
};

ox.UI.Input.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.Input.prototype */
    {

        /**
         * The default value of the input field, which is used when the model
         * does not contain the field for this input field.
         * @default an empty string
         */
        default_value: "",
    
        addContent: function(node_id) {
            this.formnode =
                newnode("input",
                        { width: this.width || this.parent.childWidth },
                        { type: this.inputType });
            this.node = this.parent.addCells(this.label, this.formnode);
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        },
    
        /**
         * Returns the current value of the input field.
         * @type String
         */
        get: function() { return this.formnode.value; },
    
        /**
         * Sets the value of the input field.
         * @param {String} value
         */
        set: function(value) { this.formnode.value = value; },
        
        /**
         * The type attribute of the HTML input element.
         * @type String
         * @default "text"
         */
        inputType: "text"
        
    });

/**
 * @class A password input field.
 * @augments ox.UI.Input
 * @param {I18nString} label The label for the input field.
 */
ox.UI.Password = function(label) {
    ox.UI.Input.apply(this, arguments);
}

ox.UI.Password.prototype = extend(ox.UI.Input,
    /** @lends ox.UI.Password.prototype */
    { inputType: "password" });

/**
 * @class a multi-line text input area.
 * @param {I18nString} label The label for the input field.
 */
ox.UI.TextArea = function(label) {
    ox.UI.Widget.apply(this);
    this.label = label;
}

ox.UI.TextArea.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.TextArea.prototype */
    {
    
        default_value: "",
    
        addContent: function(node_id) {
            this.formnode = newnode("textarea",
                { width: this.width || this.parent.childWidth }, 0);
            if (this.height) this.formnode.style.height = this.height;
            this.node = this.parent.addCells(this.label, this.formnode);
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        },
        
        /**
         * Returns the current value of the text area.
         * @type String
         */
        get: function() { return this.formnode.value; },
        
        /**
         * Sets the value of the text area.
         * @param {String} value
         */
        set: function(value) { this.formnode.value = value; }
        
    });

/**
 * @class A check box for a single boolean field.
 * @augments ox.UI.Widget
 * @param {I18nString} label The label for the checkbox.
 */
ox.UI.CheckBox = function(label) {
    ox.UI.Widget.apply(this);
    this.label = label;
};

/**
 * @private
 */
ox.UI.CheckBox.id = 0;

/**
 * This callback is called when the user changes the state of the CheckBox.
 * @name ox.UI.CheckBox.prototype.changed
 * @type Function
 */

ox.UI.CheckBox.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.CheckBox.prototype */
    {
    
        addContent: function(node_id) {
            var Self = this;
            var id = "ox.UI.CheckBox." + ox.UI.CheckBox.id++;
            this.formnode = newnode("input", 0, { type: "checkbox", id: id });
            this.node = this.parent.addRow(newnode("label", 0, { htmlFor: id }, [
                this.formnode,
                document.createTextNode(" "),
                addTranslated(this.label)
            ]), true);
            addDOMEvent(this.formnode, "click", function() {
                if (Self.changed) Self.changed();
            });
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        },
        
        /**
         * Returns the current value of the CheckBox.
         * @type Boolean
         */
        get: function() { return this.formnode.checked; },
        
        /**
         * Sets the value of the CheckBox
         * @param {Boolean} value
         */
        set: function(value) { this.formnode.checked = value; }

    });

/**
 * @class Abstract base class of selection widgets.
 * This class handles the association between displayed objects and value
 * objects.
 * @augments ox.UI.Widget
 */
ox.UI.Selection = function() {
    ox.UI.Widget.apply(this);
    this.values = [];
    this.display_values = [];
};

/**
 * This callback is called when the user selects a different value.
 * @name ox.UI.Selection.prototype.changed
 * @type Function
 */

ox.UI.Selection.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.Selection.prototype */
    {
    
        /**
         * Sets the list of possible values and the corresponding displayed
         * values.
         * The parameters are used directly, without copying. Therefore,
         * the arrays should not be modified after being passed to this method.
         * An exception is modifying the arrays and immediately calling this
         * method again to update the displayed widget.
         * @param {Array} values An array of possible values.
         * @param {Array} display_values An array of displayed values.
         * Each element represents the value in the values array with the same
         * index. The type of elements depends on the actual selection class.
         */
        setEntries: function(values, display_values) {
            this.values = values;
            this.display_values = display_values;
        }
    
    });

/**
 * @class A combination of a text input field and a drop-down list.
 * @augments ox.UI.Selection
 * @param {I18nString} label The label of the ComboBox.
 * @param {Boolean} editable Specifies whether the text input field can contain
 * values which are not in the drop-down list. Defaults to false.
 * Not implemented yet.
 *
 */
ox.UI.ComboBox = function(label, editable) {
    ox.UI.Selection.apply(this);
    this.label = label;
    this.editable = editable;
}

ox.UI.ComboBox.prototype = extend(ox.UI.Selection,
    /** @lends ox.UI.ComboBox.prototype */
    {
        
        /**
         * @private
         */
        recreate: function() {
            var nokey = {};
            var key = this.combobox ? this.combobox.getKey() : nokey;
            removeChildNodes(this.div);
            var Self = this;
            var numValues = this.values && this.values.length ? this.values.length : 0;
            this.combobox = new ComboBox3.impl(window, this.div,
                this.width || this.parent.childWidth, 0, true,
                Math.min(8, numValues),
                function(value) {
                    if (Self.changed) Self.changed(value);
                });
            if (numValues) {
                for (var i = 0; i < numValues; i++) {
                    this.combobox.addElement(this.display_values[i],
                                             this.values[i]);
                }
            } else {
                this.combobox.addElement(noI18n("\xa0"), null);
            }
            this.combobox.getDomNode();
            if (key != nokey) this.combobox.setKey(key);
            if (!this.enabled) this.combobox.disable();
        },

        addContent: function(node_id) {
            this.div = newnode("div");
            this.node = this.parent.addCells(this.label, this.div);
            this.recreate();
            ox.UI.Selection.prototype.addContent.apply(this, arguments);
        },
        
        resize: function() { this.recreate(); },
        
        /**
         * Sets the list of possible values and the corresponding displayed
         * textual descriptions.
         * The parameters are used directly, without copying. Therefore,
         * the arrays should not be modified after being passed to this method.
         * An exception is modifying the arrays and immediately calling this
         * method again to update the displayed widget.
         * @param {Array} values An array of possible values.
         * @param {I18nString[]} display_values An array of displayed texts.
         * Each element represents the value in the values array with the same
         * index.
         */
        setEntries: function(values, display_values) {
            ox.UI.Selection.prototype.setEntries.apply(this, arguments);
            if (this.initialized) this.recreate();
        },
        
        /**
         * Returns the current value of the ComboBox.
         */
        get: function() {
            return this.combobox.getKey();
        },
        
        /**
         * Sets the value of the ComboBox.
         */
        set: function(value) {
            this.combobox.setKey(value);
        },
        
        applyEnabled: function() {
            if (this.isEnabled)
                this.combobox.enable();
            else
                this.combobox.disable();
            ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
        }

    });

/**
 * @class A group of radio buttons. Only one option can be selected at a time.
 * @augments ox.UI.Selection
 */
ox.UI.RadioButtons = function() {
    ox.UI.Selection.apply(this);
    this.name = ++ox.UI.RadioButtons.lastName;
}

ox.UI.RadioButtons.lastName = 0;

ox.UI.RadioButtons.prototype = extend(ox.UI.Selection,
    /** @lends ox.UI.RadioButtons.prototype */
    {
    
        /**
         * @private
         */
        recreate: function() {
            removeChildNodes(this.div);
            this.nodes = new Array(this.values.length);
            var Self = this;
            for (var i = 0; i < this.values.length; i++) {
                this.nodes[i] = newnode("input", 0,
                    { type: "radio", name: this.name, value: this.values[i],
                      className: "noborder nobackground" });
                addDOMEvent(this.nodes[i], "change", function() {
                    if (Self.changed) Self.changed();
                });
                this.div.appendChild(newnode("label", 0,
                    { htmlFor: "ox.UI.RadioButtons." + this.name + "." + i },
                    [
                        this.nodes[i],
                        document.createTextNode(" "),
                        addTranslated(this.display_values[i])
                    ]));
                this.div.appendChild(newnode("br"));
            }
        },
        
        addContent: function(node_id) {
            this.div = newnode("div");
            this.node = this.parent.addRow(this.div, true);
            this.recreate();
            ox.UI.Selection.prototype.addContent.apply(this, arguments);
        },
    
        /**
         * Sets the list of possible values and the corresponding displayed
         * labels.
         * The parameters are used directly, without copying. Therefore,
         * the arrays should not be modified after being passed to this method.
         * An exception is modifying the arrays and immediately calling this
         * method again to update the displayed widget.
         * @param {Array} values An array of possible values.
         * @param {String[]} display_values An array of displayed labels.
         * Each element represents the value in the values array with the same
         * index.
         */
        setEntries: function() {
            ox.UI.Selection.prototype.setEntries.apply(this, arguments);
            if (this.initialized) this.recreate();
        },
        
        /**
         * Returns the current value of the RadioButtons.
         */
        get: function() {
            for (var i = 0; i < this.values.length; i++)
                if (this.nodes[i].checked) return this.values[i];
        },
        
        /**
         * Sets the value of the RadioButtons.
         */
        set: function(value) {
            for (var i = 0; i < this.values.length; i++)
                this.nodes[i].checked = this.values[i] == value;
        },
        
        applyEnabled: function() {
            for (var i = 0; i < this.nodes.length; i++)
                this.nodes[i].disabled = !this.isEnabled;
            ox.UI.Widget.setDisabledClass(this.node, !this.isEnabled);
        }
        
    });

/**
 * @class A button.
 * @augments ox.UI.Widget
 * @para {I18nStirng} text The text of the button.
 */
ox.UI.Button = function(text) {
    ox.UI.Widget.apply(this);
    this.text = text;
};

/**
 * A callback which is called when the button is clicked.
 * @name ox.UI.Button.prototype.click
 * @type Function
 */

ox.UI.Button.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.Button.prototype */
    {
    
        addContent: function(node_id) {
            this.formnode = newnode("button", 0, 0, [addTranslated(this.text)]);
            var Self = this;
            addDOMEvent(this.formnode, "click", function() {
                if (Self.click) Self.click();
            });
            this.node = this.parent.addRow(this.formnode, false);
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        }
    
    });

/**
 * @class Abstract base class of all controllers.
 * Controllers are responsible for connecting widgets to the data model.
 */
ox.UI.Controller = {};

/**
 * @class An exception object which can be thrown by ox.UI.Controller.set to
 * indicate an attempt to set an invalid value.
 * @param {String} message An optional message which, if specified, is assigned
 * to the property message of the created instance.
 */
ox.UI.Controller.InvalidData = function(message) {
    if (message != undefined) this.message = message;
},

ox.UI.Controller.InvalidData.prototype = new Error("Invalid data");
ox.UI.Controller.InvalidData.prototype.constructor =    
    ox.UI.Controller.InvalidData;
ox.UI.Controller.InvalidData.prototype.name = "ox.UI.Controller.InvalidData";

/**
 * Adds a child value to a container value. Optionally, a replacement value for
 * the entire container value may be returned. This is useful for composite
 * widgets which represent scalars (i. e. not objects) as values.
 * @name ox.UI.Controller.prototype.get
 * @function
 * @param data The value of the container.
 * @param value The value of the child widget.
 * @type Any
 * @return undefined if the data was modified in-place, or a new value of
 * the container.
 */

/**
 * Extracts a child value from a container value.
 * Throws ox.UI.Controller.InvalidData if the extracted data is invalid.
 * @name ox.UI.Controller.prototype.set
 * @function
 * @param data The value of the container.
 * @return The value of the child widget, or undefined if the default value of
 * the widget should be used.
 */

/**
 * @class Abstract base class of all containers.
 * @augments ox.UI.Widget
 */
ox.UI.Container = function() {
    ox.UI.Widget.apply(this);
    this.children = [];
    this.names = {};
};

/**
 * Adds a content row to the container.
 * @name ox.UI.Container.prototype.addRow
 * @function
 * @param {DOM Node} child A DOM node which is or contains the chlid widget.
 * @param {Boolean} table Specifies whether the contents of this row should
 * align themselves with the content of other rows (if true), or not (if false).
 * @type DOM node
 * @return The DOM TR or DIV element of the added row.
 */

/**
 * Adds a row consisting of two cells: a label and a form element.
 * The form element will align itself with other form elements 
 * @name ox.UI.Container.prototype.addCells
 * @function
 * @param {I18nString} label An optional label text for the form element.
 * @param {DOM node} input The DOM node which contains the form element. 
 * @type DOM node
 * @return The DOM TR element of the added row.
 */

ox.UI.Container.prototype = extend(ox.UI.Widget,
    /** @lends ox.UI.Container.prototype */
    {
        addContent: function(node_id) {
            this.node_id = node_id;
            for (var i = 0; i < this.children.length; i++)
                this.children[i].addContent(node_id);
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        },
        
        default_value: {},
        
        /**
         * Adds a widget to this container as a new child.
         * The new widget appears behind all previously added widgets.
         * The value of the container is an object which contains the values of
         * all children. Each child value appears in the value of the container
         * as a field with the name specified by the parameter name.
         * @param {Widget} widget The widget to add.
         * @param {String or ox.UI.Controller} name If a string, the field name
         * under which the child value will appear in the container's value.
         * If an {@link ox.UI.Controller}, the controller which is responsible
         * for building and parsing the container value. If not specified,
         * the child value will not be connected to the container value.
         * @see ox.Configuration.Group.NoField
         */
        addWidget: function(widget, name) {
            if (name !== undefined) this.names[this.children.length] = name;
            this.children.push(widget);
            widget.setParent(this);
            if (this.initialized) widget.addContent(this.node_id);
        },
        
        /**
         * Removes a previously added widget.
         * @param {Widget} widget The widget to remove.
         */
        deleteWidget: function(widget) {
            for (var i = 0; i < this.children.length; i++) {
                if (this.children[i] == widget) {
                    this.children[i].remove(this);
                    this.children.splice(i, 1);
                    widget.setParent(null);
                    break;
                }
            }
        },
        
        /**
         * Returns an object with all child values.
         * Only children with specified field names are queried.
         * @type Object
         */
        get: function() {
            var data = {};
            for (var i = 0; i < this.children.length; i++) {
                if (i in this.names) {
                    var child = this.children[i];
                    if (child.isVisible && child.isEnabled) {
                        var value = child.get();
                        if (typeof this.names[i] == "string") {
                            data[this.names[i]] = value;
                        } else {
                            var newData = this.names[i].get(data, value);
                            if (newData !== undefined) data = newData;
                        }
                    }
                }
            }
            return data;
        },
        
        /**
         * Sets the values of all children.
         * @param {Object} data An object with a field for every child.
         * If a child was added with a field name, but the object does not
         * contain that field, the child will be set to its default value.
         */
        set: function(value) {
            value = value || {};
            for (var i = 0; i < this.children.length; i++) {
                if (i in this.names) {
                    if (typeof this.names[i] == "string") {
                        var val = value[this.names[i]];
                    } else {
                        var val = this.names[i].set(value);
                    }
                    if (val === undefined || val === null)
                        val = this.children[i].default_value;
                    this.children[i].set(val);
                }
            }
        },
        
        resize: function() {
            for (var i = 0; i < this.children.length; i++)
                this.children[i].resize();
        },
        
        applyVisible: function() {
            ox.UI.Widget.prototype.applyVisible.call(this);
            for (var i = 0; i < this.children.length; i++) {
                var child = this.children[i];
                child.setVisible(child.visible);
            }
        },
        
        applyEnabled: function() {
            ox.UI.Widget.prototype.applyEnabled.call(this);
            for (var i = 0; i < this.children.length; i++) {
                var child = this.children[i];
                child.setEnabled(child.enabled);
            }
        }

    });

registerView("configuration/modules",
    function() { showNode("modules"); },
    null, null,
    function() { hideNode("modules"); });

register("Loaded", function() {
    ox.Configuration.View.i18n = new I18nNode(noI18n(""));
    ox.Configuration.View.i18n.disable();
    $("modules.header").appendChild(ox.Configuration.View.i18n.node);
});

resizeEvents.register("Resized", function() {
    var current = ox.Configuration.View.current;
    if (current) setTimeout(function() { current.resize(); }, 0);
});

/**
 * Attaches a content page to a leaf node in the configuration tree.
 * The order of events for a view is <ol>
 * <li>{@link #init} (first time only),</li>
 * <li>{@link #addContent} (first time only),</li>
 * <li>{@link #enter},</li>
 * <li>user edits the view,</li>
 * <li>{@link #viewModified}</li>
 * <li>optionally, either {@link #saveView} or {@link #cancelView},</li>
 * <li>{@link #leave}.</li></ol>
 * @class Abstract base class of configuration pages.
 * @augments ox.UI.Container
 * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
 * which will be configured to open this page.
 * @param {I18nString} title The page title.
 */
ox.Configuration.View = function(node, title) {
    ox.UI.Container.apply(this);
    var Self = this;
    var view = node.id;
    var old_configuration_changed;
    registerView(view, function() {
        if (!Self.initialized) {
            if (Self.init) Self.init();
            if (!Self.toolbar) {
                Self.toolbar = temporary.configuration.newToolbar(title, []);
            }
            ox.widgets.toolBar.views[view] = Self.toolbar;
            changeDisplay.update(view);
            Self.addContent(view);
        }
        var i18n = ox.Configuration.View.i18n;
        if (title instanceof I18nString) {
            i18n.callback = function() { return String(title); };
        } else if (debug) {
            (console.warn || console["log"] || alert)(format(
                "The string \"%s\" is not internationalized!",
                title));
            if (typeof title != "function") {
                i18n.callback = function() { return String(_(title)); };
            } else {
                i18n.callback = title;
            }
        }
        i18n.update();
        i18n.enable();
        $("modules.content").appendChild(Self.content);
        temporary.configuration.showToolbar(Self.toolbar);
    }, function() {
        ox.Configuration.View.current = Self;
        old_configuration_changed = configuration_changed;
        configuration_changed = modified;
        register("OX_SAVE_OBJECT", save);
        register("OX_Cancel_Object", cancel);
        Self.enter();
    }, function() {
        Self.leave();
        unregister("OX_SAVE_OBJECT", save);
        unregister("OX_Cancel_Object", cancel);
        configuration_changed = old_configuration_changed;
        ox.Configuration.View.current = null;
    }, function() {
        $("modules.content").removeChild(Self.content);
        ox.Configuration.View.i18n.disable();
    });
    ox.Configuration.pages.push(this);
    node.click = function() {
        configuration_askforSave(function() {
            triggerEvent("OX_Switch_View", node.id); 
        });     
    };
    
    function modified() { return Self.viewModified(); }
    
    function save() { if (Self.viewModified()) Self.saveView(); }
    
    function cancel() { Self.cancelView(); }
};

/**
 * A callback which is called before the view is opened for the first time.
 * Usually, child elements are created and added here.
 * @name ox.Configuration.View.prototype.init
 * @type Function
 */

/**
 * DOM node with the content of the view.
 * @name ox.Configuration.View.prototype.content
 * @type DOM node
 */

/**
 * Returns whether the view contents were modified and may need to be saved.
 * @name ox.Configuration.View.prototype.viewModified
 * @function
 * @type Boolean
 * @return true if the view contents were modified.
 */

ox.Configuration.View.prototype = extend(ox.UI.Container,
    /** @lends ox.Configuration.View.prototype */
    {
    
        /**
         * Performs initialization when the user enters the view.
         */
        enter: function() {},
        
        /**
         * Performs cleanup when the user leaves the view.
         */
        leave: function() {},
        
        /**
         * Saves view contents.
         * @param {Function} callback An optional callback functi which is
         * called after the view is saved. 
         */
        saveView: function(callback) {},
        
        /**
         * Reverts modifications made by the user.
         */
        cancelView: function() {},
        
        /**
         * Width of child widgets, as a CSS length value.
         * @type String
         * @default "20em"
         */
        childWidth: "20em"
        
    });

/**
 * @class Resizable vertical split view.
 * All sizes are specified as a fraction of the total available space.
 * @augments ox.Configuration.View
 * @param {ox.Configuration.LeafNode} node An {@link ox.Configuration.LeafNode}
 * which will be configured to open this page.
 * @param {I18nString} title The page title.
 * @param {Number} size The width of the left panel.
 * @param {Boolean} new_button Specifies whether the big button in the menu
 * should be a "New" button instead of a "Save" button.
 */
ox.Configuration.VSplit = function(node, title, size, new_button) {
    ox.Configuration.View.call(this, node, title);

    /**
     * Current width of the left panel.
     * @type Number
     * @private
     */
    this.size = size;
    
    /**
     * Specifies whether the currently edited data is a new list entry.
     * @type Boolean
     * @private
     */
    this.isNew = false;
    
    var Self = this;
    this.toolbar = temporary.configuration.newToolbar(title,
        new_button ? [{
            title: _("New"),
            id: "new",
            buttons: [{
                title: _("New"),
                id: "new",
                icons: ["img/new.png"],
                big: true,
                action: function() { if (Self.onNew) Self.onNew(); }
            }]
        }] : [temporary.configuration.saveButton]);
};

/**
 * A callback which is called when the big "New" button in the menu is clicked.
 * @name ox.Configuration.VSplit.prototype.onNew
 * @type Function
 */

/**
 * The LiveGrid which is displayed in the left panel. It must be set before
 * the view is entered for the first time.
 * @name ox.Configuration.VSplit.prototype.list
 * @type LiveGrid
 * @deprecated
 */

/**
 * Specifies whether the list view was modified and needs to be saved.
 * @name ox.Configuration.VSplit.prototype.listModified
 * @type Boolean
 * @default false
 */

/**
 * Enables the list view when the view is entered. Disabling is done
 * automatically by calling the disable() method of the list. 
 * @name ox.Configuration.VSplit.prototype.enableList
 * @function
 */

/**
 * A callback which is called to get the data for the detail view.
 * It has a continuation function as parameter, which should be called with
 * the data as parameter when the data becomes available. If the continuation
 * function is called without a parameter, the detail view is set to the default
 * value and disabled.
 * @name ox.Configuration.VSplit.prototype.load
 * @type Function
 */

/**
 * A callback which is called to save the data of the detail view.
 * It has two parameters: data and cont. data is the data object to save.
 * cont is a continuation functions which should be called after the data was
 * saved successfully. If the continuation function function is called with
 * a parameter, the value of the parameter is used as the new value.
 * @name ox.Configuration.VSplit.prototype.save
 * @type Function
 */

/**
 * An unmodified copy of the data object which is edited in the detail view.
 * @name ox.Configuration.VSplit.prototype.original
 */

ox.Configuration.VSplit.prototype = extend(ox.Configuration.View,
    /** @lends ox.Configuration.VSplit.prototype */
    {
    
        addContent: function(node_id) {
            var selection = this.list.selection;
            selection.events.register("Selected", function(count) {
                menuselectedfolders = [];
                triggerEvent("OX_SELECTED_ITEMS_CHANGED", selection.length);
                triggerEvent("OX_SELECTION_CHANGED", selection.length);
            });
            selection.events.trigger("Selected", 0);
            var list_parent = newnode("div", {
                position: "absolute", top: "1.6em", bottom: 0, overflow: "auto",
                width: "100%"
            });
            this.left = newnode("div", {
                position: "absolute", left: 0, top: 0, width: 0, height: "100%",
                overflow: "hidden"
            }, 0, [this.list.getHeader(), list_parent]);
            this.list.getTable(list_parent);
            this.split = newnode("div", {
                position: "absolute", left: 0, top: 0, height: "100%",
                width: this.split_size + "px", cursor: "e-resize"
            }, { className: "sizesplit-vline" }, [
                newnode("img", { top: "50%", marginTop: "-10px" },
                    { src: getFullImgSrc("img/split_grip_v.gif") })
            ]);
            addDOMEvent(this.split, "mousedown", d);
            this.right = this.table = newnode("div", {
                position: "absolute", right: 0, top: 0, height: "100%",
                left: this.split_size + "px", overflow: "auto"
            });
            this.content = newnode("div", {
                position: "absolute", left: 0, top: 0, width: "100%",
                height: "100%"
            }, 0, [this.left, this.split, this.right]);
            ox.Configuration.View.prototype.addContent.apply(this, arguments);

            var Self = this;

            function d(e) {
                hideIFrames();
                Self.content.style.cursor = "e-resize";
                addDOMEvent(body, "mousemove", m);
                addDOMEvent(body, "mouseup", u);
                var pxsize = Self.getPixel(Self.size);
                var offset = pxsize - e.clientX;
                if (!Self.animated) {
                    var movingSplit = Self.split.cloneNode(true);
                    movingSplit.style.left = pxsize + "px";
                    movingSplit.className = movingSplit.className + " moving";
                    Self.content.appendChild(movingSplit);
                }
                cancelDefault(e);
                
                function getFraction(x) {
                    return (x + offset) /
                           (Self.content.clientWidth - Self.split_size);
                }

                function m(e) {
                    stopEvent(e);
                    Self.size = getFraction(e.clientX);
                    if (Self.animated) {
                        Self.setSize(Self.size);
                    } else {
                        movingSplit.style.left = (e.clientX + offset) + "px";
                    }
                }
    
                function u(e) {
                    showIFrames();
                    removeDOMEvent(body, "mousemove", m);
                    removeDOMEvent(body, "mouseup", u);
                    Self.content.style.cursor = "";
                    if (!Self.animated) {
                        Self.content.removeChild(movingSplit);
                        movingSplit = null;
                        Self.setSize(Self.size);
                    }
                }
    
            }

        },
        
        /**
         * Computes the width of the left panel in pixels.
         * @param {Number} size The width of the left panel as a fraction of
         * the total available space.
         * @type Number
         * @return The width of the left panel in pixels.
         * @private
         */
        getPixel: function(size) {
            size = size < this.min ? this.min
                 : size > this.max ? this.max
                                   : size;
            return size * (this.content.clientWidth - this.split_size);
        },
        
        addRow: function(child, table) {
            if (table) {
                var row = newnode("tr", 0, 0, [
                    newnode("td", { paddingLeft: "20px" }, { colSpan: 2 },
                            child ? [child] : 0)
                ]);
                if (this.lastRow) {
                    this.lastRow.appendChild(row);
                } else {
                    this.lastRow = newnode("tbody", { vAlign: "top" }, 0,
                                           [row]);
                    this.table.appendChild(newnode("table", 0, 0,
                                                   [this.lastRow]));
                }
                return row;
            } else {
                var row = newnode("div", { paddingLeft: "20px" }, 0,
                                  child ? [child] : 0);
                this.table.appendChild(row);
                this.lastRow = null;
                return row;
            }
        },
        
        addCells: function(label, input) {
            var tr = this.addRow(label ? addTranslated(label) : null, true);
            tr.firstChild.colSpan = 1;
            tr.appendChild(newnode("td", { paddingLeft: "10px" }, 0,
                                   input ? [input] : 0));
            return tr;
        },

        viewModified: function() {
            return this.listModified || !equals(this.get(), this.original);
        },

        enter: function() {
            if (this.enableList) {
                var Self = this;
                this.selected_cb = selected;
                this.list.selection.events.register("Selected", selected);
                this.enableList();
                if (this.list.selection.count == 1) {
                    this.enable();
                } else {
                    this.set(this.default_value);
                    this.id = undefined;
                    this.disable();
                }
            }
            this.original = clone(this.get());

            function selected(count) {
                if (!count && Self.isNew) return;
                Self.isNew = false;
                var data = Self.get();
                if (Self.enabled && !equals(data, Self.original) && Self.save) {
                    Self.afterSave = saved;
                    configuration_askforSave();
                } else {
                    saved();
                }

                function saved() {
                    Self.afterSave = null;
                    if (count == 1) {
                        if (Self.load) {
                            try {
                                Self.load(function(data) {
                                    if (data == undefined) {
                                        Self.set(Self.default_value);
                                        Self.id = undefined;
                                        Self.disable();
                                    } else {
                                        Self.set(data);
                                        Self.enable();
                                        Self.id = data.id;
                                    }
                                    Self.original = clone(Self.get());
                                });
                            } catch (e) {
                                if (e instanceof ox.UI.Controller.InvalidData) {
                                    Self.set(Self.default_value);
                                    Self.id = undefined;
                                    Self.disable();
                                } else throw e;
                            }
                        } else {
                            Self.enable();
                            Self.original = clone(Self.get());
                        }
                    } else {
                        Self.set(Self.default_value);
                        Self.id = undefined;
                        Self.disable();
                        Self.original = clone(Self.get());
                    }
                }
            }
            
        },
        
        leave: function() {
            this.list.selection.events.unregister("Selected", this.selected_cb);
            this.list.disable();
        },
        
        saveView: function(callback) {
            this.isNew = false;
            if (this.save) {
                var data = this.get();
                var data2 = clone(data);
                if (this.id != undefined) data2.id = this.id;
                var Self = this;
                this.save(data2, function(data3) {
                    if (Self.afterSave) {
                        Self.afterSave();
                    } else if (data3) {
                        Self.set(data3);
                        Self.id = data3.id;
                        Self.original = clone(Self.get());
                    } else {
                        Self.original = clone(data);
                    }
                    if (callback) callback();
                });
            } else if (callback) {
                callback();
            }
        },

        cancelView: function() {
            this.isNew = false;
            if (this.afterSave) {
                this.afterSave();
            } else {
                this.set(this.original);
            }
        },

        /**
         * Changes the width of the left panel.
         * @param {Number} size The new width of the left panel.
         */
        setSize: function(size) {
            this.size = size;
            size = this.getPixel(size);
            this.left.style.width = size + "px";
            this.split.style.left = size + "px";
            this.right.style.left = (size + this.split_size) + "px";
            if (IE6) {
                var h = this.content.parentNode.clientHeight + "px";
                this.content.style.height = h;
                this.left.style.height = h;
                this.split.style.height = h;
                this.right.style.height = h;
                this.right.style.width =
                    (this.content.parentNode.clientWidth
                    - this.getPixel(this.size) - this.split_size) + "px";
            }
        },
        
        resize: function() {
            this.setSize(this.size);
        },
    
        /**
         * Width of the split handle in pixels.
         */
        split_size: 7,
        
        /**
         * Specifies whether resizing takes effect immediately during dragging
         * (true) or only at the end (false).
         * @type Boolean
         * @default true
         */
        animated: true,
    
        /**
         * Minimum width of the left panel.
         * @type Number
         * @default 0
         */
        min: 0,
        
        /**
         * Maximum width of the left panel.
         * @type Number
         * @default 1
         */
        max: 1,
        
        original: {},
        
        addNew: function(data) {
            this.isNew = true;
            this.list.selection.reset();
            this.set(data);
            this.enable();
            this.original = clone(this.get());
            this.id = data.id;
        }
        
    });

/**
 * @class A configuration page with a single form.
 * @augments ox.Configuration.View
 * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
 * which will be configured to open this page.
 * @param {I18nString} title The page title.
 * @param {Boolean} save_button Whether the default save button should be
 * displayed in the menu. Defaults to true. Without the save button,
 * viewModified always returns false. 
 */
ox.Configuration.Page = function(node, title, save_button) {
    ox.Configuration.View.apply(this, arguments);
    if (save_button == undefined) save_button = true;
    if (save_button) {
        this.toolbar = temporary.configuration.newToolbar(title,
            [temporary.configuration.saveButton]);
    } else {
        this.viewModified = ox.Configuration.IFrame.prototype.viewModified;
    }
};

/**
 * A callback which is called to get the data for the view.
 * It has a continuation function as parameter, which should be called with
 * the data as parameter when the data becomes available. If the continuation is
 * called without a parameter, the view is set to the default value and
 * disabled.
 * @name ox.Configuration.Page.prototype.load
 * @type Function
 */

/**
 * A callback which is called to save the page's data.
 * It has two parameters: data and cont. data is the data object to save.
 * cont is a continuation functions which should be called after the data was
 * saved successfully.
 * @name ox.Configuration.Page.prototype.save
 * @type Function
 */

/**
 * An unmodified copy of the currently edited data object.
 * @name ox.Configuration.Page.prototype.original
 */

ox.Configuration.Page.prototype = extend(ox.Configuration.View,
    /** @lends ox.Configuration.Page.prototype */
    {

        addContent: function(node_id) {
            // TODO: Framework above Page
            this.content = this.table = newnode("div");
            ox.Configuration.View.prototype.addContent.apply(this, arguments);
        },
        
        resize: function() {},
    
        addRow: ox.Configuration.VSplit.prototype.addRow,
        
        addCells: ox.Configuration.VSplit.prototype.addCells,
        
        viewModified: function() {
            return !equals(this.get(), this.original);
        },

        enter: function() {
            if (this.load) {
                var Self = this;
                this.load(function(data) {
                    if (data == undefined) {
                        Self.set(Self.default_value);
                        Self.disable();
                    } else {
                        Self.set(data);
                        Self.enable();
                    }
                    Self.original = clone(Self.get());
                });
            } else {
                this.original = clone(this.get());
            }
        },

        saveView: function(callback) {
            if (this.save) {
                var data = this.get();
                var Self = this;
                this.save(data, function() {
                    Self.original = clone(data);
                    if (callback) callback();
                });
            }
        },

        cancelView: function() {
            this.set(this.original);
        }

    });

/**
 * Creates a new widget group for a configuration page.
 * @class A widget group in a configuration page.
 * @augments ox.UI.Container
 * @param {I18nString} title The title of the group.
 */
ox.Configuration.Group = function(title) {
    ox.UI.Container.apply(this);
    this.title = title;
};

ox.Configuration.Group.prototype = extend(ox.UI.Container,
    /** @lends ox.Configuration.Group.prototype */
    {
    
        addContent: function(node_id) {
            if (this.title) {
                this.node = this.parent.addRow(addTranslated(this.title), true);
                this.node.style.paddingTop = "1.6em";
                this.node.className = "height16 font-color-default " +
                                      "font-weight-high font-style-low";
            }
            this.childWidth = this.parent.childWidth;
            ox.UI.Container.prototype.addContent.apply(this, arguments);
        },
        
        addRow: function(child, table) {
            return this.parent.addRow(child, table);
        },
        
        addCells: function(label, input) {
            return this.parent.addCells(label, input);
        }
        
    });

/**
 * A pre-defined controller object for use as the second parameter to
 * ox.UI.Container#addWidget. It inserts child values directly into the parent's
 * value object, without a nested object.
 */
ox.Configuration.Group.NoField = {
    get: function(data, value) {
        if (typeof value == "object" && value !== null) {
            for (var i in value) data[i] = value[i];
        } else {
            return value;
        }
    },
    set: function(data) { return data; }
};

/**
 * @class A group which contains an array as value. Children are usually added
 * and remoevd at runtime by the user.
 * @augments ox.UI.Container
 * @param {I18nString} title An optional title of the group.
 */
ox.Configuration.ArrayGroup = function(title) {
    ox.UI.Container.apply(this);
    this.title = title;
};

/**
 * A callback which is caled to add the widget for a new array element.
 * @name ox.Configuration.ArrayGroup.prototype.addElement
 * @type Function
 */

ox.Configuration.ArrayGroup.prototype = extend(ox.UI.Container,
    /** @lends ox.Configuration.ArrayGroup */
    {
    
        default_value: [],
    
        addContent: function(node_id) {
            if (this.title) {
                this.tnode = this.parent.addRow(
                    this.title ? addTranslated(this.title) : null, true);
                this.tnode.style.paddingTop = "1.6em";
                this.tnode.className = "height16 font-color-default " +
                                       "font-weight-high font-style-low";
            }
            this.childWidth = this.parent.childWidth;
            this.tbody = newnode("tbody");
            this.node = this.parent.addRow(newnode("table", 0, 0, [this.tbody]),
                                           false);
            ox.UI.Container.prototype.addContent.apply(this, arguments);
        },
        
        remove: function() {
            if (this.tnode) this.tnode.parentNode.removeChild(this.tnode);
            this.node.parentNode.removeChild(this.node);
        },
        
        applyVisible: function() {
            if (this.tnode)
                this.tnode.style.display = this.isVisible ? "" : "none";
            this.node.style.display = this.isVisible ? "" : "none";
        },
        
        addRow: function(child, table) {
            var row = newnode("tr", 0, 0, [
                newnode("td", { paddingLeft: "20px" }, { colSpan: 2 },
                        child ? [child] : 0)
            ]);
            this.tbody.appendChild(row);
            return row;
        },
        
        addCells: ox.Configuration.VSplit.prototype.addCells,

        get: function() {
            var value = new Array(this.children.length);
            for (var i = 0; i < this.children.length; i++)
                value[i] = this.children[i].get();
            return value;
        },
        
        set: function(value) {
            for (var i = this.children.length - 1; i >= value.length; i--)
                this.deleteWidget(this.children[i]);
            for (var i = this.children.length; i < value.length; i++)
                this.addElement();
            for (var i = 0; i < this.children.length; i++)
                this.children[i].set(value[i]);
        }

    });

/**
 * @class A container which arranges its children horizontally.
 */
ox.Configuration.HLayout = function() {
    ox.UI.Container.apply(this);
}

/**
 * @private
 */
ox.Configuration.HLayout.id = 0;

ox.Configuration.HLayout.prototype = extend(ox.UI.Container,
    /** @lends ox.Configuration.HLayout.prototype */
    {
    
        addContent: function(node_id) {
            this.tr = newnode("tr");
            this.node = this.parent.addRow(
                newnode("table", 0, { cellpadding: "5px" },
                        [newnode("tbody", 0, 0, [this.tr])]),
                true);
            ox.UI.Container.prototype.addContent.apply(this, arguments);
        },
        
        applyVisible: ox.UI.Widget.prototype.applyVisible,
        
        addRow: function(child, table) {
            var td = newnode("td", 0, 0, [child]);
            this.tr.appendChild(td);
            return td;
        },
        
        addCells: function(label, input) {
            var td = newnode("td", 0, 0, label
                ? [addTranslated(label), input]
                : [input]);
            this.tr.appendChild(td);
            return td;
        },
        
        childWidth: "10em"
    
    });

/**
 * @class An editable list with "Add" and "Remove" buttons in the menu.
 * @augments ox.UI.Widget
 * @param {I18nString} section Name of the menu section.
 * @param {String} height The height of the list as a CSS length.
 * @param {I18nString} label An optional label for the list.
 */
ox.Configuration.EditableList = function(section, height, label) {
    ox.UI.Widget.apply(this);
    this.section = section;
    this.height = height;
    this.label = label;
    this.storage = new Storage(0, []);
};

/**
 * @private
 */
ox.Configuration.EditableList.id = 31;

/**
 * This method is called when the "Add" button is clicked.
 * @name ox.Configuration.EditableList.prototype.add
 * @function
 * @param {Function} cont A callback function which should be called with
 * an array of new elements as parameter.
 */

/**
 * This method is called when the "Remove" button is clicked.
 * @name ox.Configuration.EditableList.prototype.onDelete
 * @function
 * @param {Array} removed An array with currently selected values.
 * @param {Function} cont A callback function which should be called after all
 * side effects have been performed. This function performs the actual removal.
 * If it is not called, no entries are removed from the list.
 * @param {Function} dontDelete A function which can be used to cancel
 * the removal of individual values. For each value which should be kept, this
 * function should be called with the index of the value in the array
 * <code>removed</code> as parameter.
 */

ox.Configuration.EditableList.prototype = extend(ox.UI.Widget,
    /** @lends ox.Configuration.EditableList.prototype */
    {
        
        addContent: function(node_id) {
            var selection = new Selection();
            this.grid = new LiveGrid([{
                text: addTranslated(this.label),
                index: 1,
                clear: LiveGrid.makeClear(""),
                set: LiveGrid.defaultSet
            }], selection);
            this.grid.emptylivegridtext = this.emptyText;
            this.head = newnode("div");
            this.body = newnode("div",
                { height: this.height, position: "relative" });
            this.head.appendChild(this.grid.getHeader());
            this.grid.getTable(this.body);
            this.applyEnabled();
            
            this.node = this.parent.addRow(newnode("div", 0, 0,
                [this.head, this.body]));
            var id = "ox.Configuration.EditableList."
                     + ox.Configuration.EditableList.id++;
            var menu = MenuNodes.createSmallButtonContext(id, this.section);
            var Self = this;
            MenuNodes.createSmallButton(menu, id + ".add", _("Add"),
                getFullImgSrc("img/dummy.gif"), getFullImgSrc("img/dummy.gif"),
                function() {
                    if (Self.add) Self.add(function(values) {
                        if (!values.length) return;
                        var oldlen = Self.values.length;
                        Self.values = Self.values.concat(values);
                        var data = new Array(values.length);
                        for (var i = 0; i < values.length; i++)
                            data[i] = [i + oldlen, Self.getText(values[i])];
                        Self.storage.append(data);
                        Self.grid.focus = oldlen;
                        Self.grid.showFocus();
                    });
                });
            MenuNodes.createSmallButton(menu, id + ".remove", _("Remove"),
                getFullImgSrc("img/dummy.gif"), getFullImgSrc("img/dummy.gif"),
                del);
            this.grid.events.register("Deleted", del);
            function del() {
                var indices = Self.grid.selection.getSelected();
                var deleted = {};
                var values = new Array(indices.length);
                for (var i = 0; i < indices.length; i++) {
                    deleted[indices[i]] = true;
                    values[i] = Self.values[indices[i]];
                }
                if (Self.onDelete) {
                    Self.onDelete(values, cont, dontDelete);
                } else {
                    cont();
                }
                function cont() {
                    for (var d = 0; d < Self.values.length && !deleted[d]; d++);
                    for (var s = d + 1; s < Self.values.length; s++) {
                        if (!deleted[s]) Self.values[d++] = Self.values[s];
                    }
                    Self.values.length = d;
                    Self.set(Self.values);
                }
                function dontDelete(index) { delete deleted[indices[index]]; }
            }
            addMenuNode(menu.node, MenuNodes.FIXED,
                        ox.Configuration.EditableList.id);
            changeDisplay(node_id, id);
            selection.events.register("Selected", function(count) {
                menuselectedfolders = [];
                triggerEvent("OX_SELECTED_ITEMS_CHANGED", count);
                triggerEvent("OX_SELECTION_CHANGED", count);
            });
            //menuarrows[node_id] = {};
            register("OX_SELECTED_ITEMS_CHANGED", function() {
                menuglobalzaehler = 0;
                //menuarrows[node_id][id] = [];
                menu_display_contents(node_id, id, true, id + ".add");
                menu_display_contents(node_id, id, selection.count > 0,
                    id + ".remove");
            });
            ox.UI.Widget.prototype.addContent.apply(this, arguments);
        },
        
        /**
         * The text which is displayed in the list when it contains no entries.
         * @type I18nString
         */
        emptyText: noI18n(""),
        
        default_value: [],
        
        get: function() { return this.values; },
        
        set: function(value) {
            this.values = new Array(value.length);
            var data = new Array(value.length);
            for (var i = 0; i < value.length; i++) {
                this.values[i] = value[i];
                data[i] = [i, this.getText(value[i])];
            }
            this.storage.remove(0, this.storage.ids.length);
            this.storage.append(data);
        },
        
        /**
         * A callback function which is used to extract a human-readable
         * description from an array element of the value. This description is
         * displayed in the list. The default implementation handles I18nString
         * objects and plain strings.
         * @param elem The array element from which to extract the description.
         * @type String
         * @return A textual description of the specified element.
         */
        getText: function(elem) {
            return typeof elem == "function" ? elem() : elem;
        },
        
        applyEnabled: function() {
            if (this.isEnabled) {
                this.grid.enable(this.storage);
            } else {
                this.grid.disable();
            }
        }
        
    });

/**
 * @class A group of widgets with a common caption (legend).
 * The group is represented by a &lt;fieldset&gt; element.
 * @param {I18nString} legend An optional legend text. If not specified,
 * #getLegend must be overwritten to return the legend as an array of DOM nodes.
 */
ox.UI.FieldSet = function(legend) {
    ox.UI.Container.apply(this);
    if (legend) this.legend = legend;
};

ox.UI.FieldSet.prototype = extend(ox.UI.Container,
    /** @lends ox.UI.FieldSet.prototype */
    {
        
        addContent: function(node_id) {
            this.table = this.fieldset = newnode("fieldset", 0, 0,
                [newnode("legend", 0, 0, this.getLegend())]);
            this.node = this.parent.addRow(this.fieldset, false);
            this.childWidth = this.parent.childWidth;
            ox.UI.Container.prototype.addContent.apply(this, arguments);
        },
        
        /**
         * Builds the legend of the FieldSet. Descendants can overwrite this
         * method to create different legends.
         * @type Array
         * @return An array of DOM nodes which constitute the children of the
         * &lt;legend&gt; node.
         * @protected
         */
        getLegend: function() { return [addTranslated(this.legend)]; },
        
        applyVisible: ox.UI.Widget.prototype.applyVisible,
        
        addRow: ox.Configuration.VSplit.prototype.addRow,
        
        addCells: ox.Configuration.VSplit.prototype.addCells
/*
        addRow: function(child, table) {
            var td = newnode("td", 0, 0, [child]);
            this.tr.appendChild(td);
            return td;
        },
        
        addCells: function(label, input) {
            var td = newnode("td", 0, 0, label
                ? [addTranslated(label), input]
                : [input]);
            this.tr.appendChild(td);
            return td;
        }
*/
    });

/**
 * @class A FieldSet which can be enabled and disabled by a checkbox in
 * the legend.
 * @param {I18nString} legend The text of CheckBox in the legend.
 */
ox.UI.CheckedFieldSet = function(legend) {
    ox.UI.FieldSet.apply(this, arguments);
};

ox.UI.CheckedFieldSet.id = 0;

ox.UI.CheckedFieldSet.prototype = extend(ox.UI.FieldSet,
    /** @lends ox.UI.CheckedFieldSet.prototype */
    {
    
        getLegend : function() {
            var checkbox = newnode("input", 0, {
                type: "checkbox",
                id: "ox.UI.CheckedFieldSet." + ox.UI.CheckedFieldSet.id++
            });
            var Self = this;
            addDOMEvent(checkbox, "click", function() {
                Self.setEnabled(checkbox.checked);
            });
            return [checkbox, newnode("label", 0, { htmlFor: checkbox.id },
                                      [addTranslated(this.legend)])];
        }
        
    });

/**
 * Attaches an iframe page to a leaf node in the configuration tree.
 * @class A configuration page with external content in an iframe.
 * @augments ox.Configuration.View
 * @param {ox.Configuration.LeafNode} node A {@link ox.Configuration.LeafNode}
 * which will be configured to open this page.
 * @param {I18nString} title The page title.
 * @param {String} src The URI of the iframe content.
 * @param {Boolean} save_button Whether the default save button should be
 * displayed in the menu.
 */
ox.Configuration.IFrame = function(node, title, src, save_button) {
    ox.Configuration.View.apply(this, [node, title]);
    this.src = src;
    if (save_button) {
        this.toolbar = temporary.configuration.newToolbar(title,
            [temporary.configuration.saveButton]);
    }
};

ox.Configuration.IFrame.prototype = extend(ox.Configuration.View,
    /** @lends ox.Configuration.IFrame.prototype */
    {
    
        addContent: function(node_id) {
            this.content = newnode("iframe",
                { position: "absolute", // stupid IE7
                  width: "100%", height: "100%", border: 0 },
                { src: this.src });
            initialized = true;
        },
        
        resize: function() {},
        
        viewModified: function() { return false; }
    
    });

/**
 * @name ox.Configuration.IFrame.prototype.addWidget
 * @private
 */
delete ox.Configuration.IFrame.prototype.addWidget;


/**
 * A form with dynamic contents created from a formDescription
 * @param {I18nString} title A title for the type selection.
 * @param {ox.UI.Controller or String} typeController a controller for
 * the type selection widget, as used for the second parameter to addWidget().
 * @param {ox.UI.Controller or String} dataController a controller for
 * the form contents, as use for the second parameter to addWidget().
 * @ignore
 */
ox.Configuration.DynamicForm = function(title, typeController, dataController) {
    this.meta = {};
    function makeDefault(controller) {
        if (typeof controller == "string") {
            return {
                get: function(data, value) { data[controller] = value; },
                set: function(data) { return data[controller]; }
            };
        } else {
            return controller;
        }
    }
    this.typeController = makeDefault(typeController);
    this.dataController = makeDefault(dataController);
    ox.UI.Container.call(this);
    this.select = new ox.UI.ComboBox(title);
    var Self = this;
    this.select.changed = function() {
        var type = Self.select.get();
        for (var i = Self.children.length - 1; i > 0; i--) {
            Self.deleteWidget(Self.children[i]);
        }
        if (type && (type in Self.meta)) {
            var defs = Self.meta[type].formDescription;
            for (var i = 0; i < defs.length; i++) {
                var def = defs[i];
                var widget = Self.widgets[def.widget](def);
                var controller = Self.controllers[def.widget];
                Self.addWidget(widget, controller ? controller(def.name)
                                                  : def.name);
                if ("defaultValue" in def) {
                    widget.default_value = def.defaultValue;
                    widget.set(def.defaultValue);
                }
            }
        }
    };
    this.addWidget(this.select);
}

ox.Configuration.DynamicForm.prototype = extend(ox.UI.Container, {
    addContent:  function(node_id) {
        this.childWidth = this.parent.childWidth;
        this.table = newnode("div");
        this.node = this.parent.addRow(this.table);
        ox.UI.Container.prototype.addContent.apply(this, arguments);
    },
    addRow: ox.Configuration.VSplit.prototype.addRow,
    addCells: ox.Configuration.VSplit.prototype.addCells,
    /**
     * Sets the available form descriptions.
     * @param {Object} meta An object mapping types to formDescription objects.
     */
    setMetadata: function(meta) {
        this.meta = {};
        MetaLoop: for (var i in meta) {
            var defs = meta[i].formDescription;
            for (var j = 0; j < defs.length; j++) {
                if (!(defs[j].widget in this.widgets)) continue MetaLoop;
            }
            this.meta[i] = meta[i];
        }
        var ids = [], names = [];
        for (var i in this.meta) {
            ids.push(i);
            names.push(this.meta[i].displayName);
        }
        if (ids.length) this.select.default_value = ids[0];
        this.select.setEntries(ids, names);
    },
    set: function(value) {
        this.id = value.id;
        var type = this.typeController.set(value);
        if (type === undefined || type === null) {
            type = this.select.default_value;
        }
        this.select.set(type);
        this.select.setEnabled(!this.immutableType || !this.id); 
        ox.UI.Container.prototype.set.call(this,
            this.dataController.set(value));
    },
    get: function() {
        var type = this.select.get();
        if (!type) return;
        var value = {};
        if (this.id) value.id = this.id;
        this.typeController.get(value, type);
        this.dataController.get(value,
            ox.UI.Container.prototype.get.call(this));
        return value;
    },
    /**
     * A map of widget types to widget constructors.
     * Each constructor takes a widget definition from a formDescription object
     * as it single parameter and returns the created widget.
     */
    widgets: {
        input: function(def) {
            return new ox.UI.Input(noI18n(def.displayName));
        },
        password: function(def) {
            return new ox.UI.Password(noI18n(def.displayName));
        },
        checkbox: function(def) {
            return new ox.UI.CheckBox(noI18n(def.displayName));
        },
        text: function(def) {
            return new ox.UI.Text(noI18n(def.displayName));
        },
        link: function(def) {
            var link = new ox.UI.Widget();
            link.default_value = "";
            link.addContent = function() {
                this.anchor = newnode("a", 0,
                    { href: this.default_value, target: "_blank" },
                    [document.createTextNode(this.default_value)]);
                this.node = this.parent.addCells(noI18n(def.displayName),
                                                 this.anchor);
                ox.UI.Widget.prototype.addContent.apply(this, arguments);
            };
            link.set = function(value) {
                this.anchor.href = value;
                this.anchor.firstChild.data = value;
            };
            link.get = function() { return this.anchor.firstChild.data; };
            return link;
        },
        oauthAccount: function(def) {
            var account = new ox.Configuration.HLayout();
            account.addContent = function(node_id) {
                this.tr = newnode("tr");
                this.node = this.parent.addCells(noI18n(def.displayName),
                    newnode("table", 0, { cellpadding: "5px" },
                            [newnode("tbody", 0, 0, [this.tr])]));
                ox.UI.Container.prototype.addContent.apply(this, arguments);
            };
            var combobox = new ox.UI.ComboBox();
            updateAccounts();
            function updateAccounts(cb) {
                OAuthAccounts.then(function(accounts) {
                    var ids = jQuery.map(accounts, function(a) {
                        return a.serviceId == def.options.type ? a.id : null;
                    });
                    var names = jQuery.map(accounts, function(a) {
                        return a.serviceId == def.options.type
                            ? noI18n(a.displayName) : null;
                    });
                    combobox.setEntries(ids, names);
                    if (cb) cb();
                });
            }
            account.addWidget(combobox, ox.Configuration.Group.NoField);
            var button = new ox.UI.Button(_("New account..."));
            button.click = function() {
                function s(text) { return newtext(String(text)); }
                var input = newnode("input", { marginLeft: "0.5em" },
                                             { type: "text" });
                OAuthAccounts.then(function(accounts) {
                    for (var i = 0; i < accounts.length; i++) {
                        if (accounts[i].id == def.options.type) {
                            input.value = accounts[i].displayName;
                        }
                    }
                });
                var div = newnode("div", 0, 0, [
                    s(_("Account name")), input,
                    newnode("div", { marginTop: "0.5em" }, 0,
                        //#. %s is the product name (by default 'Open-Xchange Server')
                        [s(format(_("After pressing OK, a new window will open where you can authorize %s to access your account data."),
                                 oxProductInfo.product_name))])
                ]);
                newConfirm(String(_("Add a new OAuth account")), "",
                    AlertPopup.OK + AlertPopup.CANCEL, ok, 0, 0, 0, 0, 0, 0, 0,
                    0, div);
                setTimeout(function() { input.focus(); }, 0);
                function ok() {
                    createOAuthAccount(
                        {
                            serviceId: def.options.type,
                            displayName: input.value
                        },
                        function(data) {
                            updateAccounts(function() {
                                combobox.set(data.id);
                            });
                        });
                }
            };
            account.addWidget(button);
            return account;
        }
    },
    /**
     * Overrides for controllers for individual widget types.
     * Each override is a function which takes the field name as parameter
     * and returns either an ox.UI.Controller or a string for the second
     * parameter to addWidget().
     */
    controllers: {
        password: function(name) {
            return {
                get: function(data, value) { if (value) data[name] = value; },
                set: function(data) { return data[name]; }
            };
        }
    }
});
