/*
################################################################################
ox.ui.js

requires
    jquery-1.4.js
    ox.js
################################################################################
*/

// also see test.js, in demos ...

(function() {

    var oxui = {
            defaultTheme: "classic",
            elements: {},
            getDimensions: function(orientation) {
                return orientation == "horizontal" ?
                    ["width", "height"] : ["height", "width"];
            },
            getEdges: function(orientation) {
                return orientation == "horizontal" ?
                    ["left", "right", "top", "bottom"] :
                    ["top", "bottom", "left", "right"];
            },
            getBarSize: function(size) {
                var sizes = {
                    small: 20,
                    medium: 24,
                    large: 28,
                };
                return sizes[size];
            },
            jQueryFunctions: function() {
                var functions = [],
                    $element = $("<div>");
                //delete $element.length;
                Ox.each($element, function(k, v) {
                    if (typeof v == "function") {
                        functions.push(k);
                    }
                });
                return functions.sort();
            }(),
            path: $("script[src*=ox.ui.js]").attr("src")
                .replace("js/ox.ui.js", ""),
            scrollbarSize: $.browser.mozilla ? 16 : 12,
            symbols: {
                alt: "\u2325",
                apple: "\uF8FF",
                arrow_down: "\u2193",
                arrow_left: "\u2190",
                arrow_right: "\u2192",
                arrow_up: "\u2191",
                backspace: "\u232B",
                backup: "\u2707",
                ballot: "\u2717",
                black_star: "\u2605",
                burn: "\u2622",
                caps_lock: "\u21EA",
                check: "\u2713",
                //clear: "\u2327",
                clear: "\u00D7",
                click: "\uF803",
                close: "\u2715",
                command: "\u2318",
                control: "\u2303",
                cut: "\u2702",
                "delete": "\u2326",
                diamond: "\u25C6",
                edit: "\uF802",
                eject: "\u23CF",
                escape: "\u238B",
                end: "\u2198",
                enter: "\u2324",
                fly: "\u2708",
                gear: "\u2699",
                home: "\u2196",
                info: "\u24D8",
                navigate: "\u2388",
                option: "\u2387",
                page_up: "\u21DE",
                page_down: "\u21DF",
                redo: "\u21BA",
                "return": "\u21A9",
                //select: "\u21D5",
                select: "\u25BE",
                shift: "\u21E7",
                sound: "\u266B",
                space: "\u2423",
                tab: "\u21E5",
                trash: "\u267A",
                triangle_down: "\u25BC",
                triangle_left: "\u25C0",
                triangle_right: "\u25BA",
                triangle_up: "\u25B2",
                undo: "\u21BB",
                voltage: "\u26A1",
                warning: "\u26A0",
                white_star: "\u2606"
            }
        },
        $window, $document, $body;

    $(function() {
        $window = $(window),
        $document = $(document),
        $body = $("body"),
        $elements = {};
        Ox.theme(oxui.defaultTheme);
    });

    /*
    ============================================================================
    Application
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.App
    ----------------------------------------------------------------------------
    */

    Ox.App = function() {
        /*
        options:
        requestTimeout
        requestType
        requestURL
        */
        return function(options) {

            options = options || {};
            var self = {},
                that = this;

            self.options = $.extend({
                requestTimeout: oxui.requestTimeout,
                requestType: oxui.requestType,
                requestURL: oxui.requestURL
            }, options);

            self.change = function() {
                
            };

            that.launch = function() {
                $.ajaxSetup({
                    timeout: self.options.requestTimeout,
                    type: self.options.requestType,
                    url: self.options.requestURL
                });
            };

            that.options = function() {
                return Ox.getset(self.options, Array.slice.call(arguments), self.change, that); 
            };

            that.request = function(action, data, callback) {
                if (arguments.length == 2) {
                    callback = data;
                    data = {};
                }
                return Ox.Request.send({
                    url: self.options.requestURL,
                    data: {
                        action: action,
                        data: JSON.stringify(data)
                    },
                    callback: callback
                });
            };

            return that;

        };

    }();

    /*
    ----------------------------------------------------------------------------
    Ox.Event
    ----------------------------------------------------------------------------

    naming convention for event/trigger
    verb.id.namespace, i.e. verb.sourceId.targetId (?)
    ...
    bind("keydown.shift+dot.numpad", function() {
        // ...
    })
    keyboard handler then would:
    $.each(stack, function(i, v) {
        elements[v].trigger("keydown.shift+0.numpad");
    });
    and the element would implement
    this.trigger(event, data) {
        
    }
    ...
    keyboard handler also triggers keydown.buffer
    */

    // use dom elements / jquery instead

    Ox.Event = function() {
        var keyboardEvents = {};
            $eventHandler = $("<div>");
        function isKeyboardEvent(event) {
            return event.substr(0, 4) == "key_";
        }
        return {
            bind: function(id, event, callback) {
                if (isKeyboardEvent(event)) {
                    keyboardEvents[id] = keyboardEvents[id] || {};
                    keyboardEvents[id][event] = callback;
                }
                if (!isKeyboardEvent(event) || Ox.Focus.focused() == id) {
                    $eventHandler.bind(event, callback);
                }
            },
            bindKeyboard: function(id) {
                $.each(keyboardEvents[id] || [], function(event, callback) {
                    Ox.Event.bind(id, event, callback);
                });
            },
            trigger: function(event, data) {
                Ox.print("trigger", event, data || {});
                $eventHandler.trigger(event, data || {});
            },
            unbind: function(id, event, callback) {
                if (isKeyboardEvent(event)) {
                    $.each(keyboardEvents[id] || [], function(e, callback) {
                        if (e == event) {
                            delete keyboardEvents[id][e];
                            return false;
                        }
                    });
                }
                $eventHandler.unbind(event, callback);
            },
            unbindKeyboard: function(id) {
                $.each(keyboardEvents[id] || [], function(event, callback) {
                    $eventHandler.unbind(event, callback);
                });
            }
        }
    }();

    Ox.Event_ = function() {
        var events = {};
        return {
            // make these bind, trigger, unbind
            publish: function(event, data) {
                if (events[event]) {
                    $.each(events[event], function(i, v) {
                        setTimeout(function() {
                            v(data);                                
                        }, 0);
                    });
                }
            },
            subscribe: function(event, callback) {
                if (events[event]) {
                    events[event].push(callback);
                } else {
                    events[event] = [callback];
                }
            },
            unsubscribe: function(event, callback) {
                $.each(events[event], function(i, v) {
                    if (Ox.startsWith(callback.toString(), v.toString())) {
                        events[event].splice(i, 1);
                    }
                });
            }
        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.Focus
    ----------------------------------------------------------------------------
    */

    Ox.Focus = function() {
        var stack = [];
        return {
            blur: function(id) {
                if (stack.indexOf(id) > -1) {
                    $elements[Ox.Focus.focused()].removeClass("OxFocus");
                    $(".OxFocus").removeClass("OxFocus"); // fixme: the above is better, and should work
                    stack.splice(stack.length - 2, 0, stack.pop());
                    Ox.Event.unbindKeyboard(id);
                    Ox.Event.bindKeyboard(stack[stack.length - 1]);
                    Ox.print("blur", stack);
                }
            },
            focus: function(id) {
                var index = stack.indexOf(id);
                if (stack.length) {
                    Ox.Event.unbindKeyboard(stack[stack.length - 1])
                }
                if (index > -1) {
                    stack.splice(index, 1);
                }
                stack.push(id);
                $elements[Ox.Focus.focused()].addClass("OxFocus");
                Ox.Event.bindKeyboard(id);
                Ox.print("focus", stack);
            },
            focused: function() {
                return stack[stack.length - 1];
            }
        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.History
    ----------------------------------------------------------------------------
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Keyboard
    ----------------------------------------------------------------------------
    */

    (function() {

        var buffer = "",
            bufferTime = 0,
            bufferTimeout = 1000,
            keyNames = function() {
                return {
                    0: "section",
                    8: "backspace",
                    9: "tab",
                    12: "clear",
                    13: "enter",
                    16: "shift",
                    17: "control",
                    18: "alt",
                    20: "capslock",
                    27: "escape",
                    32: "space",
                    33: "pageup",
                    34: "pagedown",
                    35: "end",
                    36: "home",
                    37: "left",
                    38: "up",
                    39: "right",
                    40: "down",
                    45: "insert",
                    46: "delete",
                    47: "help",
                    48: "0",
                    49: "1",
                    50: "2",
                    51: "3",
                    52: "4",
                    53: "5",
                    54: "6",
                    55: "7",
                    56: "8",
                    57: "9",
                    65: "a",
                    66: "b",
                    67: "c",
                    68: "d",
                    69: "e",
                    70: "f",
                    71: "g",
                    72: "h",
                    73: "i",
                    74: "j",
                    75: "k",
                    76: "l",
                    77: "m",
                    78: "n",
                    79: "o",
                    80: "p",
                    81: "q",
                    82: "r",
                    83: "s",
                    84: "t",
                    85: "u",
                    86: "v",
                    87: "w",
                    88: "x",
                    89: "y",
                    90: "z",
                    91: "meta.left",
                    92: "meta.right",
                    93: "select",
                    96: "0.numpad",
                    97: "1.numpad",
                    98: "2.numpad",
                    99: "3.numpad",
                    100: "4.numpad",
                    101: "5.numpad",
                    102: "6.numpad",
                    103: "7.numpad",
                    104: "8.numpad",
                    105: "9.numpad",
                    106: "asterisk.numpad",
                    107: "plus.numpad",
                    109: "minus.numpad",
                    108: "enter.numpad",
                    110: "dot.numpad",
                    111: "slash.numpad",
                    112: "f1",
                    113: "f2",
                    114: "f3",
                    115: "f4",
                    116: "f5",
                    117: "f6",
                    118: "f7",
                    119: "f8",
                    120: "f9",
                    121: "f10",
                    122: "f11",
                    123: "f12",
                    124: "f13",
                    125: "f14",
                    126: "f15",
                    127: "f16",
                    144: "numlock",
                    145: "scrolllock",
                    186: "semicolon",
                    187: "equal",
                    188: "comma",
                    189: "minus",
                    190: "dot",
                    191: "slash",
                    192: "backtick",
                    219: "openbracket",
                    220: "backslash",
                    221: "closebracket",
                    222: "quote"
                    // see dojo, for ex.
                };
            }(),
            modifierNames = {
                altKey: "alt", // mac: option
                ctrlKey: "control",
                // metaKey: "meta", // mac: command
                shiftKey: "shift"
            };

        $(function() {
            // fixme: how to do this better?
            // in firefox on mac, keypress doesn't fire for up/down
            // if the cursor is at the start/end of an input element
            // on linux, it doesn't seem to fire if the input element has focus
            if ($.browser.mozilla) {
                $document.keypress(keypress);
                $document.keydown(function(event) {
                    var $element = $("input:focus");
                    if ($element.length) {
                        if (
                            (
                                keyNames[event.keyCode] == "up" &&
                                $element[0].selectionStart + $element[0].selectionEnd == 0
                            ) || (
                                keyNames[event.keyCode] == "down" &&
                                $element[0].selectionStart == $element.val().length &&
                                $element[0].selectionEnd == $element.val().length
                            )
                        ) {
                            keypress(event);
                        }
                    }
                });                
            } else {
                $document.keydown(keypress);                
            }
        });
        function keypress(event) {
            var key,
                keys = [],
                //ret = true,
                time;
            $.each(modifierNames, function(k, v) {
                if (event[k]) {
                    keys.push(v);
                }
            });
            // avoid pushing modifier twice
            Ox.print("keys", keys)
            if (keyNames[event.keyCode] && keys.indexOf(keyNames[event.keyCode]) == -1) {
                keys.push(keyNames[event.keyCode]);
            }
            key = keys.join("_");
            if (key.match(/^[\w\d-]$|SPACE/)) {
                time = Ox.getTime();
                if (time - bufferTime > bufferTimeout) {
                    buffer = "";
                }
                buffer += key == "SPACE" ? " " : key;
                bufferTime = time;                    
            }
            Ox.Event.trigger("key_" + key);
            //return false;
            /*
            $.each(stack, function(i, v) {
                // fixme: we dont get the return value!
                ret = Ox.event.trigger(keyboard + Ox.toCamelCase(key) + "." + v);
                return ret;
            });
            */                
        }
        
    })();

    /*
    ----------------------------------------------------------------------------
    Ox.Mouse (??)
    ----------------------------------------------------------------------------
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Request
    ----------------------------------------------------------------------------
    */

    Ox.Request = function() {

        var cache = {},
            pending = {},
            requests = {},
            self = {
                options: {
                    timeout: 15000,
                    type: "POST",
                    url: "api"
                }
            };

        return {

            cancel: function() {
                var index;
                if (arguments.length == 0) {
                    requests = {};
                } else if (Ox.isFunction(arguments[0])) {
                    // cancel with function
                    $.each(requests, function(id, req) {
                        if (arguments[0](req)) {
                            delete requests[id];
                        }
                    })
                } else {
                    // cancel by id
                    delete requests[arguments[0]]
                }
            },

            emptyCache: function() {
                cache = {};
            },

            options: function(options) {
                return Ox.getset(self.options, options, $.noop(), this);
            },

            send: function(options) {

                var options = $.extend({
                        age: -1,
                        callback: function() {},
                        id: Ox.uid(),
                        timeout: self.options.timeout,
                        type: self.options.type,
                        url: self.options.url
                    }, options),
                    req = JSON.stringify({
                        url: options.url,
                        data: options.data
                    });

                function callback(data) {
                    delete requests[options.id];
                    Ox.length(requests) == 0 && Ox.Event.trigger("requestStop"); 
                    options.callback(data);
                }

                function debug(request) {
                    var $iframe = $("<iframe>")
                            .css({ // fixme: should go into a class
                                width: 768,
                                height: 384
                            }),
                        $dialog = new Ox.Dialog({
                                title: "Application Error",
                                buttons: [
                                    {
                                        value: "Close",
                                        click: function() {
                                            $dialog.close();
                                        }
                                    }
                                ],
                                width: 800,
                                height: 400
                            })
                            .append($iframe)
                            .open(),
                        iframe = $iframe[0].contentDocument || $iframe[0].contentWindow.document;
                    iframe.open();
                    iframe.write(request.responseText);
                    iframe.close();
                }

                function error(request, status, error) {
                    var data;
                    if (arguments.length == 1) {
                        data = arguments[0]
                    } else {
                        try {
                            data = JSON.parse(request.responseText);
                        } catch (err) {
                            data = {
                                status: {
                                    code: request.status,
                                    text: request.statusText
                                }
                            };
                        }
                    }
                    if (data.status.code < 500) {
                        callback(data);
                    } else {
                        var $dialog = new Ox.Dialog({
                                title: "Application Error",
                                buttons: [
                                    {
                                        value: "Details",
                                        click: function() {
                                            $dialog.close(function() {
                                                debug(request);                                                
                                            });
                                        }
                                    },
                                    {
                                        value: "Close",
                                        click: function() {
                                            $dialog.close(function() {
                                                callback(data);
                                            });
                                        }
                                    }
                                ],
                                width: 400,
                                height: 200
                            })
                            .append("Sorry, we have encountered an application error while handling your request. To help us find out what went wrong, you may want to report this error to an administrator. Otherwise, please try again later.")
                            .open();
                            // fixme: change this to Send / Don't Send
                        Ox.print({
                            request: request,
                            status: status,
                            error: error
                        });
                    }
                    pending[options.id] = false;
                }

                function success(data) {
                    pending[options.id] = false;
                    try {
                        data = JSON.parse(data);                                    
                    } catch (err) {
                        error({
                            status: {
                                code: 500,
                                text: "Internal Server Error"
                            },
                            data: {}
                        });
                        return;
                    }
                    cache[req] = {
                        data: data,
                        time: Ox.getTime()
                    };
                    callback(data);
                }

                if (pending[options.id]) {
                    setTimeout(function() {
                        Ox.Request.send(options);
                    }, 0);
                } else {
                    requests[options.id] = {
                        url: options.url,
                        data: options.data
                    };
                    if (cache[req] && (options.age == -1 || options.age > Ox.getTime() - cache[req].time)) {
                        setTimeout(function() {
                            callback(cache[req].data);
                        }, 0);
                    } else {
                        pending[options.id] = true;
                        $.ajax({
                            data: options.data,
                            error: error,
                            success: success,
                            timeout: options.timeout,
                            type: options.type,
                            url: options.url
                        });
                        Ox.print("request", options.data);
                        Ox.length(requests) == 1 && Ox.Event.trigger("requestStart");
                    }
                }

                return options.id;

            },

            requests: function() {
                return Ox.length(requests);
            }

        };
    }();

    /*
    ----------------------------------------------------------------------------
    Ox.URL
    ----------------------------------------------------------------------------
    */

    /*
    ============================================================================
    Core
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.Container
    ----------------------------------------------------------------------------
    */

    // fixme: wouldn't it be better to let the elements be,
    // rather then $element, $content, and potentially others,
    // 0, 1, 2, etc, so that append would append 0, and appendTo
    // would append (length - 1)?
    Ox.Container = function(options, self) {
        var that = new Ox.Element(options, self)
            .addClass("OxContainer");
        that.$content = new Ox.Element(options, self)
            .addClass("OxContent")
            .appendTo(that);
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Element
    ----------------------------------------------------------------------------
    */

    // check out http://ejohn.org/apps/learn/#36 (-#38, making fns work w/o new)

    Ox.Element = function() {

        var elements = {}; // fixme: unused, we need this outside Element (for Focus)

        return function(options, self) {

            // construct
            options = options || {};
            self = self || {};
            var that = this;

            // init
            (function() {
                // allow for Ox.Widget("tagname", self)
                if (typeof options == "string") {
                    options = {
                        element: options
                    };
                }
                that.ox = Ox.version;
                that.id = Ox.uid();
                that.$element = $("<" + (options.element || "div") + "/>", {
                    data: {
                        ox: that.id
                    }
                });
                $elements[that.id] = that;
                wrapjQuery();
            })();

            // private
            function wrapjQuery() {
                $.each(oxui.jQueryFunctions, function(i, fn) {
                    that[fn] = function() {
                        var args = arguments,
                            length = args.length,
                            id, ret;
                        $.each(args, function(i, arg) {
                            // if an ox object was passed
                            // then pass its $element instead
                            // so we can do oxObj.jqFn(oxObj)
                            if (arg.ox) {
                                args[i] = arg.$element;
                            }
                            /*
                            if (arg.ox) { // fixme: or is this too much magic?
                                if (fn == "appendTo" && arg.$content) {
                                    args[i] = arg.$content
                                } else {
                                    args[i] = arg.$element;
                                }
                            }
                            */
                        });
                        /*
                        if (fn == "html" && that.$content) { // fixme: or is this too much magic?
                            $element = that.$content;
                        } else {
                            $element = that.$element;
                        }
                        */
                        // why does this not work?
                        // ret = that.$element[v].apply(this, arguments);
                        if (length == 0) {
                            ret = that.$element[fn]();
                        } else if (length == 1) {
                            ret = that.$element[fn](args[0]);
                        } else if (length == 2) {
                            ret = that.$element[fn](args[0], args[1]);
                        } else if (length == 3) {
                            ret = that.$element[fn](args[0], args[1], args[2]);
                        } else if (length == 4) {
                            ret = that.$element[fn](args[0], args[1], args[2], args[3]);
                        }
                        if (fn == "data") {
                            // Ox.print("data ret", ret, $(ret))
                        }
                        // if the $element of an ox object was returned
                        // then return the ox object instead
                        // so we can do oxObj.jqFn().oxFn()
                        return ret.jquery && $elements[id = ret.data("ox")] ?
                                $elements[id] : ret;
                    }
                });
            }

            // shared
            self.onChange = function() {
                // self.onChange(key, value)
                // is called when an option changes
                // (to be implemented by widget)
            };

            // public
            that.bindEvent = function() {
                // fixme: shouldn't this work the other way around,
                // and bind a function to an event triggered by this widget?
                /*
                bindEvent(event, fn) or bindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.bind(that.id, event, fn);
                    });
                } else {
                    Ox.Event.bind(that.id, arguments[0], arguments[1]);
                }
                return that;
            };
            that.defaults = function(defaults) {
                /*
                    that.defaults({foo: x})         sets self.defaults
                */
                self.defaults = defaults;
                delete self.options; // fixme: hackish fix for that = Ox.Foo({...}, self).defaults({...}).options({...})
                return that;
            };
            that.gainFocus = function() {
                Ox.Focus.focus(that.id);
                return that;
            };
            that.hasFocus = function() {
                return Ox.Focus.focused() == that.id;
            };
            that.loseFocus = function() {
                Ox.Focus.blur(that.id);
                return that;
            };
            that.options = function() { // fixme: use Ox.getset
                /*
                    that.options()                  returns self.options
                    that.options("foo")             returns self.options.foo
                    that.options("foo", x)          sets self.options.foo,
                                                    returns that
                    that.options({foo: x, bar: y})  sets self.options.foo
                                                    and self.options.bar,
                                                    returns that
                */
                var length = arguments.length,
                    // args, options, ret;
                    args, ret;
                if (length == 0) {
                    // options()
                    ret = self.options;
                } else if (length == 1 && typeof arguments[0] == "string") {
                    // options(str)
                    ret = self.options[arguments[0]]
                } else {
                    // options (str, val) or options({str: val, ...})
                    // translate (str, val) to ({str: val})
                    args = Ox.makeObject.apply(that, arguments);
                    /*
                    options = self.options;
                    */
                    // if options have not been set, extend defaults,
                    // otherwise, extend options
                    self.options = $.extend(self.options || self.defaults, args);
                    $.each(args, function(key, value) {
                        self.onChange(key, value);
                        /*
                        fixme: why does this not work?
                        Ox.print("options", options, key, value)
                        //Ox.print(!options, !options || !options[key], !options || !options[key] || options[key] !== value)
                        if (!options || !options[key] || options[key] !== value) {
                            Ox.print("onChange...")
                            self.onChange(key, value);
                        } else {
                            Ox.print("NO CHANGE");
                        }
                        */
                    });
                    ret = that;                            
                }
                return ret;
            };
            that.remove = function() {
                that.$element.remove();
                delete $elements[that.ox];
                return that;
            };
            that.triggerEvent = function() {
                /*
                triggerEvent(event, fn) or triggerEvent({event0: fn0, event1: fn1, ...})
                */
                if (Ox.isObject(arguments[0])) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.trigger(event + "_" + self.options.id, fn);
                    });
                } else {
                    Ox.Event.trigger(arguments[0] + "_" + self.options.id, arguments[1] || {});
                }
                return that;
            };
            that.unbindEvent = function() {
                /*
                unbindEvent(event, fn) or unbindEvent({event0: fn0, event1: fn1, ...})
                */
                if (arguments.length == 1) {
                    $.each(arguments[0], function(event, fn) {
                        Ox.Event.unbind(that.id, event, fn);
                    })
                } else {
                    Ox.Event.unbind(that.id, arguments[0], arguments[1]);
                }
                return that;
            };

            // return
            return that;

        }

    }();

    Ox._Element = function(element) {
        var that = this;
        that.def = {};
        that.opt = {};
        that.ox = Ox.version;
        that.id = Ox.uid();
        //Ox.print("that.id", that.id)
        that.$element = $("<" + (element || "div") + "/>")
            //.addClass("OxElement")
            .data("ox", that.id);
        oxui.elements[that.id] = that;
        // Ox.print("oxui.elements", oxui.elements)
        //function setOption() {};
        that.setOption = function() {};
        /*
        */
        that.destroy = function() {
            that.$element.remove();
            delete oxui.elements[that.ox];
        }
        /*
        */
        that.disable = function() {
            
        }
        /*
        */
        that.enable = function() {
            
        }
        /*
        */
        ///*
        that.defaults = function() {
            var length = arguments.length,
                ret;
            if (length == 0) {
                ret = that.def
            } else if (length == 1 && typeof arguments[0] == "string") {
                ret = that.def[arguments[0]];
            } else {
                // translate ("key", "value") to {"key": "value"}
                that.def = $.extend(
                    that.def, Ox.makeObject.apply(that, arguments)
                );
                ret = that;
            }
            return ret;
        }
        //*/
        /*
        Ox.Element.options()
            get options
        Ox.Element.options("foo")
            get options.foo
        Ox.Element.options("foo", 0)
            set options.foo
        Ox.Element.options({foo: 0, bar: 1})
            set options.foo and options.bar
        */
        ///*
        that.options = function() {
            var length = arguments.length,
                args, ret;
            if (length == 0) {
                //Ox.print("getting all options", options);
                ret = that.opt;
            } else if (length == 1 && typeof arguments[0] == "string") {
                //Ox.print("getting one option", options, arguments[0], options[arguments[0]]);
                ret = that.opt[arguments[0]];
            } else {
                // translate ("key", "value") to {"key": "value"}
                args = Ox.makeObject.apply(that, arguments);
                // if options have been set then extend options,
                // otherwise extend defaults
                that.opt = $.extend(Ox.length(that.opt) ?
                    that.opt : that.def, args);
                // that.trigger("OxElement" + that.id + "SetOptions", args);
                $.each(args, function(k, v) {
                    that.setOption(k, v);
                    //Ox.print("triggering", "OxElement" + that.id + "SetOption", {k: v})
                    //that.trigger("OxElement" + that.id + "SetOption", {k: v});
                })
                ret = that;
            }
            return ret;
        }
        // should become self.publish
        that.publish = function(event, data) {
            Ox.Event.publish(event + that.id, data);
            return that;
        }
        that.subscribe = function(event, callback) {
            Ox.Event.subscribe(event, callback);
            return that;
        }
        //that.setOptions = function() {};
        //*/
        // wrap jquery functions
        // so we can do oxObj.jqFn()
        $.each(oxui.jqueryFunctions, function(i, v) {
            that[v] = function() {
                var args = arguments,
                    length = args.length,
                    $element, id, ret;
                $.each(args, function(i, v) {
                    // if an oxui object was passed
                    // then pass its $element instead
                    // so we can do jqObj.append(oxObj)
                    if (v.ox) {
                        args[i] = v.$element;
                    }
                });
                if (v == "html" && that.$content) {
                    $element = that.$content;
                } else {
                    $element = that.$element;
                }
                // why does this not work?
                // ret = that.$element[v].apply(this, arguments);
                // maybe because we pass this, and not that.$element[v] ... ?
                // ret = that.$element[v].apply(that.$element[v], arguments);
                // doesn't work either ...
                if (length == 0) {
                    ret = $element[v]();
                } else if (length == 1) {
                    ret = $element[v](args[0]);
                } else if (length == 2) {
                    ret = $element[v](args[0], args[1]);
                } else if (length == 3) {
                    ret = $element[v](args[0], args[1], args[2]);
                } else if (length == 4) {
                    ret = $element[v](args[0], args[1], args[2], args[3]);
                }
                // if the $element of an oxui object was returned
                // then return the oxui object instead
                // so we can do oxObj.jqFn().oxFn()
                //Ox.print("v", v, "arguments", arguments)
                if (ret.jquery) {
                    //Ox.print("ret", ret, "ret.data('id')", ret.data("ox"))
                }
                return ret.jquery && oxui.elements[id = ret.data("ox")] ?
                    oxui.elements[id] : ret;
            }
        });
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.theme()
        get theme
    Ox.theme("foo")
        set theme to "foo"
    ----------------------------------------------------------------------------
    */

    Ox.theme = function() {
        var length = arguments.length,
            classes = $body.attr("class").split(" "),
            arg, theme;
        $.each(classes, function(i, v) {
            if (Ox.startsWith(v, "OxTheme")) {
                theme = v.replace("OxTheme", "").toLowerCase();
                if (length == 1) {
                    $body.removeClass(v);
                }
                return false;
            }
        });
        if (length == 1) {
            arg = arguments[0]
            $body.addClass("OxTheme" + Ox.toTitleCase(arg));
            if (theme) {
                $("input[type=image]").each(function() {
                    var $this = $(this);
                    $this.attr({
                        src: $this.attr("src").replace(
                            "/ox.ui." + theme + "/", "/ox.ui." + arg + "/"
                        )
                    });
                });
                $(".OxLoadingIcon").each(function() {
                    var $this = $(this);
                    $this.attr({
                        src: $this.attr("src").replace(
                            "/ox.ui." + theme + "/", "/ox.ui." + arg + "/"
                        )
                    });
                })
            }
        }
        return theme;
    };

    /*
    ============================================================================
    Bars
    ============================================================================
    */

    Ox.Bar = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    orientation: "horizontal",
                    size: "medium"                  // can be int
                })
                .options(options || {})
                .addClass("OxBar Ox" + Ox.toTitleCase(self.options.orientation)),
            dimensions = oxui.getDimensions(self.options.orientation);
        self.options.size = Ox.isString(self.options.size) ?
                oxui.getBarSize(self.options.size) : self.options.size;
        that.css(dimensions[0], "100%")
            .css(dimensions[1], self.options.size + "px");
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Tabbar
    ----------------------------------------------------------------------------
    */

    Ox.Tabbar = function(options, self) {

        var self = self || {},
            that = new Ox.Bar({
                    size: 20
                }, self)
                .defaults({
                    selected: 0,
                    tabs: []
                })
                .options(options || {})
                .addClass("OxTabbar");

        Ox.ButtonGroup({
            buttons: self.options.tabs,
            group: true,
            selectable: true,
            selected: self.options.selected,
            size: "medium",
            style: "tab",
        }).appendTo(that);

        return that;

    };

    // fixme: no need for this
    Ox.Toolbar = function(options, self) {
        var self = self || {},
            that = new Ox.Bar({
                size: oxui.getBarSize(options.size)
            }, self);
        return that;
    };

    /*
    ============================================================================
    Ox.Dialog
    ============================================================================
    */

    Ox.Dialog = function(options, self) {

        // fixme: dialog should be derived from a generic draggable
        var self = self || {},
            that = new Ox.Element("div", self)
                .defaults({
                    title: "",
                    buttons: [],
                    height: 216,
                    minHeight: 144,
                    minWidth: 256,
                    width: 384
                })
                .options(options || {})
                .addClass("OxDialog");

        if (!Ox.isArray(self.options.buttons[0])) {
            self.options.buttons = [[], self.options.buttons];
        }

        that.$titlebar = new Ox.Bar({
                size: "medium"
            })
            .addClass("OxTitleBar")
            //.html(self.options.title)
            .mousedown(drag)
            .dblclick(center)
            .appendTo(that);
        that.$title = new Ox.Element()
            .addClass("OxTitle")
            .html(self.options.title)
            .appendTo(that.$titlebar);
        that.$content = new Ox.Container()
            .addClass("OxContent")
            .appendTo(that);
        that.$buttonsbar = new Ox.Bar({})
            .addClass("OxButtonsBar")
            .appendTo(that);
        that.$buttons = [];
        $.each(self.options.buttons[0], function(i, button) {
            that.$buttons[i] = new Ox.Button({
                    size: "medium",
                    value: button.value
                })
                .addClass("OxLeft")
                .click(button.click) // fixme: rather use event?
                .appendTo(that.$buttonsbar);
        });
        that.$resize = new Ox.Element()
            .addClass("OxResize")
            .mousedown(resize)
            .dblclick(reset)
            .appendTo(that.$buttonsbar);
        $.each(self.options.buttons[1].reverse(), function(i, button) {
            that.$buttons[that.$buttons.length] = new Ox.Button({
                    size: "medium",
                    value: button.value
                })
                .addClass("OxRight")
                .click(button.click) // fixme: rather use event?
                .appendTo(that.$buttonsbar);
        });
        that.$buttons[0].focus();
        that.$layer = new Ox.Element() // fixme: Layer widget, that would handle click?
            .addClass("OxLayer")
            .mousedown(mousedownLayer)
            .mouseup(mouseupLayer);

        function center() {
            that.css({
                left: 0,
                top: parseInt(-$document.height() / 10) + "px", // fixme: don't overlap menu
                right: 0,
                bottom: 0,
                margin: "auto"
            });
        }

        function drag(event) {
            var bodyWidth = $body.width(),
                bodyHeight = $document.height(),
                elementWidth = that.width(),
                offset = that.offset(),
                x = event.clientX,
                y = event.clientY;                
            $window.mousemove(function(event) {
                /*
                $("*").css({
                    WebkitUserSelect: "none"
                });
                */
                that.css({
                    margin: 0
                });
                var left = Ox.limit(
                        offset.left - x + event.clientX,
                        24 - elementWidth, bodyWidth - 24
                        //0, documentWidth - elementWidth
                    ),
                    top = Ox.limit(
                        offset.top - y + event.clientY,
                        24, bodyHeight - 24
                        //24, documentHeight - elementHeight
                    );
                that.css({
                    left: left + "px",
                    top: top + "px"
                });
            });
            $window.one("mouseup", function() {
                $window.unbind("mousemove");
                /*
                $("*").css({
                    WebkitUserSelect: "auto"
                });
                */
            });            
        }

        function mousedownLayer() {
            that.$layer.stop().animate({
                opacity: 0.5
            }, 0);
        }

        function mouseupLayer() {
            that.$layer.stop().animate({
                opacity: 0
            }, 0);
        }

        function reset() {
            that.css({
                    left: Math.max(that.offset().left, 24 - self.options.width)
                })
                .width(self.options.width)
                .height(self.options.height);
            that.$content.height(self.options.height - 80);
        }

        function resize(event) {
            var contentHeight = that.$content.height(),
                documentWidth = $document.width(),
                documentHeight = $document.height(),
                elementWidth = that.width(),
                elementHeight = that.height(),
                offset = that.offset(),
                x = event.clientX,
                y = event.clientY;
            $window.mousemove(function(event) {
                /*
                $("*").css({
                    WebkitUserSelect: "none"
                });
                */
                that.css({
                    left: offset.left,
                    top: offset.top,
                    margin: 0
                });
                var width = Ox.limit(
                        elementWidth - x + event.clientX,
                        self.options.minWidth, Math.min(documentWidth, documentWidth - offset.left)
                    ),
                    height = Ox.limit(
                        elementHeight - y + event.clientY,
                        self.options.minHeight, documentHeight - offset.top
                    );
                that.width(width);
                that.height(height);
                that.$content.height(height - 80);
            });
            $window.one("mouseup", function() {
                $window.unbind("mousemove");
                /*
                $("*").css({
                    WebkitUserSelect: "auto"
                });
                */
            });            
        }

        self.onChange = function(key, value) {
            if (key == "title") {
                that.$title.html(value);
            }
        }

        that.append = function($element) {
            that.$content.append($element);
            return that;
        }

        that.close = function(callback) {
            callback = callback || function() {};
            that.animate({
                opacity: 0
            }, 200, function() {
                that.remove();
                that.$layer.remove();
                callback();
            });
            $window.unbind("mouseup", mouseupLayer)
            return that;
        }

        that.disable = function() {
            // to be used on submit of form, like login
            that.$layer.addClass("OxFront");
        };

        that.enable = function() {
            that.$layer.removeClass("OxFront");
        }

        that.open = function() {
            that.$layer.appendTo($body);
            center();
            reset();
            that.css({
                opacity: 0
            }).appendTo($body).animate({
                opacity: 1
            }, 200);
            $window.bind("mouseup", mouseupLayer)
            return that;
        }

        return that;

    }

    /*
    ============================================================================
    Forms
    ============================================================================
    */

    Ox.Form = function(options, self) {

        var self = self || {},
            that = new Ox.Element("div", self)
                .defaults({
                    items: []
                })
                .options(options || {}); // fixme: the || {} can be done once, in the options function

        $.each(self.options.items, function(i, item) {
                
        });

        that.values = function() {
            var values = {};
            if (arguments.length == 0) {
                $.each(self.options.items, function(i, item) {
                    values[item.id] = item.val();
                });
                return values;
            } else {
                $.each(arguments[0], function(key, value) {
                    
                });
                return that;
            }
        };

        return that;

    };

    Ox.FormItem = function(options, self) {

        var self = self || {},
            that = new Ox.Element("", self)
                .defaults({
                    error: "",
                    regexp: / /,
                    size: "medium",
                    type: "text"
                })
                .options(options || {});

        that.$input = new OxInput();

        return that;

    }

    /*
    ----------------------------------------------------------------------------
    Ox.Button
    ----------------------------------------------------------------------------
    */

    Ox.Button = function(options, self) {
        /*
        events:
        click       non-selectable button was clicked
        deselect    selectable button was deselected
        select      selectable button was selected
        */
        var self = self || {},
            that = new Ox.Element("input", self)
                .defaults({
                    disabled: false,
                    group: null,
                    id: "",
                    selectable: false,
                    selected: false,
                    size: "medium",
                    style: "default", // can be default, symbol or tab
                    type: "text",
                    value: "",
                    values: [] // fixme: shouldn't this go into self.values?
                })
                .options($.extend(options, {
                    value: $.isArray(options.value) ?
                        options.value[0] : options.value,
                    values: $.makeArray(options.value)
                }))
                .attr({
                    disabled: self.options.disabled ? "disabled" : "",
                    type: self.options.type == "text" ? "button" : "image"
                })
                .addClass("OxButton Ox" + Ox.toTitleCase(self.options.size) +
                    (self.options.style != "default" ? " Ox" + Ox.toTitleCase(self.options.style) : "") +
                    (self.options.disabled ? " OxDisabled": "") +
                    (self.options.selected ? " OxSelected": ""))
                .mousedown(mousedown)
                .click(click);
        //Ox.print(self.options.value, self.options.disabled)
        /*
        that.bind("OxElement" + that.id + "SetOptions", function(e, data) {
            if (typeof data.selected != "undefined") {
                if (data.selected != that.hasClass("OxSelected")) {
                    that.toggleClass("OxSelected");
                }
            }
            if (typeof data.value != "undefined") {
                if (self.options.type == "image") {
                    that.attr({
                        src: oxui.path + "png/" + Ox.theme() +
                            "/button" + Ox.toTitleCase(options.value) + ".png"
                    });
                } else {
                    that.val(self.options.value);
                }
            }
        })
        */
        function mousedown(e) {
            if (self.options.type == "image" && $.browser.safari) {
                // keep image from being draggable
                e.preventDefault();
            }
        }
        function click() {
            if (!self.options.selectable) {
                that.triggerEvent("click");
            } else if (!self.options.group || !self.options.selected) {
                if (self.options.group) {
                    that.triggerEvent("select");
                } else {
                    that.toggleSelected();
                }
            }
            if (self.options.values.length == 2) {
                that.options({
                    value: self.options.value == self.options.values[0] ?
                        self.options.values[1] : self.options.values[0]
                }); 
            }
            //self.options.click();
        }
        self.onChange = function(key, value) {
            //Ox.print("setOption", option, value)
            if (key == "selected") {
                if (value != that.hasClass("OxSelected")) { // fixme: neccessary?
                    that.toggleClass("OxSelected");
                }
                that.triggerEvent("change");
            } else if (key == "value") {
                if (self.options.type == "image") {
                    that.attr({
                        src: oxui.path + "png/ox.ui." + Ox.theme() +
                            "/button" + Ox.toTitleCase(value) + ".png"
                    });
                } else {
                    that.val(value);
                }
            }
        }
        that.toggleDisabled = function() {
            that.options({
                enabled: !self.options.disabled
            });
        }
        that.toggleSelected = function() {
            that.options({
                selected: !self.options.selected
            });
        }
        that.options("value", self.options.value);
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.ButtonGroup
    ----------------------------------------------------------------------------
    */

    Ox.ButtonGroup = function(options, self) {

        /*
        events:
        change  {id, value}     selection within a group changed
        */

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    buttons: [],
                    group: false,
                    selectable: false,
                    selected: -1,
                    size: "medium",
                    style: "",
                    type: "text",
                })
                .options(options || {})
                .addClass("OxButtonGroup");
        self.position = {};

        that.$buttons = [];
        $.each(self.options.buttons, function(position, button) {
            that.$buttons[position] = Ox.Button({
                disabled: button.disabled,
                group: self.options.group ? that : null,
                id: button.id,
                selectable: self.options.selectable,
                selected: position == self.options.selected,
                size: self.options.size,
                style: self.options.style,
                type: self.options.type,
                value: button.value
            }).appendTo(that);
            self.position[that.$buttons.id] = position;
            that.bindEvent("select_" + that.$buttons[position].options("id"), function() {
                selectButton(position);
            });
        });

        function onChange(event, data) {
            console.log("event", event, "data", data)
            var id = event.split("_")[1];
            if (self.options.selected > -1) {
                that.$buttons[self.options.selected].toggleSelected();
            }
            self.options.selected = self.position[id];
            that.triggerEvent("change", {
                value: id
            });
        }

        function selectButton(position) {
            that.$buttons[self.options.selected].toggleSelected();
            self.options.selected = position;
            that.$buttons[self.options.selected].toggleSelected();
        };

        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Input
    ----------------------------------------------------------------------------
    */

    Ox.Input = function(options, self) {

        /*  options:
         *      autocomplete    function, or array of values, or dict with array of values per label or placeholder
         *      clear           boolean, clear button, or not
         *      highlight       boolean, highlight value in autocomplete menu, or not
         *      id
         *      label           string or array (select) -- label and placeholder are mutually exclusive
         *      labelWidth      integer (px)
         *      placeholder     string or array (select) -- label and placeholder are mutually exclusive
         *      selected        integer, selected label or placeholder
         *      size            "large", "medium" or "small"
         *      type            "text", "password", "textarea", etc.
         */

        var self = self || {},
            that = new Ox.Element("div", self)
                .defaults({
                    autocomplete: null,
                    clear: false,
                    highlight: false,
                    id: "",
                    label: "",
                    labelWidth: 64,
                    placeholder: "",
                    selected: 0,
                    size: "medium",
                    type: "text"
                })
                .options(options || {})
                .addClass("OxInput Ox" + Ox.toTitleCase(self.options.size)),
            autocomplete; // fixme: should be self.autocomplete

        if (self.options.label) {
            self.options.label = Ox.makeArray(self.options.label);
            self.option = self.options.label[self.options.selected]; // fixme: id or title? or not use this at all?
            self.items = self.options.label;
        } else if (self.options.placeholder) {
            self.options.placeholder = Ox.makeArray(self.options.placeholder);
            self.option = self.options.placeholder[self.options.selected];
            self.items = self.options.placeholder;
        }
        if (Ox.isArray(self.options.autocomplete)) {
            autocomplete = self.options.autocomplete;
            self.options.autocomplete = {};
            self.options.autocomplete[self.placeholder] = autocomplete;
        }

        if (self.options.label) {
            that.$label = new Ox.Element()
                .addClass("OxInputLabel")
                .width(self.options.labelWidth)
                .html(self.options.label[0].title)
                .appendTo(that);
        }
        if (self.options.label.length > 1 || self.options.placeholder.length > 1) {
            that.$label && that.$label.click(select);
            that.$select = new Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: "select"
                    // value: oxui.symbols.select
                })
                .click(select)
                .appendTo(that);
            self.selectId = self.options.id + "_placeholder";
            self.selectMenu = new Ox.Menu({
                element: that,
                id: self.selectId,
                items: $.map(self.items, function(item, position) {
                    return {
                        checked: position == self.options.selected,
                        id: item.id,
                        group: self.selectId, // fixme: same id, works here, but should be different
                        title: item.title
                    };
                }),
                offset: {
                    left: 4,
                    top: 0
                }
            });
            that.bindEvent("change_" + self.selectId, change);
        }

        that.$input = new Ox.Element(
                self.options.type == "textarea" ? "textarea" : "input", self
            )
            .attr({
                type: self.options.type == "textarea" ? undefined : self.options.type // fixme: make conditional?
            })
            .addClass(
                "OxInput Ox" + Ox.toTitleCase(self.options.size) +
                " OxPlaceholder"
            )
            .focus(focus)
            .blur(blur)
            .change(change)
            .appendTo(that);

        self.options.placeholder && that.$input.val(self.option);

        if (self.options.clear) {
            that.$clear = new Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: "clear"
                    //value: oxui.symbols.clear
                })
                .click(clear)
                .appendTo(that);
        }

        if (self.options.autocomplete) {
            that.$input.attr({
                autocomplete: "off"
            });
            self.autocompleteId = self.options.id + "_menu"; // fixme: we do this in other places ... are we doing it the same way? var name?
            self.autocompleteMenu = new Ox.Menu({
                element: that.$input,
                id: self.autocompleteId,
                offset: {
                    left: 4,
                    top: 0
                },
                size: self.options.size
            });
            that.bindEvent("click_" + self.autocompleteId, onClick);
        }

        if (self.options.type != "textarea") {
            that.bindEvent({
                key_enter: submit,
            });
        }
        that.bindEvent({
            key_escape: cancel
        });

        function autocomplete(value, callback) {
            var value = value.toLowerCase(),
                items = [];
            if (value === "") {
                // items = self.options.autocomplete[self.placeholder];
            } else {
                $.each(self.options.autocomplete[self.option], function(i, item) {
                    if (item.toLowerCase().indexOf(value) > -1) {
                        items.push(item);
                    }
                });
            }
            callback(items);
        }

        function blur() {
            that.loseFocus();
            if (self.options.placeholder && that.$input.val() === "") {
                that.$input.addClass("OxPlaceholder").val(self.option);
            }
            if (self.options.autocomplete) {
                $document.unbind("keydown", keypress);
                $document.unbind("keypress", keypress);
            }
        }

        function call() {
            var value = that.$input.val();
            Ox.print("autocomplete call", self.option, value)
            if (self.options.autocomplete) {
                if (value !== "") {
                    Ox.isFunction(self.options.autocomplete) ? (
                        self.option ?
                        self.options.autocomplete(self.option.id, value, callback) :
                        self.options.autocomplete(value, callback)
                    ) : autocomplete(value, callback);
                } else {
                    callback();
                }
            }
        }

        function callback(items) {
            Ox.print("autocomplete callback", items)
            var items = items || [],
                selected = items.length == 1 ? 0 : -1,
                value = that.$input.val().toLowerCase();
            if (items.length) {
                items = $.map(items, function(title, position) {
                    if (value == Ox.stripTags(title.toLowerCase())) {
                        selected = position;
                    }
                    return {
                        id: title.toLowerCase(), // fixme: need function to do lowercase, underscores etc?
                        title: self.options.highlight ? title.replace(
                            new RegExp("(" + value + ")", "ig"),
                            "<span class=\"OxHighlight\">$1</span>"
                        ) : title
                    };
                });
                self.selectMenu.hideMenu();
                self.autocompleteMenu.options({
                    items: items,
                    selected: selected
                }).showMenu();
            } else {
                self.autocompleteMenu.hideMenu();
            }
        }

        function cancel() {
            that.$input.blur();
        }

        function change(event, data) {
            Ox.print("input change", event, data)
            if (data) {
                self.option = { 
                    id: data.id,
                    title: data.value // fixme: should be data.title
                };
            }
            if (self.options.label) {
                that.$label.html(self.option.title);
                that.$input.focus();
                call();
            } else {
                if (that.$input.is(".OxPlaceholder")) {
                    that.$input.val(self.option);
                    //that.$input.focus();
                } else {
                    that.$input.focus();
                    call();
                }
            }
        }

        function clear() {
            that.$input.val("").focus();
            //call();
        }

        function focus() {
            var value;
            that.gainFocus();
            if (that.$input.is(".OxPlaceholder")) {
                that.$input.val("").removeClass("OxPlaceholder");
            }
            value = that.$input.val();
            if (self.options.autocomplete) {
                // fixme: different in webkit and firefox (?), see keyboard handler, need generic function
                $document.bind("keydown", keypress);
                $document.bind("keypress", keypress);
                value !== "" && Ox.isFunction(self.options.autocomplete) ?
                        self.options.autocomplete(self.option.id, value, callback) :
                        autocomplete(value, callback);
            }
        }

        function keypress(event) {
            if (event.keyCode != 13) {
                setTimeout(function() {
                    var value = that.$input.val();
                    if (value != self.value) {
                        self.value = value;
                        call();
                    }
                }, 25);
            }
        }

        function onClick(event, data) {
            Ox.print("onClick", data);
            that.$input.val(Ox.stripTags(data.title));
            self.autocompleteMenu.hideMenu();
            submit();
        }

        function select() {
            self.selectMenu.showMenu();
        }

        function selection() {
            // fixme: not used!
            var start, end;
            if (arguments.length == 0) {
                return [self.element.selectionStart, self.element.selectionEnd];
            } else {
                start = arguments[0];
                end = arguments[1] || start;
                self.element.setSelectionRange(start, end);
            }
        }

        function submit() {
            Ox.print("input submit", that.$input.val())
            that.$input.trigger("blur");
            that.triggerEvent("submit", self.option ? {
                key: self.option.id,
                value: that.$input.val()
            } : that.$input.val());
        }

        that.height = function(value) {
            var stop = 8 / value;
            if (self.options.type == "textarea") {
                that.$element
                    .height(value)
                    .css({
                        background: "-moz-linear-gradient(top, rgb(224, 224, 224), rgb(208, 208, 208) " + (stop * 100) + "%, rgb(208, 208, 208) " + (100 - stop * 100) + "%, rgb(192, 192, 192))"
                    })
                    .css({
                        background: "-webkit-gradient(linear, left top, left bottom, from(rgb(224, 224, 224)), color-stop(" + stop + ", rgb(208, 208, 208)), color-stop(" + (1 - stop) + ", rgb(208, 208, 208)), to(rgb(192, 192, 192)))"
                    });
                that.$input
                    .height(value)
                    .css({
                        background: "-moz-linear-gradient(top, rgb(224, 224, 224), rgb(240, 240, 240) " + (stop * 100) + "%, rgb(240, 240, 240) " + (100 - stop * 100) + "%, rgb(255, 255, 255))"
                    })
                    .css({
                        background: "-webkit-gradient(linear, left top, left bottom, from(rgb(224, 224, 224)), color-stop(" + stop + ", rgb(240, 240, 240)), color-stop(" + (1 - stop) + ", rgb(240, 240, 240)), to(rgb(255, 255, 255)))"
                    });
            }
            return that;
        }

        that.width = function(value) {
            that.$element.width(value);
            that.$input.width(value - (self.options.type == "textarea" ? 0 : 2) -
                    (self.options.label ? self.options.labelWidth + 18 : 0) -
                    (self.options.placeholder.length > 1 ? 26 : 0) -
                    (self.options.clear ? 31 : 0));
            // fixme: the values above are all weird guesswork
            return that;
        }

        return that;

    };

    /*
    ----------------------------------------------------------------------------
    Ox.Label
    ----------------------------------------------------------------------------
    */

    Ox.Label = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    id: "",
                    title: ""
                })
                .options(options)
                .addClass("OxLabel");
        that.html(self.options.title);
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Range

    options:
        animate      boolean         if true, animate thumb
        arrows       boolean         if true, show arrows
        arrowStep    number          step when clicking arrows
        arrowSymbols array           arrow symbols, like ["minus", "plus"]
        max          number          maximum value
        min          number          minimum value
        orientation  string          "horizontal" or "vertical"
        step         number          step between values
        size         number          width or height, in px
        thumbSize    number          minimum width or height of thumb, in px
        thumbValue   boolean         if true, display value on thumb
        trackImages  string or array one or multiple track background image URLs          
        trackStep    number          0 (scroll here) or step when clicking track
        value        number          initial value
    ----------------------------------------------------------------------------
    */

    Ox.Range = function(options, self) {

        /*
            init
        */
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    animate: false,
                    arrows: false,
                    arrowStep: 1,
                    arrowSymbols: ["previous", "next"],
                    max: 100,
                    min: 0,
                    orientation: "horizontal",
                    step: 1,
                    size: 128,
                    thumbSize: 16,
                    thumbValue: false,
                    trackImages: [],
                    trackStep: 0,
                    value: 0
                })
                .options($.extend(options, {
                    arrowStep: options.arrowStep ?
                        options.arrowStep : options.step,
                    trackImages: $.makeArray(options.trackImages || [])
                }))
                .addClass("OxRange");

        // fixme: self. ... ?
        var trackImages = self.options.trackImages.length,
            values = (self.options.max - self.options.min + self.options.step) /
                    self.options.step;

        /*
            construct
        */
        that.$element
            .css({
                width: self.options.size + "px"
            });
        if (self.options.arrows) {
            var $arrowDec = Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: self.options.arrowSymbols[0]
                })
                .addClass("OxArrow")
                .mousedown(mousedownArrow)
                .click(clickArrowDec)
                .appendTo(that.$element);
        }
        var $track = new Ox.Element()
                .addClass("OxTrack")
                .mousedown(clickTrack)
                .appendTo(that.$element); // fixme: make that work

        if (trackImages) {
            var width = parseFloat(screen.width / trackImages),
                $image = $("<canvas/>")
                    .attr({
                        width: width * trackImages,
                        height: 14
                    })
                    .addClass("OxImage")
                    .appendTo($track.$element), // fixme: make that work
                c = $image[0].getContext('2d');
                c.mozImageSmoothingEnabled = false; // we may want to remove this later
            $.each(self.options.trackImages, function(i, v) {
                $("<img/>")
                    .attr({
                        src: v
                    })
                    .load(function() {
                        c.drawImage(this, i * width, 0, width, 14);
                    });
            });
        }
        var $thumb = Ox.Button({})
            .addClass("OxThumb")
            .appendTo($track);
        if (self.options.arrows) {
            var $arrowInc = Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: self.options.arrowSymbols[1]
                })
                .addClass("OxArrow")
                .mousedown(mousedownArrow)
                .click(clickArrowInc)
                .appendTo(that.$element);
        }
        var rangeWidth, trackWidth, imageWidth, thumbWidth;
        setWidth(self.options.size);

        /*
            private functions
        */

        function clickArrowDec() {
            that.removeClass("OxActive");
            setValue(self.options.value - self.options.arrowStep, 200)
        }
        function clickArrowInc() {
            that.removeClass("OxActive");
            setValue(self.options.value + self.options.arrowStep, 200);
        }
        function clickTrack(e) {
            Ox.Focus.focus();
            var left = $track.offset().left,
                offset = $(e.target).hasClass("OxThumb") ?
                    e.clientX - $thumb.offset().left - thumbWidth / 2 - 2 : 0;
            function val(e) {
                return getVal(e.clientX - left - offset);
            }
            setValue(val(e), 200);
            $window.mousemove(function(e) {
                setValue(val(e));
            });
            $window.one("mouseup", function() {
                $window.unbind("mousemove");
            });
        }
        function getPx(val) {
            var pxPerVal = (trackWidth - thumbWidth - 2) /
                (self.options.max - self.options.min);
            return Math.ceil((val - self.options.min) * pxPerVal + 1);
        }
        function getVal(px) {
            var px = trackWidth / values >= 16 ? px : px - 8,
                valPerPx = (self.options.max - self.options.min) /
                        (trackWidth - thumbWidth);    
            return Ox.limit(self.options.min +
                Math.floor(px * valPerPx / self.options.step) * self.options.step,
                self.options.min, self.options.max);
        }
        function mousedownArrow() {
            that.addClass("OxActive");
        }
        function setThumb(animate) {
            var animate = typeof animate != "undefined" ? animate : 0;
            $thumb.animate({
                marginLeft: (getPx(self.options.value) - 2) + "px",
                width: thumbWidth + "px"
            }, self.options.animate ? animate : 0, function() {
                if (self.options.thumbValue) {
                    $thumb.options({
                        value: self.options.value
                    });
                }
            });
        }
        function setValue(val, animate) {
            val = Ox.limit(val, self.options.min, self.options.max);
            if (val != self.options.value) {
                that.options({
                    value: val
                });
                setThumb(animate);
                that.triggerEvent("change", { value: val });
            }
        }
        function setWidth(width) {
            trackWidth = width - self.options.arrows * 32;
            thumbWidth = Math.max(trackWidth / values - 2, self.options.thumbSize - 2);
            that.$element.css({
                width: (width - 2) + "px"
            });
            $track.css({
                width: (trackWidth - 2) + "px"
            });
            if (trackImages) {
                $image.css({
                    width: (trackWidth - 2) + "px"
                });
            }
            $thumb.css({
                width: (thumbWidth - 2) + "px",
                padding: 0
            });
            setThumb();
        }

        /*
            shared functions
        */

        self.onChange = function(option, value) {
            
        }

        return that;

    };

    /*
    ----------------------------------------------------------------------------
    Ox.Select
    ----------------------------------------------------------------------------
    */

    Ox.Select = function(options, self) {

        var self = self || {},
            that = new Ox.Element("div", self) // fixme: do we use "div", or {}, or "", by default?
                .defaults({
                    id: "",
                    items: [],
                    size: "medium"
                })
                .options(options)
                .addClass("OxSelect Ox" + Ox.toTitleCase(self.options.size));
        self.buttonId = self.options.id + "_button";
        self.groupId = self.options.id; // + "_group"
        self.menuId = self.options.id + "_menu",

        $.each(self.options.items, function(i, item) {
            self.options.items[i] = $.extend(self.options.items[i], {
                checked: item.checked || false,
                group: self.groupId
            });
            if (item.checked) {
                self.selected = i;
            }
        });

        that.$button = new Ox.Button($.extend(self.options, {
                id: self.buttonId,
                type: "text", // fixme: this shouldn't be necessary
                value: self.options.items[self.selected].title // fixme: title instead of value?
            }), {})
            .click(clickButton)
            .appendTo(that);

        that.$symbol = new Ox.Button({
                style: "symbol",
                type: "image",
                value: "select"
            })
            .click(clickButton)
            .appendTo(that);

        that.$menu = new Ox.Menu({
            element: that.$button,
            id: self.menuId,
            items: self.options.items,
            offset: {
                left: 8,
                top: 0
            },
            side: "bottom",
            size: self.options.size
        });
        
        that.bindEvent("change_" + self.groupId, clickMenu);

        function clickButton() {
            that.$menu.toggleMenu();
        }

        function clickMenu(event, data) {
            that.$button.options({
                value: data.value
            });
            that.triggerEvent("change", data.value);
        }

        self.onChange = function(key, value) {
            
        };

        that.width = function(val) {
            // fixme: silly hack, and won't work for css()
            that.$element.width(val + 16);
            that.$button.width(val);
            //that.$symbol.width(val);
            return that;
        };

        return that;

    }

    /*
    ============================================================================
    Lists
    ============================================================================
    */

    Ox.IconList = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    id: "",
                    item: function() {},
                    keys: [],
                    orientation: "both",
                    request: function() {},
                    size: 128,
                    sort: [],
                })
                .options(options || {});

        $.extend(self, {
            itemHeight: self.options.size * 1.5,
            itemWidth: self.options.size
        });

        that.$element = new Ox.List({
                construct: constructItem,
                itemHeight: self.itemHeight,
                itemWidth: self.itemWidth,
                keys: $,
                orientation: "both",
                request: function() {},
                rowLength: 1,
                size: 128,
                type: "icon",            
            }, self)
            .click(click)
            .dblclick(dblclick)
            .scroll(scroll);

        function click() {
            
        }

        function constructItem(data) {
            var data = self.options.item(data, self.options.sort);
            return new Ox.IconItem($.extend(data, {
                size: self.options.size
            }));
        }

        function dblclick() {
            
        }

        function scroll() {
            
        }

        return that;

    };

    Ox.IconItem = function(options, self) {

        var self = self || {}
            that = new Ox.Element({}, self)
                .defaults({
                    height: 0,
                    id: "",
                    info: "",
                    size: 128,
                    title: "",
                    width: 0,
                    url: ""
                })
                .options(options || {});

        $.extend(self, {
            height: self.options.size * 1.5,
            url: oxui.path + "/png/ox.ui." + Ox.theme() + "/icon.png",
            width: self.options.size + 4
        });

        that.css({
            width: self.width + "px",
            height: self.height + "px"
        });
        that.$icon = $("<div>")
            .addClass("OxIcon")
            .css({
                top: self.options.size == 64 ? -70 : -128,
                width: self.options.size + "px",
                height: self.options.size + "px"
            });
        that.$iconImage = $("<img>")
            .attr({
                src: self.url
            })
            .css({
                width: iconWidth + "px",
                height: iconHeight + "px"
            })
            .mouseenter(mouseenter)
            .mouseleave(mouseleave)
            .one("load", function() {
                that.$iconImage.attr({
                    src: self.options.url
                });
            });
        that.$textBox = $("<div>")
            .addClass("OxText")
            .css({
                top: (self.options.size / 2 + 2) + "px",
                width: self.width + "px",
                height: (self.options.size == 64 ? 38 : 58) + "px"
            })
        that.$text = $("<div>")
            .html(self.options.title + "<br/><span class=\"OxInfo\">" + self.options.info + "</span>")
            .mouseenter(mouseenter)
            .mouseleave(mouseleave)
        that.$reflection = $("<div>")
            .addClass("OxReflection")
            .css({
                top: (self.options.size + 2) + "px",
                width: self.options.size + "px",
                height: (self.options.size / 2) + "px"
            });
        that.$reflectionImage = $("<img>")
            .addClass("OxReflection")
            .attr({
                src: self.url
            })
            .css({
                width: self.options.width + "px",
                height: self.options.height + "px"
            })
            .one("load", function() {
                that.$reflectionImage.attr({
                    src: self.options.url
                });
            });
        that.$gradient = $("<div>")
            .addClass("OxGradient")
            .css({
                top: (-self.options.size - 2) + "px"
            });

        that.append(
            that.$reflection.append(
                that.$reflectionImage
            ).append(
                that.$gradientImage
            )
        ).append(
            that.$textBox.append(
                that.$text
            )
        ).append(
            that.$icon.append(
                that.$iconImage
            )
        );

        function mouseenter() {
            that.addClass("OxHover");
        }

        function mouseleave() {
            that.removeClass("OxHover");
        }

        return that;

    };

    Ox.List = function(options, self) {

        var self = self || {},
            that = new Ox.Container({}, self)
                .defaults({
                    construct: function() {},
                    itemHeight: 16,
                    itemWidth: 16,
                    keys: [],
                    orientation: "vertical",
                    request: function() {}, // {sort:, range:, callback:}, without parameter returns {items, size etc.}
                    rowLength: 1,
                    sort: [],
                    type: "text",
                    unique: ""
                })
                .options(options || {})
                .click(click)
                .scroll(scroll);

        $.extend(self, {
            $items: [],
            $pages: [],
            ids: {},
            keyboardEvents: {
                key_alt_control_a: invertSelection,
                key_control_a: selectAll,
                key_control_shift_a: selectNone,
                key_end: scrollToFirst,
                key_home: scrollToLast,
                key_pagedown: scrollPageDown,
                key_pageup: scrollPageUp
            },
            page: 0,
            pageLength: 100,
            requests: [],
            selected: []
        });
        self.keyboardEvents["key_" + (self.options.orientation == "horizontal" ? "left" : "up")] = selectPrevious;
        self.keyboardEvents["key_" + (self.options.orientation == "horizontal" ? "right" : "down")] = selectNext;
        self.keyboardEvents["key_" + (self.options.orientation == "horizontal" ? "shift_left" : "shift_up")] = addPreviousToSelection;
        self.keyboardEvents["key_" + (self.options.orientation == "horizontal" ? "shift_right" : "shift_down")] = addNextToSelection;

        updateQuery();
        that.bindEvent(self.keyboardEvents);

        function addAllToSelection(pos) {
            var arr,
                len = self.$items.length;
            if (!isSelected(pos)) {
                if (self.selected.length == 0) {
                    addToSelection(pos);
                } else {
                    if (Ox.min(self.selected) < pos) {
                         var arr = [pos];
                         for (var i = pos - 1; i >= 0; i--) {
                             if (isSelected(i)) {
                                 $.each(arr, function(i, v) {
                                     addToSelection(v);
                                 });
                                 break;
                             }
                             arr.push(i);
                         }
                    }
                    if (Ox.max(self.selected) > pos) {
                        var arr = [pos];
                        for (var i = pos + 1; i < len; i++) {
                            if (isSelected(i)) {
                                $.each(arr, function(i, v) {
                                    addToSelection(v);
                                });
                                break;
                            }
                            arr.push(i);
                        }
                    }
                }
            }
        }

        function addNextToSelection() {
            var pos = getNext();
            if (pos > -1) {
                addToSelection(pos);
                scrollTo(pos);
            }
        }

        function addPreviousToSelection() {
            var pos = getPrevious();
            if (pos > -1) {
                addToSelection(pos);
                scrollTo(pos);
            }
        }

        function addToSelection(pos) {
            if (!isSelected(pos)) {
                self.selected.push(pos);
                if (!Ox.isUndefined(self.$items[pos])) {
                    self.$items[pos].addClass("OxSelected");
                }
                that.triggerEvent("select", {
                    ids: $.map(self.selected, function(v, i) {
                        return self.ids[v];
                    })
                });
            }
        }

        function clear() {
            $.each(self.requests, function(i, v) {
                Ox.print("Ox.Request.cancel", v);
                Ox.Request.cancel(v);
            });
            $.extend(self, {
                $items: [],
                $pages: [],
                page: 0,
                requests: []
            });
        }

        function click(e) {
            var $element = $(e.target), pos;
            that.gainFocus();
            while (!$element.hasClass("OxItem") && !$element.hasClass("OxPage")) {
                $element = $element.parent();
            }
            if ($element.hasClass("OxItem")) {
                Ox.print($element.attr("id"), $element.data("position"));
                pos = $element.data("position");
                if (e.shiftKey) {
                    addAllToSelection(pos);
                } else if (e.metaKey) {
                    toggleSelection(pos);
                } else {
                    select(pos);
                }
            } else {
                selectNone();
            }
        }

        function deselect(pos) {
            if (isSelected(pos)) {
                self.selected.splice(self.selected.indexOf(pos), 1);
                if (!Ox.isUndefined(self.$items[pos])) {
                    self.$items[pos].removeClass("OxSelected");
                }
                that.triggerEvent("select", {
                    ids: $.map(self.selected, function(v, i) {
                        return self.ids[v];
                    })
                });
            }
        }

        function getHeight() {
            return that.height() - (that.$content.width() > that.width() ? oxui.scrollbarSize : 0);
        }

        function getNext() {
            var pos = -1;
            if (self.selected.length) {
                pos = Ox.max(self.selected) + 1;
                if (pos == self.$items.length) {
                    pos = -1;
                }
            }
            return pos;
        }

        function getPage() {
            return self.options.orientation == "horizontal"
                ? Math.floor(that.scrollLeft() / self.pageWidth)
                : Math.floor(that.scrollTop() / self.pageHeight);
        }

        function getPositions() {
            Ox.print("getPositions", $.map(self.selected, function(v, i) {
                return self.ids[v];
            }));
            // fixme: optimize: send non-selected ids if more than half of the items are selected
            if (self.selected.length /*&& self.selected.length < self.listLength*/) {
                self.requests.push(self.options.request({
                    callback: getPositionsCallback,
                    ids: $.map(self.selected, function(v, i) {
                        return self.ids[v];
                    }),
                    sort: self.options.sort
                }));
            } else {
                getPositionsCallback();
            }
        }

        function getPositionsCallback(result) {
            Ox.print("getPositionsCallback", result)
            if (result) {
                $.extend(self, {
                    ids: {},
                    selected: []
                });
                $.each(result.data.positions, function(id, pos) {
                    Ox.print("id", id, "pos", pos)
                    self.selected.push(pos);
                });
            }
            load();
        }

        function getPrevious() {
            var pos = -1;
            if (self.selected.length) {
                pos = Ox.min(self.selected) - 1;
            }
            return pos;
        }

        function getWidth() {
            return that.width() - (that.$content.height() > that.height() ? oxui.scrollbarSize : 0);
        }

        function invertSelection() {
            $.each(Ox.range(self.listLength), function(i, v) {
                toggleSelection(v);
            });
        }

        function isSelected(pos) {
            return self.selected.indexOf(pos) > -1;
        }

        function load() {
            that.scrollTop(0);
            that.$content.empty();
            loadPages(self.page);
        }

        function loadPage(page, callback) {
            Ox.print("loadPage", page)
            if (page < 0 || page >= self.pages) {
                !Ox.isUndefined(callback) && callback();
                return;
            }
            var keys = $.inArray("id", self.options.keys) > -1 ? self.options.keys :
                        $.merge(self.options.keys, ["id"]),
                offset = page * self.pageLength,
                range = [offset, offset + (page < self.pages - 1 ?
                        self.pageLength : self.listLength % self.pageLength)];
            if (Ox.isUndefined(self.$pages[page])) {
                self.requests.push(self.options.request({
                    callback: function(result) {
                        self.$pages[page] = new Ox.ListPage();
                        if (self.options.type == "text") {
                            self.$pages[page].css({
                                top: (page * self.pageHeight) + "px"
                            });
                        } else {
                            self.$pages[page].css({

                            });
                        }
                        $.each(result.data.items, function(i, v) {
                            var pos = offset + i;
                            self.$items[pos] = new Ox.ListItem({
                                construct: self.options.construct,
                                data: v,
                                id: v[self.options.unique],
                                position: pos
                            });
                            self.ids[pos] = v[self.options.unique];
                            if (isSelected(pos)) {
                                self.$items[pos].addClass("OxSelected");
                            }
                            self.$items[pos].appendTo(self.$pages[page]);
                        });
                        if (self.options.type == "text" && page == 0) {
                            var height = that.height(),
                                visibleItems = Math.ceil(height / self.options.itemHeight);
                            if (result.data.items.length < visibleItems) {
                                self.$pages[page].height(height).css({
                                    overflow: "hidden"
                                });
                                $.each(Ox.range(result.data.items.length, visibleItems), function(i, v) {
                                    new Ox.ListItem({
                                        construct: self.options.construct,
                                        data: {},
                                        id: "",
                                        position: v
                                    }).appendTo(self.$pages[page]);
                                });
                            }
                        }
                        self.$pages[page].appendTo(that.$content);
                        !Ox.isUndefined(callback) && callback();
                    },
                    keys: keys,
                    range: range,
                    sort: self.options.sort
                }));
            } else {
                self.$pages[page].appendTo(that.$content);
            }
        }

        function loadPages(page, callback) {
            var counter = 0,
                fn = function() {
                    counter++;
                    counter == 2 && !Ox.isUndefined(callback) && callback();
                };
            loadPage(page, function() {
                loadPage(page - 1, fn);
                loadPage(page + 1, fn);
            });
        }

        function scroll() {
            var page = self.page;
            self.page = getPage();
            if (self.page == page - 1) {
                unloadPage(self.page + 2);
                loadPage(self.page - 1);
            } else if (self.page ==page + 1) {
                unloadPage(self.page - 2);
                loadPage(self.page + 1);
            } else if (self.page == page - 2) {
                unloadPage(self.page + 3);
                unloadPage(self.page + 2);
                loadPage(self.page);
                loadPage(self.page - 1);
            } else if (self.page == page + 2) {
                unloadPage(self.page - 3);
                unloadPage(self.page - 2);
                loadPage(self.page);
                loadPage(self.page + 1);                    
            } else if (self.page != page) {
                unloadPages(page);
                loadPages(self.page);
            }
        }

        function scrollPageDown() {
            that.scrollBy(getHeight());
        }

        function scrollPageUp() {
            that.scrollBy(-getHeight());
        }

        function scrollTo(pos) {
            var positions = [], scroll, size; 
            if (self.options.orientation == "horizontal") {

            } else if (self.options.orientation == "vertical") {
                positions[0] = self.options.itemHeight * pos;
                positions[1] = positions[0] + self.options.itemHeight;
                scroll = that.scrollTop();
                size = getHeight();
                if (positions[0] < scroll) {
                    that.animate({
                        scrollTop: positions[0] + "px"
                    }, 0);
                } else if (positions[1] > scroll + size) {
                    that.animate({
                        scrollTop: (positions[1] - size) + "px"
                    }, 0);
                }
            } else {
                
            }
        }

        function scrollToFirst() {
            that.scrollTop(0);
        }

        function scrollToLast() {
            that.scrollTop(self.listHeight);
        }

        function select(pos) {
            if (!isSelected(pos) || self.selected.length > 1) {
                selectNone();
                addToSelection(pos);
            }
        }

        function selectAll() {
            $.each(Ox.range(self.listLength), function(i, v) {
                Ox.print("adding", v);
                addToSelection(v);
            });
        }

        function selectNext() {
            var pos = getNext();
            if (pos > -1) {
                select(pos);
                scrollTo(pos);
            }
        }

        function selectNone() {
            $.each(self.$items, function(i, v) {
                deselect(i);
            });
        }

        function selectPrevious() {
            var pos = getPrevious();
            if (pos > -1) {
                select(pos);
                scrollTo(pos);
            }
        }

        function selectQuery(str) {
            $.each(self.$items, function(i, v) {
                if (Ox.toLatin(v.title).toUpperCase().indexOf(str) == 0) {
                    select(i);
                    scrollTo(i);
                    return false;
                }
            });
        }

        function toggleSelection(pos) {
            if (!isSelected(pos)) {
                addToSelection(pos);
            } else {
                deselect(pos);
            }
        }

        function unloadPage(page) {
            if (page < 0 || page >= self.pages) {
                return;
            }
            Ox.print("unloadPage", page)
            Ox.print("self.$pages", self.$pages)
            Ox.print(!Ox.isUndefined(self.$pages[page]))
            !Ox.isUndefined(self.$pages[page]) && self.$pages[page].remove();
        }

        function unloadPages(page) {
            unloadPage(page);
            unloadPage(page - 1);
            unloadPage(page + 1)
        }

        function updateQuery() {
            clear();
            self.requests.push(self.options.request({
                callback: function(result) {
                    var keys = {};
                    that.triggerEvent("load", result.data);
                    $.extend(self, {
                        listHeight: result.data.items * self.options.itemHeight, // fixme: should be listSize
                        listLength: result.data.items,
                        pages: Math.ceil(result.data.items / self.pageLength),
                        pageWidth: self.options.orientation == "horizontal" ?
                                self.pageLength * self.options.itemWidth : 0,
                        pageHeight: self.options.orientation == "horizontal" ? 0 :
                                self.pageLength * self.options.itemHeight / self.options.rowLength
                    });
                    that.$content.css({
                        height: self.listHeight + "px"
                    });
                    getPositions();
                }
            }));
        }

        function updateSort() {
            if (self.listLength > 1) {
                clear();
                getPositions();
            }
        }

        self.onChange = function(key, value) {
            Ox.print("list onChange", key, value);
            if (key == "request") {
                updateQuery();
            }
        };

        that.clearCache = function() { // fixme: unused? make private?
            self.$pages = [];
        };

        that.sort = function(key, operator) {
            if (key != self.options.sort[0].key || operator != self.options.sort[0].operator) {
                self.options.sort[0] = {
                    key: key,
                    operator: operator
                }
                that.triggerEvent("sort", self.options.sort[0]);
                updateSort();
            }
        }

        return that;

    };

    Ox.ListItem = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    construct: function() {},
                    data: {},
                    id: "",
                    position: 0
                })
                .options(options || {});

        $.each(self.options.data, function(k, v) {
            self.options.data[k] = $.isArray(v) ? v.join(", ") : v;
        });

        that.$element = self.options.construct(self.options.data)
            .addClass("OxItem")
            .attr({
                id: self.options.id
            })
            .data("position", self.options.position);

        return that;

    };

    Ox.ListPage = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .addClass("OxPage");
        return that;
    };

    Ox.TextList = function(options, self) {

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    columns: [],
                    columnWidth: [40, 800],
                    id: "",
                    request: function() {}, // {sort, range, keys, callback}
                    sort: []
                })
                .options(options || {})
                .addClass("OxTextList");

        $.each(self.options.columns, function(i, v) { // fixme: can this go into a generic ox.js function?
            if (Ox.isUndefined(v.unique)) {
                v.unique = false;
            }
            if (Ox.isUndefined(v.visible)) {
                v.visible = false;
            }
            if (v.unique) {
                self.unique = v.id;
            }
        });

        $.extend(self, {
            columnPositions: [],
            columnWidths: [],
            itemHeight: 16,
            page: 0,
            pageLength: 100,
            scrollLeft: 0,
            selectedColumn: getColumnIndexById(self.options.sort[0].key),
            visibleColumns: $.map(self.options.columns, function(v, i) {
                return v.visible ? v : null;
            })
        });
        $.extend(self, {
            pageHeight: self.pageLength * self.itemHeight
        });

        // Head

        that.$bar = new Ox.Bar({
            orientation: "horizontal",
            size: 16
        }).appendTo(that);
        that.$head = new Ox.Container()
            .addClass("OxHead")
            .appendTo(that.$bar);
        that.$head.$content.addClass("OxTitles");
        that.$titles = [];
        $.each(self.visibleColumns, function(i, v) {
            var $order, $resize, $left, $center, $right, timeout = 0;
            self.columnWidths[i] = v.width;
            that.$titles[i] = $("<div>")
                .addClass("OxTitle OxColumn" + Ox.toTitleCase(v.id))
                .css({
                    width: (v.width - 9) + "px",
                    textAlign: v.align
                })
                .html(v.title)
                .mousedown(function(e) {
                    timeout = setTimeout(function() {
                        dragColumn(v.id, e);
                        timeout = 0;
                    }, 250);
                })
                .mouseup(function() {
                    if (timeout) {
                        clearTimeout(timeout);
                        timeout = 0;
                        clickColumn(v.id);
                    }
                })
                .appendTo(that.$head.$content.$element);
            self.columnPositions[i] = Ox.sum(self.columnWidths) - self.columnWidths[i] / 2;
            $order = $("<div>")
                .addClass("OxOrder")
                .html(oxui.symbols["triangle_" + (
                    v.operator == "+" ? "up" : "down"
                )])
                .click(function() {
                    $(this).prev().trigger("click")
                })
                .appendTo(that.$head.$content.$element);
            $resize = $("<div>")
                .addClass("OxResize")
                .mousedown(function(e) {
                    var startWidth = self.columnWidths[i],
                        startX = e.clientX;
                    $window.mousemove(function(e) {
                        var x = e.clientX,
                            width = Ox.limit(
                                startWidth - startX + x,
                                self.options.columnWidth[0],
                                self.options.columnWidth[1]
                            );
                            resizeColumn(v.id, width);
                    });
                    $window.mouseup(function() {
                        $window.unbind("mousemove");
                        $window.unbind("mouseup");
                    });
                })
                .dblclick(function() {
                    resizeColumn(v.id, v.width);
                })
                .appendTo(that.$head.$content.$element);
            $left = $("<div>").addClass("OxLeft").appendTo($resize);
            $center = $("<div>").addClass("OxCenter").appendTo($resize);
            $right = $("<div>").addClass("OxRight").appendTo($resize);
        });
        that.$head.$content.css({
            width: (Ox.sum(self.columnWidths) + 2) + "px"
        });
        toggleSelected(self.options.columns[self.selectedColumn].id);
        that.$titles[getColumnPositionById(self.options.columns[self.selectedColumn].id)].css({
            width: (self.options.columns[self.selectedColumn].width - 25) + "px"
        });
        that.$select = new Ox.Button({
            style: "symbol",
            type: "image",
            value: "select"
        }).appendTo(that.$bar.$element);

        // Body

        that.$body = new Ox.List({
                construct: constructItem,
                id: self.options.id,
                itemHeight: 16,
                itemWidth: getItemWidth(),
                keys: $.map(self.visibleColumns, function(v, i) {
                    return v.id;
                }),
                orientation: "vertical",
                request: self.options.request,
                sort: self.options.sort,
                type: "text",
                unique: self.unique
            })
            .addClass("OxBody")
            .scroll(function() {
                var scrollLeft = $(this).scrollLeft();
                if (scrollLeft != self.scrollLeft) {
                    self.scrollLeft = scrollLeft;
                    that.$head.scrollLeft(scrollLeft);
                }
            })
            .appendTo(that);
        that.$body.$content.css({
            width: getItemWidth() + "px"
        });

        function addColumn(id) {
            
        }

        function clickColumn(id) {
            Ox.print("clickColumn", id);
            var i = getColumnIndexById(id),
                isSelected = self.options.sort[0].key == self.options.columns[i].id;
            that.sort(
                self.options.columns[i].id, isSelected ?
                (self.options.sort[0].operator == "+" ? "-" : "+") :
                self.options.columns[i].operator
            );
        }

        function constructItem(data) {
            var $item = $("<div>")
                    .css({
                        width: Math.max(Ox.sum(self.columnWidths), that.$element.width() - oxui.scrollbarSize) + "px"
                    });
            $.each(self.visibleColumns, function(i, v) {
                var $cell = $("<div>")
                        .addClass("OxCell OxColumn" + Ox.toTitleCase(v.id))
                        .css({
                            width: (self.columnWidths[i] - 9) + "px",
                            textAlign: v.align
                        })
                        .html(!$.isEmptyObject(data) ? data[v.id] : "")
                        .appendTo($item);
            });
            return $item;
        }

        function dragColumn(id, e) {
            var startX = e.clientX,
                startPos = getColumnPositionById(id),
                pos = startPos,
                stopPos = startPos,
                positions = $.map(self.visibleColumns, function(v, i) {
                    return self.columnPositions[i] - self.columnPositions[startPos]
                });
            $(".OxColumn" + Ox.toTitleCase(id)).css({
                opacity: 0.1
            });
            that.$titles[startPos].addClass("OxDrag").css({ // fixme: why does the class not work?
                cursor: "move"
            });
            Ox.print("positions", positions)
            $window.mousemove(function(e) {
                var d = e.clientX - startX;
                $.each(positions, function(i, v) {
                    if (d < 0 && d < v) {
                        stopPos = i;
                        return false;
                    } else if (d > 0 && d > v) {
                        stopPos = i;
                    }
                });
                if (stopPos != pos) {
                    pos = stopPos;
                    moveColumn(id, pos);
                }
            });
            $window.mouseup(function() {
                dropColumn(id, pos);
                $window.unbind("mousemove");
                $window.unbind("mouseup");
            });
        }

        function dropColumn(id, pos) {
            Ox.print("dropColumn", id, pos)
            var startPos = getColumnPositionById(id),
                stopPos = pos,
                $title = that.$titles.splice(startPos, 1)[0],
                column = self.visibleColumns.splice(startPos, 1)[0],
                width = self.columnWidths.splice(startPos, 1)[0];
            that.$titles.splice(stopPos, 0, $title);
            self.visibleColumns.splice(stopPos, 0, column);
            self.columnWidths.splice(stopPos, 0, width);
            Ox.print("s.vC", self.visibleColumns)
            $(".OxColumn" + Ox.toTitleCase(id)).css({
                opacity: 1
            });
            that.$titles[stopPos].removeClass("OxDrag").css({
                cursor: "pointer"
            });
        }

        function getColumnIndexById(id) {
            var pos = -1;
            $.each(self.options.columns, function(i, v) {
                if (v.id == id) {
                    pos = i;
                    return false;
                }
            });
            return pos;
        }

        function getColumnPositionById(id) {
            var pos = -1;
            $.each(self.visibleColumns, function(i, v) {
                if (v.id == id) {
                    pos = i;
                    return false;
                }
            });
            return pos;
        }

        function getItemWidth() {
            return Math.max(Ox.sum(self.columnWidths), that.$element.width() - oxui.scrollbarSize);
        }

        function moveColumn(id, pos) {
            Ox.print("moveColumn", id, pos)
            var startPos = getColumnPositionById(id),
                stopPos = pos,
                startClassName = ".OxColumn" + Ox.toTitleCase(id),
                stopClassName = ".OxColumn" + Ox.toTitleCase(self.visibleColumns[stopPos].id),
                $column = $(".OxTitle" + startClassName),
                $order = $column.next(),
                $resize = $order.next();
            $column.detach().insertBefore($(".OxTitle" + stopClassName));
            $order.detach().insertAfter($column);
            $resize.detach().insertAfter($order);
            $.each(that.$body.find(".OxItem"), function(i, v) {
                var $v = $(v);
                $v.children(startClassName).detach().insertBefore($v.children(stopClassName));
            });
        }

        function removeColumn(id) {
            
        }

        function resize() {
            
        }

        function resizeColumn(id, width) {
            var i = getColumnIndexById(id),
                pos = getColumnPositionById(id);
            self.columnWidths[pos] = width;
            that.$head.$content.css({
                width: (Ox.sum(self.columnWidths) + 2) + "px"
            });
            that.$titles[pos].css({
                width: (width - 9 - (i == self.selectedColumn ? 16 : 0)) + "px"
            });
            that.$body.$content.find(".OxItem").css({ // fixme: can we avoid this lookup?
                width: getItemWidth() + "px"
            });
            that.$body.$content.css({
                width: getItemWidth() + "px" // fixme: check if scrollbar visible, and listen to resize/toggle event
            });
            $(".OxColumn" + Ox.toTitleCase(self.options.columns[i].id)).css({
                width: (width - 9) + "px"
            });
            that.$body.clearCache();
        }

        function toggleSelected(id) {
            var pos = getColumnPositionById(id);
            updateOrder(id);
            pos > 0 && that.$titles[pos].prev().children().eq(2).toggleClass("OxSelected");
            that.$titles[pos].toggleClass("OxSelected");
            that.$titles[pos].next().toggleClass("OxSelected");
            that.$titles[pos].next().next().children().eq(0).toggleClass("OxSelected");
            that.$titles[pos].css({
                width: (
                    that.$titles[pos].width() + (that.$titles[pos].hasClass("OxSelected") ? -16 : 16)
                ) + "px"
            });
        }

        function updateOrder(id) {
            var pos = getColumnPositionById(id);
            Ox.print(id, pos)
            that.$titles[pos].next().html(oxui.symbols[
                "triangle_" +  (self.options.sort[0].operator == "+" ? "up" : "down")
            ]);
        }

        self.onChange = function(key, value) {
            if (key == "request") {
                that.$body.options(key, value);
            }
        };

        that.sort = function(key, operator) {
            var isSelected = key == self.options.sort[0].key;
            self.options.sort = [
                {
                    key: key,
                    operator: operator
                }
            ];
            if (isSelected) {
                updateOrder(self.options.columns[self.selectedColumn].id);
            } else {
                toggleSelected(self.options.columns[self.selectedColumn].id);
                self.selectedColumn = getColumnIndexById(key);
                toggleSelected(self.options.columns[self.selectedColumn].id);
            }
            that.$body.sort(self.options.sort[0].key, self.options.sort[0].operator);
        };

        return that;

    };

    /*
    ============================================================================
    Menus
    ============================================================================
    */

    Ox.MainMenu = function(options, self) {

        /*  options:
         *      extras
         *      menus
         *      size
         */

        var self = self || {},
            that = new Ox.Bar({}, self)
                .defaults({
                    extras: [],
                    menus: [],
                    size: "medium"
                })
                .options(options || {})
                .addClass("OxMainMenu Ox" + Ox.toTitleCase(self.options.size)) // fixme: bar should accept small/medium/large ... like toolbar
                .click(click)
                .mousemove(mousemove);

        self.focused = false;
        self.selected = -1;
        that.menus = [];
        that.titles = [];
        that.layer = $("<div>").addClass("OxLayer");

        $.each(self.options.menus, function(position, menu) {
            that.titles[position] = $("<div>")
                .addClass("OxTitle")
                .html(menu.title)
                .data("position", position)
                .appendTo(that.$element);
            that.menus[position] = new Ox.Menu($.extend(menu, {
                element: that.titles[position],
                mainmenu: that,
                size: self.options.size
            }));
            that.bindEvent("hide_" + that.menus[position].options("id"), onHideMenu);
        });

        if (self.options.extras.length) {
            that.extras = $("<div>")
                .addClass("OxExtras")
                .appendTo(that.$element);
            $.each(self.options.extras, function(position, extra) {
                extra.css({
                    float: "left" // fixme: need class!
                }).appendTo(that.extras);
            });
        }

        function click(event) {
            var $target = $(event.target),
                position = typeof $target.data("position") != "undefined" ?
                        $target.data("position") : -1;
            clickTitle(position);
        }

        function clickTitle(position) {
            var selected = self.selected;
            if (self.selected > -1) {
                that.menus[self.selected].hideMenu();
            }
            if (position > -1) {
                if (position != selected) {
                    self.focused = true;
                    self.selected = position;
                    that.titles[self.selected].addClass("OxSelected");
                    that.menus[self.selected].showMenu();
                }
            }
        }

        function mousemove(event) {
            var $target = $(event.target),
                focused,
                position = typeof $target.data("position") != "undefined" ?
                        $target.data("position") : -1;
            if (self.focused && position != self.selected) {
                if (position > -1) {
                    clickTitle(position);                        
                } else {
                    focused = self.focused;
                    that.menus[self.selected].hideMenu();
                    self.focused = focused;
                }
            }
        }

        function onHideMenu() {
            if (self.selected > -1) {
                that.titles[self.selected].removeClass("OxSelected");
                self.selected = -1;
            }
            self.focused = false;
        }

        self.onChange = function(key, value) {

        };

        that.addMenuAfter = function(id) {
            
        };

        that.addMenuBefore = function(id) {
            
        };

        that.disableItem = function(id) {
            
        };

        that.enableItem = function(id) {

        };

        that.getItem = function(id) {
            var item;
            $.each(that.menus, function(i, menu) {
                item = menu.getItem(id);
                return !item;
            });
            return item;
        };

        that.removeMenu = function() {
            
        };

        that.selectNextMenu = function() {
            if (self.selected < self.options.menus.length - 1) {
                clickTitle(self.selected + 1);
            }
        };

        that.selectPreviousMenu = function() {
            if (self.selected) {
                clickTitle(self.selected - 1);
            }
        };

        return that;

    };

    Ox.Menu = function(options, self) {

        /*

        options:
        element         the element the menu is attached to
        id              the menu id
        items           array of menu items
        mainmenu        the main menu this menu is part of, if any
        offset          offset of the menu, in px
        parent          the supermenu, if any
        selected        the position of the selected item
        side            open to "bottom" or "right"
        size            "large", "medium" or "small"

        events:
        change_groupId  {id, value}     checked item of a group has changed
        click_itemId                    item not belonging to a group was clicked
        click_menuId    {id, value}     item not belonging to a group was clicked
        deselect_menuId {id, value}     item was deselected                             not needed, not implemented
        hide_menuId                     menu was hidden
        select_menuId   {id, value}     item was selected                               not needed, not implemented

        */

        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    element: null,
                    id: "",
                    items: [],
                    mainmenu: null,
                    offset: {
                        left: 0,
                        top: 0
                    },
                    parent: null,
                    selected: -1,
                    side: "bottom",
                    size: "medium",
                })
                .options(options)
                .addClass(
                    "OxMenu Ox" + Ox.toTitleCase(self.options.side) +
                    " Ox" + Ox.toTitleCase(self.options.size)
                )
                .click(click)
                .mouseenter(mouseenter)
                .mouseleave(mouseleave)
                .mousemove(mousemove),
            itemHeight = self.options.size == "small" ? 12 : (self.options.size == "medium" ? 16 : 20),
            // menuHeight,
            scrollSpeed = 1,
            $item; // fixme: used?
            // fixme: attach all private vars to self?

        // construct
        that.items = [];
        that.submenus = {};
        that.$scrollbars = [];
        that.$top = $("<div>")
            .addClass("OxTop")
            .appendTo(that.$element);
        that.$scrollbars.up = constructScrollbar("up")
            .appendTo(that.$element);
        that.$container = $("<div>")
            .addClass("OxContainer")
            .appendTo(that.$element);
        that.$content = $("<table>")
            .addClass("OxContent")
            .appendTo(that.$container);
        constructItems(self.options.items);
        that.$scrollbars.down = constructScrollbar("down")
            .appendTo(that.$element);
        that.$bottom = $("<div>")
            .addClass("OxBottom")
            .appendTo(that.$element);
        that.$layer = $("<div>")
            .addClass(self.options.mainmenu ? "OxMainMenuLayer" : "OxLayer");

        function click(event) {
            var item,
                position,
                $target = $(event.target);
            if ($target.is(".OxCell")) {
                position = $target.parent().data("position");
                item = that.items[position];
                if (!item.options("disabled")) {
                    clickItem(position);
                } else {
                    that.hideMenu();
                }
            } else {
                that.hideMenu();
            }
        }

        function clickItem(position) {
            var item = that.items[position];
            if (!item.options("items").length) {
                if (that.options("parent")) {
                    that.options("parent").hideMenu().triggerEvent("click");
                }
                if (item.options("checked") !== null && (!item.options("group") || !item.options("checked"))) {
                    item.options({
                        checked: !item.options("checked")
                    });
                    Ox.Event.trigger("change_" + item.options("group"), {
                        id: item.options("id"),
                        value: item.options("title")[0] // fixme: value or title?
                    });
                } else {
                    Ox.Event.trigger("click_" + self.options.id, {
                        id: item.options("id"),
                        title: item.options("title")[0]
                    });
                    Ox.Event.trigger("click_" + item.options("id"));
                }
                if (item.options("title").length == 2) {
                    item.toggleTitle();
                }
            }
            that.hideMenu();
        }

        function clickSelectedItem() {
            // called on key.enter
            if (self.options.selected > -1) {
                clickItem(self.options.selected);
            } else {
                that.hideMenu();
            }
        }

        function constructItems(items) {
            that.items = [];
            that.$content.empty();
            scrollMenuUp();
            $.each(items, function(i, item) {
                var position;
                if (item.id) {
                    that.items.push(new Ox.MenuItem($.extend(item, {
                        menu: that,
                        position: position = that.items.length
                    })).data("position", position).appendTo(that.$content)); // fixme: jquery bug when passing {position: position}? does not return the object?;
                    if (item.items) {
                        that.submenus[item.id] = new Ox.Menu({
                            element: that.items[position],
                            id: Ox.toCamelCase(self.options.id + "/" + item.id),
                            items: item.items,
                            mainmenu: self.options.mainmenu,
                            offset: {
                                left: 0,
                                top: -4
                            },
                            parent: that,
                            side: "right",
                            size: self.options.size,
                        });
                    }
                } else {
                    that.$content.append(constructSpace());
                    that.$content.append(constructLine());
                    that.$content.append(constructSpace());
                }
            });
            if (!that.is(":hidden")) {
                that.hideMenu();
                that.showMenu();
            }
        }

        function constructLine() {
            return $("<tr>").append(
                $("<td>", {
                    "class": "OxLine",
                    colspan: 5
                })
            );
        }

        function constructScrollbar(direction) {
            var interval,
                speed = direction == "up" ? -1 : 1;
            return $("<div/>", {
                "class": "OxScrollbar Ox" + Ox.toTitleCase(direction),
                html: oxui.symbols["triangle_" + direction],
                click: function() { // fixme: do we need to listen to click event?
                    return false;
                },
                mousedown: function() {
                    scrollSpeed = 2;
                    return false;
                },
                mouseenter: function() {
                    var $otherScrollbar = that.$scrollbars[direction == "up" ? "down" : "up"];
                    $(this).addClass("OxSelected");
                    if ($otherScrollbar.is(":hidden")) {
                        $otherScrollbar.show();
                        that.$container.height(that.$container.height() - itemHeight);
                        if (direction == "down") {
                            that.$content.css({
                                top: -itemHeight + "px"
                            });
                        }
                    }
                    scrollMenu(speed);
                    interval = setInterval(function() {
                        scrollMenu(speed);
                    }, 100);
                },
                mouseleave: function() {
                    $(this).removeClass("OxSelected");
                    clearInterval(interval);
                },
                mouseup: function() {
                    scrollSpeed = 1;
                    return false;
                }
            });
        }

        function constructSpace() {
            return $("<tr>").append(
                $("<td>", {
                    "class": "OxSpace",
                    colspan: 5
                })
            );
        }

        function getElement(id) {
            // fixme: needed?
            return $("#" + Ox.toCamelCase(options.id + "/" + id));
        }

        function isFirstEnabledItem() {
            var ret = true;
            $.each(that.items, function(i, item) {
                if (i < self.options.selected && !item.options("disabled")) {
                    return ret = false;
                }
            });
            return ret;
        }

        function isLastEnabledItem() {
            var ret = true;
            $.each(that.items, function(i, item) {
                if (i > self.options.selected && !item.options("disabled")) {
                    return ret = false;
                }
            });
            return ret;
        }

        function mouseenter() {
            that.gainFocus();
        }

        function mouseleave() {
            if (self.options.selected > -1 && !that.items[self.options.selected].options("items").length) {
                selectItem(-1);
            }
        }

        function mousemove(event) {
            var item,
                position,
                $target = $(event.target);
            if ($target.is(".OxCell")) {
                position = $target.parent().data("position");
                item = that.items[position];
                if (!item.options("disabled") && position != self.options.selected) {
                    selectItem(position);
                }
            } else {
                mouseleave();
            }
        }

        function scrollMenu(speed) {
            var containerHeight = that.$container.height(),
                contentHeight = that.$content.height(),
                top = parseInt(that.$content.css("top")) || 0,
                min = containerHeight - contentHeight + itemHeight,
                max = 0;
            top += speed * scrollSpeed * -itemHeight;
            if (top <= min) {
                top = min;
                that.$scrollbars.down.hide().trigger("mouseleave");
                that.$container.height(containerHeight + itemHeight);
                that.items[that.items.length - 1].trigger("mouseover");
            } else if (top >= max - itemHeight) {
                top = max;
                that.$scrollbars.up.hide().trigger("mouseleave");
                that.$container.height(containerHeight + itemHeight);
                that.items[0].trigger("mouseover");
            }
            that.$content.css({
                top: top + "px"
            });
        }

        function scrollMenuUp() {
            if (that.$scrollbars.up.is(":visible")) {
                that.$content.css({
                    top: "0px"
                });
                that.$scrollbars.up.hide();
                if (that.$scrollbars.down.is(":hidden")) {
                    that.$scrollbars.down.show();
                } else {
                    that.$container.height(that.$container.height() + itemHeight);
                }
            }
        }

        function selectItem(position) {
            var item;
            if (self.options.selected > -1) {
                item = that.items[self.options.selected]
                item.removeClass("OxSelected");
            }
            if (position > -1) {
                item = that.items[position];
                $.each(that.submenus, function(id, submenu) {
                    if (!submenu.is(":hidden")) {
                        submenu.hideMenu();
                        return false;
                    }
                });
                item.options("items").length && that.submenus[item.options("id")].showMenu(); // fixme: do we want to switch to this style?
                item.addClass("OxSelected");
            }
            self.options.selected = position;
        }

        function selectNextItem() {
            var offset,
                selected = self.options.selected;
            if (!isLastEnabledItem()) {
                if (selected == -1) {
                    scrollMenuUp();
                } else {
                    that.items[selected].removeClass("OxSelected");
                }
                do {
                    selected++;                    
                } while (that.items[selected].options("disabled"))
                selectItem(selected);
                offset = that.items[selected].offset().top + itemHeight -
                        that.$container.offset().top - that.$container.height();
                if (offset > 0) {
                    if (that.$scrollbars.up.is(":hidden")) {
                        that.$scrollbars.up.show();
                        that.$container.height(that.$container.height() - itemHeight);
                        offset += itemHeight;
                    }
                    if (selected == that.items.length - 1) {
                        that.$scrollbars.down.hide();
                        that.$container.height(that.$container.height() + itemHeight);
                    } else {
                        that.$content.css({
                            top: ((parseInt(that.$content.css("top")) || 0) - offset) + "px"
                        });
                    }
                }
            }            
        }

        function selectPreviousItem() {
            var offset,
                selected = self.options.selected;
            if (selected > - 1) {
                if (!isFirstEnabledItem()) {
                    that.items[selected].removeClass("OxSelected");
                    do {
                        selected--;                    
                    } while (that.items[selected].options("disabled"))
                    selectItem(selected);
                }
                offset = that.items[selected].offset().top - that.$container.offset().top;
                if (offset < 0) {
                    if (that.$scrollbars.down.is(":hidden")) {
                        that.$scrollbars.down.show();
                        that.$container.height(that.$container.height() - itemHeight);
                    }
                    if (selected == 0) {
                        that.$scrollbars.up.hide();
                        that.$container.height(that.$container.height() + itemHeight);
                    }
                    that.$content.css({
                        top: ((parseInt(that.$content.css("top")) || 0) - offset) + "px"
                    });
                }
            }
        }

        function selectSubmenu() {
            if (self.options.selected > -1) {
                var submenu = that.submenus[that.items[self.options.selected].options("id")];
                if (submenu && submenu.hasEnabledItems()) {
                    submenu.gainFocus();
                    submenu.selectFirstItem();
                } else if (self.options.mainmenu) {
                    self.options.mainmenu.selectNextMenu();
                }
            } else if (self.options.mainmenu) {
                self.options.mainmenu.selectNextMenu();
            }
        }

        function selectSupermenu() {
            if (self.options.parent) {
                that.items[self.options.selected].trigger("mouseleave");
                self.options.parent.gainFocus();
            } else if (self.options.mainmenu) {
                self.options.mainmenu.selectPreviousMenu();
            }
        }

        self.onChange = function(key, value) {
            if (key == "items") {
                constructItems(value);
            } else if (key == "selected") {
                selectItem(value);
            }
        }

        that.addItem = function(item, position) {
            
        };

        that.addItemAfter = function(item, id) {

        };

        that.addItemBefore = function(item, id) {

        };

        that.getItem = function(id) {
            var item;
            $.each(this.items, function(i, v) {
                if (v.options("id") == id) {
                    item = v;
                    return false;
                }
            });
            return item;
        };

        that.hasEnabledItems = function() {
            var ret = false;
            $.each(that.items, function(i, item) {
                if (!item.options("disabled")) {
                    return ret = true;
                }
            });
            return ret;
        };

        that.hideMenu = function() {
            if (that.is(":hidden")) {
                return;
            }
            $.each(that.submenus, function(i, submenu) {
                if (submenu.is(":visible")) {
                    submenu.hideMenu();
                    return false;
                }
            });
            selectItem(-1);
            scrollMenuUp();
            that.$scrollbars.up.is(":visible") && that.$scrollbars.up.hide();
            that.$scrollbars.down.is(":visible") && that.$scrollbars.down.hide();
            //that.$scrollbars.down.hide();
            if (self.options.parent) {
                self.options.element.removeClass("OxSelected");
            }
            that.hide()
                .loseFocus()
                .unbindEvent({
                    key_up: selectPreviousItem,
                    key_down: selectNextItem,
                    key_left: selectSupermenu,
                    key_right: selectSubmenu,
                    key_escape: that.hideMenu,
                    key_enter: clickItem
                })
                .triggerEvent("hide");
            that.$layer.hide();
            $document.unbind("click", click);
            return that;
            //that.triggerEvent("hide");
        };

        that.removeItem = function() {

        };

        that.selectFirstItem = function() {
            selectNextItem();
        };

        that.showMenu = function() {
            if (!that.is(":hidden")) {
                return;
            }
            if (!self.options.parent && !that.$layer.parent().length) {
                that.$layer.appendTo($body);
            }
            that.parent().length || that.appendTo($body);
            that.css({
                left: "-1000px",
                top: "-1000px",
            }).show();
            var offset = self.options.element.offset(),
                width = self.options.element.outerWidth(),
                height = self.options.element.outerHeight(),
                left = offset.left + self.options.offset.left + (self.options.side == "bottom" ? 0 : width),
                top = offset.top + self.options.offset.top + (self.options.side == "bottom" ? height : 0),
                menuHeight = that.$content.outerHeight(); // fixme: why is outerHeight 0 when hidden?
                menuMaxHeight = Math.floor($window.height() - top - 16);
            if (self.options.parent) {
                if (menuHeight > menuMaxHeight) {
                    top = Ox.limit(top - menuHeight + menuMaxHeight, self.options.parent.offset().top, top); 
                    menuMaxHeight = Math.floor($window.height() - top - 16);
                }
            }
            that.css({
                left: left + "px",
                top: top + "px"
            });
            if (menuHeight > menuMaxHeight) {
                that.$container.height(menuMaxHeight - itemHeight - 8); // margin
                that.$scrollbars.down.show();
            } else {
                that.$container.height(menuHeight);
            }
            !self.options.parent && that.gainFocus();
            that.bindEvent({
                key_up: selectPreviousItem,
                key_down: selectNextItem,
                key_left: selectSupermenu,
                key_right: selectSubmenu,
                key_escape: that.hideMenu,
                key_enter: clickSelectedItem
            });
            setTimeout(function() {
                $document.bind("click", click);
            }, 100);
            return that;
            //that.triggerEvent("show");
        };

        that.toggleMenu = function() {
            that.is(":hidden") ? that.showMenu() : that.hideMenu();
        };

        return that;

    };

    Ox.MenuItem = function(options, self) {

        var self = self || {},
            that = new Ox.Element("tr", self)
                .defaults({
                    bind: [],
                    checked: null,
                    disabled: false,
                    group: "",
                    icon: "",
                    id: "",
                    items: [],
                    keyboard: "",
                    menu: null, // fixme: is passing the menu to 100s of menu items really memory-neutral?
                    position: 0,
                    title: [],
                })
                .options($.extend(options, {
                    keyboard: parseKeyboard(options.keyboard || self.defaults.keyboard),
                    title: Ox.makeArray(options.title || self.defaults.title)
                }))
                .addClass("OxItem" + (self.options.disabled ? " OxDisabled" : ""))
                .attr({
                    id: Ox.toCamelCase(self.options.menu.options("id") + "/" + self.options.id)
                })
                .data("group", self.options.group); // fixme: why?

        // construct
        that.append(
                that.$status = $("<td>", {
                    "class": "OxCell OxStatus",
                    html: self.options.checked ? oxui.symbols.check : ""
                })
            )
            .append(
                that.$icon = $("<td>", {
                    "class": "OxCell OxIcon"
                })
                .append(self.options.icon ?
                    $("<img>", {
                        src: self.options.icon
                    }) : null
                )
            )
            .append(
                that.$title = $("<td>", {
                    "class": "OxCell OxTitle",
                    html: self.options.title[0]
                })
            )
            .append(
                $("<td>", {
                    "class": "OxCell OxModifiers",
                    html: $.map(self.options.keyboard.modifiers, function(modifier) {
                        return oxui.symbols[modifier];
                    }).join("")
                })
            )
            .append(
                $("<td>", {
                    "class": "OxCell Ox" + (self.options.items.length ? "Submenu" : "Key"),
                    html: self.options.items.length ? oxui.symbols.triangle_right :
                            oxui.symbols[self.options.keyboard.key] ||
                            self.options.keyboard.key.toUpperCase()
                })
            );            

        function parseKeyboard(str) {
            var modifiers = str.split(" "),
                key = modifiers.pop();
            return {
                modifiers: modifiers,
                key: key
            }
        }

        self.onChange = function(key, value) {
            if (key == "checked") {
                if (value && self.options.group) {
                    $.each(self.options.menu.items, function(i, item) {
                        if (
                            item.options("id") != self.options.id &&
                            item.options("group") == self.options.group &&
                            item.options("checked")
                        ) {
                            item.options({
                                checked: false
                            });
                            return false;
                        }
                    });
                }
                that.$status.html(value ? oxui.symbols.check : "")
            } else if (key == "disabled") {
                that.toggleClass("disabled"); // fixme: this will only work if onChange is only invoked on actual change
            } else if (key == "title") {
                
            }
        }

        that.toggle = function() {
            // toggle id and title
        };

        that.toggleChecked = function() {

        };

        that.toggleDisabled = function() {

        };

        that.toggleTitle = function() {
            that.options({
                title: that.$title.html() == self.options.title[0] ?
                        self.options.title[1] : self.options.title[0]
            });
        };

        return that;

    };

    /*
    ============================================================================
    Panels
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.CollapsePanel
    ----------------------------------------------------------------------------
    */

    Ox.CollapsePanel = function(options, self) {
        var self = self || {},
            that = new Ox.Panel({}, self)
                .defaults({
                    collapsed: false,
                    size: 20,
                    title: ""
                })
                .options(options)
                .addClass("OxCollapsePanel"),
            value = self.options.collapsed ?
                ["expand", "collapsed"] : ["collapse", "expand"],
            $titlebar = new Ox.Bar({
                    orientation: "horizontal",
                    size: self.options.size,
                })
                .dblclick(dblclickTitlebar)
                .appendTo(that),
            $switch = new Ox.Button({
                    style: "symbol",
                    type: "image",
                    value: value,
                })
                .click(toggleCollapsed)
                .appendTo($titlebar),
            $title = new Ox.Element()
                .addClass("OxTitle")
                .html(self.options.title/*.toUpperCase()*/)
                .appendTo($titlebar);
        that.$content = new Ox.Element()
            .addClass("OxContent")
            .appendTo(that);
        // fixme: doesn't work, content still empty
        // need to hide it if collapsed
        if (self.options.collapsed) {
            that.$content.css({
                marginTop: -that.$content.height() + "px"
            });
        }
        function dblclickTitlebar(e) {
            if (!$(e.target).hasClass("OxButton")) {
                toggleCollapsed();
            }
        }
        function toggleCollapsed() {
            that.options({
                collapsed: !self.options.collapsed
            });
            var top = self.options.collapsed ?
                -that.$content.height() : 0;
            that.$content.animate({
                marginTop: top + "px"
            }, 200);
        }
        self.onChange = function(option, value) {
            if (option == "collapsed") {
                $switch.options({
                    value: value ? "expand" : "collapse"
                });
            } else if (option == "title") {
                $title.html(self.options.title);
            }
        };
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.Panel
    ----------------------------------------------------------------------------
    */

    Ox.Panel = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .addClass("OxPanel");
        return that;
    };

    /*
    ----------------------------------------------------------------------------
    Ox.SplitPanel
    options:
        orientation: ""     "horizontal" or "vertical"
        elements: [{
            element,            Ox Element
            size: 0,            size in px
            resizable: false    resizable or not
        }]
        
    ----------------------------------------------------------------------------
    */

    Ox.SplitPanel = function(options, self) {
        var self = self || {},
            that = new Ox.Element({}, self)
                .defaults({
                    elements: [],
                    orientation: "horizontal"
                })
                .options(options || {})
                .addClass("OxSplitPanel"),
            length = self.options.elements.length,
            dimensions = oxui.getDimensions(self.options.orientation),
            edges = oxui.getEdges(self.options.orientation);
        $.each(self.options.elements, function(i, v) {
            var element = v.element
                .css({
                    position: "absolute" // fixme: this can go into a class
                })
                .css(edges[2], 0)
                .css(edges[3], 0);
            if (v.size != undefined) {
                element.css(dimensions[0], v.size + "px");
            }
            if (i == 0) {
                element.css(edges[0], 0);
                if (v.size == undefined) {
                    element.css(
                        edges[1],
                        (self.options.elements[1].size + (length == 3 ? self.options.elements[2].size : 0)) + "px"
                    );
                }
            } else if (i == 1) {
                if (self.options.elements[0].size != undefined) {
                    element.css(edges[0], self.options.elements[0].size + "px");
                }
                if (self.options.elements[0].size == undefined || v.size == undefined) {
                    element.css(
                        edges[1],
                        (length == 3 ? self.options.elements[2].size : 0) + "px"
                    );
                }
            } else {
                element.css(edges[1], 0);
                if (v.size == undefined) {
                    element.css(
                        edges[0],
                        (self.options.elements[0].size + self.options.elements[1].size) + "px"
                    );
                }
            }
            element.appendTo(that);
            //that.append(element)
        });
        return that;
    };

    Ox.TabPanel = function(options, self) {
        
    };

    /*
    ============================================================================
    Requests
    ============================================================================
    */

    /*
    ----------------------------------------------------------------------------
    Ox.LoadingIcon
    ----------------------------------------------------------------------------
    */

    Ox.LoadingIcon = function(options, self) {
        var self = self || {},
            that = new Ox.Element("img", self)
                .defaults({
                    size: "medium"
                })
                .options(options || {})
                .attr({
                    src: oxui.path + "/png/ox.ui." + Ox.theme() + "/loading.png" // fixme: oxui.themePath needed?
                })
                .addClass(
                    "OxLoadingIcon Ox" + Ox.toTitleCase(self.options.size)
                );
        self.deg = 0;
        self.interval = 0;
        self.isRunning = false;
        function clear() {
            clearInterval(self.interval);
            self.deg = 0;
            self.interval = 0;
            update();
        }
        function update() {
            that.css({
                MozTransform: "rotate(" + self.deg + "deg)",
                WebkitTransform: "rotate(" + self.deg + "deg)"
            });
        }
        that.start = function() {
            self.isRunning = true;
            clear();
            that.animate({
                opacity: 1
            }, 250);
            self.interval = setInterval(function() {
                self.deg = (self.deg + 30) % 360;
                update();
            }, 83);
        };
        that.stop = function() {
            that.animate({
                opacity: 0
            }, 250, function() {
                !self.isRunning && clear();
                self.isRunning = false;
            });
        }
        return that;
    }

    /*
    ----------------------------------------------------------------------------
    Ox.Progressbar
    ----------------------------------------------------------------------------
    */


})();